Bien que cela puisse être une question agnostique en langage de programmation, je suis intéressé par les réponses ciblant l'écosystème .NET.
Voici le scénario: supposons que nous devions développer une application console simple pour l’administration publique. L'application concerne la taxe sur les véhicules. Ils ont (uniquement) les règles commerciales suivantes:
1.a) Si le véhicule est une voiture et que le dernier paiement de la taxe par son propriétaire remonte à 30 jours, il doit alors payer à nouveau.
1.b) Si le véhicule est une moto et que le dernier paiement de la taxe par son propriétaire a eu lieu il y a 60 jours, il doit payer à nouveau.
En d'autres termes, si vous avez une voiture, vous devez payer tous les 30 jours ou si vous avez une moto, vous devez payer tous les 60 jours.
L'application doit tester ces règles pour chaque véhicule du système et imprimer les véhicules (numéro de plaque et informations sur le propriétaire) qui ne les satisfont pas.
Ce que je veux c'est:
2.a) Conforme aux principes SOLID (notamment principe ouvert / fermé).
Ce que je ne veux pas, c'est (je pense):
2.b) Un domaine anémique, la logique métier doit donc être intégrée aux entités métier.
J'ai commencé avec ça:
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: le principe d'ouverture / fermeture est garanti; s'ils veulent une taxe sur le vélo dont vous héritez simplement de Vehicle, vous remplacez la méthode HasToPay et vous avez terminé. Principe ouvert / fermé satisfait par un héritage simple.
Les "problèmes" sont:
3.a) Pourquoi un véhicule doit-il savoir s'il doit payer? N'est-ce pas un problème d'administration publique? Si l'administration publique décide de modifier la taxe sur les voitures, pourquoi Car doit-elle changer? Je pense que vous devriez vous demander: "alors pourquoi vous mettez une méthode HasToPay dans Vehicle?", Et la réponse est: parce que je ne veux pas tester le type de véhicule (typeof) dans PublicAdministration. Voyez-vous une meilleure alternative?
3.b) Après tout, le paiement des taxes est peut-être une affaire de véhicules, ou peut-être que mon concept initial de personne-véhicule-voiture-moto-administration publique est tout simplement faux, ou j'ai besoin d'un philosophe et d'un meilleur analyste commercial. Une solution pourrait être: déplacer la méthode HasToPay dans une autre classe, nous pouvons l'appeler TaxPayment. Ensuite, nous créons deux classes dérivées de TaxPayment; CarTaxPayment et MotorbikeTaxPayment. Ensuite, nous ajoutons une propriété de paiement abstraite (de type TaxPayment) à la classe Vehicle et nous renvoyons la bonne instance TaxPayment des classes Car et 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; }
}
}
Et nous invoquons l'ancien processus de cette façon:
public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
{
return this.GetAllVehicles().Where(vehicle => vehicle.Payment.HasToPay());
}
Mais maintenant, ce code ne sera pas compilé car il n'y a pas de membre LastPaidTime dans CarTaxPayment / MotorbikeTaxPayment / TaxPayment. Je commence à voir CarTaxPayment et MotorbikeTaxPayment plus comme des "implémentations d'algorithmes" plutôt que des entités commerciales. D'une manière ou d'une autre, ces "algorithmes" ont besoin de la valeur LastPaidTime. Bien sûr, nous pouvons transmettre la valeur au constructeur de TaxPayment, mais cela violerait l’encapsulation du véhicule, ou les responsabilités, ou peu importe ce que ces évangélistes de la POO appellent, non?
3.c) Supposons que nous ayons déjà un ObjectContext Entity Framework (qui utilise nos objets de domaine: Personne, Véhicule, Voiture, Moto, Administration publique). Comment feriez-vous pour obtenir une référence ObjectContext à partir de la méthode PublicAdministration.GetAllVehicles et donc implémenter la fonctionnalité?
3.d) Si nous avons également besoin de la même référence ObjectContext pour CarTaxPayment.HasToPay mais d'une valeur différente pour MotorbikeTaxPayment.HasToPay, qui est chargé de "l'injection" ou comment transmettriez-vous ces références aux classes de paiement? Dans ce cas, éviter un domaine anémique (et donc pas d'objets de service), ne nous mène pas au désastre?
Quels sont vos choix de conception pour ce scénario simple? Il est clair que les exemples que j'ai présentés sont "trop complexes" pour une tâche facile, mais en même temps, l'idée est d'adhérer aux principes SOLID et de prévenir les domaines anémiques (dont la destruction a été commandée).