2

Assume we are building a system for managing large, tree-like domain-structures like banking loans or insurance contract. At the root of the domain model is a Loan class. It serves as an entry point to the layers of the domain model e.g. loan parts, collection information, contractual parties, collateral, associated insurances,...

Lets say our software can manage different types of Loans (e.g. real estate credit and consumer credit). The Loan class has an (Enum) attribute loanType which describes the type of Loan we are lloking at (e.g. RealEstateLoan, ConsumerLoan, InvestmentGoodsLoan,...).

Depending on the type of Loan the individual classes of the domain model should expose specific behaviour (e.g. a Loan Part in a RealEstateLoan needs certain types of collateral, in a ConsumerLoan only natural persons are allowed as borrowers,...).

The important point is that the type of the root of the domain model should influence the behaviour of some (not all) leaf nodes of the model (e.g. the ContractualParty class which is several layers apart from the Loan class).

Our first implementation used simple switch statements like

public class ContractualParty extends DomainObject

public Loan getLoanRoot() {
     // navigation magic to navigate through several layers of domain
     // objects all the way to the root Loan object 
     return root;
}

public List<PartyType> getAllowedTypes {
    switch (getLoanRoot().getLoantype()) {
        case ConsumerLoan: return new ArrayList<PartyType> (PartyType.NaturalPerson, PartyType.Couple) break;
        case InvestmentGoodsLoan: return new ArrayList<PartyType> (PartyType.Company) break;
...
    }

}

Obviously there are shortcomings of this kind of design as it does not scale well when the domain complexity is increasing and/or if the number of behavioural differences in the individual classes is increasing.

What I'd like to achieve is an implementation that: - allows to introduce new Loan types to the system without breaking the old ones - is type-safe (or as type safe as possible) when introducing new loan types in the LoanType Enumeration. Instead of sifting through pages of references to loanType or loanType switch statements,... the compiler tells me where extensions are necessary when introducing a new loan type

We started thinking about introducing subclasses of Loan. The subclasses could then introduce methods containg the loanType dependent logic like Loan.getAllowedPartyType(). Individual domain objects could then "ask" their Loan instance for the list of allowed party types. Each Loan subclass could implement these methods according to its specific rules. The problem with this approach appeared to us that the Loan subclasses would start to suck up logic from all over the domain objects and the domain objects would simply relegate many requests to the Loan root object.

A slight twist on this design would be to introduce a new Interface, abstract base class Loantype which would offer the type specific business logic to the domain objects. Instead of subclassing the Loan class it would get a reference on a Loantype instance which could then be used by the individual domain objects. This however is just a slight difference to the first design and suffers the same problem of turning the domain tree upside down and sucking the domain logic out of the individual domain objects.

I'd appreciate your thoughts, idea, pointers, ... on how to design these domain classes.

5
  • 2
    "sucking the domain logic out of the individual domain objects" I really don't get why it would have to be like this. Do you have concrete examples of behaviors? Is Loan your only aggregate root? It may be possible, but large aggregates are usually a sign of erroneous designs that do not align well with the business realities. The fact that you even have a getLoanRoot method is suspicious.
    – plalx
    Commented Feb 1, 2017 at 13:59
  • By behaviors I mean business use cases that have to be fulfilled as well as the business invariants that apply. For instance, in which process you need getAllowedTypes?
    – plalx
    Commented Feb 1, 2017 at 14:07
  • You could refactor your code by applying "Replace Type Code with Subclasses" tehnique, as stated here: sourcemaking.com/refactoring/replace-type-code-with-subclasses . Anyway, those switch statements increase the CRAP index and you should get rid of them Commented Feb 1, 2017 at 15:26
  • @plalx no Loan is not the only aggregate root (but the one I am working on:-)) We have e.g. BusinessPartner as another aggregate root. I agree with your comment about getLoanRoot(). I am looking at a relatively large codebase with a relatively procedural/anemic/... business model. A lot of business logic was placed outside the model in services classes (including lots of switch or if/else constructs). Then came a shift towards moving logic (e.g. validation rules) to the domain classes.
    – markus
    Commented Feb 1, 2017 at 16:06
  • @plalx cont'd And now we are trying to figure out a way of allowing non-aggregate domain models to change their behaviour depending on the kind of root object. Regarding your second comment: E.g. a Loan is only valid if its contractual parties are of a certain type ==> E.g. ConsumerLoan must not be associated with a company as creditor whereas a InvestmentGoodsLoan must have a company as creditor.
    – markus
    Commented Feb 1, 2017 at 16:09

