Java-Tipp 107: Maximieren Sie die Wiederverwendbarkeit Ihres Codes

Diese Wiederverwendung ist ein Mythos und scheint unter Programmierern immer häufiger anzutreffen. Möglicherweise ist eine Wiederverwendung jedoch schwierig zu erreichen, da der traditionelle objektorientierte Programmieransatz zur Wiederverwendung Mängel aufweist. Dieser Tipp beschreibt drei Schritte, die einen anderen Ansatz zum Aktivieren der Wiederverwendung bilden.

Schritt 1: Verschieben Sie die Funktionalität aus Klasseninstanzmethoden

Die Klassenvererbung ist aufgrund ihrer mangelnden Genauigkeit ein suboptimaler Mechanismus für die Wiederverwendung von Code. Sie können nämlich keine einzelne Methode einer Klasse wiederverwenden, ohne die anderen Methoden dieser Klasse sowie ihre Datenelemente zu erben. Dieses Übergepäck erschwert unnötig den Code, der die Methode wiederverwenden möchte. Die Abhängigkeit einer ererbenden Klasse von ihrem übergeordneten Element führt zu zusätzlicher Komplexität: Änderungen an der übergeordneten Klasse können die Unterklasse beschädigen. Wenn Sie eine Klasse ändern, kann es schwierig sein, sich zu merken, welche Methoden überschrieben werden oder nicht. und es kann unklar sein, ob eine überschriebene Methode die entsprechende übergeordnete Methode aufrufen soll oder nicht.

Jede Methode, die eine einzelne konzeptionelle Aufgabe ausführt, sollte als erstklassiger Kandidat für die Wiederverwendung für sich allein stehen können. Um dies zu erreichen, müssen wir zur prozeduralen Programmierung zurückkehren, indem wir Code aus Klasseninstanzmethoden in global sichtbare Prozeduren verschieben. Um die Wiederverwendung solcher Prozeduren zu fördern, sollten Sie sie wie statische Dienstprogrammmethoden codieren: Jede Prozedur sollte nur ihre Eingabeparameter und / oder Aufrufe anderer global sichtbarer Prozeduren verwenden, um ihre Arbeit zu erledigen, und keine nichtlokalen Variablen verwenden. Diese Verringerung der externen Abhängigkeiten verringert die Komplexität der Verwendung des Verfahrens und erhöht dadurch die Motivation, es an anderer Stelle wiederzuverwenden. Selbstverständlich profitiert auch Code, der nicht zur Wiederverwendung bestimmt ist, von dieser Organisation, da seine Struktur immer sauberer wird.

In Java können Methoden außerhalb einer Klasse nicht alleine stehen. Stattdessen können Sie verwandte Prozeduren verwenden und sie für statische Methoden einer einzelnen Klasse öffentlich sichtbar machen. Als Beispiel könnten Sie eine Klasse nehmen, die ungefähr so ​​aussieht:

Klasse Polygon {. . public int getPerimeter () {...} public boolean isConvex () {...} public boolean enthältPoint (Punkt p) {...}. . }}

und ändern Sie es so, dass es ungefähr so ​​aussieht:

Klasse Polygon {. . public int getPerimeter () {return pPolygon.computePerimeter (this);} public boolean isConvex () {return pPolygon.isConvex (this);} public boolean enthältPoint (Punkt p) {return pPolygon.containsPoint (this, p);}. . }}

Hier pPolygonwäre das:

Klasse pPolygon {statisches öffentliches int computePerimeter (Polygonpolygon) {...} statisches öffentliches Boolesches isConvex (Polygonpolygon) {...} statisches öffentliches Boolesches enthältPoint (Polygonpolygon, Punkt p) {...}} 

Der Klassenname gibt an, pPolygondass die von der Klasse eingeschlossenen Prozeduren sich am meisten mit Objekten vom Typ befassen Polygon. Das pvor dem Namen zeigt an, dass der einzige Zweck der Klasse darin besteht, öffentlich sichtbare statische Prozeduren zu gruppieren. Während es in Java nicht Standard ist, einen Klassennamen mit einem Kleinbuchstaben beginnen zu lassen, führt eine Klasse wie pPolygondie die normale Klassenfunktion nicht aus. Das heißt, es stellt keine Klasse von Objekten dar; es ist eher nur eine organisatorische Einheit, die von der Sprache benötigt wird.

Der Gesamteffekt der im obigen Beispiel vorgenommenen Änderungen besteht darin, dass der Clientcode nicht mehr erben muss Polygon, um seine Funktionalität wiederzuverwenden. Diese Funktionalität ist jetzt in der pPolygonKlasse prozedurweise verfügbar . Client-Code verwendet nur die Funktionalität, die er benötigt, ohne sich mit der Funktionalität befassen zu müssen, die er nicht benötigt.

