Ist die Implementierung einer in einem Unterpaket definierten Schnittstelle ein Anti-Pattern?

9

Angenommen, ich habe Folgendes:

package me.my.pkg;
public interface Something {
    /* ... couple of methods go here ... */
}

und:

package me.my;
import me.my.pkg.Something;
public class SomeClass implements Something {
    /* ... implementation of Something goes here ... */
    /* ... some more method implementations go here too ... */
}

Das heißt, die Klasse, die eine Schnittstelle implementiert, befindet sich näher am Stamm der Pakethierarchie als die von ihr implementierte Schnittstelle, aber beide gehören zur selben Pakethierarchie.

Der Grund dafür in dem speziellen Fall, an den ich denke, ist, dass es ein zuvor vorhandenes Paket gibt, das die Funktionen gruppiert, zu denen die SomethingSchnittstelle logisch gehört, und die logische (wie sowohl bei "der erwarteten" als auch bei "der einen") wo es angesichts der aktuellen Architektur gehen muss ") Implementierungsklasse existiert zuvor und lebt eine Ebene" höher "von der logischen Platzierung der Schnittstelle. Die implementierende Klasse gehört logischerweise nirgendwo unter me.my.pkg.

In meinem speziellen Fall implementiert die betreffende Klasse mehrere Schnittstellen, aber das scheint hier keinen (oder zumindest keinen signifikanten) Unterschied zu machen.

Ich kann mich nicht entscheiden, ob dies ein akzeptables Muster ist oder nicht. Ist es oder nicht und warum?

ein CVn
quelle

Antworten:

6

Ja, es ist akzeptabel, solange die Klasse und die Schnittstelle definitiv in den richtigen Paketen sind.

Das Organisieren von Klassen in einer hierarchischen Gruppe von Paketen ähnelt dem Organisieren von Klassen in einer Vererbungshierarchie. Es funktioniert die meiste Zeit ziemlich gut, aber es gibt viele legitime Fälle, die einfach nicht passen.

Beachten Sie, dass Java nicht einmal eine Pakethierarchie hat - die hierarchische Natur erstreckt sich nicht weiter als bis zur Ordnerstruktur. Ich kann nicht für die Java-Sprachdesigner sprechen, aber ich bezweifle, dass sie diese Entscheidung versehentlich getroffen haben!

Ich finde es manchmal hilfreich, sich die 'Pakethierarchie' als Hilfe vorzustellen, um Programmierern zu helfen, das gesuchte Paket zu finden.

Vaughandroid
quelle
3

Obwohl dies formal legitim ist, kann dies auf einen Codegeruch hinweisen . Um herauszufinden, ob dies in Ihrem Fall der Fall ist, stellen Sie sich drei Fragen:

  1. Wirst du es brauchen?
  2. Warum haben Sie es so verpackt?
  3. Wie können Sie feststellen, ob Ihre Paket-API bequem zu verwenden ist?

Wirst du es brauchen?

Wenn Sie keinen Grund sehen, zusätzlichen Aufwand in die Wartbarkeit von Code zu investieren , sollten Sie ihn unverändert lassen. Dieser Ansatz basiert auf dem YAGNI-Prinzip

Programmierer sollten keine Funktionen hinzufügen, bis dies als notwendig erachtet wird ... "Implementieren Sie Dinge immer dann, wenn Sie sie tatsächlich benötigen, niemals, wenn Sie nur vorhersehen, dass Sie sie benötigen."

Die folgenden Überlegungen gehen davon aus, dass der Aufwand für die weitere Analyse gerechtfertigt ist, wenn Sie davon ausgehen, dass der Code langfristig beibehalten wird, oder wenn Sie daran interessiert sind, Fähigkeiten zum Schreiben von wartbarem Code zu üben und zu verbessern. Wenn dies nicht zutrifft, können Sie den Rest überspringen - weil Sie ihn nicht brauchen werden .

Warum haben Sie es so verpackt?

Das erste, was erwähnenswert ist, ist, dass keine der offiziellen Kodierungskonventionen und Tutorials eine Anleitung dazu gibt, was ein starkes Signal dafür ist, dass diese Art von Entscheidungen im Ermessen eines Programmierers liegen wird.

