Laden...

Designfrage: Vererbung oder Komposition in bestimmtem Szenario: Ausfüllen von Formularen

Erstellt von herbivore vor 10 Jahren Letzter Beitrag vor 10 Jahren 2.761 Views
herbivore Themenstarter:in
49.485 Beiträge seit 2005
vor 10 Jahren
Designfrage: Vererbung oder Komposition in bestimmtem Szenario: Ausfüllen von Formularen

Hallo Community,

ich möchte euch um Rat bei einer Entscheidung zwischen zwei Möglichkeiten bitten. Es geht um das Ausfüllen von Formularen. Ziel ist es für den nachgelagerten Verarbeitungsschritt eine List<String> zu erzeugen, die in jeder Zeile neben einer laufenden Nummer ein Schlüssel-Wert-Paar enthält. Hier ein Beispiel:

001 FORMNAME xyz  
002 NAME     Müller  
003 ID       4711  
004 END  

Dieses Format ist durch die weiterverarbeitenden (Serienbrief-)Software vorgegeben und kann nicht geändert werden. Außerdem gibt es folgende Rahmenbedingungen:
*Je nach Typ des konkreten (ausgefüllten) Formulars können unterschiedliche Einträge auftauchen. *Alle Formulare beginnen aber immer mit FORMNAME und enden mit END. *Es soll eine Klasse Formular geben, die eigentlichen Formulardaten enthält und sich darum kümmert, den Formularnamen davor zu setzen und END dahinter. *Für jedes konkrete Formular soll es eine neue Klasse geben, die das Formular ausfüllt. Nehmen wir als Beispiel eine Rechnung oder eine Mahnung, was in diesem Kontext bedeutet, dass die konkreten Klassen als Ergebnis ein ausgefülltes (Rechnungs-)Formular bzw. ein ausgefülltes (Mahnungs-)Formular produzieren würden.

Nun gibt es zwei Möglichkeiten:

Vererbung: Formular wäre die Oberklasse und Rechnung und Mahnung (bzw. genauer Rechnungsformular und Mahnungsformular) die Unterklassen. Das wäre der klassisch objektorientierte Ansatz.

Komposition: Formular könnte quasi ein (fast) reiner Datencontainer sein, der von den Logikklassen Rechnung und Mahnung (bzw. genauer Rechnungsformularlogik und Mahnungsformularlogik) gefüllt wird. Die Logikklassen würden sich als erstes ein Formular-Objekt erzeugen und dieses dann entsprechend der Logik füllen. Das würde dem alternativen Ansatz folgen, dass - wo es sinnvoll möglich ist - Komposition der Vererbung vorzuziehen ist.

In beiden Fällen würde man am Ende eine Methode oder eine Property von Formular aufrufen, um die gewünschte List<String> zu erhalten.

Meine Frage ist nun: Welche Erfahrungen habt ihr in solchen Situationen gemacht? Welche der Möglichkeiten wäre vorzuziehen und aus welchen Gründen? Welche späteren Änderungen sind bei welcher der Möglichkeiten einfacher oder schwieriger zu realisieren? Anders formuliert: Welche der Möglichkeiten ist hinsichtlich späterer Änderungen flexibler?

herbivore

EDIT: Präzisiert bezüglich der Unterklassen, die ich zuerst vereinfachend und etwas ungenau nur Rechnung oder Mahnung genannt hatte.

P
40 Beiträge seit 2011
vor 10 Jahren

Wenn man es sehr großzügig sieht, habe ich in der Arbeit mit so etwas ähnlichem zu tun. 😁
Um es genau zu erklären, reicht mir gerade die Zeit nicht wirklich. Aber grob gesagt:
Wir haben Maps/Views für die Anzeige der Daten und befüllt werden diese von "Record"s, welche aus Items bestehen. Ein Item besteht dazu aus einem eindeutigen Namen/Key, einer maximalen Länge und einem Attribut für den Wert.

Ich würde aber eher zur Vererbungsvariante greifen. Ein Vorteil einer abstrakten Formularklasse wäre eine abstrakte Methode zum Überprüfen des Formulars. So dass jedes spezielles Formular die Methode implementieren muss ob alles benötigte vorhanden ist und/oder vorhandene "Kombinationen" gültig sind (zum Beispiel Ort+PLZ).
EDIT 1: Möglich wäres es auch einem Item eine Liste von Validatoren zu setzen, welches das Feld inhaltlich prüfen können (E-Mail, Telefonnummer, ...)
EDIT 2: Je nach Aufbau sollte eine leichte Wartbarkeit möglich sein, auch wenn es für jedes Feld des Formular ein Attribut vom Typ Item (Key#, Name, Wert, ...)

Bekommst du die Daten aber schon fertig validiert etc, und musst sie nur in die entsprechende Struktur bringen, wäre der Ansatz der Komposition angenehmer. Hier muss keine großartige Klassenstruktur dahinter entwickelt werden

PS: Was du in deinem Beispiel nicht verratest, wie schaut das Ganze bei Tabellen aus, wie zum Beispiel auf der Rechnung?

6.911 Beiträge seit 2009
vor 10 Jahren

Hallo herbivore,

ich würde hier die klassische Vererbung verwenden, da ich weniger Probleme in der Weiterentwicklung davon sehe.
In diesem Kontext sind Rechnung und Mahnung auch eher ein Formular, als dass sie ein Formular haben (is-a vs. has-a).

Wie du erwähnt hast, könnte bei der Komposition die Formular-Klasse ein Datencontainer sein. Dies impliziert aber fast, dass zur Entwicklungszeit alle möglichen Daten bekannt sein müssen um entsprechende Eigenschaften in der Formular-Klasse anlegen zu können. Bzw. müsste in Folge bei der Weiterentwicklung die Formular-Klasse ständig angefasst und ebenfalls erweitert werden.

Beim Vererbungsansatz könnte die Oberklasse, hier also das Formular, jene Daten enthalten die immer vorhanden sein müssen. Die Ableitungen davon ergänzen die Oberklasse dann mit ihren jeweils eigenen Eigenschaften.
Außerdem kann die Einhaltung der "Workflows" besser einghalten werden, indem abstrakte od. virtuelle Methoden zum Überschreiben vorhanden sind.

Testbarer finde ich hier auch die Vererbungslösung.
Bei der Komposition müssten die gesamten Tests für Rechnung und Mahnung separat und nahezu doppelt erstellt werden, da es eben zwei unabhängige Klassen sind. Das entfällt bei der Vererbungslösung, da die Formular-Klasse getestet werden kann und für Rechnung als auch Mahnung genügt ein Test der nur ihre Eigenheiten abdeckt. Es ist somit hinsichtlich Weiterentwicklung wohl die bessere Wahl.

Also grob so:


public abstract class Formular
{
    private List<string> _items = new List<string>();

    // Die gemeinsamen Eigenschaften
    public string ...

    public void AddItem(string item)
    {
        _items.Add(item);
    }

    public string GetRecord()
    {
        string record = // FORMNAME

        record = this.WriteItems(record)

        record += // END

        return record;
    }

    protected virtual string WriteItems(string record)
    {
        // gemeiname Eigenschaften schreiben
    } 
}

public class Rechnung : Formular
{
    // Eigenschaften spezifisch für Rechnung
    public string ...

    protected override string WriteItems(string record)
    {
        record = base.WriteItems(record);

        // Eigenschaften von Rechnung schreiben.
    }
}

Für die laufende Nummer WriteItems ev. in eine weitere Methode WriteItemsCore o.ä. aufbrechen, welche nur das Item schreibt und die laufende Nummer aus einer Schleife in WriteItems gesetzt wird.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

1.361 Beiträge seit 2007
vor 10 Jahren

Hi herbivore,

ich votiere für Komposition.
Meiner Meinung bringt einem Vererbung zwei Dinge:

  1. Ein gemeinsame Schnittstelle
  2. Wiederverwendung von Funktionalität

Nun ist 1) leider überhaupt kein Grund, da es direkt Interfaces gibt, die ich auch immer bevorzugen würde. So lässt sich schließlich noch leichter mocken. Selbst wenn ich eine abstrakte Basisklasse besitze, baue ich dafür oftmals noch ein Interface und verwende dies statt der Basisklasse.
Dann ist es auch genauso testbar.

Für 2) sehe ich hier keinen Anlass. Eine "Rechnung" oder eine "Mahnung" hat nach meinem Bauchgefühl kein inhärentes, gemeinsames Verhalten. Jegliches Verhalten wie Validierung, Exportierung, ... fühlt sich eher extern an. Also ich würde lieber einen zusätzlichen Validierer, einen separaten SerienBriefFormatExporter, etc. schreiben. Wenn sich das externe Format mal ändert, dann muss nicht der Kern (das Formular) mit den Daten angepasst werden, sondern nur der FormatExporter. Oder du willst dann zwei (alt und neu) Formate unterstützen, es wäre eklig, wenn du dann 2 Export Funktionen direkt im Formular drin hast. Schließlich erfüllt dieser Export bei dir eine Adapter-Funktion zu dem externen Programm. Und der Adapter sollte eben wie ein Adapter lose sein und nicht direkt an deiner internen Repräsentation hängen.
Nun all das bedeutet, dass ich keine echte Funktion sehe, die es zu sharen gilt. Daher bringt Vererbung für mich hier keine Vorteile.

Und ja, ich empfinde Mahnung und Rechnung auch mehr als "Builder" für eine Formular-Datenklasse.

So viel von meinem Bauch 😉

beste Grüße
zommi

6.911 Beiträge seit 2009
vor 10 Jahren

Hallo zommi,

mit dem "separaten SerienBriefFormatExporter" und dem "Adapter" hast du sehr gute Punkte angesprochen und dadurch erinnert mich das Thema ein wenig an harte vs. weiche (Daten-)Objekte / OOP vs. ActiveRecods. Demnach sehe ich (nun) die Komposition auch eher vorteilhaft an.

Zu einem bestimmten Maß lassen sich auch beide Varianten umsetzen. Für die Datenhaltung Vererbung (entsprechend meinem obigen Vorschlag) und für den Export eine Komposition (entsprechend zommis Vorschlag). Es stimmt sicherlich, dass man mit einem separaten Exportierer flexibler ist.

Beim Exporter ist die Frage wie performant dieser sein. Ev. lässt sich durch Reflection/Attribute das "generischer" umsetzen.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

herbivore Themenstarter:in
49.485 Beiträge seit 2005
vor 10 Jahren

Hallo zusammen,

zunächst vielen Dank für die bisherigen Rückmeldungen. Weitere wären mir willkommen, weil ich die Vor- und Nachteile der eine oder anderen Lösung immer noch nicht ganz überblicke.

Hallo Pippl,

Eine Validierung ist bei uns nicht nötig. Die Werte sind bereits alle geprüft.

wie schaut das Ganze bei Tabellen aus, wie zum Beispiel auf der Rechnung?

Das wäre dann ganz primitiv sowas wie:

[pre]
...
007 POS01     7,50 Euro
008 POS02     3,25 Euro
009 POS03     9,99 Euro
...[/pre]

Wobei ich denke, dass das für den Kern der Sache unerheblich ist.

Hallo gfoidl,

In diesem Kontext sind Rechnung und Mahnung auch eher ein Formular ...

richtig, ich habe den Startbeitrag überarbeitet, damit das klarer rauskommt. Es geht also nicht um allgemeine Rechnungs- und Mahnungsklassen, sondern um Klassen, die im Vererbung_sanssatz ein Rechnungs- bzw. Mahn_formular sind bzw. im Komposition_sansatz Rechnungs- bzw. Mahn_formularlogik-Klassen sind.

Ich neige, wie du zuerst, eigentlich auch dem Vererbungsansatz zu.

Zum ersten, weil ich eben finde, dass ein Rechnungs- oder ein Mahnformular mit einem Formular in einer IST-EIN-Beziehung stehen und und ich es daher eigentlich verwirrend fände, wenn diese nicht in einer Vererbungsbeziehung stehen würden.

Zum zweiten, weil man dann so eine Methode wie AddItem protected machen könnte und so keiner auf die Idee kommen könnte, an dem schon fertig ausgefüllten Formular rumzupfuschen. Ansonsten könnte der Implementierer z.B. des Druck(er)-Objekt, an das das ausgefüllte Formular anschließend übergeben wird, noch auf die Idee kommen, per AddItem irgendwelche Last-Minutes-Changes durchzuführen, was nicht möglich sein soll.

Zum dritten, weil für mich Daten und Operationen zusammen gehören. Das ist für mich eigentlich das Grundprinzip der Objektorientierung. Deshalb tue ich mich mit dem Gedanken an eine reine Datenhaltungsklasse und separaten Klassen zum Füllen dieser Datenklasse immer etwas schwer. Wenn die konkreten Formularklassen von der allgemeinen Formularklasse erben würden, wäre zwar vermutlich der Datenteil auch eher in der Oberklasse und der Operationsteil eher in der Unterklasse, aber aus Sicht eines Benutzers der Unterklasse spielt diese Aufteilung keine Rolle. Er arbeitet letztlich mit Objekten, die Daten und Verhalten als Paket bieten.

dadurch erinnert mich das Thema ein wenig an ...

Hat mich auch daran erinnert, aber dort wird das Thema das recht allgemein bzw. recht prinzipiell besprochen und ich wollte es gerne wesentlich konkreter aufziehen.

Hallo zommi,

Eine "Rechnung" oder eine "Mahnung" hat nach meinem Bauchgefühl kein inhärentes, gemeinsames Verhalten.

sie haben z.B. beide eine Anschrift (wenn auch nur als Einträge in einer Liste von Schlüssel-Wert-Paaren). Ok, das sind erstmal nur gemeinsame Daten, aber auch gemeinsame Daten können ja schon ein Grund für Vererbung sein. Außerdem muss auch den Code geben, der die Anschrift ausfüllt und das wäre dann so ein gemeinsames Verhalten.

Oder du willst dann zwei (alt und neu) Formate unterstützen

Nein, das ist nicht erforderlich und wird es wohl auch in Zukunft nicht sein. Wenn doch, gebe ich dir Recht, dass ein Adapter zum Einsatz kommen sollte. Aber ich denke, das hätte auf den Kern der Sache so oder so keinen Einfluss.

Daher bringt Vererbung für mich hier keine Vorteile.

Drei Vorteile, die ich sehe, habe ich weiter oben in dieser Antwort beschrieben. Findest du die nicht stichhaltig?

herbivore