Das soll nicht bedeuten, dass Klassen in diesem neoprozeduralen Programmierstil keinen nützlichen Zweck erfüllen. Im Gegenteil, Klassen führen die notwendige Aufgabe aus, die Datenelemente der Objekte, die sie darstellen, zu gruppieren und zu kapseln. Darüber hinaus ist ihre Fähigkeit, durch Implementierung mehrerer Schnittstellen polymorph zu werden, der herausragende Aktivierer für die Wiederverwendung, wie im nächsten Schritt erläutert wird. Sie sollten jedoch die Wiederverwendung und den Polymorphismus durch Klassenvererbung in Ihrem Arsenal an Techniken auf einen ungünstigeren Status zurückführen, da es für die Erreichung der Wiederverwendung nicht optimal ist, die Funktionalität innerhalb der Instanzmethoden zu verschränken.

Eine leichte Variante dieser Technik wird in dem viel gelesenen Buch Design Patterns der Gang of Four kurz erwähnt . Ihr Strategiemuster befürwortet die Kapselung jedes Familienmitglieds verwandter Algorithmen hinter einer gemeinsamen Schnittstelle, damit der Clientcode diese Algorithmen austauschbar verwenden kann. Da ein Algorithmus normalerweise entweder als eine oder mehrere isolierte Prozeduren codiert ist, betont diese Kapselung die Wiederverwendung von Prozeduren, die eine einzelne Aufgabe (dh einen Algorithmus) ausführen, gegenüber der Wiederverwendung von Objekten, die Code und Daten enthalten und mehrere Aufgaben ausführen können. Dieser Schritt fördert die gleiche Grundidee.

Das Einkapseln eines Algorithmus hinter einer Schnittstelle impliziert jedoch das Codieren des Algorithmus als ein Objekt, das diese Schnittstelle implementiert. Das heißt, wir sind immer noch an eine Prozedur gebunden, die an die Daten und andere Methoden des einschließenden Objekts gekoppelt ist, was die Wiederverwendung erschwert. Es besteht auch die Frage, dass diese Objekte jedes Mal instanziiert werden müssen, wenn der Algorithmus verwendet werden muss, was die Programmleistung verlangsamen kann. Zum Glück bietet Design Patterns eine Lösung, die diese beiden Probleme angeht. Sie können das Fliegengewicht verwendenMuster beim Codieren von Strategieobjekten, sodass jeweils nur eine bekannte gemeinsam genutzte Instanz vorhanden ist (die das Leistungsproblem behebt) und jedes gemeinsam genutzte Objekt keinen Status zwischen den Zugriffen beibehält (sodass das Objekt keine Mitgliedsdaten hat, welche Adressen ein Großteil des Kopplungsproblems). Das resultierende Flyweight-Strategy-Muster ähnelt stark der Technik dieses Schritts, Funktionen in global verfügbaren, zustandslosen Prozeduren zu kapseln.

Schritt 2: Ändern Sie nichtprimitive Eingabeparametertypen in Schnittstellentypen

Die Nutzung des Polymorphismus durch Schnittstellenparametertypen anstelle von Klassenvererbung ist die wahre Grundlage für die Wiederverwendung in der objektorientierten Programmierung, wie von Allen Holub in "Erstellen von Benutzeroberflächen für objektorientierte Systeme, Teil 2" angegeben.

"... Sie werden wiederverwendet, indem Sie auf Schnittstellen anstatt auf Klassen programmieren. Wenn alle Argumente für eine Methode Verweise auf eine bekannte Schnittstelle sind, die von Klassen implementiert wurde, von denen Sie noch nie gehört haben, kann diese Methode auf Objekte angewendet werden, deren Klassen dies nicht getan haben Es existiert nicht einmal, als der Code geschrieben wurde. Technisch gesehen ist es die Methode, die wiederverwendbar ist, nicht die Objekte, die an die Methode übergeben werden. "

Wenn Sie die Holub-Anweisung auf die Ergebnisse von Schritt 1 anwenden, können Sie, sobald ein Funktionsblock als global sichtbare Prozedur für sich allein stehen kann, sein Wiederverwendungspotenzial weiter erhöhen, indem Sie jeden seiner Eingabeparameter vom Klassentyp in einen Schnittstellentyp ändern. Dann können Objekte jeder Klasse, die den Schnittstellentyp implementiert, verwendet werden, um den Parameter zu erfüllen, und nicht nur diejenigen der ursprünglichen Klasse. Somit kann die Prozedur mit einem möglicherweise größeren Satz von Objekttypen verwendet werden.

Angenommen, Sie haben eine global sichtbare statische Methode:

statischer öffentlicher Boolescher Wert enthält (Rectangle rect, int x, int y) {...} 

Diese Methode soll beantworten, ob das angegebene Rechteck die angegebene Position enthält. Hier würden Sie den Typ des rectParameters vom Klassentyp Rectanglein einen hier gezeigten Schnittstellentyp ändern :

statischer öffentlicher Boolescher Wert enthält (Rechteckiges Rechteck, int x, int y) {...} 

Rectangular könnte die folgende Schnittstelle sein:

öffentliche Schnittstelle Rectangular {Rectangle getBounds (); }}

Jetzt können Objekte einer Klasse, die als rechteckig beschrieben werden können (dh die RectangularSchnittstelle kann implementiert werden), als rectParameter für bereitgestellt werden pRectangular.contains(). Wir haben diese Methode wiederverwendbarer gemacht, indem wir die Beschränkungen für die Weitergabe gelockert haben.

Für das obige Beispiel fragen Sie sich jedoch möglicherweise, ob die Verwendung der RectangularSchnittstelle einen echten Vorteil hat, wenn ihre getBoundsMethode a zurückgibt Rectangle. Das heißt, wenn wir wissen, dass das Objekt, das wir übergeben möchten, auf Nachfrage ein solches Objekt erzeugen kann Rectangle, warum nicht einfach den Rectangleanstelle des Schnittstellentyps übergeben? Der wichtigste Grund, dies nicht zu tun, betrifft Sammlungen. Angenommen, Sie haben eine Methode:

statischer öffentlicher boolescher Wert areAnyOverlapping (Collection rects) {...} 

that is meant to answer whether any of the rectangular objects in the given collection are overlapping. Then, in the body of that method, as you iterate through each object in the collection, how do you access that object's rectangle if you can't cast the object to an interface type such as Rectangular? The only option would be to cast the object to its specific class type (which we know has a method that can provide the rectangle), meaning the method would have to know ahead of time on which class types it will operate, limiting its reuse to those types. That's just what that step tries to avoid in the first place!

Step 3: Choose less-coupling input parameter interface types

When performing Step 2, which interface type should be chosen to replace a given class type? The answer is whichever interface fully represents what the procedure needs from that parameter with the least amount of excess baggage. The smaller the interface the parameter object has to implement, the better the chances for any particular class to be able to implement that interface -- and therefore, the larger the number of classes whose objects can be used as that parameter. It is easy to see that if you have a method such as:

static public boolean areOverlapping(Window window1, Window window2) {...} 

which is meant to answer whether two (assumed to be rectangular) windows overlap, and if that method only requires from its two parameters their rectangular coordinates, then it would be better to reduce the types of the parameters to reflect that fact:

static public boolean areOverlapping(Rectangular rect1, Rectangular rect2) {...} 

The above code assumes that the objects of the previous Window type can also implement Rectangular. Now you can reuse the functionality contained in the first method for all rectangular objects.

You may experience times when the available interfaces that sufficiently specify what is needed from a parameter have too many unnecessary methods. In that case, you should define a new interface publicly in the global namespace for reuse by other methods that might face the same dilemma.

You may also find times when it is best to create a unique interface to specify what is needed from just one parameter to a single procedure. You would use that interface for that parameter only. That usually occurs in situations where you want to treat the parameter as if it's a function pointer in C. For example, if you have a procedure:

static public void sort(List list, SortComparison comp) {...} 

that sorts the given list by comparing all of its objects, using the provided comparison object comp, then all sort wants from comp is to call a single method on it that makes the comparison. SortComparison should therefore be an interface with just one method:

public interface SortComparison { boolean comesBefore(Object a, Object b); } 

The only purpose of that interface is to provide sort with a hook to the functionality it needs to do its job, so SortComparison should not be reused elsewhere.

Conclusion

Those three steps are meant to be performed on existing code that was written using more traditional object-oriented methodologies. Together, those steps combined with OO programming can constitute a new methodology that you can employ when writing future code, one that increases the reusability and cohesion of methods while reducing their coupling and complexity.

Obviously, you should not perform those steps on code that is inherently ill-suited for reuse. Such code is usually found in a program's presentation layer. The code that creates a program's user interface and the control code that ties input events to the procedures that do the actual work are both examples of functionality that change so much from program to program that their reuse becomes infeasible.

Jeff Mather arbeitet für eBlox.com in Tucson, Arizona, wo er Applets für Unternehmen aus der Werbematerial- und Biotechnologiebranche erstellt. In seiner Freizeit schreibt er auch Shareware-Spiele.