4
\$\begingroup\$

preface: I have a part of my code that is repeated more than once and I want to create a generic method or follow a design pattern to make the code cleaner.

What I'm trying to achieve is:

  • I have a JSON array each array represents a different type of employee.
  • I want to sort them by joiningDate then convert each List to JSON again then send them to the server.

this following class represents data record for an employee. records denotes the data and it's in JSON format. I parse the records data depending on the employeeType

data class EmployeeRecord(
    val recordId: Long?,
    val employeeType: EmployeeType,
    val records: String)

This is the employee type class

@JsonClass(generateAdapter = false)
enum class EmployeeType(val role: String) {
    CEO("CEO"),
    CTO("CTO"),
    Accountant("Accountant"),
    Developer("Developer");

    companion object {
        val tierOneTypes: List<EmployeeType>
            get() = listOf(
                Developer,
                Accountant
            )

        val tierTwoTypes: List<EmployeeType>
            get() = listOf(
                CEO,
                CTO
            )
    }
}

this is the employee types

sealed class EmployeeDataRecord(open val employeeType: EmployeeType)

@JsonClass(generateAdapter = true)
internal data class CeoData(
    @Json(name = "JoiningDate") val joiningDate: Long,
    @Json(name = "NumberOfBranches") val numberOfBranches: Int,
    @Json(name = "Address") val address: String,
    @Json(name = "Salary") val salary: Long
) : EmployeeDataRecord(EmployeeType.CEO) {
    companion object {

        fun create(
            joiningDate: Long,
            numberOfBranches: Int,
            address: String,
            salary: Long
        ): CeoData {

            return CeoData(
                joiningDate = joiningDate,
                numberOfBranches = numberOfBranches,
                address = address,
                salary = salary
            )
        }
    }
}

@JsonClass(generateAdapter = true)
internal data class CtoData(
    @Json(name = "HiringDate") val joiningDate: Long,
    @Json(name = "NumberOfTeams") val numberOfTeams: Int,
    @Json(name = "Address") val address: String,
    @Json(name = "Salary") val salary: Long
) : EmployeeDataRecord(EmployeeType.CTO) {
    companion object {

        fun create(
            joiningDate: Long,
            numberOfTeams: Int,
            address: String,
            salary: Long
        ): CtoData {

            return CtoData(
                joiningDate = joiningDate,
                numberOfTeams = numberOfTeams,
                address = address,
                salary = salary
            )
        }
    }
}

@JsonClass(generateAdapter = true)
internal data class AccountantData(
    @Json(name = "JoiningDate") val joiningDate: Long,
    @Json(name = "Address") val address: String,
    @Json(name = "Salary") val salary: Long
) : EmployeeDataRecord(EmployeeType.Accountant) {
    companion object {

        fun create(
            joiningDate: Long,
            address: String,
            salary: Long
        ): AccountantData {

            return AccountantData(
                joiningDate = joiningDate,
                address = address,
                salary = salary
            )
        }
    }
}

@JsonClass(generateAdapter = true)
internal data class DeveloperData(
    @Json(name = "JoiningDate") val joiningDate: Long,
    @Json(name = "TeamName") val teamName: String,
    @Json(name = "Address") val address: String,
    @Json(name = "Salary") val salary: Long
) : EmployeeDataRecord(EmployeeType.Developer) {
    companion object {

        fun create(
            joiningDate: Long,
            teamName: String,
            address: String,
            salary: Long
        ): DeveloperData {

            return DeveloperData(
                joiningDate = joiningDate,
                teamName = teamName,
                address = address,
                salary = salary
            )
        }
    }
}

here is the function that has the repeated code; as you can see createTierOneData and createTierTwoData have the same logic and it's repeated

class DataTest {

    private fun <T> listAdapter(modelClass: Class<T>): JsonAdapter<List<T>> {
        val type = Types.newParameterizedType(List::class.java, modelClass)
        return moshi.adapter(type)
    }


