it-swarm.com.de

Gibt es in C # eine Möglichkeit, die Verhaltenskopplung in Schnittstellenmethoden zu erzwingen, oder ist die Tatsache, dass ich versuche, dies zu tun, ein Designgeruch?

Oft möchte ich eine Schnittstelle mit einigen Methoden definieren, die eine Verhaltensbeziehung zwischen ihnen aufrechterhalten.

Ich habe jedoch das Gefühl, dass diese Beziehung oft implizit ist. Vor diesem Hintergrund habe ich mich gefragt: Gibt es eine Möglichkeit, eine Verhaltensbeziehung zwischen Schnittstellenmethoden durchzusetzen?

Ich dachte darüber nach, dieses Verhalten durch Vererbung zu definieren (indem ich eine gemeinsame Implementierung definiere). Da C # jedoch keine Mehrfachvererbung zulässt, ist eine Schnittstelle meiner Meinung nach oft ratsamer und die Vererbung nicht flexibel genug.


Zum Beispiel:

public interface IComponent
{
    void Enable();
    void Disable();
    bool IsEnabled();
}

Für diese Schnittstelle wollte ich, dass die folgende Beziehung erfüllt wird:

  • Wenn Enable() aufgerufen wird, sollte IsEnabled() true zurückgeben.
  • Wenn Disable() aufgerufen wird, sollte IsEnabled() false zurückgeben.

In diesem Beispiel lautet die Verhaltensbeschränkung, die ich erzwingen möchte:

  • Bei der Implementierung von Enable() sollte der Implementierer sicherstellen, dass IsEnabled() true zurückgibt.
  • Bei der Implementierung von Disable() sollte der Implementierer sicherstellen, dass IsEnabled() false zurückgibt.

Gibt es eine Möglichkeit, diese Implementierungsbeschränkung durchzusetzen? Oder ist die Tatsache, dass ich darüber nachdenke, diese Art von Einschränkung durchzusetzen, selbst ein Zeichen dafür, dass das Design fehlerhaft ist?

27
Albuquerque

Lassen Sie uns zunächst Ihre Benutzeroberfläche ein wenig optimieren.

public interface IComponent
{
    void Enable();
    void Disable();
    bool IsEnabled { get; }
}

Nun dann. Was könnte hier möglicherweise schief gehen? Könnte beispielsweise eine Ausnahme in den Methoden Enable() oder Disable() ausgelöst werden? In welchem ​​Zustand würde sich IsEnabled dann befinden?

Selbst wenn Sie Codeverträge verwenden, sehe ich nicht, wie IsEnabled mit der Verwendung Ihrer Enable- oder Disable-Methoden korreliert werden kann, es sei denn, diese Methoden sind garantiert erfolgreich. IsEnabled sollte den tatsächlichen Zustand darstellen, in dem sich Ihr Objekt befindet, nicht einen hypothetischen Zustand.


Das heißt, alles was Sie wirklich brauchen ist

public interface IComponent
{
    bool IsEnabled { get; set; }
}

Löschen Sie es, und die Komponente deaktiviert sich selbst.

33
Robert Harvey

Was Sie suchen, ist ein bekannter Ansatz namens Design by Contract . Es war wird direkt im Framework in Version 4.0 unterstützt .

OFFENLEGUNG: Seien Sie vorsichtig, wenn Sie einem neuen Projekt im Jahr 2019 Codeverträge hinzufügen. Der aktuelle Status der weiteren Wartung durch Microsoft ist nicht vollständig klar, siehe diesen SO Beitrag =, vielleicht wegen fehlender Popularität.

Mit DBC können Vorbedingungen, Nachbedingungen und Invarianten für Funktionen sowie für Schnittstellen angegeben werden. Sie müssen also lediglich einen Vertrag schreiben, der erzwingt, dass IsEnabled nach einem Aufruf von Enabled() wahr ist.

Wie andere Antworten gezeigt haben, kann es alternative Designs geben, bei denen diese Einschränkungen nicht erforderlich sind. Nehmen wir jedoch für dieses Beispiel an, dass diese Anforderungen aus irgendeinem Grund gerechtfertigt sind. Die Verwendung von Codeverträgen für Robert Harveys Variante der Beispielschnittstelle kann dann folgendermaßen aussehen:

