it-swarm.com.de

Funktionen, die einfach eine andere Funktion aufrufen, schlechte Designauswahl?

Ich habe ein Setup einer Klasse, die ein Gebäude darstellt. Dieses Gebäude hat einen Grundriss, der Grenzen hat.

Die Art und Weise, wie ich es eingerichtet habe, ist wie folgt:

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;
}

Diese Objekte haben ihre eigenen Gründe zu existieren, da sie verschiedene Dinge tun. Es gibt jedoch eine Situation, in der ich einen Punkt vor Ort zum Gebäude bringen möchte.

In dieser Situation mache ich im Wesentlichen:

Building.GetLocalPoint(worldPoint);

Dies hat dann:

public Vector3 GetLocalPoint(Vector3 worldPoint){    
    return floor.GetLocalPoint(worldPoint);
}

Was zu dieser Funktion in meinem Floor Objekt führt:

internal Vector3 GetLocalPoint(Vector3 worldPoint){
    return bounds.GetLocalPoint(worldPoint);
}

Und dann erledigt das Bounds-Objekt natürlich die erforderliche Mathematik.

Wie Sie sehen können, sind diese Funktionen ziemlich redundant, da sie nur an eine andere Funktion weiter unten weitergegeben werden. Das fühlt sich für mich nicht klug an - es riecht nach schlechtem Code, der mich in den Hintern beißen wird.

Alternativ schreibe ich meinen Code wie unten, aber ich muss mehr der Öffentlichkeit zugänglich machen, was ich irgendwie nicht tun möchte:

building.floor.bounds.GetLocalPoint(worldPoint);

Dies wird auch ein bisschen albern, wenn Sie zu vielen verschachtelten Objekten gehen und zu großen Kaninchenlöchern führen, um Ihre gegebene Funktion zu erhalten, und Sie werden möglicherweise vergessen, wo sie sich befindet - was auch nach schlechtem Code-Design riecht.

Wie kann man das alles richtig gestalten?

53
WDUK

Vergiss niemals das Gesetz von Demeter :

Das Gesetz des Demeter (LoD) oder Prinzip des geringsten Wissens ist eine Konstruktionsrichtlinie für die Entwicklung von Software, insbesondere objektorientierten Programmen. In seiner allgemeinen Form ist der LoD ein spezieller Fall einer losen Kopplung. Die Richtlinie wurde von Ian Holland an der Northeastern University gegen Ende 1987 vorgeschlagen und kann auf jede der folgenden Arten kurz zusammengefasst werden: [1]

  • Jede Einheit sollte nur begrenzte Kenntnisse über andere Einheiten haben: nur Einheiten, die "eng" mit der aktuellen Einheit verwandt sind.
  • Jede Einheit sollte nur mit ihren Freunden sprechen. Sprich nicht mit Fremden.
  • Sprich nur mit deinen unmittelbaren Freunden .

Der Grundgedanke ist, dass ein gegebenes Objekt so wenig wie möglich von der Struktur oder den Eigenschaften von irgendetwas anderem ( einschließlich seiner Unterkomponenten) gemäß dem annehmen sollte Prinzip des "Versteckens von Informationen".
Es kann als eine Folge des Prinzips der geringsten Privilegien angesehen werden, das vorschreibt, dass ein Modul nur die Informationen und Ressourcen besitzt, die für seinen legitimen Zweck erforderlich sind.


building.floor.bounds.GetLocalPoint(worldPoint);

Dieser Code verletzt die LOD. Ihr aktueller Verbraucher muss irgendwie wissen:

  • Dass das Gebäude ein floor hat
  • Dass der Boden bounds hat
  • Dass die Grenzen eine GetLocalPoint Methode haben

In Wirklichkeit sollte Ihr Verbraucher jedoch nur mit building umgehen, nicht mit irgendetwas im Gebäude (er sollte nicht direkt mit den Unterkomponenten umgehen).

Wenn sich eine dieser zugrunde liegenden Klassen strukturell ändert, müssen Sie plötzlich auch diesen Verbraucher ändern, obwohl er möglicherweise mehrere Stufen von der Klasse entfernt ist, die Sie sind tatsächlich geändert.
Dies führt zu einer Verletzung der Trennung von Ebenen, da eine Änderung mehrere Ebenen betrifft (mehr als nur die direkten Nachbarn).

