Assuming we have polymorphic entities such as the following, with constructors enforcing invariants (assume there could be lots of sub-classes). What would be an effective/elegant approach to construct each concrete instance type while avoiding excessive code duplication?
abstract class Something {
Data fieldA;
Data fieldB;
Something(Data fieldA, Data fieldB) {
...
}
}
class SomethingA extends Something {
Data fieldC;
Data fieldD;
Something(Data fieldA, Data fieldB, Data fieldC, Data fieldD) {
...
}
}
class SomethingB extends Something {
Data fieldE;
Data fieldF;
Something(Data fieldA, Data fieldB, Data fieldE, Data fieldF) {
...
}
}
For instance, assume fieldA
and fieldB
are complex to resolve, we'd want to avoid:
createSomethingA(dto) {
fieldA = fieldAResolver.resolveFor(dto); //this will get duplicated everywhere
fieldB = fieldBResolver.resolveFor(dto);
return new SomethingA(fieldA, fieldB, dto.fieldC, dto.fieldD); //mapping logic as well
}
createSomethingB(dto) {
fieldA = fieldAResolver.resolveFor(dto);
fieldB = fieldBResolver.resolveFor(dto);
return new SomethingB(fieldA, fieldB, dto.fieldE, dto.fieldF);
}
When entities don't have to be constructed in a valid state, it's easy as you can just start with an empty entity, and then apply a series of transformations. But that can't be done for always-valid entities. The only approaches I could think of right now are:
Group fine-grained data bits into more coarse-grained bits. For instance, we could group
fieldA
andfieldB
into aBasicInfo
value object, and then do something likebasicInfo = basicInfoFactory.from(dto)
, reducing the number of duplicate calls, but there would still be at the very leastN
duplicated calls forN
concrete sub-classes.Change the whole constructor signature to take a single
SomethingData
class instance which is mutable. This allows us to constructSomethingData
through a series of transformations instead. In the end, theSomething
classes would have null checks to perform, orOptional
checks to ensure the data they need is present or else throw. (Same as #1 really, but with mutability).
Are there other alternatives or patterns for that?
EDIT:
Just to give a bit more context, it's a request management system with disparate request types. The requests have a bit of common info and workflow behaviors, but have very distinctive shapes and rules overall. Some of the rules may span the entire request's data. It's a CRUD-based system for the most part or at least this seemed to be the simplest route.
Perhaps inheritance is a mistake. Another drastically different approach could have been to use a schema-driven design, where we shape requests through linked data components. Components would just be associated (no composition) and most likeky changed together in a single tx for consistency.
2 Answers 2
Let us assume you don't want to turn SomethingData
into a mutable class (of course, a blurry name like SomethingData
isn't particular helpful to evaluate such design decisions, but let us assume this makes most sense for this case).
Without introducing a DI container, your solution #1 is the straightforward solution to this I would recommend. The basicInfo
object can now be initialized step-by-step ("through a series of transformations"), and any duplicate code in this initializations can be refactored out into separate functions (or, as the question demonstrates it, into some basicInfoFactory
class).
but there would still be at the very least N duplicated calls for N concrete sub-classes.
Yes, but these calls are just delegation calls, they don't duplicate any real logic, hence they are not a DRY violation. They have actually the opposing purpose of reusing non-repeated logic.
When you have code which deals with N different subclasses, you will need at least N different constructor calls. If those subclasses are immutable, there will be some individual initialization logic required for each of those subclasses which cannot be put into member functions of that class, since all the initialization must be completed before an object is constructed.
There is indeed another solution, as I mentioned at the beginning: making use of a DI container. For this, you would introduce an interface IBasicInfo
into your constructor parameters, safe the factory calls, and let the container figure out the necessary constructor calls.
-
Yeah I guess that's the only solutions there is without a complete redesign using composition instead of inheritance or representing the large cluster as individual associated data components (schema-driven), which are changed in a single TX for consistency.plalx– plalx02/03/2021 12:37:59Commented Feb 3, 2021 at 12:37
Depends of course, but I would try to extract the polymorphic behavior in its purest form, preferably into an interface. This would avoid inheritance, which is a tricky and hard to get right anyway.
So basically, extract the thing that needs those common values into its own thing. Try to find an abstraction that makes sense in your domain. Then what's left is the polymorphic stuff that is now unburdened with all those constructor parameters.
-
Wouldn't that be similar to my suggestion #2? In our situation it's a case management system where there's a multitude of concrete cases. We could have used composition instead of polymorphism I guess (e.g. Request--Type--DataSchema) where the type defines the data requests can have, but it seemed a little too abstract for now.plalx– plalx02/01/2021 20:29:23Commented Feb 1, 2021 at 20:29
-
I don't think so. Both of your solutions essentially just group the data, which the objects still need. I'm suggestion to move not just the data, but its behavior with it, so the polymorphic logic is not burdened by it.Robert Bräutigam– Robert Bräutigam02/01/2021 20:52:11Commented Feb 1, 2021 at 20:52
-
Could you give an example. Imagine large CRUD document entities. There's a core bloc of data they all have. Then all documents have their own shape and behaviors according to the data they are composed of. For instance, an entity may be a PrinterRepairRequest where another could be an AccountPasswordChangeRequest. Requests have little in common except some core data & workflow behaviors. However, some rules may span across many of their data components making difficult to break the large clusters.plalx– plalx02/02/2021 01:38:54Commented Feb 2, 2021 at 1:38
-
I've always been a big supporter of always valid entities and small cluster ARs, but DDD tactical patterns don't seem to be a good fit here.plalx– plalx02/02/2021 01:48:52Commented Feb 2, 2021 at 1:48
Explore related questions
See similar questions with these tags.
Customer extends Person
is a possible situation in theory, but in practice, it will lead to undesired coupling.