Introduzione delle differenze comportamentali in un modello di dominio complesso

3

Supponiamo di costruire un sistema per la gestione di strutture di dominio di grandi dimensioni come i prestiti bancari o il contratto di assicurazione. Alla radice del modello di dominio è una classe di prestito. Serve come un punto di ingresso per i livelli del modello di dominio, ad es. parti di prestito, informazioni sulla raccolta, parti contrattuali, garanzie, assicurazioni associate, ...

Diciamo che il nostro software può gestire diversi tipi di prestiti (ad esempio, credito immobiliare e credito al consumo). La classe di prestito ha un loanType di attributo (Enum) che descrive il tipo di prestito a cui stiamo parlando (ad esempio, RealEstateLoan, ConsumerLoan, InvestmentGoodsLoan, ...).

A seconda del tipo di prestito, le singole classi del modello di dominio dovrebbero esporre un comportamento specifico (ad esempio, una parte di prestito in un RealEstateLoan richiede determinati tipi di garanzie, in una ConsumerLoan sono ammesse solo persone fisiche come mutuatari, ...).

Il punto importante è che il tipo di root del modello di dominio dovrebbe influenzare il comportamento di alcuni (non tutti) nodi foglia del modello (ad esempio la classe ContractualParty che è diversi livelli oltre alla classe di prestito).

La nostra prima implementazione usava semplici istruzioni switch come

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;
...
    }

}

Ovviamente ci sono carenze di questo tipo di design in quanto non si adatta bene quando la complessità del dominio aumenta e / o se il numero di differenze comportamentali nelle singole classi aumenta.

Quello che mi piacerebbe ottenere è un'implementazione che: - consente di introdurre nuovi tipi di prestito nel sistema senza rompere quelli vecchi - è di tipo sicuro (o il più sicuro possibile) quando si introducono nuovi tipi di prestito nell'enumerazione LoanType. Invece di passare da una pagina all'altra di riferimenti alle dichiarazioni switchType o loanType, ... il compilatore mi dice dove sono necessarie le estensioni quando si introduce un nuovo tipo di prestito

Abbiamo iniziato a pensare di introdurre sottoclassi di prestito. Le sottoclassi potrebbero quindi introdurre metodi che contengono la logica dipendente da loanType come Loan.getAllowedPartyType (). I singoli oggetti del dominio potrebbero quindi "chiedere" la loro istanza di prestito per l'elenco dei tipi di partito autorizzati. Ciascuna sottoclasse di prestiti potrebbe implementare questi metodi in base alle proprie regole specifiche. Il problema con questo approccio ci è sembrato che le sottoclassi di prestito iniziassero ad assorbire la logica da tutti gli oggetti del dominio e gli oggetti del dominio avrebbero relegato semplicemente molte richieste all'oggetto radice di prestito.

Una leggera svolta su questo progetto sarebbe l'introduzione di una nuova interfaccia, la classe base astratta Loantype che offrirebbe la logica di business specifica del tipo agli oggetti del dominio. Invece di sottoclassi della classe di prestito, otterrebbe un riferimento su un'istanza di Loantype che potrebbe quindi essere utilizzata dai singoli oggetti di dominio. Tuttavia, questa è solo una leggera differenza rispetto al primo progetto e presenta lo stesso problema di capovolgere la struttura del dominio e svuotare la logica del dominio dai singoli oggetti del dominio.

Apprezzerei i tuoi pensieri, idee, indicazioni, ... su come progettare queste classi di dominio.

    
posta markus 01.02.2017 - 14:08
fonte

2 risposte

2

Ignorando completamente la questione di dove risieda la logica (in quale classe), determinare innanzitutto quali operazioni richiedono una logica che dipende da più "tipi" di oggetti (ignorando anche se un tipo è rappresentato come enum o classe) .

Crea un tavolo su carta o una lavagna. Nella prima colonna, elencare ciascun tipo di prestito su una riga separata. Quindi aggiungi una colonna per ogni tipo di oggetto foglia, con il tipo come intestazione (raggruppa tipi simili in una colonna). Ora per ogni operazione che desideri eseguire, utilizzando colori diversi o un piccolo simbolo per rappresentare ogni operazione, prova a capire quanta variazione deve verificarsi in base al tipo di prestito e al tipo di foglia. Se non c'è variazione, lascia una cella vuota. Se c'è una variazione, inserisci un segno specifico per quell'operazione nella cella (una cella interseca sia il tipo di prestito che il tipo di foglia).

Dove dovrebbe risiedere la logica nell'implementazione? Idealmente, la logica risiede vicino ai dati, nel senso che al fine di implementare qualsiasi incapsulamento, la logica che utilizza i dati è nella classe che contiene i dati. Tuttavia, la scelta migliore potrebbe essere ambigua se si hanno dipendenze sia dal tipo di prestito che dal tipo di foglia.

Puoi dividere le tue operazioni come segue:

  • Varia in base al tipo di prestito o al tipo di foglia: posiziona la logica di operazione più vicina ai dati. Questa potrebbe essere una classe base per gli oggetti foglia.
  • Varia in base al tipo di prestito e non al tipo di foglia: inserisci la logica nelle sottoclassi del prestito.
  • Varia in base al tipo di foglia e non al tipo di prestito: inserisci la logica nelle sottoclassi della foglia.
  • Varia in base al tipo di prestito e al tipo di foglia: ambiguo. Prova a utilizzare l'opzione meno goffa, che richiede la minor quantità di codice, pur consentendo l'incapsulamento, se possibile.

Una seconda analisi potrebbe essere quella di riesaminare le operazioni, creare una serie di operazioni che soddisfino tutti i requisiti, ma non richiedere più varianti che dipendono dal tipo di prestito e dal tipo di foglia.

Una terza analisi potrebbe essere quella di capire se i dati devono essere distribuiti in modo diverso. Forse la confezione dei dati negli oggetti foglia non è l'ideale considerando la logica di funzionamento richiesta.

    
risposta data 02.02.2017 - 20:10
fonte
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,...).

Pensa alla programmazione delle Interfacce di ruolo .

Esempio: qualsiasi client che deve interagire con RealEstateLoans necessita di un riferimento a un'implementazione dell'interfaccia RealEstateLoans.EntryPoint. Tutte le interazioni con RealEstateLoans passano attraverso EntryPoint o passano attraverso altre interfacce ottenute attraverso il punto di ingresso. Quindi il cliente del contratto immobiliare non parla con ContractualParty, ma invece parla con RealEstateLoans.ContractualParty.

Se hai quel pezzo in posizione, puoi iniziare a fornire implementazioni che prendono le tue strutture dati comuni e le gettano nei ruoli che stanno servendo. Ancora più importante, puoi avviare il refactoring delle strutture dati sottostanti senza dover modificare il codice del client (o del tuo test ).

We started thinking about introducing subclasses of Loan

Nel tuo refactoring, pensa seriamente se puoi ottenere i risultati desiderati attraverso la composizione prima di iniziare a esaminare l'ereditarietà.

    
risposta data 01.02.2017 - 18:05
fonte