public Vector3 GetLocalPoint(Vector3 worldPoint){    
    return floor.GetLocalPoint(worldPoint);
}

Angenommen, Sie führen einen zweiten Gebäudetyp ein, einen ohne Boden. Ich kann mir kein reales Beispiel vorstellen, aber ich versuche, einen verallgemeinerten Anwendungsfall zu zeigen. Nehmen wir also an, dass EtherealBuilding ein solcher Fall ist.

Da Sie über die Methode building.GetLocalPoint Verfügen, können Sie die Funktionsweise ändern, ohne dass der Verbraucher Ihres Gebäudes davon Kenntnis hat, z.

public class EtherealBuilding : Building {
    public Vector3 GetLocalPoint(Vector3 worldPoint){    
        return universe.CenterPoint; // Just a random example
    }
}

Was dies schwieriger zu verstehen macht, ist, dass es keinen klaren Anwendungsfall für ein Gebäude ohne Boden gibt. Ich kenne Ihre Domain nicht und kann nicht beurteilen, ob/wie dies geschehen würde.

Entwicklungsrichtlinien sind jedoch verallgemeinerte Ansätze, die auf bestimmte kontextbezogene Anwendungen verzichten. Wenn wir den Kontext ändern, wird das Beispiel klarer:

// 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();

Dies beweist, warum die Methode für die Klasse Player (oder eine ihrer abgeleiteten Klassen) einen Zweck hat, auch wenn ihre aktuelle Implementierung trivial ist .


Nebenbemerkung
Zum Beispiel habe ich einige tangentiale Diskussionen umgangen, wie man sich der Vererbung nähert. Dies ist nicht der Schwerpunkt der Antwort.

110
Flater

Wenn Sie hier und da gelegentlich solche Methoden anwenden, kann dies nur ein Nebeneffekt (oder ein zu zahlender Preis, wenn Sie so wollen) eines einheitlichen Designs sein.

Wenn Sie viel von ihnen haben, dann würde ich es als Zeichen dafür betrachten, dass dieses Design selbst problematisch ist.

In Ihrem Beispiel ist vielleicht sollte es keine geben eine Möglichkeit, von außerhalb des Gebäudes einen Punkt lokal zum Gebäude zu bringen. Stattdessen sollten die Methoden des Gebäudes auf einer höheren Abstraktionsebene liegen und mit solchen arbeiten Punkte nur intern.

21

Das berühmte "Gesetz von Demeter" ist ein Gesetz, das vorschreibt, welche Art von Code geschrieben werden soll, aber nichts Nützliches erklärt. Flaters Antwort ist in Ordnung, weil sie Beispiele enthält, aber ich würde diese nicht als "Verletzung/Einhaltung des Gesetzes von Demeter" bezeichnen. Wenn das "Gesetz von Demeter" dort durchgesetzt wird, wo Sie sich befinden, wenden Sie sich bitte an Ihre örtliche Demeter-Polizeistation. Diese wird die Probleme gerne mit Ihnen klären.

Denken Sie daran, dass Sie den Code, den Sie schreiben, immer beherrschen. Daher ist es eine Frage Ihres eigenen Urteils, ob Sie "delegierende Funktionen" erstellen oder nicht schreiben. Es gibt keine scharfe Linie, daher kann keine scharfe Regel definiert werden. Im Gegenteil, wir können Fälle finden, wie es Flater getan hat, in denen das Erstellen solcher Funktionen einfach nutzlos ist und in denen das Erstellen solcher Funktionen nützlich ist. ( Spoiler: Im ersten Fall besteht der Fix darin, die Funktion zu inline. Im zweiten Fall besteht der Fix darin, die Funktion zu erstellen.)

Beispiele, bei denen es sinnlos ist, eine Delegierungsfunktion zu definieren, sind, wenn der einzige Grund wäre:

  • Zugriff auf ein Mitglied eines von einem Mitglied zurückgegebenen Objekts, wenn das Mitglied kein Implementierungsdetail ist, das gekapselt werden soll.
  • Ihr Schnittstellenmitglied wird von .NETs Quasi-Implementierung korrekt implementiert
  • Demeter-konform sein