using System.Diagnostics.Contracts;

[ContractClass(typeof(ComponentContract))]
public interface IComponent
{
    void Enable();
    void Disable();
    bool IsEnabled { get; } 
}

[ContractClassFor(typeof(IComponent))]
sealed class ComponentContract : IComponent
{
    [Pure]
    public bool IsEnabled => Contract.Result<bool>();

    public void Disable()
    {
        Contract.Ensures(IsEnabled == false);
    }

    public void Enable()
    {
        Contract.Ensures(IsEnabled == true);
    }
}

Siehe hier für ein kurzes Tutorial zu Codeverträgen.

Schauen Sie sich auch meine zweite Antwort auf diese Frage an, die eine Lösung bietet, die nicht von Bibliotheken abhängt, die in Zukunft möglicherweise veraltet sind.

39
Doc Brown

Sie fordern zu viel von C # -Schnittstellen.

C # -Schnittstellen sind Verträge. Sie sagen, welches Methodenpaket eine bestimmte Klasse implementiert, und sie garantieren, dass diese Methoden vorhanden sind , wenn jemand sie aufruft.

Das heißt, das ist auch das einzige, was C # -Schnittstellen tun.

Sie wissen überhaupt nicht, was die implementierten Methoden bewirken. Sie können tun, was sie wollen.

Eine bestimmte Klasse kann "isEnabled" implementieren, um immer true zurückzugeben. Ein anderer kann "Deaktivieren" mit einem Datenbankaufruf verknüpfen und die Deaktivierung ablehnen, wenn etwas Bestimmtes passiert.

Sie können nichts davon kontrollieren. Das ist nicht die Aufgabe Ihrer C # -Schnittstelle.


Wie erzwinge ich dieses Verhalten dann?

Verwenden Sie Tests.

Erstellen Sie eine Gruppe von Komponententests, die ein Objekt des betreffenden Typs akzeptieren können, und testen Sie das Verhalten.

Wenn der Test bestanden ist, können Sie loslegen. Wenn sie fehlschlagen, stimmt etwas nicht und Sie sollten Ihren Code überprüfen.

Sie haben jedoch keine elegante Möglichkeit, dies einem Dritten aufzuzwingen, wenn Sie eine API entwickeln, und sollten dies auch nicht tun. Dafür sind C # -Schnittstellen nicht gedacht.

32
T. Sar

Zustandsübergänge können durch separate Schnittstellen pro Zustand dargestellt werden:

public interface IEnabledComponent
{
    IDisabledComponent ToDisabled();
}

public interface IDisabledComponent
{
    IEnabledComponent ToEnabled();
}

Dies ist viel leistungsfähiger und sicherer, da Sie je nach aktuellem Status verschiedene Methoden verfügbar machen können. Insbesondere würden Sie nicht einmal die Eigenschaft IsEnabled benötigen.

Alternativ könnten Sie eine einzige wirklich einfache Oberfläche haben:

public interface IComponent
{
    bool IsEnabled {get;set;}
}

Hier wird der Staat in einem einzelnen Mitglied dargestellt, sodass nicht mehrere verwandte Mitglieder koordiniert werden müssen.

Sie würden die erste Option wählen, wenn die verschiedenen Zustände ein unterschiedliches Verhalten verursachen, die zweite, wenn der aktivierte Zustand kein anderes Verhalten beeinflusst.

Die Tatsache, dass Sie eine Verhaltensbeziehung zwischen verschiedenen Mitgliedern einer Schnittstelle definieren können, zeigt an, dass sie dieselben Informationen ausdrücken und daher redundant sind. Um ein einfacheres Beispiel zu nehmen:

interface IComponent 
{
  bool IsEnabled {get;set;}
  bool IsDisabled {get;set;}
}

Hier können Sie die Beziehung definieren, dass IsEnabled falsch ist, wenn IsDisabled wahr ist. Dies zeigt jedoch nur, dass einer von ihnen beseitigt werden könnte, da er keine unabhängigen Informationen oder Verhaltensweisen darstellt.

