Need good design: Anemic Model, Inheritance and Pattern Matching
https://softwareengineering.stackexchange.com/questions/400607
-
03-03-2021 - |
Question
I have Handler classes which accepts Queries and returns Results. Handlers is anemic. They accept input data bag and returns output data bag. Handlers can be many so I created common generic interface for handlers and common interfaces for query bags and result bags. Additionally, I created HandlerManager class, which accepts Query instance and using pattern matching, decide, which instance of Handler need to use:
sealed class Query {
class QueryA(val data: Int) : Query()
class QueryB(val data: Double) : Query()
class QueryC(val data: String) : Query()
}
sealed class Result {
class ResultA(val data: Int) : Result()
class ResultB(val data: Double) : Result()
class ResultC(val data: String) : Result()
}
interface Handler<Q : Query, R : Result> {
fun handle(query: Q): R
}
class HandlerA : Handler<Query.QueryA, Result.ResultA> {
override fun handle(query: Query.QueryA): Result.ResultA {
return Result.ResultA(query.data)
}
}
class HandlerB : Handler<Query.QueryB, Result.ResultB> {
override fun handle(query: Query.QueryB): Result.ResultB {
return Result.ResultB(query.data)
}
}
class HandlerC : Handler<Query.QueryC, Result.ResultC> {
override fun handle(query: Query.QueryC): Result.ResultC {
return Result.ResultC(query.data)
}
}
class HandlerManager() {
private val handlerA = HandlerA()
private val handlerB = HandlerB()
private val handlerC = HandlerC()
fun handle(query: Query): Result {
return when (query) {
is Query.QueryA -> handlerA.handle(query)
is Query.QueryB -> handlerB.handle(query)
is Query.QueryC -> handlerC.handle(query)
}
}
}
Question is simple: is this model is suitable and valid? Or there are OOP-style/Rich domain alternatives?
Solution
I'd go with "No", I think you have a number of code smells in your example.
HandlerManager inspects the type of the query
Ideally instead of checking types you should be using inheritance
Does your use of generics even work? ie, al the handlers implement
Handler
, but you have to know the type of Handler before you can call thehandle
function on itSince you check the query type in the manager you could simply have a non generic interface for Handler.
I think the problem with your code is that you are attempting to squeeze OOP into a ADM design. Simply go full ADM:
public class Query
{
string QueryString
string Type
}
public interface IHandler
{
Result RunQuery(Query q)
}
public class HandlerManager
{
Dictionary<string, Handler> handlers
public Result RunQuery(Query q)
{
return this.handers[q.Type].RunQuery(q)
}
}
Or full OOP
public class Query
{
string QueryString
Result RunQuery() //override in descendants
}
OTHER TIPS
Rich vs Anemic models: A model being rich or anemic is decided by the availability of the methods in the entities and aggregates. It's not about the Handler. It wouldn't make sense to have many methods other than handle in the handler.
What changes together stays together: Why make all queries (QueryA, QueryB, QueryC) as inner class of Query
class. It is unusual that these queries change all change at the same time, so grouping them together is not the best idea. The similar thing for Results
HandlerManager is not open-close: When ever a query is added to the system the HandlerManager has to be modified. There is a beautiful solution using reflection by Jimmy Bogard called mediatR in C#. MediatR automatically calls the Handler using reflection. I am sure there should be good java alternatives. You could check here. It would be best to make the queries open-close as they change very frequently