Evitare il dominio anemico - Come decidere quale singola responsabilità ha una classe

5

Anche dopo aver letto un mucchio, sto ancora cadendo nella stessa trappola. Ho una classe, di solito un'entità. Devo implementare più di una, operazioni simili su questo tipo. Sembra sbagliato (apparentemente arbitrariamente) scegliere una di queste operazioni per appartenere all'entità e spingere gli altri in una classe separata; Finisco per spingere le operazioni tutte alle classi di servizio e sono rimasto con un dominio anemico.

Come esempio grezzo, immagina la tipica classe Employee con proprietà numeriche per contenere il numero di giorni retribuiti a cui il dipendente ha diritto sia per malattia che per ferie e una serie di giorni presi per ciascuno.

public class Employee
{
    public int PaidHolidayAllowance { get; set; }

    public int PaidSicknessAllowance { get; set; }

    public IEnumerable<Holiday> Holidays { get; set; }

    public IEnumerable<SickDays> SickDays { get; set; }
}

Voglio due operazioni, una per calcolare le vacanze rimanenti, un'altra per i giorni di malattia pagati. Sembra strano includere, ad esempio, CalculateRemaingHoliday () nella classe Employee e bump CalculateRemainingPaidSick () in una classe di PaidSicknessCalculator. Vorrei finire con un PaidSicknessCalculator e un RemainingHolidayCalculator e l'entità Employee anemica come visto sopra. L'altra alternativa sarebbe quella di mettere entrambe le operazioni nella classe Employee e dare la Responsabilità Unica al freno. Ciò non rende il codice particolarmente gestibile.

Suppongo che la classe Employee debba avere una logica di inizializzazione / convalida (non accettare invenzioni negative ecc.) Quindi forse mi limito ad inizializzare e convalidare in modo semplice nelle entità stesse ed essere felice con le mie classi di calcolatrici separate. O forse dovrei chiedermi se Anemic Domain mi sta causando alcuni problemi tangibili con il mio codice.

    
posta SeeNoWeevil 26.06.2013 - 11:26
fonte

5 risposte

5

Dato che stiamo parlando di SRP, penso che sia una buona idea analizzare esattamente cosa può significare una responsabilità nel contesto di un'entità. Per me le loro responsabilità sono di 2 tipi:

Responsabilità primarie di un'entità

  • Identificazione stessa
  • Componendo se stesso con oggetti ad esso assegnati (aggiungendo sub-entità / oggetti valore ecc.)
  • Se ti trovi in DDD: per una radice aggregata, imponi invarianti aggregati

Responsabilità secondarie di un'entità

  • Qualsiasi altro comportamento definito nella lingua del dominio che sembra naturalmente appartenere all'entità

Nel tuo esempio, CalculateRemainingHoliday() e CalculateRemainingPaidSick() rientrano chiaramente nella seconda categoria.

Anche se le responsabilità primarie prese insieme possono essere considerate come una sola responsabilità (quella di "essere un'entità"), penso che tutti possiamo essere d'accordo sul fatto che la maggior parte delle entità di dominio avanzato tendono ancora a non aderire rigorosamente a SRP, a causa di responsabilità secondarie - queste sono una folla in crescita per natura, e ogni volta che ne aggiungi o ne rimuovi una, l'entità cambia in modo efficace per un motivo diverso. Tuttavia, puoi progettare le tue entità in modo che siano conformi a un tipo minore di SRP: cambiare qualcosa all'interno di una responsabilità secondaria non dovrebbe cambiare la classe di entità stessa.

In altre parole, dovresti progettare le tue entità in modo che le responsabilità secondarie siano delegate ad altri oggetti. Questi possono essere sottoentità, oggetti valore o oggetti che sono stati iniettati nell'entità. Ad esempio, Employee potrebbe avere un HolidayCalculationStrategy e un SickDaysCalculationStrategy che si occupano di questi problemi. Sebbene siano strettamente correlati a Employee e in effetti manipolino Employee di dati, questi oggetti saranno gli unici modificati quando tali strategie devono cambiare, l'entità stessa rimane intatta.

