… und damit meine ich nicht etwa Hunde, die auf Gegenständen herumkauen …


 


Was ist so gefährlich an Settern?

Ein “klassischer” Setter, also eine öffentliche Methode, die direkt ein bestimmtes Attribut eines Objekts auf einen von außen übergebenen Wert setzt, untergräbt die Integrität dieses Objekts. Das Objekt kann nicht mehr selbständig dafür Sorge tragen, welches seiner Attribute welchen Inhalt hat. Damit können insbesondere implizite Invarianten des Objekts zerstört werden, so dass das Objekt inkonsistent wird.

Oft werden Setter auch verwendet, um Objekte zu initialisieren. Dies birgt gleich mehrere Gefahren:

  • Es ist nicht klar, welche Setter aufgerufen werden müssen, und leicht wird ein Aufruf vergessen.
  • Es ist nicht klar, ob es eine bestimmte Reihenfolge gibt, in der mehrere Setter nacheinander aufgerufen werden sollen / dürfen / müssen.
  • Die Setter können im weiteren Verlauf des Programms erneut aufgerufen werden und so die Daten des Objekts durcheinanderbringen.

Ein Leben ohne Setter

Setter werden dafür verwendet, um ein Objekt zu initialisieren und/oder seinen Zustand später von außen zu verändern. Allerdings gibt es bessere Alternativen, um diese Ziele zu erreichen:

Initialisierung zur rechten Zeit

Die Initialisierung eines Objekts sollte in einem Schritt mit seiner Erzeugung erfolgen, um sicherzustellen, dass jedes Objekt von Anfang an voll funktionsfähig ist. Werden Setter verwendet, ist die Erzeugung des Objekts bereits abgeschlossen, die “Initialisierung” kommt also zu spät.

Statt Setter zur Initialisierung zu verwenden, kann ein Konstruktor angeboten werden, der die erforderlichen Argumente zur Initialisierung des Objekts erwartet. So kann das Objekt sich selbst initialisieren und damit seine Konsistenz wahren.

Falls es mehrere mögliche Argumentsätze zur Initialisierung gibt, können auch mehrere Konstruktoren implementiert werden. Diese sollten sich intern nach Möglichkeit gegenseitig aufrufen.

Beispiel: Ein Vektor in einem zweidimensionalen Koordinatensystem.

public class Vektor {

  private final double x;
  private final double y;

  public Vektor( double x, double y ){
    this.x = x;
    this.y = y;
  }

  public Vektor( int x, int y ){
    this( (double) x, y );
  }

  public Vektor( Point2D p ){
    this( p.getX(), p.getY() );
  }
}

Zustandsänderung konsistent gemacht

Werden Setter verwendet, um den Zustand eines Objekts zu verändern, können leicht Inkonsistenzen entstehen. Hier kann dem Objekt stattdessen eine geeignete Nachricht geschickt werden, die mit genügend Argumenten versehen ist, so dass das Objekt seinen Zustand in der gewünschten Weise verändern kann, ohne dabei seine Konsistenz zu verlieren.

Beispiel: Skalarmultiplikation eines Vektors

public void multipliziereMit( double skalar ){
  x = x * skalar;
  y = y * skalar;
}

Beide Attribute des Vektors werden durch einen Methodenaufruf verändert. Es gibt keinen inkonsistenten Zustand.

(Anmerkung: Dieser Code funktioniert im Zusammenhang mit dem ersten Beispiel natürlich nur dann, wenn die beiden Attribute nicht als final deklariert werden.)

Unveränderliche Objekte

Alternativ kann ein Objekt auch als Immutable (= unveränderlich) bzw. “Value Object” implementiert werden. In diesem Fall erzeugt jede Zustandsänderung ein neues Objekt mit der geänderten Information. Diese Vorgehensweise empfiehlt sich insbesondere für Objekte, die einerseits Teil des inneren Zustandes eines anderen Objekts sind, die andererseits aber von diesem Objekt über seine Schnittstelle herausgegeben werden. Hier darf es nicht möglich sein, von außen den inneren Zustand des herausgegebenen Objekts zu manipulieren, weswegen die Unveränderbarkeit eines solchen Objekts sehr wichtig ist.

Beispiel: Skalarmultiplikation eines Vektors liefert einen neuen Vektor:

public Vektor multipliziereMit( double skalar ){
  return new Vektor( x * skalar, y * skalar );
}

Der neue Vektor ist ein modifiziertes Abbild des ursprünglichen Vektors, natürlich voll initialisiert und mit konsistenten Werten befüllt.

Mehr Respekt gegenüber Objekten!

Wenn wir Objekten mit etwas Respekt begegnen und ihr Bedürfnis nach Autonomie berücksichtigen, werden sie es uns hoffentlich durch Robustheit und Fehlertoleranz danken.


Kommentare

04.03.2012 von Johannes

Hey Nicole,

danke für deinen Artikel - Ich bin ein großer Fan von immutability und einer Art wohldefinierten “contract” unter dem Objekte garantiert funktionieren und freue mich über jeden Input in diese Richtung.

Wie du schon erwähnst ist es oftmals sicherer, zum Setzen aller Werte die Konstruktoren zu verwenden, um die eben diese Contracts und auch die Aufrechterhaltung der Klasseninvariante zu gewährleisten.

Ein paar Dinge möchte ich noch anmerken: Setter an- und für sich sind nichts inhärent destruktives, wie der Artikeltitel suggeriert. Sie sind ein Workaround um das Uniform Access Principle (http://en.wikipedia.org/wiki/Uniform_access_principle) in Sprachen, die keine Properties oder andere Sprachfeatures zur Unterstützung des UAP bieten.

Grundsätzlich denke ich getter und setter sollten für optionale Attribute verwendet werden. Weil das manchmal gar nicht so einfach ist, und lange Argumentlisten in Sprachen wie Java absolut unleserlich sind, greife ich dann gerne auch gerne mal zum Builder Pattern, um die Regeln unter denen ein Funktionierendes Objekt produziert wird explizit zu machen.

Eine Kleinigkeit noch: das Beispiel, zur Skalarmultiplikation eines Vektors ist nich thread safe - teilen sich mehrere Klassen den gleichen Vektor kann es doch vorkommen dass ein inkonsistenter interner Zustand ausgelesen wird - eine immutable copy wie du direkt danach vorschlägst hat diesen Fehler nicht - das würde ich unbedingt noch erwähnen.


23.01.2013 von TutNichtsZurSache

Offenbar haben Sie den Sinn von Setter-Methoden nicht begriffen. Der Grund, wieso man eine Methode verwendet, statt das betreffende Feld einfach public zu machen, ist ja gerade, dass eine Methode sicherstellen kann, dass Invarianten erhalten werden.

Zudem wirkt sich eine Initialisierung mittels eines Konstruktors nachteilig auf die Lesbarkeit aus, da für den Leser nicht klar ist, welches Argument im Konstruktor welchem Feld entspricht:

new Person("Walter", "Gerhard")

Ist das nun Herr Gerhard oder Herr Walter? Mit Settern ist das eindeutig. Und in Sprachen, die anders als Java nicht in den 90ern stecken geblieben sind, z. B. C#, kann man das auch recht hübsch aufschreiben:

new Person { GivenName = "Walter", Surname = "Gerhard" }