Felder und Methoden entwerfen

Die Ausgabe von Design Techniques in diesem Monat ist die zweite in einer Miniserie von Spalten zum Entwerfen von Objekten. In der Kolumne des letzten Monats, in der das Entwerfen von Objekten für eine ordnungsgemäße Initialisierung behandelt wurde, habe ich darüber gesprochen, wie Konstruktoren und Initialisierer entworfen werden. Diesen und nächsten Monat werde ich Entwurfsprinzipien für die tatsächlichen Felder und Methoden der Klasse diskutieren. Danach werde ich über Finalizer schreiben und zeigen, wie Objekte für eine ordnungsgemäße Bereinigung am Ende ihres Lebens entworfen werden.

Das Material für diesen Artikel (Vermeiden spezieller Datenwerte, Verwenden von Konstanten, Minimieren der Kopplung) und des nächsten Artikels (Maximieren der Kohäsion) ist vielen Lesern möglicherweise bekannt, da das Material auf allgemeinen Entwurfsprinzipien basiert, die von der Java-Programmiersprache völlig unabhängig sind . Da ich im Laufe der Jahre auf so viel Code gestoßen bin, dass diese Prinzipien nicht ausgenutzt werden, denke ich, dass sie es verdienen, von Zeit zu Zeit angepasst zu werden. Außerdem versuche ich in diesem Artikel zu zeigen, wie diese allgemeinen Prinzipien insbesondere für die Java-Sprache gelten.

Felder gestalten

Beim Entwerfen von Feldern gilt als Faustregel, dass die Verwendung einer Variablen zur Darstellung mehrerer Attribute einer Klasse vermieden wird. Sie können gegen diese Regel verstoßen, indem Sie innerhalb einer Variablen spezielle Werte angeben, von denen jeder seine eigene Bedeutung hat.

Wie hier verwendet, ist ein Attribut ein Unterscheidungsmerkmal eines Objekts oder einer Klasse. Zwei Attribute eines CoffeeCupObjekts können beispielsweise sein:

  • Die Menge an Kaffee, die die Tasse enthält
  • Ob die Tasse sauber oder schmutzig ist

Um bei dieser Regel einen genaueren Blick, stellen Sie eine Gestaltung CoffeeCupKlasse für das virtuelle Café im letzten Monat beschrieben Design - Techniken Spalte. Angenommen, Sie möchten modellieren, ob eine Kaffeetasse in Ihrem virtuellen Café gewaschen wurde und für den nächsten Kunden einsatzbereit ist. Mit diesen Informationen können Sie sicherstellen, dass Sie eine Kaffeetasse vor dem Waschen nicht wiederverwenden.

Wenn Sie sich nur darum kümmern, ob eine Tasse gewaschen wurde oder nicht, wenn sie leer ist, können Sie einen speziellen Wert des innerCoffeeFeldes verwenden, der normalerweise verwendet wird, um die Kaffeemenge in der Tasse zu verfolgen, um eine ungewaschene Tasse darzustellen . Wenn 473 Milliliter (16 Flüssigunzen) die maximale Kaffeemenge in Ihrer größten Tasse sind, beträgt der maximale Wert von innerCoffeenormalerweise 473. Sie können also einen innerCoffeeWert von beispielsweise 500 (einen speziellen Wert) verwenden, um einen leeren Wert anzuzeigen Tasse, die ungewaschen ist:

// Im Quellpaket in Dateifeldern / ex1 / CoffeeCup.java Klasse CoffeeCup {private int innerCoffee; public boolean isReadyForNextUse () {// Wenn die Kaffeetasse nicht gewaschen wird, // ist sie nicht bereit für die nächste Verwendung, wenn (innerCoffee == 500) {return false; } return true; } public void setCustomerDone () {innerCoffee = 500; // ...} public void wash () {innerCoffee = 0; // ...} // ...}

Dieser Code gibt CoffeeCupObjekten das gewünschte Verhalten. Das Problem bei diesem Ansatz ist, dass spezielle Werte nicht ohne weiteres verstanden werden und das Ändern von Code erschwert wird. Selbst wenn Sie spezielle Werte in einem Kommentar beschreiben, kann es länger dauern, bis andere Programmierer verstehen, was Ihr Code tut. Außerdem verstehen sie Ihren Code möglicherweise nie. Sie verwenden Ihre Klasse möglicherweise falsch oder ändern sie so, dass sie einen Fehler verursachen.

Wenn zum Beispiel später jemand eine 20-Unzen-Tasse zu den Angeboten des virtuellen Cafés hinzufügt, können bis zu 592 Milliliter (ml) Kaffee in einer Tasse aufbewahrt werden. Wenn ein Programmierer die neue Tassengröße hinzufügt, ohne zu bemerken, dass Sie 500 ml verwenden, um anzuzeigen, dass eine Tasse gewaschen werden muss, ist es wahrscheinlich, dass ein Fehler auftritt. Wenn ein Kunde in Ihrem virtuellen Café eine 20-Unzen-Tasse gekauft und dann einen großen Schluck von 92 ml genommen hat, hat er oder sie genau 500 ml in der Tasse übrig. Der Kunde wäre schockiert und unzufrieden, wenn die Tasse nach dem Trinken von nur 92 ml aus seiner Hand verschwand und zum Waschen in der Spüle erschien. Und selbst wenn der Programmierer, der die Änderung vornimmt, feststellt, dass Sie einen speziellen Wert verwenden, muss ein anderer spezieller Wert für das ungewaschene Attribut ausgewählt werden.

Ein besserer Ansatz für diese Situation besteht darin, ein separates Feld zum Modellieren des separaten Attributs zu haben:

// Im Quellpaket in Dateifeldern / ex2 / CoffeeCup.java Klasse CoffeeCup {private int innerCoffee; private boolesche BedürfnisseWaschen; public boolean isReadyForNextUse () {// Wenn die Kaffeetasse nicht gewaschen wird, // ist sie nicht bereit für die nächste Verwendung. return! needWashing; } public void setCustomerDone () {needWashing = true; // ...} public void wash () {needWashing = false; // ...} // ...}

Hier wird das innerCoffeeFeld nur verwendet, um die Kaffeemenge im Tassenattribut zu modellieren. Das Attribut "Tasse muss gewaschen werden" wird vom needsWashingFeld modelliert . Dieses Schema ist leichter zu verstehen als das vorherige Schema, das einen speziellen Wert von verwendet innerCoffeeund niemanden daran hindert, den Maximalwert für zu erweitern innerCoffee.

Konstanten verwenden

Eine andere Faustregel beim Erstellen von Feldern ist die Verwendung von Konstanten (statischen Endvariablen) für konstante Werte, die an Methoden übergeben, von diesen zurückgegeben oder in Methoden verwendet werden. Wenn eine Methode einen endlichen Satz von Konstantenwerten in einem ihrer Parameter erwartet, hilft das Definieren von Konstanten den Client-Programmierern, klarer zu machen, was in diesem Parameter übergeben werden muss. Wenn eine Methode einen endlichen Satz von Werten zurückgibt, wird durch das Deklarieren von Konstanten für Client-Programmierer klarer, was als Ausgabe zu erwarten ist. Zum Beispiel ist es einfacher, dies zu verstehen:

if (cup.getSize () == CoffeeCup.TALL) {} 

als es zu verstehen ist:

if (cup.getSize () == 1) {} 

Sie sollten auch Konstanten für den internen Gebrauch durch die Methoden einer Klasse definieren - auch wenn diese Konstanten nicht außerhalb der Klasse verwendet werden -, damit sie leichter zu verstehen und zu ändern sind. Die Verwendung von Konstanten macht Code flexibler. Wenn Sie feststellen, dass Sie einen Wert falsch berechnet haben und keine Konstante verwendet haben, müssen Sie Ihren Code durchgehen und jedes Vorkommen des fest codierten Werts ändern. Wenn Sie jedoch eine Konstante verwendet haben, müssen Sie sie nur dort ändern, wo sie als Konstante definiert ist.

Konstanten und der Java-Compiler

Eine nützliche Information über den Java-Compiler ist, dass er statische Endfelder (Konstanten) anders behandelt als andere Arten von Feldern. Verweise auf statische Endvariablen, die mit einer Konstante zur Kompilierungszeit initialisiert wurden, werden zur Kompilierungszeit in eine lokale Kopie des Konstantenwerts aufgelöst. Dies gilt für Konstanten aller primitiven Typen und des Typs java.lang.String.

Wenn Ihre Klasse auf eine andere Klasse verweist, z. B. eine Klasse, fügt java.lang.Mathder Java-Compiler normalerweise symbolische Verweise auf die Klasse Mathin die Klassendatei für Ihre Klasse ein. Wenn beispielsweise eine Methode Ihrer Klasse aufgerufen wird Math.sin(), enthält Ihre Klassendatei zwei symbolische Verweise auf Math:

  • Ein symbolischer Hinweis auf die Klasse Math
  • Ein symbolischer Verweis auf Mathdie sin()Methode

Um den in Ihrer Klasse enthaltenen Code auszuführen, auf den verwiesen wird Math.sin(), muss die JVM die Klasse laden Math, um die symbolischen Referenzen aufzulösen.

Wenn sich Ihr Code andererseits nur auf die in der Klasse PIdeklarierte statische endgültige Klassenvariable bezieht, Mathwürde der Java-Compiler keinen symbolischen Verweis Mathin die Klassendatei für Ihre Klasse einfügen . Stattdessen wird einfach eine Kopie des Literalwerts von Math.PIin die Klassendatei Ihrer Klasse eingefügt. Um den in Ihrer Klasse enthaltenen Code auszuführen, der die Math.PIKonstante verwendet, muss die JVM keine Klasse laden Math.

Das Ergebnis dieser Funktion des Java-Compilers ist, dass die JVM nicht härter arbeiten muss, um Konstanten zu verwenden, als um Literale zu verwenden. Das Bevorzugen von Konstanten gegenüber Literalen ist eine der wenigen Entwurfsrichtlinien, die die Programmflexibilität verbessern, ohne die Programmleistung zu beeinträchtigen.

Drei Arten von Methoden

Im Rest dieses Artikels werden Methodenentwurfstechniken erläutert, die sich mit den Daten befassen, die eine Methode verwendet oder ändert. In diesem Zusammenhang möchte ich drei grundlegende Arten von Methoden in Java-Programmen identifizieren und benennen: die Utility-Methode, die State-View-Methode und die State-Change-Methode .

Die Dienstprogrammmethode

A utility method is a class method that doesn't use or modify the state (class variables) of its class. This kind of method simply provides a useful service related to its class of object.

Some examples of utility methods from the Java API are:

  • (In class Integer) public static int toString(int i) -- returns a new String object representing the specified integer in radix 10
  • (In class Math) public static native double cos(double a) -- returns the trigonometric cosine of an angle

The state-view method

A state-view method is a class or instance method that returns some view of the internal state of the class or object, without changing that state. (This kind of method brazenly disregards the Heisenberg Uncertainty Principle -- see Resources if you need a refresher on this principle.) A state-view method may simply return the value of a class or instance variable, or it may return a value calculated from several class or instance variables.

Some examples of state-view methods from the Java API are:

  • (In class Object) public String toString() -- returns a string representation of the object
  • (In class Integer) public byte byteValue() -- returns the value of the Integer object as a byte
  • (In class String) public int indexOf(int ch) -- returns the index within the string of the first occurrence of the specified character

The state-change method

The state-change method is a method that may transform the state of the class in which the method is declared, or, if an instance method, the object upon which it is invoked. When a state-change method is invoked, it represents an "event" to a class or object. The code of the method "handles" the event, potentially changing the state of the class or object.

Some examples of state-change methods from the Java API are:

  • (In class StringBuffer) public StringBuffer append(int i) -- appends the string representation of the int argument to the StringBuffer
  • (In class Hashtable) public synchronized void clear() -- clears the Hashtable so that it contains no keys
  • (In class Vector) public final synchronized void addElement(Object obj) -- adds the specified component to the end of the Vector, increasing its size by one

Minimizing method coupling

Armed with these definitions of utility, state-view, and state-change methods, you are ready for the discussion of method coupling.

As you design methods, one of your goals should be to minimize coupling -- the degree of interdependence between a method and its environment (other methods, objects, and classes). The less coupling there is between a method and its environment, the more independent that method is, and the more flexible the design is.

Methods as data transformers

To understand coupling, it helps to think of methods purely as transformers of data. Methods accept data as input, perform operations on that data, and generate data as output. A method's degree of coupling is determined primarily by where it gets its input data and where it puts its output data.

Figure 1 shows a graphical depiction of the method as data transformer: A data flow diagram from structured (not object-oriented) design.

Input and output

A method in Java can get input data from many sources:

  • It can require that the caller specify its input data as parameters when it is invoked
  • It can grab data from any accessible class variables, such as the class's own class variables or any accessible class variables of another class
  • If it is an instance method, it can grab instance variables from the object upon which it was invoked

Likewise, a method can express its output in many places:

  • It can return a value, either a primitive type or an object reference
  • It can alter objects referred to by references passed in as parameters
  • It can alter any class variables of its own class or any accessible class variables of another class
  • If it is an instance method, it can alter any instance variables of the object upon which it was invoked
  • It can throw an exception

Note that parameters, return values, and thrown exceptions are not the only kinds of method inputs and outputs mentioned in the above lists. Instance and class variables also are treated as input and output. This may seem non-intuitive from an object-oriented perspective, because access to instance and class variables in Java is "automatic" (you don't have to pass anything explicitly to the method). When attempting to gauge a method's coupling, however, you must look at the kind and amount of data used and modified by the code, regardless of whether or not the code's access to that data was "automatic."

Minimally coupled utility methods

Die am wenigsten gekoppelte Methode, die in Java möglich ist, ist eine Dienstprogrammmethode, die:

  1. Nimmt nur Eingaben von seinen Parametern entgegen
  2. Drückt seine Ausgabe nur durch seine Parameter oder seinen Rückgabewert aus (oder durch Auslösen einer Ausnahme)
  3. Akzeptiert als Eingabe nur Daten, die von der Methode tatsächlich benötigt werden
  4. Gibt als Ausgabe nur Daten zurück, die tatsächlich von der Methode erzeugt wurden

Eine gute Gebrauchsmethode

Die convertOzToMl()unten gezeigte Methode akzeptiert beispielsweise eine intals einzige Eingabe und gibt eine intals einzige Ausgabe zurück: