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).
la source
Je pense que dans ce type de situation, où vous souhaitez que les règles métier existent en dehors des objets cibles, le test du type est plus qu'acceptable.
Certes, dans de nombreuses situations, vous devez utiliser un modèle de stratégie et laisser les objets gérer les choses eux-mêmes, mais ce n'est pas toujours souhaitable.
Dans le monde réel, un greffier examinerait le type de véhicule et consulterait ses données fiscales sur cette base. Pourquoi votre logiciel ne devrait-il pas suivre la même règle de busienss?
la source
Vous avez cela à l'envers. Un domaine anémique est un domaine dans lequel la logique est en dehors des entités commerciales. Il y a de nombreuses raisons pour lesquelles utiliser des classes supplémentaires pour implémenter la logique est une mauvaise idée. et seulement un couple pour pourquoi vous devriez le faire.
D'où la raison pour laquelle cela s'appelle anémique: la classe n'a rien dedans ..
Ce que je ferais:
la source
Non. Si je comprends bien, toute votre application concerne la taxation. La question de savoir si un véhicule doit être taxé ne concerne donc plus l'administration publique, ni rien d'autre dans l'ensemble du programme. Il n'y a aucune raison de le mettre ailleurs que chez nous.
Ces deux extraits de code ne diffèrent que par la constante. Au lieu de remplacer la totalité de la fonction HasToPay (), remplacez une
int DaysBetweenPayments()
fonction. Appelez cela au lieu d'utiliser la constante. Si c'est tout ce sur quoi ils diffèrent, je laisserais tomber les cours.Je suggérerais également que Moto / Voiture devrait être une propriété de catégorie de véhicule plutôt que de sous-classes. Cela vous donne plus de flexibilité. Par exemple, il est facile de changer de catégorie de moto ou de voiture. Bien sûr, il est peu probable que les vélos soient transformés en voitures dans la vie réelle. Mais vous savez qu'un véhicule va être mal entré et doit être changé plus tard.
Pas vraiment. Un objet qui transmet délibérément ses données à un autre objet en tant que paramètre n'est pas un gros problème d'encapsulation. La véritable préoccupation concerne d'autres objets qui atteignent l'intérieur de cet objet pour extraire des données ou manipuler les données à l'intérieur de l'objet.
la source
Vous êtes peut-être sur la bonne voie. Le problème est, comme vous l'avez remarqué, qu'il n'y a pas de membre LastPaidTime dans TaxPayment . C'est exactement ce à quoi il appartient, même s'il n'a pas besoin d'être exposé publiquement du tout:
Chaque fois que vous recevez un paiement, vous créez une nouvelle instance de
ITaxPayment
selon les règles en vigueur, qui peuvent avoir des règles complètement différentes de la précédente.la source
Je souscris à la réponse de @sebastiangeiger selon laquelle le problème concerne réellement les propriétaires de véhicules plutôt que les véhicules. Je vais suggérer d'utiliser sa réponse, mais avec quelques changements. Je ferais de la
Ownership
classe une classe générique:Et utilisez héritage pour spécifier l'intervalle de paiement pour chaque type de véhicule. Je suggère également d'utiliser la
TimeSpan
classe fortement typée au lieu des entiers simples:Maintenant, votre code réel doit seulement traiter avec une classe -
Ownership<Vehicle>
.la source
Je n'aime pas vous en parler, mais vous avez un domaine anémique , c'est-à-dire la logique métier en dehors des objets. Si c'est ce que vous allez faire, c'est bien, mais vous devriez lire les raisons pour en éviter un.
Essayez de ne pas modéliser le domaine problématique, mais de modéliser la solution. C'est pourquoi vous vous retrouvez avec des hiérarchies d'héritage parallèles et avec plus de complexité que nécessaire.
Quelque chose comme ceci est tout ce dont vous avez besoin pour cet exemple:
Si vous voulez suivre SOLID, c’est génial, mais comme l’oncle Bob l’a dit lui-même, ce ne sont que des principes techniques . Elles ne sont pas censées être appliquées de manière stricte, mais constituent des lignes directrices à prendre en compte.
la source
isMotorBike
méthode n'est pas un bon choix de conception POO. C'est comme remplacer le polymorphisme par un code de type (bien qu'un code booléen).enum Vehicles
Je pense que vous avez raison, le délai de paiement n’est pas la propriété de la voiture / moto. Mais c'est un attribut de la classe de véhicule.
Bien que vous puissiez choisir d'avoir un if / select sur le type d'objet, cela n'est pas très évolutif. Si vous ajoutez plus tard un camion et un Segway et que vous poussez un vélo, un bus et un avion (cela peut sembler improbable, mais vous ne le savez jamais avec les services publics), la situation s'aggrave. Et si vous voulez changer les règles pour un camion, vous devez le trouver dans cette liste.
Alors, pourquoi ne pas en faire, littéralement, un attribut et utiliser la réflexion pour le retirer? Quelque chose comme ça:
Vous pouvez également utiliser une variable statique, si cela est plus confortable.
Les inconvénients sont
que ce sera un peu plus lent, et je suppose que vous devez traiter beaucoup de véhicules. Si cela pose un problème, je pourrais adopter une vision plus pragmatique et rester fidèle à la propriété abstraite ou à l’interface. (Bien que, je considérerais aussi la mise en cache par type.)
Vous devrez valider au démarrage que chaque sous-classe Vehicle a un attribut IPaymentRequiredCalculator (ou autorisez une valeur par défaut), car vous ne souhaitez pas que 1000 voitures soient traitées, mais uniquement en panne, Motorbike ne disposant pas de PaymentPeriod.
L'avantage est que si vous rencontrez plus tard un mois de calendrier ou un paiement annuel, vous pouvez simplement écrire une nouvelle classe d'attribut. C’est la définition même de OCP.
la source