    private fun createTierOneData(employeeRecords: List<EmployeeRecord>):
            Pair<List<ServerAData>, List<ServerBData>> {
        val serverADataList = mutableListOf<ServerAData>()
        val serverBDataList = mutableListOf<ServerBData>()

        val accountantDataList = mutableListOf<AccountantData>()
        val developerDataList = mutableListOf<DeveloperData>()

        employeeRecords.forEach {
            if (it.employeeType in EmployeeType.tierOneTypes) {
                when (it.employeeType) {
                    EmployeeType.Accountant -> listAdapter(AccountantData::class.java).fromJson(
                        it.records
                    )?.let { records -> accountantDataList.addAll(records) }

                    EmployeeType.Developer -> listAdapter(DeveloperData::class.java).fromJson(
                        it.records
                    )?.let { records -> developerDataList.addAll(records) }

                }
            }
        }

        if (accountantDataList.isNotEmpty()) {
            if (accountantDataList.size > 1) {
                accountantDataList.sortBy { it.joiningDate }
            }

            val serverAData = ServerAData(
                EmployeeType.Accountant,
                listAdapter(AccountantData::class.java).toJson(accountantDataList)
            )
            val serverBData = ServerBData(
                EmployeeType.Developer, developerDataList.size,
                159025890000
            )

            serverADataList.add(serverAData)
            serverBDataList.add(serverBData)
        }

        if (developerDataList.isNotEmpty()) {
            if (developerDataList.size > 1) {
                developerDataList.sortBy { it.joiningDate }
            }

            val serverAData = ServerAData(
                EmployeeType.Developer,
                listAdapter(DeveloperData::class.java).toJson(developerDataList)
            )
            val serverBData = ServerBData(
                EmployeeType.Developer, developerDataList.size,
                159025890000
            )

            serverADataList.add(serverAData)
            serverBDataList.add(serverBData)
        }

        return Pair(serverADataList, serverBDataList)

    }

    private fun createTierTwoData(employeeRecords: List<EmployeeRecord>):
            Pair<List<ServerAData>, List<ServerBData>> {
        val serverADataList = mutableListOf<ServerAData>()
        val serverBDataList = mutableListOf<ServerBData>()

        val ctoDataList = mutableListOf<CtoData>()
        val ceoDataList = mutableListOf<CeoData>()

        employeeRecords.forEach {
            if (it.employeeType in EmployeeType.tierTwoTypes) {
                when (it.employeeType) {
                    EmployeeType.CTO -> listAdapter(CtoData::class.java).fromJson(
                        it.records
                    )?.let { records -> ctoDataList.addAll(records) }

                    EmployeeType.CEO -> listAdapter(CeoData::class.java).fromJson(
                        it.records
                    )?.let { records -> ceoDataList.addAll(records) }

                }
            }
        }

        if (ctoDataList.isNotEmpty()) {
            if (ctoDataList.size > 1) {
                ctoDataList.sortBy { it.joiningDate }
            }

            val serverAData = ServerAData(
                EmployeeType.CTO,
                listAdapter(CtoData::class.java).toJson(ctoDataList)
            )
            val serverBData = ServerBData(
                EmployeeType.CTO, ctoDataList.size,
                159025890000
            )

            serverADataList.add(serverAData)
            serverBDataList.add(serverBData)
        }

        if (ceoDataList.isNotEmpty()) {
            if (ceoDataList.size > 1) {
                ceoDataList.sortBy { it.joiningDate }
            }

            val serverAData = ServerAData(
                EmployeeType.CEO,
                listAdapter(CeoData::class.java).toJson(ceoDataList)
            )
            val serverBData = ServerBData(
                EmployeeType.CEO, ceoDataList.size,
                159025890000
            )

            serverADataList.add(serverAData)
            serverBDataList.add(serverBData)
        }

        return Pair(serverADataList, serverBDataList)

    }

}

and finally here are the ServersData classes

data class ServerAData(
    val type: EmployeeType,
    val dataRecord: String
)

data class ServerBData(
    val type: EmployeeType,
    val numberOfDataRecord: Int,
    val hiringDate: Long
)

