Anche se questa potrebbe essere una domanda agnostica per il linguaggio di programmazione, mi interessano le risposte rivolte all'ecosistema .NET.
Questo è lo scenario: supponiamo di dover sviluppare una semplice applicazione di console per la pubblica amministrazione. L'applicazione riguarda la tassa sui veicoli. Hanno (solo) le seguenti regole aziendali:
1.a) Se il veicolo è un'auto e l'ultima volta che il suo proprietario ha pagato l'imposta era di 30 giorni fa, il proprietario deve pagare di nuovo.
1.b) Se il veicolo è una moto e l'ultima volta che il suo proprietario ha pagato la tassa era 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 testare tali regole e stampare quei veicoli (numero di targa e informazioni sul proprietario) che non li soddisfano.
Quello che voglio è:
2.a) Conforme al S.O.L.I.D. principi (in particolare principio Aperto / chiuso).
Quello che non voglio è (penso):
2.b) Un dominio anemico, quindi la logica di business 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 sulle biciclette, erediti semplicemente da Vehicle, quindi sostituisci il metodo HasToPay e il gioco è fatto. Principio aperto / chiuso soddisfatto attraverso l'ereditarietà semplice.
I "problemi" sono:
3.a) Perché un veicolo deve sapere se deve pagare? Non è una preoccupazione di PublicAdministration? Se le regole della pubblica amministrazione per il cambiamento delle tasse automobilistiche, perché l'auto deve cambiare? Penso che dovresti chiederti: "allora perché metti 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 per il veicolo, o forse il mio progetto iniziale di Persona-Veicolo-Auto-Moto-Pubblica Amministrazione è semplicemente sbagliato, o ho bisogno di un filosofo e di un analista di business migliore. Una soluzione potrebbe essere: spostare il metodo HasToPay in un'altra classe, possiamo chiamarlo TaxPayment. Quindi creiamo due classi derivate dal TaxPayment; CarTaxPayment e MotoTaxPayment. Quindi aggiungiamo una proprietà di pagamento astratta (di tipo TaxPayment) alla classe Vehicle e restituiamo l'istanza TaxPayment giusta per le classi Auto 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 ci sono membri LastPaidTime all'interno di CarTaxPayment / MotorbikeTaxPayment / TaxPayment. Sto iniziando a vedere CarTaxPayment e MotorbikeTaxPayment più come "implementazioni di algoritmi" piuttosto che come entità aziendali. In qualche modo ora quegli "algoritmi" hanno bisogno del valore LastPaidTime. Certo, possiamo passare il valore al costruttore di TaxPayment, ma ciò violerebbe l'incapsulamento del veicolo, o le responsabilità, o qualsiasi altra cosa chiamino gli evangelisti dell'OOP, vero?
3.c) Supponiamo di disporre già di un Entity Framework ObjectContext (che utilizza i nostri oggetti di dominio: Person, Vehicle, Car, Motorbike, PublicAdministration). Come faresti per ottenere un riferimento a ObjectContext dal metodo PublicAdministration.GetAllVehicles e quindi implementare la funzionalità?
3.d) Se abbiamo bisogno anche dello stesso riferimento a ObjectContext per CarTaxPayment.HasToPay ma diverso per MotorbikeTaxPayment.HasToPay, chi è responsabile di "iniettare" o come vorresti passare quei riferimenti alle classi di pagamento? In questo caso, evitando un dominio anemico (e quindi senza oggetti di servizio), non portarci al disastro?
Quali sono le tue scelte progettuali 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 al S.O.L.I.D. principi e prevenire domini anemici (dei quali sono stati commissionati per distruggere).