0

I have the following module structure:

  • core
  • persistence
  • extension

In my core module, I have an interface: Handler. This interface has multiple implementations in the core module as well as extension module. Some implementations depend on other complex types.

I would like to persist instances of Handler in a database. My persistence module has a HandlerRepository which needs to somehow save any Handler to the database and later read them back. Since these are very dynamic, I would like to save them as JSON documents. I do not want to add any serialization/deserialization logic to my core module.

Here is the repository signature:

interface HandlerRepository {
    fun findById(id: String): Handler
    fun save(id: String, handler: Handler)
}

One idea I had was to introduce HandlerSerializer<Handler> which could serialize/deserialize a specific type of a Handler. I could do something like the following:

interface HandlerSerializer<T: Handler> {
    fun serialize(handler: T): String
    fun deserialize(json: String): T
}

object HandlerSerializerRegistry {
    private val serializers = mutableMapOf<KClass<*>, HandlerSerializer<*>>()

    fun <T : Handler> registerSerializer(type: KClass<T>, serializer: HandlerSerializer<T>) {
        serializers[type] = serializer
    }

    fun <T : Handler> getSerializer(type: KClass<T>): HandlerSerializer<T>? {
        return serializers[type] as? HandlerSerializer<T>
    }
}

Then, to save a Handler, I would get the HandlerSerializer for that specific Handler from the HandlerSerializerRegistry and serialize it.

This seems like an acceptable solution and separation of concerns to me. However, I am having issues figuring out where to put HandlerSerializer implementations and tests for them? Putting these in core and extension does not seem correct to me and I cannot add them in persistence since that module does not know which Handler implementations exist.

Any advice? Is my design wrong? Am I missing something obvious?

3
  • I cannot add them in persistence since that module does not know which Handler implementations exist. neither Hibernate (to mention a known persisten framework) knows all the posible objects it will persist and yet the serialization/deserialization is implemented by the the framework. Don't draw bold and hard lines between modules. Don't let their names to limit the design.
    – Laiv
    Commented Nov 25, 2023 at 13:00
  • I agree with your point about Hibernate. That is why my persistence layer has a serializatiin support via HandlerSerializer. It does not know about implementations, just as how Hibernate does not either. I think I am missing something obvious :) Where would you suggest I put implementations and tests of HandlerSerializer classes?
    – alturkovic
    Commented Nov 25, 2023 at 13:08
  • I assume you meant in the core and extension modules and have them depend on the persistence module?
    – alturkovic
    Commented Nov 25, 2023 at 13:09

1 Answer 1

3

If you need some serialization code which is specific for certain Handler implementations, my first choice would be to make it part of the type itself (i.e. derive the handler directly from interface HandlerSerialize) and hence put it into the same module where the specific Handler lives (core or extension). I would not discard that solution so quickly just because of a superstitious feeling of being "not correct" - in case the serialization code requires access to private members, it may be simply the most straightforward solution.

If the serialization code is separable from the specific Handler type (which means it does not need access to non-public members), and core and extension become so large maintenance gets confusing, you may introduce two new modules coreSerializers and extensionSerializers and put the code there. As always, introducing more structure comes for the price for increased management efforts, so I would recommend against starting with this design - better refactor to this structure when the code base grows in this area.

Generic serialization code like HandlerSerializer<T: Handler>, however, can be placed in a neutral location, since it does not create a dependency on specific handler implementation. It could by either placed in persistence or in some new module serialization, whatever you prefer.

1
  • This is the closest to what I had in mind. Introduce a serialization module with HandlerSerializer, put core implementations in the serialization module, but have all other modules be self-contained. Meaning, each other module should also depend on the serialization module and add their HandlerSerialization implementations directly in it. That way I could avoid having persistence directly in core. Hiwever, you raise a valid point about private fields. I might just simplify and have persistence directly in core. Thanks for the advice.
    – alturkovic
    Commented Nov 26, 2023 at 10:15

Your Answer

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

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