In sintesi, le entità di dominio avanzato non possono rispettare SRP alla lettera, ma è necessario il male per assicurare coesione ed evitare anemie. Possono ancora essere SRP-ish abbastanza da poter modificare comportamenti di dominio non quintessenziali esposti da un'entità senza toccare il codice dell'entità stessa.

Link interessanti su SRP vs Rich Domain Model:

link

link

    
risposta data 26.06.2013 - 15:20
fonte
3

Se hai molte operazioni molto simili, un approccio è renderle una singola operazione parametrizzata, e un altro è dividerle in una classe dedicata e aggregare più istanze.

Esempio di pseudo-codice parametrizzato:

public class Employee
{
    public enum PaidAbsenceType { Vacation, Sickness, MPternity, Training, Travel };

    public void setAllowance(PaidAbsenceType type, int amount);
    public int getAllowance(PaidAbsenceType type);

    public void useAllowance(PaidAbsenceType type, int amount);
    public int getUsed(PaidAbsenceType type);
}

Esempio di pseudo-codice aggregato:

public class Employee
{
    public class PaidAbsence {
        public int allowance { get; set; }
        public int used { get; set; }
    }

    public PaidAbsence Vacation;
    public PaidAbsence Sickness;
    public PaidAbsence MPternity;
    public PaidAbsence Training;
    public PaidAbsence Travel;
}

In entrambi i casi, la responsabilità della classe contenente è quella di aggregare questi membri e, se necessario, di coordinarli.

Esegui l'hard-coding di due versioni della stessa logica nella tua classe, a meno che non ci siano effettive differenze comportamentali, gli odori un po 'strani.

Si noti che in entrambi i casi precedenti, sarebbe sufficiente un singolo calcolatore (passando il PaidAbsenceType di interesse o passando direttamente l'oggetto PaidAbsence appropriato), assumendo nuovamente che la logica effettiva sia la stessa.

    
risposta data 26.06.2013 - 12:28
fonte
2

The other alternative would be to put both operations in the Employee class and kick Single Responsibility to the curb. That doesn't make for particularly maintainable code.

Perché? Sembra che tu abbia un concetto troppo ristretto che cosa significhi una "responsabilità" nel singolo principio di responsabilità. Certamente non significa che ogni classe possa avere un solo metodo che contiene la logica.

Se la classe Employee contiene tutti i dati necessari a quei metodi, inseriscili entrambi nella classe. Sono davvero una singola responsabilità: tracciare giorni liberi pagati. Se la classe Employee diventa più complessa, potresti voler spostare quell'aspetto in una classe a se stante, ma quella che rappresenta una sub-entità di Employee, non una classe di servizio.

Come sidenote completamente non tecnico: trovo che il concetto di "PaidSicknessAllowance" sia davvero folle e rotto.

    
risposta data 26.06.2013 - 11:42
fonte
1

Devi aver letto su Pattern di strategia . Implementando questo modello, ti stai trasferendo la responsabilità di calcolare i giorni rimanenti di vacanza e malattia in un oggetto separato. Ma questo oggetto rimane ancora collegato al Dipendente. Ciò potrebbe anche consentire di implementare "Vacanza" come singola classe, ma avere proprietà "SickDays" e "Vacanze" di questa classe all'interno e dipendente. Riducendo così il codice e la duplicazione del comportamento.

    
risposta data 26.06.2013 - 12:33
fonte
1

Se la suddivisione di una responsabilità ti sembra assurda, è probabile che tu stia già seguendo l'SRP abbastanza bene, anche se non l'hai applicato consapevolmente.

Per me è molto difficile determinare se sto seguendo l'SRP semplicemente chiedendo quale sia la responsabilità, per la ragione che hai già scoperto: puoi definire una responsabilità essere molto grande o molto piccola, quando l'ideale è da qualche parte nel mezzo.

Il modo più semplice per individuare una violazione SRP è notare che si tende a raggruppare i metodi all'interno di una classe. Alcune persone inseriscono anche commenti di sezione, come questo:

/******** Time off calculations ********/

Se ti sorprendi a farlo, o se sei tentato di farlo, è un segno che probabilmente stai violando il principio di responsabilità individuale.

    
risposta data 26.06.2013 - 16:31
fonte