2 Answers 2

2

Ignoring entirely the question of where the logic should reside (in which class), first determine what operations require logic that is dependent on multiple object "types" (ignoring as well whether a type is represented as an enum or a class).

Create a table on paper or a whiteboard. On the first column, list each loan type on a separate row. Then add a column for each leaf object type, with the type as the header (group similar types into one column). Now for each operation you wish to perform, using different colors or a small symbol to represent each operation, try to figure out how much variation must occur based on both loan type and leaf type. If there is no variation, leave a cell empty. If there is a variation, put a mark specific to that operation in the cell (a cell intersects both loan type and leaf type).

Where should the logic reside in the implementation? Ideally, logic resides close to data, in the sense that in order to implement any encapsulation, the logic using data is in the class containing the data. However, the best choice may be ambiguous if you have dependencies on both loan type and leaf type.

You can divide your operations as follows:

  • Vary neither by loan type or leaf type: put the operation logic closest to the data. This is may be a base class for leaf objects.
  • Vary by loan type and not leaf type: put the logic in the loan sub classes.
  • Vary by leaf type and not loan type: put the logic in the leaf sub classes.
  • Vary by both loan type and leaf type: ambiguous. Try to use the least clumsy option, that requires the least amount of code, while still enabling encapsulation, if possible.

A second analysis might be to re-analyse the operations, to create a set of operations that satisfy all requirements, but no longer require variants that are dependent on both loan type and leaf type.

A third analysis might be to figure out if data needs to be distributed in a different way. Perhaps the packaging of the data in the leaf objects is not ideal considering the operation logic required.

0
1

Lets say our software can manage different types of Loans (e.g. real estate credit and consumer credit). The Loan class has an (Enum) attribute loanType which describes the type of Loan we are lloking at (e.g. RealEstateLoan, ConsumerLoan, InvestmentGoodsLoan,...).

Depending on the type of Loan the individual classes of the domain model should expose specific behaviour (e.g. a Loan Part in a RealEstateLoan needs certain types of collateral, in a ConsumerLoan only natural persons are allowed as borrowers,...).

Think about programming to Role Interfaces.

Example: any client that needs to interact with RealEstateLoans needs a reference to an implementation of the RealEstateLoans.EntryPoint interface. All interactions with RealEstateLoans go through the EntryPoint, or go through some other interface that you obtained through the entry point. So the real estate contract client doesn't talk to ContractualParty, but instead talks to RealEstateLoans.ContractualParty.

If you have that piece in place, you can then start providing implementations that take your common data structures and cast them in the roles they are serving. More importantly, you can start refactoring your underlying data structures without needing to change your client (or your test) code.

We started thinking about introducing subclasses of Loan

In your refactoring, think hard about whether you can achieve the results you want through composition before you start looking into inheritance.

2
  • Thanks for your post. Role Interfaces are a very interesting concept. However I think they are more important for the clients of our model. We on the other hand are looking for a way how to provide a mechanism/structure/. for the internal parts of our model. The ContractualParty class for example is a role (interface) in itself. It serves as a reference to an external system managing the contractual parties details (name, adress,...) and specifies its relevance in the Loan (e.g. in Loan #4711 PersonA is the creditor, PersonB is the guarantor in Loan #9876 PersonB is creditor).
    – markus
    Commented Feb 3, 2017 at 6:17
  • cont'd We were also thinking about how to avoid the inheritance problem especially since there is a good chance we will end up with several intersecting types of behaviour (e.g. loan type vs. tenant specific regulations). Right now we are looking at defining interfaces for domain object extensions. A domain model can request an instance of its extension interface from a provider which in turn provides an instance based on the type of Loan, etc.
    – markus
    Commented Feb 3, 2017 at 6:22

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.