L'héritage vs codage à une interface est quelque chose que je me suis demandé en ce qui concerne la conception appropriée de l'architecture, mais n'a réellement pas eu de problèmes lors de l'utilisation de l'héritage abstraite sur le codage vers une interface dans mes propres projets.
Selon SOLID Principes de OOP Conception (et généraliste OOP conseils), vous devez coder à une interface et ne jamais utiliser Héritage (résumé ou autrement) (et oui, je me rends compte que je parle dans le contexte des absolus parce que je trouve très peu d'arguments autrement). Bien que cela ait un sens total pour des situations telles que des cours telles que:
class A {}
class B : A {}
class C : B {}
class D : C {}
class E : D {}
...
Dans ce contexte, il existe un fort couplage entre les classes qui peuvent facilement se casser à n'importe quel niveau. Je suis d'accord à 100% que l'héritage est le problème clé. Cependant, je me demande souvent si ce qui suit serait une utilisation acceptable de l'héritage
public abstract class Vehicle
{
public enum Gear
{
Drive,
Reverse,
Park,
Low,
High,
Neutral
}
public string Color { get; set; }
// Other common properties here …
public virtual void Drive()
{
if (KeyInIgnition() && HasFuel() && SeatBeltsOn() && GearMode == Gear.Drive)
{
// Some logic to move car forward
}
// other logic ...
}
public virtual void Reverse()
{
// Logic here ...
}
// ...
}
public class Sedan : Vehicle
{
// Set base class properties here ...
public int UniqueSedanMethod() // Let’s say this method does not fit the standard to put something in an interface (i.e. it is completely unique to a sedan)
{
// Logic here ...
return 3;
}
// Gets base class logic because driving a sedan does not differ (in the basics) from the driving of an SUV
}
public class SUV : Vehicle
{
public int CalcAdditionalSeatsAdded()
{
// Logic here ...
return 5;
}
}
Dans ce contexte, il semble que cela semble avoir un sens d'avoir une classe de base logique pour regrouper les comportements communs de tous les véhicules partageraient. Le lecteur ne doit jamais être différent, quel que soit le type de voiture, cependant, si le besoin doit survenir, la méthode peut être complètement remplacée (et utiliser la mise en œuvre par défaut si nécessaire dans la substitution). Cela semble être la meilleure pratique car il est clair, propre, simple, et bien sûr DRY code. Cependant, cela rend la berline, le SUV, etc. étroitement couplé à la classe de base du véhicule.
De la vue opposée, si j'avais utilisé une interface, je devrais copier plusieurs lignes à chaque classe pour sa mise en œuvre malgré le fait qu'ils sont tous identiques. Ce serait une violation énorme de DRY et après avoir suivi ultérieurement une méthode dans toutes les classes si le comportement général change (c'est-à-dire plus de travail et d'erreur).
D'une autre vue opposée, si je mettez des méthodes telles que conduire, inverser, etc. dans une classe de type de service qui accepte une forme d'un véhicule que j'aurais encore 2 problèmes majeurs.
D'un autre point de vue du premier problème, je pourrais créer une classe de type de service externe pour chaque type de véhicule (c'est-à-dire Seranservice, Suvservice, etc.) avec les différentes méthodes, mais cela fait à nouveau que les services dépendent de leur type respectif qui est en fait quel principe d'inversion de dépendance vise à accomplir (au moins je pense). Le problème de ne pas avoir DRY Code se pose à nouveau parce que je n'échange que pour mettre les détails dans la classe de type de véhicule pour les mettre en classe de service. Cela ne résout pas le problème mais prend plutôt le titanique La stratégie commerciale de "mélanger les chaises longues et espérons que le résultat est meilleur".
Avec cet exemple et les points de vue susmentionnés sur l'utilisation du principe d'interface/d'inversion de dépendance, je ne comprends pas les avantages du codage à une interface ou autrement en utilisant l'héritage d'une classe de base abstrait commune qui contient la mise en œuvre par défaut qui doit seulement nécessiter Soyez surestimé dans de rares circonstances sans affecter la base ou d'autres dérivations. L'inconvénération principale est étroitement couplant de la base et du dérivé avec le potentiel d'introduction de chaînes de héritage fragiles si les dérivés sont toujours hérités de (c'est-à-dire pourquoi je les ferais scellé, mais pour le bien de L'exemple, je n'ai pas).
Avec tout cela, mes questions sont:
Suis-je mal comprendre le concept de "code à une interface et n'utiliser pas d'héritage" et/ou le principe d'inversion de dépendance? (Est-ce que je fais mal ces deux?) Si oui, quelle est la bonne interprétation et exemples/solution?
Sinon, existe-t-il un autre moyen d'atteindre l'inversion de dépendance/un couplage lâche que je ne pense pas à ne pas penser (ni au courant) de la compréhension?
Si je ne suis pas mal compris le "code à une interface", l'inversion de dépendance et ne pas savoir non au courant d'une meilleure solution, et n'utilisant pas de héritage correctement, pourquoi l'héritage serait-elle considérée comme étant considérée comme {insert négatif et/ou un adjectif de rafreat ici}? Y a-t-il un inconvénient que je ne considère pas?
Devrait DRY et la maintenabilité future soit sacrifié pour un couplage lâche pour la portabilité et l'évolutivité? Semble être un compromis qui est vraiment raide à faire sans calculs extrêmes et soigneusement considérés.
P.s.
Je crois comprendre que les interfaces sont généralement pour établir un contrat entre quelque chose et un ensemble de caractéristiques et de comportements établis qui culminent en eux étant un dispositif de type euphémisme pour indiquer qu'une classe "a a(n) { Insérer le nom de l'interface/nom ici} "Relation. Contrairement, héritage est qu'une classe" est a(n) {insérer nom de classe de base ici} "En ce qui concerne la classe de base et le dérivé La relation de classe. C'est comme ça que j'interprète les termes, cela pourrait donc être la source de ma confusion, mais je ne le pense pas vraiment.
Suis-je mal comprendre le concept de "code à une interface et n'utilise pas d'héritage"?
Oui, vous êtes mal compris que, parce que vous semblez confondre deux principes différents.
D'une part, il y a le principe de " code à une interface " et il est très regrettable que des langages comme C # et Java ont un mot-clé interface
, parce que ce n'est pas ce que le principe fait référence. Le principe de codage à une interface signifie que l'utilisateur d'une classe n'a besoin que de savoir quels membres publics existent et ne doivent avoir aucune connaissance de la manière dont les méthodes sont mises en œuvre. Le mot clé interface
peut Aidez ici, mais le principe s'applique de manière égale aux langues qui n'ont pas le mot clé ou même le concept dans leur conception linguistique.
D'autre part, il y a le principe de "la composition préférée sur l'héritage".
[.
Personnellement, je pense que l'héritage a sa place dans une boîte à outils d'une concepteur et que les personnes qui disent "jamais utiliser l'héritage" font trop de pendule dans l'autre sens. Mais vous devez également savoir que si votre seul outil est un marteau, tout commence à ressembler à un clou.
En ce qui concerne l'exemple du véhicule dans la question, si les utilisateurs de Vehicle
ne ont pas besoin de savoir si elles ont affaire à un Sedan
ou SUV
ni besoin de savoir comment les méthodes de Vehicle
fonctionne, vous suivez le principe de codage à une interface.
[.____] aussi, le niveau unique d'héritage utilisé ici est bon pour moi. Mais si vous commencez à penser à ajouter un autre niveau, vous devez sérieusement repenser votre conception et voir si l'héritage est toujours l'outil approprié à utiliser.
Pour ajouter aux autres bonnes réponses:
Nous ne devrions pas créer automatiquement une hiérarchie de classe, en particulier dans les cas où une seule classe fera. Beaucoup de choses sont de meilleurs attributs modélisés dans le cas général plutôt que comme des offres de spécialisations (sous-classes).
Évidemment, dans votre exemple, UniqueSedanMethod
et CalcAdditionalSeatsAdded
sont artificiellement. Cependant, en regardant cet exemple de contreveille, je pourrais affirmer que tout Vehicle
s devrait avoir une capacité de siège et une capacité de cargaison ceinturé, etc. que ce ne sont pas propres à Sedan
vs. SUV
, et que l'utilisation de la spécialisation et de la hiérarchie de la classe ici est prématurée. Un public arbitraire consommant cette hiérarchie de classe est tout aussi susceptible de trouver la spécialisation illustrée (bien que conçue) un obstacle.
Juste parce que le domaine a des "types" de véhicules, cela ne signifie pas automatiquement que le OOP "est-un" mécanisme (héritage) est la meilleure approche des objets de domaine modèle. Si le style du corps du véhicule Des informations sont nécessaires, pouvant également être fournies comme un attribut plutôt que de recourir à l'héritage pour capturer le style corporel.
Il y a des odeurs de code qui nous diront quand OOP Hiérarchie de classe peut être indiquée (par exemple, les clients utilisent des relevés de commutation sur certains attributs dans lesquels les différents cas s'engagent dans des comportements différents et spécialisés).
Dans un exemple séparé d'un domaine d'enseignement, nous voyons parfois la modélisation d'un enseignant en tant que sous-classe de personnes et étudiant en tant que sous-classe de personnes. Cependant, les enseignants et les étudiants sont des rôles éphémères plutôt que sont-une relation; La composition est indiquée sur l'héritage ici. Cet exemple de hiérarchie de classe a en fait des erreurs logiques, car un enseignant qui prend un cours devra assumer une identité différente - alors que sous la composition, la même personne peut être à la fois un enseignant et un étudiant.
Lorsque nous réduisons la hiérarchie de la classe, cela simplifie également la persistance, devrait faire partie de la demande.
Les éléments ci-dessus sont toutes des raisons de ne pas utiliser l'héritage comme premier choix dans la modélisation.
DO DRY et la maintenabilité future est-il sacrifié pour un couplage lâche pour la portabilité et l'évolutivité?
Lorsqu'il est utilisé de manière appropriée, l'héritage fournit un mécanisme de réutilisation de code (sec); Cependant, il est facilement abusé et peut causer le contraire exact (duplication du code). Par exemple, si le Sedan
introduit une méthode de capacité de siège, mais le SUV
a CalcAdditionalSeatsAdded()
, puis pour trouver la capacité de siège dans le cas général, les clients doivent déterminer le véhicule Tapez, puis appelez les méthodes appropriées et dans l'affaire SUV, effectuez un calcul supplémentaire (ajoutant des sièges supplémentaires). (Ceci lorsque le nombre de siège aurait pu être fourni comme une interface de l'affaire général.)
Le concept général ici est que nous devrions examiner les constructions primitives et les fonctionnalités de nos langages de programmation tels Outils pour la programmation, comme Outils pour la création des abstractions qui peut Portez le fossé entre la programmation/codage et modélisation de domaine - cela plutôt que de regarder les constructions primitives et les fonctionnalités de nos langages de programmation en tant que modélisation de domaine (outils).
En d'autres termes, la saveur particulière (d'héritage et de capacités d'objet de base dans une langue donnée) n'est pas un outil de modélisation de domaine; Il s'agit d'un outil de codage des abstractions qui modélisent ensuite le domaine.
Drive
ne devrait jamais être différent quel que soit le type de voiture que nous sommes, cependant, si le besoin devrait survenir, la méthode peut être complètement remplacée (et utiliser la mise en œuvre par défaut si nécessaire dans la substitution).
Je suis complètement en désaccord. Remplacer Drive
_ et supprimer tout le comportement de base seraient une violation claire du LSP.
Personnellement, je ne comprends personnellement pas votre raisonnement. Mais si je devais remplacer l'héritage avec la composition dans votre exemple, ce serait assez simple. Premièrement, je déplacerais tout le comportement commun Vehicle
dans des méthodes non virtuelles, laissant uniquement des méthodes abstraites à mettre en œuvre par des véhicules concrets. Comme je l'ai dit ci-dessus, je ne m'attendrais pas à ce que ce générique Vehicle
comportement change, ainsi de faire toutes ces méthodes non virtuelles n'est pas une grosse affaire. Les méthodes abstraites restantes définiraient ensuite une interface, que je m'attendais à ce que les véhicules concrets fournissent. Alors pourquoi ne pas diviser ces méthodes abstraites en interface distincte. Permet de l'appeler IVehicleStrategy
. Ensuite, les véhicules en béton cesseront d'être hérité de Vehicle
, mais mettra la mise en œuvre IVehicleStrategy
. Ils peuvent toujours avoir leurs propres méthodes et propriétés. Et pourquoi ai-je appelé cela une "stratégie"? Eh bien, parce que c'est un modèle de stratégie .
Je suis un défenseur de la " composition de l'héritage ". Vous essayez d'écrire du code maintenable droite? Dans mon expérience, en utilisant uniquement " interfaces " et " mises en œuvre " donne une conception extrêmement maintenable. Il y a aussi une très bonne raison de ne pas mélanger l'héritage: elle rend l'ensemble plus complexe. La complexité fait mal maintenabilité parce maintenabilité découle de la simplicité entre autres.
Dans votre exemple de véhicule spécifique, je pense qu'un bon plan d'action serait d'extraire les données du code. Qu'est-ce que cela implique exactement? Mettre en place une source de données qui contient toutes les données sur les véhicules et le système d'engrenage peut-être aussi bien. La seule mise en œuvre vous reste suppose simplement l'une des voitures concrètes en lisant les données. Non seulement avez-vous écrit le code moins de cette façon, il est devenu plus simple de quelques degrés. Deuxième réusinage votre conception afin qu'aucune mise en œuvre du véhicule requiert des méthodes spécifiques. " méthodes spécifiques " violent LSP (L de SOLID) et blessé maintenabilité globale à son tour.
Maintenant, il y a un léger inconvénient avec cette solution: il peut nuire à la performance. Bien qu'il ne faut pas d'importance si vous vous souciez de maintenabilité plus, comme moi.
Selon les principes SOLID de la conception OOP (et générale OOP conseils), vous devez coder à une interface et ne jamais utiliser l'héritage
"Code à une interface" = est en fait le principe "D" de solide, et il dit en fait que vos modules doivent dépendre des abstractions, pas des concrétions.
"N'utilisez jamais d'héritage" = non-sens, car le principe "L" de SOLID vous guide dans la manière d'utiliser correctement l'héritage.
Fondamentalement, l'exemple de voiture que vous avez donné n'est pas assez bon pour décrire quels scénarios nécessitent un héritage et quels scénarios nécessitent des implémentations d'interfaces.
Dans la vie réelle, vous pouvez indiquer que la berline "est un" véhicule. Dans votre logiciel, vous n'avez pas de berline réelle, vous avez une représentation de code. Et ensuite, vous devez déterminer si la représentation du code de votre berline "est une" représentation de code "de votre véhicule. Si c'est le cas, vous pouvez utiliser en toute sécurité l'héritage. Cela dépend de vos besoins de projet et n'est pas interdit.
Si vous souhaitez réutiliser le code et que vous n'avez pas de dépendance explicite "est une" dépendance entre deux classes, vous devez utiliser la composition, vous n'avez pas à reproduire aucune ligne de code.
Pour résumer, vous pouvez utiliser toutes ces approches, cela dépend de vos besoins et de la manière dont vous modélisez correctement vos objets. De plus, veuillez vérifier tous les principes SOLID _ et assurez-vous de bien comprendre: https://fr.wikipedia.org/wiki/solid_ (objet-orientation_design)
Il y a une chose qui m'embarrasse: votre classe abstraite a des propriétés pouvant être définies par une classe enfant. Pourquoi est-ce mauvais? Il y a un certain nombre de inconvénients , le plus important et le plus pratique est le problème problème de classe de base fragile . Mettez peu de temps, toute méthode surchargée peut faire des choses qui ne sont pas attendus par la classe parente. Par conséquent, l'un des plus fondamentaux =OOP = === est violé - - encapsulation . Donc, ma règle de pouce personnelle lorsque vous utilisez l'héritage, c'est que mes classes d'enfants ne peuvent pas mutair l'état des parents .
Alors, tes questions:
Suis-je mal comprendre le concept de "code à une interface et n'utiliser pas d'héritage" et/ou le principe d'inversion de dépendance?
et
Si je ne suis pas mal compris le "code à une interface", l'inversion de dépendance et ne pas savoir non au courant d'une meilleure solution, et n'utilisant pas de héritage correctement, pourquoi l'héritage serait-elle considérée comme étant considérée comme {insert négatif et/ou un adjectif de rafreat ici}? Y a-t-il un inconvénient que je ne considère pas?
Si votre domaine n'implique aucun autre type de véhicules, je pense que votre conception va bien (en gardant à l'esprit ce que j'ai écrit ci-dessus). À l'heure actuelle, votre comportement réside là où il devrait. L'introduction de classes de services rendrait votre conception pire. Ne hâtez pas de introduire des interfaces . Tout d'abord, en ne garantit pas automatiquement que votre décomposition de domaine est bon. Deuxièmement, rappelez-vous la règle de trois . Les gens sont généralement mauvais à prévoir l'avenir. Troisièmement, décomposer votre domaine First - votre logiciel commence beaucoup plus tôt que vous n'écrirez la première ligne de code.
Et sur toute la fin de la terminologie. À l'origine, l'héritage a été mis en œuvre comme mécanisme de réutilisation . Mais ce n'est que la mise en œuvre de l'héritage est la même que la mise en œuvre de sous-typing dans toutes les langues dont je suis au courant. Et néanmoins, ils sont presque toujours utilisés comme synonymes, les forces de motivation pour utiliser l'une ou l'autre différaient beaucoup.
Enfin, la mise en œuvre. Je me trouve souvent implémenter de nombreuses méthodes dans la classe abstraite de base. Après cela, je commence à me rendre compte que presque tous peuvent être déplacés vers de petites valeurs-objets. C'est ainsi que je voudrais implémenter votre code (désolé pour PHP :)):
abstract class Vehicle
{
public function drive()
{
if ($this->readyToDrive()) {
// absolutely the same for all kinds of vehicle
}
}
abstract protected function readyToDrive();
}
class Sedan extends Vehicle
{
private $gear;
private $ignition;
private $tank;
private $seatBelts;
public function __construct(IGear $gear, IIgnition $ignition, Itank $tank, ISeatBelts $seatBelts)
{
$this->gear = $gear;
$this->ignition = $ignition;
$this->tank = $tank;
$this->seatBelts = $seatBelts;
}
protected function readyToDrive()
{
return $this->gear->isDrive() && $this->ignition->hasKey() && $this->tank->isEnoughFuel() && $this->seatBelts->areOn();
}
}