21
JacquesB

Eine weitere Option, wenn das Verwendungsmuster "traditionell" aussehen soll* * Sie können die Schnittstelle jedoch einschränken, sodass keine Methoden miteinander verknüpft werden müssen. Sie können Erweiterungsmethoden verwenden, um fehlende Methoden "hinzuzufügen".

In Ihrem Beispiel kann die Schnittstelle nur IsEnabled {get;set;} Enthalten und Enable und Disable können Erweiterungen sein, die Teil Ihrer Bibliothek sind, die die Schnittstelle definiert:

public interface IComponent
{
    bool IsEnabled {get;set;}
}

public static class IComponentExtensions
{
    public static void Enable(this IComponent component)
    {
         component.IsEnabled = true;
    } 
    public static void Disable(this IComponent component)
    {
         component.IsEnabled = false;
    } 
}

Beachten Sie, dass Instanzmethoden Vorrang vor Erweiterungen haben und konkrete Klassen möglicherweise Methoden mit derselben Signatur implementieren, um den Compiler zu zwingen, die klassenspezifische Implementierung auszuwählen, was möglicherweise zu Verwirrung führen kann. Auf der anderen Seite hat so ziemlich jeder bedeutende Erfahrungen mit diesem Muster in LINQ - es lohnt sich also zu überlegen, ob Ihre Benutzeroberfläche eingegrenzt werden kann, um dies zu ermöglichen.


* * "traditionelle" Schnittstelle im Sinne von Personen, die bestimmte Methoden für bestimmte Typen erwarten - zum Beispiel Stream Klassen in Framework - man erwartet, dass die Datei "Öffnen" und "Schließen" hat, wenn reguläre Streams nur "neu"/sind. "Entsorgen".

5
Alexei Levenkov

Hier ist eine andere Idee, um dies mit einem völlig anderen Ansatz zu lösen als dem in meine andere Antwort (daher poste ich es separat): Verwenden Sie das Muster der Vorlagenmethode . Nehmen wir noch einmal an: Nehmen wir für das Beispiel an, dass die Anforderung, dass IsEnabled wie beschrieben funktioniert, aus irgendeinem Grund gerechtfertigt ist und Sie sicherstellen möchten, dass der Status korrekt aktualisiert wird.

Dann könnten Sie die Schnittstelle durch eine abstrakte Klasse ersetzen, IsEnabled zu einer booleschen Eigenschaft machen, die bedingungslos umgeschaltet wird (auch im Falle einer Ausnahme), und einen Benutzer dieser abstrakten Klasse zwei Vorlagenmethoden anstelle des Originals implementieren lassen Einsen:

public abstract class Component  // replacement for IComponent 
{
    public bool IsEnabled{get;private set;}

    public void Enable()
    {
       try
       {
          EnableImpl();
       }
       finally
       {
          IsEnabled=true;
       }
    }
    public void Disable()
    {
       try
       {
          DisableImpl();
       }
       finally
       {
          IsEnabled=false;
       }
    }

    protected virtual void EnableImpl();
    protected virtual void DisableImpl();
}

Jetzt können Benutzer EnableImpl und DisableImpl durch ihre eigenen Implementierungen überschreiben, während die Statusverfolgung garantiert durchgeführt wird.

Dieser Ansatz ist definitiv mehr Standard als mein erster Vorschlag und ich würde nicht erwarten, dass er bald von Microsoft abgelehnt wird.

3
Doc Brown

Eine alternative Lösung (die ich nicht allgemein empfehlen würde, aber das erreicht, wonach Sie suchen) wäre diese.

public class Switch //make sealed if you really want to enforce behaviour
{  

    public bool IsEnabled {get; private set;}

    public void Enable() //make virtual if you don't want to enforce this behaviour
    { 
        IsEnabled = true;
    }

    public void Disable() //make virtual if you don't want to enforce this behaviour
    {
        IsEnabled = false;
    }
}

public interface IComponent {
    Switch Switch{get;}
}
0
Guran