so I am trying to find a way to refactor createTierOneData and createTierTwoData to apply DRY. I tried to make a generic function and I failed to do it properly. is there an approach to make these methods generic?

or if you can guide me to a design pattern that I can apply here that would be great.

\$\endgroup\$
1
  • \$\begingroup\$ @Tenfour04 the point here is the logic is the same, for each list I have to check if is not empty then sort it the construct two objects. so I want to remove this repeated code or do it in a smarter way. \$\endgroup\$ Aug 25, 2020 at 20:52

1 Answer 1

2
\$\begingroup\$

I'd add the data class type to the enum entries (or alternatively you could put these in a global map).

@JsonClass(generateAdapter = false)
enum class EmployeeType(val role: String, val dataType: Class<out EmployeeDataRecord>) {
    CEO("CEO", CeoData::class.java),
    CTO("CTO", CtoData::class.java),
    Accountant("Accountant", AccountantData::class.java),
    Developer("Developer", DeveloperData::class.java);

    companion object {
        //...
    }
}

Since we're using covariant Class type, update the parameter for listAdapter() as well:

private fun <T> listAdapter(modelClass: Class<out T>): JsonAdapter<List<T>> {

Also, you are sorting all the data types by joiningDate, so it would help to make that a member of the super class:

sealed class EmployeeDataRecord(open val employeeType: EmployeeType){
    abstract val joiningDate: Long
}

@JsonClass(generateAdapter = true)
internal data class CeoData(
    @Json(name = "JoiningDate") override val joiningDate: Long,
    @Json(name = "NumberOfBranches") val numberOfBranches: Int,
    @Json(name = "Address") val address: String,
    @Json(name = "Salary") val salary: Long
) : EmployeeDataRecord(EmployeeType.CEO) { 
    //...
}
//etc.

Then you can add a parameter for a list of the types to process. When you call this function you can pass EmployeeType.tierOneTypes or EmployeeType.tierTwoTypes. This makes it flexible if you modify the input types. You can loop through this list (even though it's just a list of two) since you're doing the same thing to both input types. But you could change it to a list of any size and this function would still work.

To process the input, you could set up MutableLists and forEach like you're already doing, but it's more concise (and easier to read) to use associateWith and flatMap.

private fun createData(employeeRecords: List<EmployeeRecord>, typesToProcess: Iterable<EmployeeType>):
        Pair<List<ServerAData>, List<ServerBData>> {

    val employeeDataListsByType = typesToProcess.associateWith { type ->
        employeeRecords.filter { it.employeeType == type }
            .flatmap { listAdapter(type.dataType).fromJson(it.records) ?: emptyList() }
            .sortedBy(EmployeeDataRecord::joiningDate)
    }

    val serverADataList = employeeDataListsByType.mapNotNull { (type, list) ->
        if (list.isEmpty())
            null
        else
            ServerAData(type, listAdapter(type.dataType).toJson(list))
    }

    val serverBDataList = employeeDataListsByType.mapNotNull { (type, list) ->
        if (list.isEmpty())
            null
        else
            ServerBData(type, list.size, 159025890000)
    }

    return Pair(serverADataList, serverBDataList)

}

Depending on the size of your data, it might be more efficient to insert .toSequence() before the filter call, and then add .toList() after the sortedBy call

I didn't test this so I might have a few syntax errors.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for answering. I like what you did with the emum EmployeeType the only thing that didn't work out for me is that I want the map to contain the value of the sub data type, not the super data type. for example, the map is of type <EmployeeType, List<EmployeeDataRecord>>, but I want it to be <EmployeeType, List<CeoData>>, because when I try to create ServerAData the listAdapter doesn't work because it expects subtype. I get an error that toJson expects Nothing \$\endgroup\$ Aug 26, 2020 at 15:54
  • \$\begingroup\$ Try changing the type of the modelClass parameter of listAdapter() to Class<out T>. \$\endgroup\$
    – Tenfour04
    Aug 26, 2020 at 17:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.