Laden...

Standardverhalten eines Interfaces in einer Basisklassen implementieren: Ist das ok/sinnvoll?

Erstellt von tom-essen vor 9 Jahren Letzter Beitrag vor 9 Jahren 4.509 Views
tom-essen Themenstarter:in
1.820 Beiträge seit 2005
vor 9 Jahren
Standardverhalten eines Interfaces in einer Basisklassen implementieren: Ist das ok/sinnvoll?

Hallo zusammen,

ich habe mir in letzter Zeit etwas angewöhnt, wo ich mir nicht sicher bin, ob dies richtig ist.

Ich habe ein Interface, welches von mehreren Klassen implementiert wird. Das Interface enthält einige Properties und eine überschaubare Menge an Methoden (bis zu 5).


public interface IStep
{
    ReadOnlyList<object> Connectors { get; }

    bool Apply(object data);
}

public class Destination : IStep
{
    public ReadOnlyList<object> Connectors { get; private set; }

    public Destination()
    {
      Connectors = new List<object>();
    }

    public bool Apply(object data)
    {
      return false;
    }
}

Nun habe ich in einigen Fällen eine abstrakte Klasse von diesem Interface angeleitet und davon wiederum die o.g. Klassen.


public interface IStep
{
    ReadOnlyList<object> Connectors { get; }

    bool Apply(object data);
}

public abstract class StepBase : IStep
{
    public ReadOnlyList<object> Connectors { get; private set; }

    public StepBase()
    {
      Connectors = new List<object>();
    }

    public virtual bool Apply(object data)
    {
      return false;
    }
}

public class Destination : StepBase
{
}

Ich kann dadurch viele Dinge bereits in der Basisklasse durchführen (Einrichten der Properties, Standardverhalten der Methoden) und muss nur noch die Zusatzfunktionen in der abgeleiteten Klasse implementieren. Erweiterungen im Interface muss ich primär ertmal nur in der abstrakten Basisklasse implementieren, für die abgeleiteten Klassen kann ich dies nach und nach einzeln implementieren, testen und z.B. in Subversion committen.

Mein persönlicher Eindruck ist allerdings, dass ich dadurch das Prinzip der Interfaces etwas verwässere, weil die Zwischenstufe mit der abstrakten Klasse zwischen dem Interface und dem letztendlichen Abonennten liegt.

Wie seht ihr das.

Nobody is perfect. I'm sad, i'm not nobody 🙁

U
282 Beiträge seit 2008
vor 9 Jahren

Ich finde das Konzet eigentlich sehr sauber, verwende es selbst sehr gerne.
Du musst nur sicherstellen, dass die Nutzer sich eben nur auf das generelle Interface verlassen und nicht auf Implementierungsdetails der abstrakten Klasse.

Die Seperation of Concerns:* Interface stellt das nach außen verbindliche Interface dar.

  • Abstrakte Klasse vermeidet Redundanzen in der Implementierung.

Die abstrake Basisklasse bleibt ein Implementierungsdetail, was du jederzeit entfernen könntest ohne dass die Anwendenden Klassen etwas davon mitbekommen.

Ich halte das Konzept sogar eher für eine Best Practice.

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo tom-essen,

solange es hier um Unterklassen geht, die genau ein Interface implementieren und außerdem von keiner anderen Oberklasse erben müssen/sollen, ist alles in Ordnung. Dann ist das Vorgehen auch sinnvoll. Redundanzen sollten vermieden werden und werden es hierdurch. Eine wesentliche Aufgabe von Basisklassen ist die Redundanzvermeidung.

Problematisch wird es, wenn eine (Unter-)Klasse mehrere Interfaces implementieren soll. Zum Beispiel werden im Zuge von DataBinding oft INotifyPropertyChanged und IDataErrorInfo gleichzeitig implementiert. Wenn man dafür eine NotifyPropertyChangedBase und eine DataErrorInfoBase Klasse anbieten würde, müsste sich der Implementierer der Unterklasse entscheiden, welche davon er verwendet. Die andere ist dann für ihn mehr oder minder nutzlos (um so mehr, wenn sie abstrakt ist). Er kann nicht von beiden Klassen erben, weil C# keine Mehrfachvererbung unterstützt (als "Ersatz" gibt es gerade Interfaces).

Das gleiche Entscheidungsproblem taucht auf, wenn aus die Unterklassen aus anderen Gründen von einer bestimmten Oberklasse erben müssen und zusätzlich ein Interface implementieren soll. Dann kann aus dem gleichen Grund von der zum Interface gehören Basisklasse nicht geerbt werden.

Da man nie weiß, ob eine Unterklasse später noch ein weiteres Interface implementieren oder von einer anderen Basisklasse erben soll, ist das Vorgehen, zu einem Interface auch eine Basisklasse anzubieten, leider nicht so universell, wie man es sich wünschen würde.