Wenn Sie sich jedoch das von Entwicklern von JDK implementierte Design ansehen , werden Sie feststellen, dass das "Durchsickern" von Unterpaket-APIs in übergeordnete APIs dort nicht praktiziert wird. Schauen Sie sich zum Beispiel das gleichzeitige util-Paket und seine Unterpakete an - Locks und Atomic .

  • Beachten Sie, dass die Verwendung der API für Unterpakete im Inneren als OK angesehen wird. Beispielsweise importiert die ConcurrentHashMap-Implementierung Sperren und verwendet ReentrantLock. Die Locks-API wird einfach nicht als öffentlich angezeigt.

Okay, Kern-API-Entwickler scheinen das zu vermeiden und herauszufinden, warum es so ist, fragen Sie sich, warum Sie es so verpackt haben? Der natürliche Grund dafür wäre der Wunsch , den Paketbenutzern eine Art Hierarchie, eine Art von Ebenen , mitzuteilen, so dass das Unterpaket den Konzepten niedrigerer Ebene / spezialisierter / engerer Verwendung entspricht.

Dies geschieht häufig, um die Verwendung und das Verständnis der API zu vereinfachen und API-Benutzern den Betrieb innerhalb einer bestimmten Ebene zu erleichtern, ohne sie mit Bedenken anderer Ebenen zu belästigen. Wenn dies der Fall ist, würde das Offenlegen einer API auf niedrigerer Ebene aus Unterpaketen Ihrer Absicht widersprechen.

  • Randnotiz: Wenn Sie nicht sagen können, warum Sie es so verpacken, sollten Sie zu Ihrem Design zurückkehren und es gründlich analysieren, bis Sie dies verstanden haben. Denken Sie daran, wenn es Ihnen selbst jetzt noch schwer zu erklären ist, wie schwierig wäre es dann für diejenigen, die Ihr Paket in Zukunft warten und verwenden werden?

Wie können Sie feststellen, ob Ihre Paket-API bequem zu verwenden ist?

Aus diesem Grund empfehle ich dringend , Tests zu schreiben, die die von Ihrem Paket bereitgestellte API ausführen. Es würde auch nicht schaden, jemanden zur Überprüfung zu bitten, aber ein erfahrener API-Prüfer wird Sie höchstwahrscheinlich trotzdem bitten, solche Tests durchzuführen. Es ist schwer zu übertreffen, ein echtes Anwendungsbeispiel zu sehen, wenn man die API überprüft.

  • Man kann sich die Klasse mit einer einzelnen Methode mit einem einzelnen Parameter und einem Traum ansehen, wow, wie einfach , aber wenn der Test zum Aufrufen die Erstellung von 10 anderen Objekten erfordert, die wie 20 Methoden mit insgesamt 50 Parametern aufrufen, bricht dies eine gefährliche Illusion und zeigt die wahre Komplexität von das Design.

Ein weiterer Vorteil von Tests besteht darin, dass diese die Bewertung verschiedener Ideen, die Sie zur Verbesserung Ihres Designs in Betracht ziehen, erheblich vereinfachen können.

Es kam mir manchmal vor, dass relativ geringfügige Implementierungsänderungen zu erheblichen Vereinfachungen bei Tests führten und die Anzahl der Importe, Objekte, Methodenaufrufe und Parameter reduzierten. Dies ist bereits vor dem Ausführen von Tests ein guter Hinweis darauf, wie es sich lohnt, weiter zu verfolgen.

Das Gegenteil ist mir auch passiert - das heißt, als große, ehrgeizige Änderungen in meinen Implementierungen, die die API vereinfachen sollen, durch entsprechende geringfügige, unbedeutende Auswirkungen auf Tests als falsch erwiesen wurden, was mir half, die fruchtlosen Ideen fallen zu lassen und den Code unverändert zu lassen (mit vielleicht nur einem Kommentar) hinzugefügt, um den Hintergrund des Designs zu verstehen und anderen zu helfen, den falschen Weg zu lernen).


Für Ihren konkreten Fall ist das erste, was Ihnen in den Sinn kommt, die Änderung der Vererbung in Komposition in Betracht zu ziehen - das heißt, anstatt SomeClassdirekt zu implementieren Something, ein Objekt einzuschließen, das diese Schnittstelle innerhalb der Klasse implementiert.

package me.my;
import me.my.pkg.Something;
public class SomeClass {
    private Something something = new Something() {
        /* ... implementation of Something goes here ... */
    }
    /* ... some more method implementations go here too ... */
}

Dieser Ansatz kann sich in Zukunft auszahlen und das Risiko verhindern, dass eine Unterklasse SomeClassversehentlich eine Methode überschreibt Something, sodass sie sich so verhält, wie Sie es nicht geplant haben.

Mücke
quelle