Beispiele, bei denen es nützlich ist, eine Delegierungsfunktion zu erstellen, sind:

  • Ausklammern einer Anrufkette, die immer wieder wiederholt wird
  • Wenn die Sprache Sie dazu zwingt, z. Implementieren Sie ein Schnittstellenmitglied, indem Sie es an ein anderes Mitglied delegieren oder einfach eine andere Funktion aufrufen
  • Wenn sich die von Ihnen aufgerufene Funktion nicht auf derselben konzeptionellen Ebene befindet wie die anderen Aufrufe auf derselben Ebene (z. B. ein LoadAssembly -Aufruf auf derselben Ebene wie die Plugin-Introspektion).
1

Vergessen Sie für einen Moment, dass Sie die Implementierung von Building kennen. Jemand anderes hat es geschrieben. Vielleicht ein Lieferant, der Ihnen nur kompilierten Code gibt. Oder ein Auftragnehmer, der nächste Woche tatsächlich mit dem Schreiben beginnt.

Alles, was Sie wissen, ist die Schnittstelle zum Erstellen und die Aufrufe, die Sie an diese Schnittstelle tätigen. Sie sehen alle recht vernünftig aus, also geht es dir gut.

Jetzt ziehst du einen anderen Mantel an und bist plötzlich der Implementierer von Building. Sie kennen die Implementierung von Floor nicht, Sie kennen nur die Schnittstelle. Sie verwenden die Floor-Schnittstelle, um Ihre Gebäudeklasse zu implementieren. Sie kennen die Schnittstelle für Floor und die Aufrufe, die Sie an diese Schnittstelle senden, um Ihre Building-Klasse zu implementieren, und sie sehen alle recht vernünftig aus, sodass es Ihnen wieder gut geht.

Alles in allem kein Problem. Alles ist gut.

1
gnasher729

building.floor.bounds.GetLocalPoint (worldPoint);

ist schlecht.

Objekte sollten sich nur mit ihren unmittelbaren Nachbarn befassen, da Ihr System sonst SEHR schwer zu ändern ist.

0
kiwicomb123

Es ist in Ordnung, nur Funktionen aufzurufen. Es gibt viele Designmuster, die diese Technik verwenden, zum Beispiel Adapter und Fassade, aber auch Muster wie Dekorateur, Proxy und viele mehr.

Es geht um Abstraktionsebenen. Sie sollten keine Konzepte aus verschiedenen Abstraktionsebenen mischen. Dazu müssen Sie manchmal innere Objekte anrufen, damit Ihr Kunde nicht gezwungen ist, dies selbst zu tun.

Zum Beispiel (Auto Beispiel wird einfacher sein):

Sie haben Fahrer-, Auto- und Radobjekte. Haben Sie in der realen Welt, um ein Auto zu fahren, einen Fahrer, der etwas direkt mit Rädern macht, oder interagiert er nur mit dem Auto als Ganzes?

Woher weiß ich, dass etwas NICHT in Ordnung ist:

  • Die Kapselung ist unterbrochen, innere Objekte sind in der öffentlichen API verfügbar. (z. B. Code wie car.Wheel.Move ()).
  • Das SRP-Prinzip ist gebrochen, Objekte tun viele verschiedene Dinge (z. B. das Vorbereiten von E-Mail-Nachrichtentext und das tatsächliche Senden in demselben Objekt).
  • Es ist schwierig, eine bestimmte Klasse zu testen (z. B. gibt es viele Abhängigkeiten).
  • Es gibt verschiedene Domain-Experten (oder Unternehmensabteilungen), die Dinge erledigen, die Sie in derselben Klasse erledigen (z. B. Verkauf und Paketzustellung).

Mögliche Probleme bei Verstößen gegen das Demeter-Gesetz:

  • Harte Unit-Tests.
  • Abhängigkeit von der internen Struktur anderer Objekte.
  • Hohe Kopplung zwischen Objekten.
  • Interne Daten verfügbar machen.
0
0lukasz0