Auf der sicheren Seite ist man, wenn man zu jedem Interface eine Hilfsklasse anbietet, die die nötigen Methoden zum Aufrufen (und nicht zum Erben) anbietet. Das könnte z.B. auch eine statische Klasse sein, deren Methoden das zu bearbeitende (Interface-)Objekt per Parameter bekommt.

Der Nachteil von Hilfs-(im Gegensatz zu Basis-)Klassen ist, dass man in den konkreten (Unter-)Klassen den Implementierungsaufwand hat, um die Hilfsmethoden aufzurufen. Aber dafür funktionieren sie immer und man kann sie in allen (zukünftigen) Situationen garantiert einsetzen.

herbivore

2.079 Beiträge seit 2012
vor 9 Jahren

Stichwort Hilfsklassen:

Was ich für etwas in der Art mache sind Erweiterungsmethoden für das Interface.
Manchmal gibt es die Möglichkeit, das Interface rundimentär zu halten, also dass es nur die grundlegenden Methoden anbietet. Im selben Namespace gibt es dann die Hilfsmethoden, die die Überladungen bereit stellen.

Dennoch finde ich das Konzeot Hilfsmethoden gut, so habe ich z.B. für INotifyPropertyChanged und INotifyPropertyChanging eine Klasse, die genau das verwaltet. Es kann eine Variable per ref mit gegeben werden oder die Property-Werte werden in einem Dictionary gehalten. Darüber wird dann jeder Aufruf geleitet und es wird eine im Konstruktor mit gegebene Methode (z.B. OnPropertyChanged) ausgeführt, wenn ein Event gefeuert werden soll.

Wenn eine weitere Basisklasse notwenig ist:

Dann bietet es sich eventuell an, eine Klasse zu schreiben, die von dieser weiteren Basisklasse erbt und zusätzlich die benötigten Interfaces implementiert.
Ich denke, so ist der beste Weg, Redundanzen zu vermeiden, wenn weitere Basisklassen nötig sind. Das funktioniert aber auch nur dann sinnvoll, wenn das nicht für meherere Vererbungs-Ebenen gemacht werden muss.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

P
1.090 Beiträge seit 2011
vor 9 Jahren

Hallo Tom,

ich denke das du in dem dargestellten Beispiel schon recht hast und das Interface nicht ganz zweckmäßig verwendet wird.
Wo mit ich meinen Vorreden nicht widersprechen möchte, aber deren Schwerpunkt lag auf der gemeinsamen Basis Klasse und nicht direkt auf dem Interface.

Wenn nur die Basis Klasse das Interface implementiert und alle anderen von ihr Erben. Brauchst du das Interface nicht. Die Basis Klasse ist dann deine Schnittstelle. Du kannst dir also die Arbeit sparen Interface und Basis Klasse immer anzupassen.

Grundlegende Ziel ist es gegen die Schnittellen (Interfaces im Englischen, was oft falsch interpretiert wird) der größten Abstraktion zu implementieren.

Z.B. ist von Kunde und Mitarbeiter, eine größere Abstraktion Person. Wenn ich jetzt gegen die Schnittellen von Person (Vorname, Name) Implementiere kann ich die Methoden auch für Kunde und Mitarbeiter verwenden.

Interfaces sind eher dafür gedacht sehr unterschiedliche Sachen gleich behandeln zu können.
Z.B. Mensch, Tier, Fahrzeug könnten ein Interface mit MakeSound implementieren, so das alle Geräusche machen können. (Gibt es als Beispiel auch irgendwo im Internet).

Dazwischen gibt es natürlich viele Abstufungen und manchmal ist es nicht ganz klar ob ich jetzt besser ein Interface oder eine Basis Klasse verwende. Da halte ich mich dann an den Ratschlag von Kent Beck (Buch: Implementation Patterns) Interfaces nur zu Implementieren wenn ich muss.

MFG
Björn

p.s. die Stadt Essen?

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

W
955 Beiträge seit 2010
vor 9 Jahren

Hallo,

Problematisch wird es, wenn eine (Unter-)Klasse mehrere Interfaces implementieren soll. Man könnte in dem Zusammenhang auch über Entwurfsmuster nachdenken, Decorator, Proxy, Bridge (oder Monaden wenn hier jmd funktional programmiert).

U
282 Beiträge seit 2008
vor 9 Jahren

Ich finde immernoch, dass die meisten Argumente eigentlich den Kern des Problems nicht treffen.

Die abstrakte Basisklasse ist ein Implementierungsdetail, was nach außen nicht bekannt ist. Ich kann es jederzeit ändern.

Wenn nun 8 Klassen von dieser Basisklasse ableiten, für eine neunte passt es nicht mehr, dann implementiere ich halt das Interface direkt. Wenn ich eine Klasse erweitern will und die Basisklasse nicht mehr passt, dann implementiere ich das Interface direkt oder erbe gar von einer anderen Basisklasse.

