Anche se questa potrebbe essere una domanda agnostica del linguaggio di programmazione, sono interessato a risposte rivolte all'ecosistema .NET.
Questo è lo scenario: supponiamo di dover sviluppare una semplice applicazione console per la pubblica amministrazione. L'applicazione riguarda l'imposta sui veicoli. Hanno (solo) le seguenti regole aziendali:
1.a) Se il veicolo è un'auto e l'ultima volta che il proprietario ha pagato l'imposta è stata 30 giorni fa, il proprietario deve pagare di nuovo.
1.b) Se il veicolo è una moto e l'ultima volta che il proprietario ha pagato l'imposta è stata 60 giorni fa, il proprietario deve pagare di nuovo.
In altre parole, se hai un'auto devi pagare ogni 30 giorni o se hai una moto devi pagare ogni 60 giorni.
Per ogni veicolo nel sistema, l'applicazione deve verificare tali regole e stampare quei veicoli (numero di targa e informazioni sul proprietario) che non li soddisfano.
Quello che voglio è:
2.a) Conforme ai principi SOLIDI (in particolare principio aperto / chiuso).
Quello che non voglio è (penso):
2.b) Un dominio anemico, quindi la logica aziendale dovrebbe andare all'interno delle entità aziendali.
Ho iniziato con questo:
public class Person
// You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.
{
public string Name { get; set; }
public string Surname { get; set; }
}
public abstract class Vehicle
{
public string PlateNumber { get; set; }
public Person Owner { get; set; }
public DateTime LastPaidTime { get; set; }
public abstract bool HasToPay();
}
public class Car : Vehicle
{
public override bool HasToPay()
{
return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
}
}
public class Motorbike : Vehicle
{
public override bool HasToPay()
{
return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
}
}
public class PublicAdministration
{
public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
{
return this.GetAllVehicles().Where(vehicle => vehicle.HasToPay());
}
private IEnumerable<Vehicle> GetAllVehicles()
{
throw new NotImplementedException();
}
}
class Program
{
static void Main(string[] args)
{
PublicAdministration administration = new PublicAdministration();
foreach (var vehicle in administration.GetVehiclesThatHaveToPay())
{
Console.WriteLine("Plate number: {0}\tOwner: {1}, {2}", vehicle.PlateNumber, vehicle.Owner.Surname, vehicle.Owner.Name);
}
}
}
2.a: il principio di apertura / chiusura è garantito; se vogliono la tassa sulla bicicletta che erediti dal Veicolo, allora sostituisci il metodo HasToPay e il gioco è fatto. Principio aperto / chiuso soddisfatto per semplice eredità.
I "problemi" sono:
3.a) Perché un veicolo deve sapere se deve pagare? Non è un problema di pubblica amministrazione? Se le regole della pubblica amministrazione per le tasse sulle auto cambiano, perché Car deve cambiare? Penso che dovresti chiedere: "allora perché hai inserito un metodo HasToPay all'interno di Vehicle?", E la risposta è: perché non voglio testare il tipo di veicolo (typeof) all'interno di PublicAdministration. Vedi un'alternativa migliore?
3.b) Forse il pagamento delle tasse è una preoccupazione del veicolo, o forse il mio progetto iniziale di Person-Vehicle-Car-Motorbike-PublicAdministration è semplicemente sbagliato, o ho bisogno di un filosofo e un migliore analista aziendale. Una soluzione potrebbe essere: spostare il metodo HasToPay in un'altra classe, possiamo chiamarlo TaxPayment. Quindi creiamo due classi derivate da TaxPayment; CarTaxPayment e MotorbikeTaxPayment. Quindi aggiungiamo una proprietà di pagamento astratta (di tipo TaxPayment) alla classe Vehicle e restituiamo l'istanza TaxPayment corretta dalle classi Car e Moto:
public abstract class TaxPayment
{
public abstract bool HasToPay();
}
public class CarTaxPayment : TaxPayment
{
public override bool HasToPay()
{
return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
}
}
public class MotorbikeTaxPayment : TaxPayment
{
public override bool HasToPay()
{
return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
}
}
public abstract class Vehicle
{
public string PlateNumber { get; set; }
public Person Owner { get; set; }
public DateTime LastPaidTime { get; set; }
public abstract TaxPayment Payment { get; }
}
public class Car : Vehicle
{
private CarTaxPayment payment = new CarTaxPayment();
public override TaxPayment Payment
{
get { return this.payment; }
}
}
public class Motorbike : Vehicle
{
private MotorbikeTaxPayment payment = new MotorbikeTaxPayment();
public override TaxPayment Payment
{
get { return this.payment; }
}
}
E invochiamo il vecchio processo in questo modo:
public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
{
return this.GetAllVehicles().Where(vehicle => vehicle.Payment.HasToPay());
}
Ma ora questo codice non verrà compilato perché non c'è alcun membro LastPaidTime all'interno di CarTaxPayment / MotorbikeTaxPayment / TaxPayment. Sto iniziando a vedere CarTaxPayment e MotorbikeTaxPayment più come "implementazioni di algoritmi" che come entità aziendali. In qualche modo ora quegli "algoritmi" hanno bisogno del valore LastPaidTime. Certo, possiamo trasmettere il valore al costruttore di TaxPayment, ma ciò violerebbe l'incapsulamento o le responsabilità del veicolo, o come lo chiamano quegli evangelisti della OOP, no?
3.c) Supponiamo di avere già un ObjectContext di Entity Framework (che utilizza i nostri oggetti di dominio; Persona, Veicolo, Auto, Moto, Amministrazione pubblica). Come faresti per ottenere un riferimento ObjectContext dal metodo PublicAdministration.GetAllVehicles e quindi implementare la funzionalità?
3.d) Se avessimo bisogno anche dello stesso riferimento ObjectContext per CarTaxPayment.HasToPay ma di uno diverso per MotorbikeTaxPayment.HasToPay, che si occupa di "iniettare" o come passeresti quei riferimenti alle classi di pagamento? In questo caso, evitando un dominio anemico (e quindi nessun oggetto di servizio), non ci conduce al disastro?
Quali sono le tue scelte di design per questo semplice scenario? Chiaramente gli esempi che ho mostrato sono "troppo complessi" per un compito facile, ma allo stesso tempo l'idea è di aderire ai principi SOLIDI e prevenire domini anemici (di cui è stato commissionato la distruzione).