J'ai une configuration d'une classe qui représente un bâtiment. Ce bâtiment a un plan d'étage, qui a des limites.
La façon dont je l'ai configuré est la suivante:
public struct Bounds {} // AABB bounding box stuff
//Floor contains bounds and mesh data to update textures etc
//internal since only building should have direct access to it no one else
internal class Floor {
private Bounds bounds; // private only floor has access to
}
//a building that has a floor (among other stats)
public class Building{ // the object that has a floor
Floor floor;
}
Ces objets ont leurs propres raisons d'exister car ils font des choses différentes. Cependant, il y a une situation, où je veux obtenir un point localement pour le bâtiment.
Dans cette situation, je fais essentiellement:
Building.GetLocalPoint(worldPoint);
Cela a alors:
public Vector3 GetLocalPoint(Vector3 worldPoint){
return floor.GetLocalPoint(worldPoint);
}
Ce qui conduit à cette fonction dans mon objet Floor
:
internal Vector3 GetLocalPoint(Vector3 worldPoint){
return bounds.GetLocalPoint(worldPoint);
}
Et puis, bien sûr, l'objet bounds fait les calculs nécessaires.
Comme vous pouvez le voir, ces fonctions sont assez redondantes car elles passent simplement à une autre fonction plus bas. Cela ne me semble pas intelligent - ça sent le mauvais code qui va me mordre dans les fesses quelque part en bas avec le désordre de code.
Sinon, j'écris mon code comme ci-dessous, mais je dois en exposer davantage au public, ce que je ne veux pas faire:
building.floor.bounds.GetLocalPoint(worldPoint);
Cela commence également à devenir un peu idiot lorsque vous accédez à de nombreux objets imbriqués et conduit à de grands trous de lapin pour obtenir votre fonction donnée et vous pouvez finir par oublier où elle se trouve - ce qui sent aussi la mauvaise conception du code.
Quelle est la bonne façon de concevoir tout cela?
N'oubliez jamais la loi de Déméter :
La loi de Demeter (LoD) ou principe de moindre connaissance est une directive de conception pour le développement de logiciels, en particulier de programmes orientés objet. Dans sa forme générale, le LoD est un cas spécifique de couplage lâche. La directive a été proposée par Ian Holland à la Northeastern University vers la fin de 1987, et peut être résumée de manière succincte de chacune des manières suivantes: [1]
- Chaque unité ne devrait avoir qu'une connaissance limitée des autres unités: seules les unités "étroitement" liées à l'unité actuelle.
- Chaque unité ne devrait parler qu'à ses amis; ne parlez pas à des étrangers.
- Ne parlez qu'à vos amis immédiats .
La notion fondamentale est qu'un objet donné doit assumer le moins possible la structure ou les propriétés de toute autre chose ( y compris ses sous-composants) , conformément à la principe de "dissimulation de l'information".
Il peut être considéré comme un corollaire au principe du moindre privilège, qui veut qu'un module ne possède que les informations et les ressources nécessaires à son objectif légitime.
building.floor.bounds.GetLocalPoint(worldPoint);
Ce code viole le LOD. Votre consommateur actuel doit en quelque sorte connaître:
floor
bounds
GetLocalPoint
Mais en réalité, votre consommateur ne devrait manipuler que le building
, pas quoi que ce soit à l'intérieur du bâtiment (il ne devrait pas gérer les sous-composants directement).
Si l'une de ces classes sous-jacentes change structurellement, vous devez soudainement également changer ce consommateur, même s'il peut être à plusieurs niveaux de la classe que vous effectivement changé.
Cela commence à empiéter sur la séparation des couches que vous avez, car un changement affecte plusieurs couches (plus que ses voisins directs).
public Vector3 GetLocalPoint(Vector3 worldPoint){
return floor.GetLocalPoint(worldPoint);
}
Supposons que vous introduisiez un deuxième type de bâtiment, un sans étage. Je ne peux pas penser à un exemple réel, mais j'essaie de montrer un cas d'utilisation généralisé, supposons donc que EtherealBuilding
est un tel cas.
Parce que vous avez le building.GetLocalPoint
méthode, vous êtes en mesure de modifier son fonctionnement sans que le consommateur de votre bâtiment en soit conscient, par exemple:
public class EtherealBuilding : Building {
public Vector3 GetLocalPoint(Vector3 worldPoint){
return universe.CenterPoint; // Just a random example
}
}
Ce qui rend cela plus difficile à comprendre, c'est qu'il n'y a pas de cas d'utilisation clair pour un bâtiment sans étage. Je ne connais pas votre domaine et je ne peux pas juger si/comment cela se produirait.
Mais les directives de développement sont des approches généralisées qui renoncent à des applications contextuelles spécifiques. Si nous changeons le contexte, l'exemple devient plus clair:
// Violating LOD
bool isAlive = player.heart.IsBeating();
// But what if the player is a robot?
public class HumanPlayer : Player {
public bool IsAlive() {
return this.heart.IsBeating();
}
}
public class RobotPlayer : Player {
public bool IsAlive() {
return this.IsSwitchedOn();
}
}
// This code works for both human and robot players, and thus wouldn't need to be changed when new (sub)types of players are developed.
bool isAlive = player.IsAlive();
Ce qui prouve pourquoi la méthode sur la classe Player
(ou l'une de ses classes dérivées) a un but, même si son implémentation actuelle est triviale .
Sidenote
Par exemple, j'ai contourné quelques discussions tangentielles, telles que la façon d'aborder l'héritage. Ce ne sont pas l'objet de la réponse.
Si vous avez parfois de telles méthodes ici et là, cela peut simplement être un effet secondaire (ou le prix à payer, si vous voulez) d'une conception cohérente.
Si vous en avez beaucoup alors je considérerais cela comme un signe que cette conception est elle-même problématique.
Dans votre exemple, peut-être qu'il ne devrait pas y en avoir un moyen "d'obtenir un point localement vers le bâtiment" depuis l'extérieur du bâtiment et à la place les méthodes du bâtiment devraient être à un niveau d'abstraction plus élevé et travailler avec de telles pointe uniquement en interne.
La fameuse "Loi de Déméter" est une loi qui dicte le type de code à écrire, mais elle n'explique rien d'utile. La réponse de Flater est bonne parce qu'elle donne des exemples, mais je n'appellerais pas cela "violation/respect de la loi de Demeter". Si la "loi de Demeter" est appliquée là où vous vous trouvez, veuillez contacter votre poste de police Demeter local, ils seront heureux de régler les problèmes avec vous.
N'oubliez pas que vous êtes toujours maître du code que vous écrivez et que, par conséquent, entre la création de "fonctions de délégation" et leur non-écriture, c'est une question de jugement. Il n'y a pas de ligne nette, donc aucune règle précise ne peut être définie. Au contraire, nous pouvons trouver des cas, comme l'a fait Flater, où la création de telles fonctions est tout à fait inutile et où la création de telles fonctions est utile. ( Spoiler: Dans le premier cas, le correctif consiste à aligner la fonction. Dans le second, le correctif consiste à créer la fonction)
Des exemples où il est inutile de définir une fonction de délégation comprennent quand la seule raison serait:
Voici des exemples où il est utile de créer une fonction de délégation:
LoadAssembly
au même niveau que l'introspection du plugin)Oubliez que vous connaissez la mise en œuvre de Building pour un moment. Quelqu'un d'autre l'a écrit. Peut-être un fournisseur qui ne vous donne que du code compilé. Ou un entrepreneur qui commence à l'écrire la semaine prochaine.
Tout ce que vous savez, c'est l'interface pour la construction et les appels que vous passez à cette interface. Ils ont tous l'air assez raisonnables, donc vous allez bien.
Maintenant, vous mettez un manteau différent et soudain, vous êtes le réalisateur de Building. Vous ne connaissez pas l'implémentation de Floor, vous connaissez juste l'interface. Vous utilisez l'interface Floor pour implémenter votre classe Building. Vous connaissez l'interface de Floor et les appels que vous passez à cette interface pour implémenter votre classe Building, et ils semblent tous assez raisonnables, donc tout va bien.
Dans l'ensemble, pas de problème. Tout va bien.
building.floor.bounds.GetLocalPoint (worldPoint);
est mauvais.
Les objets ne devraient traiter qu'avec leurs voisins immédiats car votre système sera TRÈS difficile à changer autrement.
Il est juste d'appeler des fonctions. Il existe de nombreux modèles de conception qui utilisent cette technique, par exemple l'adaptateur et la façade, mais aussi, dans une certaine mesure, des modèles tels que le décorateur, le proxy et bien d'autres.
Il s'agit de niveaux d'abstractions. Vous ne devez pas mélanger des concepts de différents niveaux d'abstractions. Pour ce faire, vous devez parfois appeler des objets intérieurs afin que votre client ne soit pas obligé de le faire lui-même.
Par exemple (l'exemple de voiture sera plus simple):
Vous avez des objets Pilote, Voiture et Roue. Dans le monde réel, pour conduire une voiture, avez-vous un conducteur qui fait quelque chose directement avec des roues ou interagit-il uniquement avec la voiture dans son ensemble?
Comment savoir que quelque chose ne va PAS:
Problèmes potentiels lors de la violation de la loi de Déméter: