Wieso es so aufwendig ist, den Typ eines Querschnitts anzuzeigen


Thomas Wölfer
Thomas Wölfer

28. Februar 2006


Heute hatte ich hier einen Vorgang, an dem man sehr schön ablesen kann, wieso einige Dinge länger dauern, als man annehmen würde. Außerdem ist es auch eine Art Bilderbuch-Designprozess: Alle Schritte sind leicht nachzuvollziehen – wie ich hoffe, auch für Statiker. Es wird allerdings ein bisschen technisch, ganz ohne Technik geht’s eben nicht.

 

Zur Vorgeschichte: Auf der Dialogbox im Faltwerksprogramm, in der man die Eigenschaften von Stäben einstellen kann, wird auch der Querschnitt des Balkens angezeigt. Nachdem der Querschnitt einfach ein Objekt ist wird er so angezeigt wie auch alle anderen Objekte im Programm: Mit seinem Namen. Nun ist es aber so, das der Name des Querschnitts nicht besonders aussagekräftig ist, denn der ist meist numerisch. Der erste Querschnitt heißt also „1“, der zweite „2“, usw.

 

Nun kann man natürlich für alle Objekte völlig beliebige Namen vergeben – nur ist es bei Querschnitten eben so, dass es hilfreicher wäre, wenn man den Typ des Querschnitts wüsste. Das Ding sollte also immer mit seinem Namen und seinem Typ angegeben werden. Also etwa „1 – IPE 80“.

 

Soweit also die Anforderung. Klingt total einfach – doch das täuscht.

 

Alle Typen (Knoten, Balken, Querschnitte, Materialien…) im Programm haben eine gemeinsame Basisklasse (d.h.: Sie haben ein gewisses Maß an gemeinsamer Funktionalität) und ein gemeinsames Interface. (Für Nicht-Programmierer: Interfaces spezifizieren die Syntax, die man braucht, um ein Objekt zu benutzen. Es legt die Funktionen und Eigenschaften fest, die das Objekt hat.)

 

Dieses gemeinsame Interface ist „IDocObject“ und besagt unter anderem, das alle Objekte eine Eigenschaft namens „ObjectName“ haben, mit der man an den Namen kommt. Für die Anzeige der Objekte auf Dialogen (und an anderen Stellen) gibt es nun ein Objekt namens „DocOb jectNameConverter“. Dieses Objekt kann ein IDocObject in einen String konvertieren – und einen String zurück in ein Objekt. Im ersten Fall bekommt der Converter das Objekt übergeben, und liefert dessen ObjectName zurück. Im zweiten Fall bekommt der Converter einen String (den Namen) und einen Typ und sucht dann in den aktuellen Eingabedaten nach einem Objekt vom richtigen Typ mit dem richtigen Namen, und liefert dieses dann zurück.

 

Nun ist es nicht so einfach diese Basisfunktionalität zu verändern: Die wird überall im Programm verwendet – eine grundlegende Änderung würde massive Umbaumaßnahmen zu Folge haben, und so etwas möchte man lieber verhindern.

 

Daraus resultierte die erste Überlegung: Man definiert ein neues Interface das „detaillierte“ Namen liefern kann. Dieses Interface können Typen dann optional implementieren: Die meisten Objekte braucht man also nicht anzufassen, die Querschnitte implementieren das neue Interface – und schon hat man den „detaillierten Namen“.

 

Nach einiger Zeit kam dabei das folgende Interface bei raus:

 

public interface IProvideDetailedName

{

   string DetailedName { get; }

   string NameFromDetailedName( string detailedName);

}

 

Es gab verschiedene andere Versuche bis genau dieses Interface herauskam: Sowas passiert nicht „zufällig“. Was tut nun dieses Interface?

 

Zunächst einmal haben Typen die es implementieren, eine DetailedName Eigenschaft: Die Instanzen der Typen können also einen ausführlichen Namen liefern „1 – IPE 80“. Die zweite Methode ist aber auch wichtig, denn nur damit kann man von einem detaillierten Namen auf den „echten“ zurückkommen. Hier das Szenario:

 

Der erste Teil ist einfach: Dabei wird die Combo-Box auf einem Dialog mit den Namen aller Objekte eines bestimmten Typs ausgefüllt. Dazu werden alle passenden Objekte eingesammelt und durch den DocObjectNameConverter in strings konvertiert. Beim konvertieren schaut der Converter, ob das übergebe Objekt das IProvideDetailedName Interface hat. Ist das nicht der Fall macht er so weiter wie früher. Gibt es das Interface aber, dann liefert der Converter eben den „DetailedName“ zurück. Pronto: Objekte mit dem Interface werden mit „ausführlichem“ Namen angezeigt, Objekte ohne nur mit ihrem „ObjectName“.

 