In all diesen Fällen bekommen die Benutzer des Interfaces das garnicht mit, weil die Entkopplung eben über das Interface stattfindet. Die Basisklasse entspricht eher einer "privaten Vererbung" aus dem C++-Umfeld.

Daher würde ich das weiterhin machen und mich um ggf. auftretende Probleme erst später kümmern. Denn Probleme mit einer Klasse in dieser Architektur können nur dann auftreten, wenn sich die Aufgaben dieser Klasse ändern. Dann muss ich sie sowieso anfassen. Und die Entkopplung zu den Verwendungen der Klasse bleibt durch das Interface unverändert.

P
1.090 Beiträge seit 2011
vor 9 Jahren

Hallo Uwe,

grundlegend sollte keine Klasse Implementierungsdetails nach außen sichtbar machen.

An dem Punkt ist es auch einfach unwichtig ob ich eine Basis Klasse verwende oder ein Interface.

Interfaces werden wirklich wichtig, wenn ich möchte das ganz unterschiedlichen Klassen an gewissen Schnittstellen gleich behandelt werden können.

Ob ich jetzt gegen IKunde oder gegen Kunde implementiere macht nach außen keinen Unterschied.
Wenn beide die gleichen Schnitstellen nach außen bereit stellen.

Der Zentrale unterschied ist, das ich bei Kunde schon Funktionalitäten bereitstellen kann, während ich sie bei IKunde immer Implementieren muss.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo Palin,

ich schließe mich Uwe81 an. Eine Basisklasse ersetzt hier das Interface nicht. Die meisten Gründe hat Uwe81 genannt(*). Dazu kommt noch, dass es Fälle gibt, in denen die Basisklasse absichtlich nicht das gesamte Interface abdeckt. Dann würde die Basisklasse das Interface gar nicht (vollständig) implementieren, sondern z.B. nur ein paar Member davon. Erst die konkreten Unterklassen würden dann das (vollständige) Interface implementieren. Oder als Code ausgedrückt:

public interface IStep {...}
public abstract class StepBase  {...}
public class Destination : StepBase, IStep {...}

Davon abgesehen hattest du ja selbst geschrieben, dass Interfaces für etwas (leicht) anderes gedacht sind als Basisklassen. Schon deshalb sollte man aus dem einen nicht das andere machen und auch nicht umgekehrt.

Interface und Basisklasse ergänzen sich, aber die ersetzen sich nicht.

herbivore

(*) Schau sie dir noch mal an. Sie sind stichhaltig! Du hast sie nicht entkräftet und schon gar nicht widerlegt. Man kann auf das Interface nicht verzichten oder sollte es zumindest nicht, nur weil man eine Basisklasse für das Standardverhalten anbietet.

P
1.090 Beiträge seit 2011
vor 9 Jahren

Wenn ich zu einem Interface genau eine Abstracte Basis Klasse habe die dieses Implementiert, von der alle Erben. Kann ich auch die Schnittstellen in der Basis Klasse Definierern und brauche kein Interface. (Yagni).

Und ja Interfaces und Abstracte Klassen sind verschieden, deshalb sollte ich auch kein Interface verwenden wenn ich eigentlich eine Abstracte Klasse brauche.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo Palin,

rein technisch ist dein erster Absatz richtig, konzeptionell nicht. Begründung oben.

Beim zweiten Absatz gehst du von falschen Voraussetzungen aus. Hier wird ein Interface gebraucht. Die abstrakte Klasse hat dagegen eine reine Hilfskonstruktion, die vom Implementierer des Interfaces optional benutzt werden kann (und selbst das funktioniert nicht in alle Situationen s.o.), aber nicht benutzt werden muss.

Die wichtigste Einschränkung, von abstrakten Klassen gegenüber Interfaces - die du hier anscheinend übersiehst oder für nicht relevant hältst - ist, dass eine Klasse zwar mehrere Interfaces implementieren, aber nur von maximal einer Oberklasse erben kann. Deshalb kann man Interfaces nicht einfach durch abstrakte Klassen ersetzen.

Ich hoffe, wir können diesen Nebenkriegsschauplatz jetzt verlassen. Letztlich muss sich sowieso jeder Leser sein eigenes Bild machen. Die Argumente liegen auf dem Tisch.

herbivore

tom-essen Themenstarter:in
1.820 Beiträge seit 2005
vor 9 Jahren

Hallo zusammen,

ich danke euch für die interessante Diskussion meines "Problems".
Ich denke, ich werde die abstrakten Hilfsklassen weiter verwenden, und habe ja auch für die weitere Verwendung ein paar Hinweise erhalten, die beachtet werden sollten.

Nobody is perfect. I'm sad, i'm not nobody 🙁