web-dev-qa-db-fra.com

Des fonctions qui appellent simplement une autre fonction, un mauvais choix de conception?

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?

53
WDUK

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:

  • Que le bâtiment a un floor
  • Que le sol a bounds
  • Que les bornes ont une méthode 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.

110
Flater

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.

21
Michael Borgwardt

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:

  • Pour accéder à un membre d'un objet renvoyé par un membre, lorsque le membre n'est pas un détail d'implémentation qui doit être encapsulé.
  • Votre membre d'interface est correctement implémenté par .NET's quasi-implementation
  • Être conforme à Demeter

Voici des exemples où il est utile de créer une fonction de délégation:

  • Prise en compte d'une chaîne d'appels qui se répète encore et encore
  • Lorsque la langue vous oblige à, par exemple implémenter un membre d'interface en déléguant à un autre membre ou en appelant simplement une autre fonction
  • Lorsque la fonction que vous appelez n'est pas au même niveau conceptuel que les autres appels au même niveau (par exemple, un appel LoadAssembly au même niveau que l'introspection du plugin)
1
Laurent LA RIZZA

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.

1
gnasher729

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.

0
kiwicomb123

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:

  • L'encapsulation est rompue, les objets internes sont disponibles dans l'API publique. (par exemple, code comme car.Wheel.Move ()).
  • Le principe SRP est cassé, les objets font beaucoup de choses différentes (par exemple, préparer le texte d'un message électronique et l'envoyer réellement dans le même objet).
  • Il est difficile de tester une classe particulière (par exemple, il existe de nombreuses dépendances).
  • Il existe différents experts du domaine (ou services de l'entreprise) qui traitent les choses que vous gérez dans la même classe (par exemple, les ventes et la livraison de colis).

Problèmes potentiels lors de la violation de la loi de Déméter:

  • Tests unitaires durs.
  • Dépendance à la structure interne d'autres objets.
  • Couplage élevé entre les objets.
  • Exposer des données internes.
0
0lukasz0