J'essaie de m'entraîner à suivre les principes SOLID.
Je suis un peu perplexe à propos de l'exemple suivant (qui est un exemple de remodelage/artificiel basé sur du code réel, que je ne peux pas poster ici):
public class Driver{
ICar car;
public Driver(ICar car){
this.car = car;
}
public void Drive(){
car.Drive(this);
}
public void ChangeCar(ICar car){
this.car = car;
}
}
public class ICar {
void Drive(Driver owner);
}
public class ExampleCar : ICar{
private int fuel;
public ExampleCar(int fuel){
this.fuel = fuel;
}
public void Drive(Driver owner){
if(fuel > 0){
fuel--;
Console.Writeline("driving exampleCar. Fuel: " + fuel);
return;
}
owner.ChangeCar(new AnotherCar(200));
}
}
public class Main{
ICar car = new ExampleCar(100);
Driver mcLaren = new Driver(car);
for(int i = 0; i < 200; i++){
mcLaren.Drive();
}
}
Donc, ce qui se passe essentiellement ici est "conduire ExampleCar, si le carburant ExampleCar est vide, passer à AnotherCar"
Une sorte de "pseudo machine à états finis" (je ne sais pas si c'est même proche d'une machine à états finis, mais c'est la meilleure interprétation que je puisse comprendre).
Donc, avec cela, j'essaie de réaliser un changement d'état basé sur une condition que seul l'état actuel est capable de vérifier (c'est-à-dire si le carburant est vide).
Il ne semble pas logique de déplacer la condition dans la classe de pilote et de la faire vérifier, car cela violerait le principe d'ouverture et de fermeture (en supposant que je veuille mettre une nouvelle ICar, qui a une condition complètement différente ou aucune condition du tout par exemple).
Cependant, cette implémentation actuelle semble violer le principe d'inversion de dépendance, car ICar dépend du pilote.
Je ne sais pas quoi faire ici, ni si cette mise en œuvre est correcte ou non.
J'apprécierais vos réflexions à ce sujet.
Il n'est pas approprié que le Car
décide de ce que le propriétaire doit faire quand il n'y a plus d'essence, et certainement pas acceptable pour la voiture de choisir quel autre type de voiture le conducteur doit conduire si elle manque d'essence.
L'interface ICar
devrait ressembler à ceci:
public class ICar {
// return false if the car is undriveable
boolean Drive();
}
Alors:
public class Driver{
ICar car;
public Driver(ICar car){
this.car = car;
}
// false if the car is undriveable
public boolean Drive(){
return car.Drive();
}
public void ChangeCar(ICar car){
this.car = car;
}
}
public class Main{
ICar car = new ExampleCar(100);
Driver mcLaren = new Driver(car);
for(int i = 0; i < 200; i++){
while (!mcLaren.Drive()) {
car = new AnotherCar(200);
mcLaren.changeCar(car);
}
}
}
Ce n'est que le principe de responsabilité unique au travail - Vous avez décidé au départ que c'était le travail de Main
de décider quelle voiture le conducteur conduisait, que c'était le travail de Driver
de conduire la voiture qu'on lui avait donnée, et que c'est le travail de ICar
à conduire le plus longtemps possible.
Pour faire son travail, Main
doit savoir que parfois les voitures deviennent irrécupérables.
Pour des exemples artificiels hors de tout contexte, il est souvent indécidable s'ils violent ou non SOLID, et il est impossible d'évaluer le code comme "bon" ou "mauvais" d'une manière sensée. Pour par exemple, le DIP dit
"Les modules de haut niveau ne devraient pas dépendre de modules de bas niveau. Les deux devraient dépendre d'abstractions (par exemple des interfaces)."
mais qu'est-ce que dans cet exemple "un module de haut niveau" et qu'est-ce qu'un "module de bas niveau"? C'est probablement une question de goût dans un tel exemple de voiture/conducteur. On peut voir les classes Car et Driver ensemble comme un seul "module", ou on peut voir chacune d'elles comme un module à part entière. Pour ce dernier cas, il est assez simple de résoudre le problème DIP mentionné: introduisez simplement une autre interface IDriver
et faites dépendre ICar
de cela au lieu de Driver
.
Mais est-ce "mieux"? L'ajout de l'interface rend le code un peu plus compliqué, et si cela ne sert à rien, c'est une violation du principe KISS .
Un autre détail de votre code est cet effet secondaire étrange dans ExampleCar.Drive
. Changer la référence à un objet voiture complètement nouveau à l'intérieur de Drive
ne me semble pas évident - "faire le plein" de l'objet voiture existant serait probablement plus évident, comme
public void Drive(Driver owner){
if(fuel > 0){
fuel--;
Console.Writeline("driving exampleCar. Fuel: " + fuel);
return;
}
fuel=200;
Console.Writeline("exampleCar refuled. Fuel: " + fuel);
}
Alors peut-être que cela pourrait être une violation du "principe du moindre étonnement" . Si c'est vraiment une violation ne peut être évaluée que dans le contexte d'une application réelle, avec des classes réelles et des cas d'utilisation réels. Peut-être qu'il y a des raisons pour lesquelles l'effet secondaire d'origine est nécessaire, et peut-être que dans le contexte "réel", l'effet ne semble pas tellement inattendu à un utilisateur de ces classes.
En bref, il y a plus de principes de programmation que juste SOLIDE, leur application nécessite souvent des compromis, et les exemples artificiels manquent généralement de contexte suffisant pour les évaluer comme une "bonne" ou une "mauvaise" conception.
La plupart de l'interaction entre une voiture et un conducteur ne devrait pas avoir lieu à l'aide de méthodes publiques non plus. Au lieu de cela, un conducteur souhaitant monter à bord d'une voiture devrait créer un objet privé conçu pour recevoir des notifications d'une voiture et le transmettre à une fonction qui "montera" à bord de la voiture. La voiture doit ensuite donner au conducteur une référence à un objet privé de "contrôle de voiture" que le conducteur peut ensuite utiliser pour le faire fonctionner.
Si des objets wrapper sont utilisés de cette manière, et ils sont expressément invalidés lorsqu'un conducteur quitte une voiture, cela garantira qu'une voiture ne peut pas recevoir par erreur des commandes d'un conducteur qui n'y est plus, et qu'un conducteur ne peut pas recevoir par erreur mises à jour de statut d'une voiture que quelqu'un d'autre conduit.
Je suis un peu perplexe à propos de l'exemple suivant:
Êtes-vous pas le créateur de l'exemple? Le code affiché, accompagné de votre question, indique que vous vous inquiétez des principes bien avant d'avoir résolu votre problème, de l'analyser et de créer une conception équitable pour le résoudre, peu importe sa taille, qu'il soit petit ou grand.
Vous dites:
Une sorte de "pseudo machine à états finis" (je ne sais pas si c'est même proche d'une machine à états finis, mais c'est la meilleure interprétation que je puisse comprendre).
Techniquement, chaque programme informatique est une machine à états finis. Oups, mal ... Permettez-moi de reformuler:
Techniquement, chaque programme informatique est une machine à états finis. Remarquez comment l'accent est mis sur la Parole "techniquement"? C'est un appareil intentionnel pour moi de mettre en évidence ce qui semble ne pas fonctionner avec votre point de vue.
Vous utilisez la programmation orientée objet, les classes, les interfaces, l'encapsulation et d'autres choses, vous vous inquiétez des principes de conception orientée objet et essayez de les suivre, mais vous ne pensez pas objets, vous pensez technicalities.
Alors, laissez-moi vous dire la vérité non conventionnelle: Techniquement, votre code est parfait! Il fait (probablement) ce que vous voulez, il est correct, fonctionnel et sans erreur, il peut même être performant! Autrement dit, techniquement.
Le problème est que la conception/programmation orientée objet n'est pas une méthodologie à suivre aveuglément pour résoudre chaque problème, mais plutôt une philosophie pour vous aider à analyser un problème que vous pouvez, alors, résoudre techniquement. Si votre problème provient du monde réel (notez qu'il pourrait tout aussi bien NE PAS provenir du monde réel), la conception orientée objet vous convient parfaitement. Vous pouvez penser aux objets naturels, aux relations entre eux, à la façon dont les limites naturelles existent qui ne peuvent pas être violées et à la façon dont chaque objet est responsable de lui-même. Les objets peuvent être composés d'autres objets dont ils dépendent.
Essayons d'appliquer une réflexion semblable à un objet à votre cas/problème (que je ne suis même pas sûr de ce que c'est, mais de toute façon), en empruntant aux concepts du monde réel correspondants que vous essayez de modéliser. Pendant que vous y êtes, oubliez SOLID pendant un certain temps.
C'est-à-dire que toute représentation d'une voiture ou d'un conducteur ne dépendra PAS d'un conducteur ou d'une voiture (en conséquence), ou, en d'autres termes, les implémentations de type conducteur ou enfant de celui-ci ne devraient pas avoir de voiture dans leur constructeur. Par conséquent, les implémentations de Car ou leurs spécialisations ne devraient pas avoir de pilote dans leur constructeur.
Lorsque deux classes se rencontrent, une référence d'une instance de l'une d'elles peut finir à l'intérieur le code d'une instance de l'autre. Quand ils doivent être ensemble, constamment, il est généralement temps pour l'injection de constructeur, donc ils peuvent être "mariés". Quand ils doivent être ensemble à l'occasion, pensez à injection de méthode, où ils se rencontrent, font ce qu'ils font ensemble, puis s'oublient. N'oubliez pas que nous modélisons l'acte de conduire ici, d'où la logique. Si une voiture devait être utilisée comme lieu de couchage, par exemple, notre analyse serait très différente .
Le point 2 signifie que nous nous attendons à une méthode quelque part dans une voiture ou un pilote (ou les deux), en prenant un paramètre de l'autre. En se basant sur le verbe et la transitivité dans l'analyse du point 2 (un conducteur conduit une voiture), il semble que ... eh bien, le conducteur conduit la voiture. Une voiture ayant une méthode nommée "Drive" prenant un paramètre Driver n'a pas de sens dans le monde réel, car une voiture ne conduit pas de Driver. En outre, un conducteur doit indiquer explicitement dans quelle mesure (ou combien de temps) il conduira la voiture.
public interface IDriver
{
void Drive(ICar car, double miles);
}
Pour mettre cela en perspective, une voiture dépense du carburant, mais seule la voiture elle-même sait comment. C'est un secret de la voiture. Les conducteurs pouvant contrôler le carburant signifient que le carburant restant ne doit pas être caché à l'intérieur de la voiture. Ici, les lignes commencent à s'estomper un peu. Le carburant doit-il être public ou privé? Cela dépend de vos cas d'utilisation. Si vous allez prendre en charge un grand nombre de choses en fonction des quantités exactes de carburant, tôt ou tard, vous devrez peut-être le rendre public. Oublions le cas pour le moment, vous ne vous souciez que si une voiture peut se déplacer ou non.
Le point 3 montre clairement qu'une voiture dépense du carburant à sa manière, donc un conducteur roule simplement et le carburant diminue. Parce que la programmation orientée objet vous offre suffisamment de flexibilité, vous pouvez demander à une voiture de se déplacer, puis laissez-la vous dire si elle a bougé et combien il a bougé. Si une voiture peut ne pas bouger pour diverses raisons, vous pouvez créer l'équivalent technique de certaines ... raisons!
public interface ICar
{
bool Move(double miles, out double actuallyTravelledMiles);
}
Pas en utilisant votre imagination, mais en pensant à des objets du monde réel, vous pouvez même faire:
public enum CarTripResult
{
Successful,
OutOfGas,
NoIgnition,
//...
}
public interface ICar
{
CarTripResult MakeTrip(double miles, out double travelledMiles);
}
Donc, avant de le savoir, sur la base du code ci-dessus, vous avez:
public class Car : ICar
{
private double fuelCapacity_in_gallons;
private double fuel_in_gallons;
private double consumptionInMPG;
public Car(double mpgConsumption, fuelCapacity)
{
consumptionInMPG = mpgConsumption;
fuelCapacity_in_gallons = fuelCapacity;
}
//Simplistic representation of the action of Refueling.
public void Refuel(double gallons)
{
fuel_in_gallons += gallons;
if (fuel_in_gallons > fuelCapacity_in_gallons)
{
fuel_in_gallons = fuelCapacity_in_gallons;
}
}
public CarTripResult MakeTrip(double miles, out double travelledMiles)
{
double neededFuel = miles / consumptionInMPG; //to get gallons.
if (fuel_in_gallons > neededFuel)
{
fuel_in_gallons -= neededFuel;
travelledMiles = miles;
return CarTripResult.Successful;
}
else
{
//Calculate how much you can travel in miles.
double distanceCapacity = fuel_in_gallons * consumptionInMPG;
travelledMiles = distanceCapacity;
return CarTripResult.OutOfGas;
}
}
}
public class Driver : IDriver
{
public void Drive(ICar car, double miles)
{
CarTripResult = car.MakeTrip(miles, out double actualMiles);
Console.WriteLine("Travelled a distance of " + actualMiles.ToString("0.00"));
//The code from this point on will depend on
//what it is you are trying to achieve.
if (CarTripResult != CarTripResult.Successful)
{
//Do something, depending on your actual problem/scenario.
}
else if (...)
{
//...etc
}
}
}
public class Application
{
public void Main()
{
//Remember, parameters are (mpg, capacity).
ICar cheapCar = new Car(20, 40);
ICar expensiveCar = new Car(10, 40);
cheapCar.Refuel(40);
expensiveCar.Refuel(40);
Driver driver = new Driver();
driver.Drive(cheapCar, 50);
driver.Drive(expensiveCar, 50);
}
}
Maintenant que la logique du domaine a été analysée et suivie de près, comment cette conception résultante semble-t-elle "SOLIDE"? Notez que nous n'avons même pas - une fois réfléchi aux principes SOLID lors de la définition de la logique. De plus, une chose à retenir ici est que toute cette conception est basée sur un spécifique besoin de résoudre un problème spécifique. Si votre point de vue sur le exactement le même domaine était différent, votre solution, la conception et le code correspondant pourraient finir beaucoup différents.
Pour (enfin) répondre à votre (vos) question (s) d'origine, vous faites pas traitez les principes SOLID) comme le guide principal d'une conception. Votre guide devrait être le domaine que vous essayez de modéliser, combiné avec les problèmes que vous essayez de résoudre. Si vous suivez le domaine de près et attentivement, les choses se mettront en place par elles-mêmes. Cela peut sembler une simplification excessive, mais dans tous les cas, il est facile pour les principes SOLID) de gêner vos tentatives de déplier votre modèle, en particulier au début, tandis que votre conception est incomplet. Ne vous concentrez pas sur les principes SOLID au détriment de faire une analyse "orientée objet", basée sur le domaine (enfin, le cas échéant ...) de votre problème. Enregistrez les objets, étudiez les relations, représentez-les, faites quelques prototypes. Le monde réel est souvent plutôt "SOLIDE", alors laissez cette entité "SOLIDE" se traduire dans votre domaine.
En bref, concentrez-vous sur votre conception et "invoquez" les principes après vous avez quelque chose. Les principes viennent après vous commencez à concevoir, à ne pas vous montrer si vous faites les choses correctement (rappelez-vous, "bien" est un technicité), mais combien de problèmes vous allez avoir à long terme.