Wird nun in der Combobox etwas ausgewählt, muss der ausgewählte Text aber nun in ein Objekt konvertiert werden. Für die Combobox ist die Auswahl aber eben nur ein string – von eventuellen „Details“ weiss sie nichts. Außerdem ist auch nicht bekannt, wie der Implementierer von „DetailedName“ diesen Namen zusammensetzt: Es kann ja zum Beispiel „1 – IPE 80“, „IPE 80 (1)“ oder auch „IPE 80 – 1“ geliefert werden.

 

Das gesuchte Objekt ist aber nur unter dem Namen „1“ zu finden: Es braucht also einen Weg, um aus einem string von dem nicht bekannt ist wie er formatiert wurde, den reinen Namen zu gewinnen. Genau dafür ist NameFromDetailedName() zuständig. Derjenige, der DetailedName implementiert muss eben auch diese Funktion implementieren – dann kommt man auch an den Namen.

 

Damit wäre das Design abgeschlossen gewesen – wenn es denn richtig wäre. Ist es aber nicht. Grund: Beim konvertieren aus einem String und einem Zieltyp in ein Objekt hat man als Information eben nur das: Den string (der Name) und der Typ des gewünschten Objektes. Nun ist es aber so, das die Implementierung von IProvideDetailedName zum Beispiel im Querschnitts-Objekt vorliegt. Das bedeutet aber, dass man, um „NameFromDetailedName()“ aufrufen zu können, eine Instanz dieses Typs braucht. Die hat man aber nicht – man hat nur den Typ. (Damit könnte man natürlich eine Instanz synthetisieren – das ist aber im gegebenen Kontext aus verschiedenen Gründen auf die ich hier nicht weiter eingehe unschön und musste vermieden werden.)

 

Lange Rede kurzer Sinn: Das so schön geplante Interface mit allen dazugehörigen Überlegungen kann man schlicht und ergreifend nicht einsetzen.

 

Was es braucht ist eine Lösung die mit dem Minimalset an Informationen auskommt: Im Fall „konvertieren nach string“ hat man eine Instanz eines Objektes (und damit auch dessen Typ). Im Fall „konvertieren aus string“ hat man im Wesentlichen nur den Typ. Die Lösung muss also eine Implementierung sein, die mit dem Typ alleine auskommt.

 

Also geht’s zum zweiten Ansatz: Man definiert ein neues Attribut, das man an alle Typen vergibt, die einen detaillierten Namen haben sollen: HasDetailedNameAttribute. Das Attribut wird mit einem Parameter erzeugt, der Parameter ist der Typ eines Objektes, das den detaillierten Namen generieren kann, und das auch über die Möglichkeit zur Umwandlung eines detaillierten Namens in einen einfachen verfügt.

 

Im Wesentlichen lagert man also die Erzeugung/Umwandlung des detaillierten Namens in einen externen Typ aus, und erzeugt dann eine Verbindung zwischen diesem externen Typ und dem eigentlichen Typ (Querschnitt) indem man dem Querschnitt ein entsprechendes Attribut gibt.

 

Der DocObjectNameConverter kann dann nachsehen, ob der Typ des zu konvertierenden Objektes (oder der Typ des Zieltyps) ein solches Attribut hat. Ist das der Fall, dann kann er die Instanz des Attributes verwenden, um den passenden externen Konverter zu ermitteln – und den dann die Arbeit tun lassen.

 

Und siehe da: Hier liegt nun tatsächlich ein Design vor, das funktioniert.

 

Es gäbe noch ein drittes, das mit anderen Mechanismen ebenfalls funktionieren würde: Man stellt die Verbindung zwischen dem Typ (1) und dem Typ für die detaillierten Namen (2) her, indem man (2) als Singleton anlegt, der sich im Zuge der Konstruktion bei einem SingeltonManager anmeldet – und sich als zuständig für Typ (1) ausweist. Danach kann man einfach den Singleton-Manager nach einem Umwandler für (1) fragen – wenn man keinen bekommt, dann gibt’s keinen und der „bisherige“ Weg der Namensbildung kann verwendet werden.

 

Was lernt man nun daraus?

 

  1. Ich bin unheimlich geschwätzig.
  2. Bevor man sich an ein Design setzt muss man sich überlegen ob sich die neue Funktionalität mit Typen oder mit Instanzen beschäftigt.
  3. Alles ist viel komplizierter als es beim konsumieren einer bestimmten Funktionalität aussieht.