Was bedeuten Programmierer, wenn sie sagen: "Code gegen eine Schnittstelle, nicht gegen ein Objekt"?

78

Ich habe die sehr lange und mühsame Suche begonnen, TDD zu lernen und auf meinen Workflow anzuwenden . Ich habe den Eindruck, dass TDD sehr gut zu den IoC-Prinzipien passt.

Nachdem ich einige Fragen mit TDD-Tags hier in SO durchsucht habe, habe ich gelesen, dass es eine gute Idee ist, gegen Schnittstellen und nicht gegen Objekte zu programmieren.

Können Sie einfache Codebeispiele dafür bereitstellen, wie dies ist und wie es in realen Anwendungsfällen angewendet werden kann? Einfache Beispiele sind für mich (und andere Leute, die lernen wollen) der Schlüssel, um die Konzepte zu verstehen.

ROMANIA_engineer
quelle
7
Dies ist eher eine OOP-Sache als eine C #
-spezifische
2
@ Billy ONeal: Könnte sein, aber da Schnittstellen in C # / Java anders funktionieren, möchte ich zuerst die Sprache lernen, mit der ich am besten vertraut bin.
11
@Sergio Boombastic: Das Konzept der Programmierung auf ein Interface hat weder mit interfaceJava noch mit C # zu tun . Als das Buch, aus dem dieses Zitat stammt, geschrieben wurde, existierten weder Java noch C #.
Jörg W Mittag
1
@ Jörg: na ja , das hat etwas damit zu tun. Schnittstellen in neueren OOP-Sprachen sollen sicherlich wie im Zitat beschrieben verwendet werden.
Michael Petrotta
1
@ Michael Petrotta: Sie sind aber nicht sehr gut darin. Beispielsweise sagt die Schnittstelle von List, dass sich das addElement nach dem Hinzufügen eines Elements zur Liste in der Liste befindet und die Länge der Liste um zunimmt 1. Wo steht das eigentlich im [ interface List] ( Download.Oracle.Com/javase/7/docs/api/java/util/List.html#add )?
Jörg W Mittag

Antworten:

82

Erwägen:

class MyClass
{
    //Implementation
    public void Foo() {}
}

class SomethingYouWantToTest
{
    public bool MyMethod(MyClass c)
    {
        //Code you want to test
        c.Foo();
    }
}

Da MyMethodnur a akzeptiert wird MyClass, können Sie nicht durch MyClassein Scheinobjekt ersetzen, um einen Unit-Test durchzuführen. Besser ist es, eine Schnittstelle zu verwenden:

interface IMyClass
{
    void Foo();
}

class MyClass : IMyClass
{
    //Implementation
    public void Foo() {}
}

class SomethingYouWantToTest
{
    public bool MyMethod(IMyClass c)
    {
        //Code you want to test
        c.Foo();
    }
}

Jetzt können Sie testen MyMethod, da nur eine Schnittstelle und keine bestimmte konkrete Implementierung verwendet wird. Dann können Sie diese Schnittstelle implementieren, um jede Art von Mock oder Fake zu erstellen, die Sie zu Testzwecken benötigen. Es gibt sogar Bibliotheken wie die von Rhino Mocks Rhino.Mocks.MockRepository.StrictMock<T>(), die jede Schnittstelle verwenden und Ihnen im Handumdrehen ein Scheinobjekt erstellen.

Billy ONeal
quelle
14
Randbemerkung: Es muss nicht immer hat eine tatsächliche Schnittstelle im Sinne der Sprache , die Sie Anwendungen verwenden wird das Wort. Es könnte sich auch um eine abstrakte Klasse handeln, die angesichts der Einschränkungen, die Java oder C # für die Vererbung haben, nicht völlig unangemessen ist.
Joey
2
@Joey: Sie könnten eine abstrakte Klasse verwenden, ja, aber wenn Sie dies tun, müssen Sie die Klasse so gestalten, dass sie vererbbar ist, was etwas mehr Arbeit bedeuten kann. In Sprachen wie C ++, die keine Schnittstelle auf Sprachebene haben, tun Sie genau das - erstellen Sie eine abstrakte Klasse. Beachten Sie jedoch, dass es in Java und C # immer noch besser ist, die Schnittstelle zu verwenden, da Sie von mehreren Schnittstellen, aber nur einer Klasse, erben können. Das Zulassen der Vererbung mehrerer Schnittstellen ermutigt Sie, Ihre Schnittstellen zu verkleinern, was eine gute Sache ist. TM :)
Billy ONeal
5
@Joey: du musst gar nichts benutzen . In Ruby beispielsweise wird die Schnittstelle , für die Sie programmieren, normalerweise, wenn überhaupt, nur in Englisch dokumentiert. Das macht es jedoch nicht weniger zu einer Schnittstelle . Umgekehrt bedeutet das Schlagen von interfaceSchlüsselwörtern im gesamten Code nicht, dass die Programmierung gegen Schnittstellen erfolgt . Gedankenexperiment: Nehmen Sie schrecklich eng gekoppelten inkohäsiven Code. Kopieren Sie sie für jede Klasse einfach und fügen Sie sie ein, löschen Sie alle Methodenkörper, ersetzen Sie das classSchlüsselwort durch interfaceund aktualisieren Sie alle Verweise im Code auf diesen Typ. Ist der Code jetzt besser?
Jörg W Mittag
Jörg, das ist in der Tat eine schöne Sichtweise :-)
Joey
Über das Ausblenden von Implementierungsdetails hinaus können Sie den darunter liegenden Code ändern, um die Leistung usw. zu verbessern, ohne den aufrufenden Code zu ändern. Java tut dies beispielsweise mit List. Die Implementierung darunter kann ein Array, ein Stapel usw. sein
Brian Pan
18

Es ist alles eine Frage der Intimität. Wenn Sie eine Implementierung (ein realisiertes Objekt) codieren, stehen Sie als Verbraucher in einer ziemlich engen Beziehung zu diesem "anderen" Code. Es bedeutet, dass Sie wissen müssen, wie es zu konstruieren ist (dh welche Abhängigkeiten es hat, möglicherweise als Konstruktorparameter, möglicherweise als Setter), wann Sie es entsorgen müssen, und Sie können wahrscheinlich nicht viel ohne es tun.

Über eine Schnittstelle vor dem realisierten Objekt können Sie einige Dinge tun -

  1. Zum einen können / sollten Sie eine Factory nutzen, um Instanzen des Objekts zu erstellen. IOC-Container erledigen dies sehr gut für Sie, oder Sie können Ihre eigenen erstellen. Bei Bauarbeiten, die außerhalb Ihrer Verantwortung liegen, kann Ihr Code einfach davon ausgehen, dass er das erhält, was er benötigt. Auf der anderen Seite der Factory-Wand können Sie entweder reale Instanzen oder Scheininstanzen der Klasse erstellen. In der Produktion würden Sie natürlich real verwenden, aber zum Testen möchten Sie möglicherweise gestoppelte oder dynamisch verspottete Instanzen erstellen, um verschiedene Systemzustände zu testen, ohne das System ausführen zu müssen.
  2. Sie müssen nicht wissen, wo sich das Objekt befindet. Dies ist in verteilten Systemen nützlich, in denen das Objekt, mit dem Sie sprechen möchten, möglicherweise lokal für Ihren Prozess oder sogar Ihr System ist oder nicht. Wenn Sie jemals Java RMI oder Old Skool EJB programmiert haben, kennen Sie die Routine des "Gesprächs mit der Schnittstelle", bei der ein Proxy versteckt war, der die Remote-Netzwerk- und Marshalling-Aufgaben erledigte, um die sich Ihr Client nicht kümmern musste. WCF hat eine ähnliche Philosophie: "Mit der Schnittstelle sprechen" und das System bestimmen lassen, wie mit dem Zielobjekt / -dienst kommuniziert werden soll.

** UPDATE ** Es wurde ein Beispiel für einen IOC-Container (Factory) angefordert. Es gibt viele für so ziemlich alle Plattformen, aber im Kern funktionieren sie so:

  1. Sie initialisieren den Container in Ihrer Anwendungsstartroutine. Einige Frameworks tun dies über Konfigurationsdateien oder Code oder beides.

  2. Sie "registrieren" die Implementierungen, die der Container für Sie als Factory für die von ihnen implementierten Schnittstellen erstellen soll (z. B.: Registrieren Sie MyServiceImpl für die Service-Schnittstelle). Während dieses Registrierungsprozesses können Sie normalerweise einige Verhaltensrichtlinien angeben, z. B. wenn jedes Mal eine neue Instanz erstellt wird oder eine einzelne (Tonne) Instanz verwendet wird

  3. Wenn der Container Objekte für Sie erstellt, fügt er im Rahmen des Erstellungsprozesses alle Abhängigkeiten in diese Objekte ein (dh wenn Ihr Objekt von einer anderen Schnittstelle abhängt, wird wiederum eine Implementierung dieser Schnittstelle bereitgestellt usw.).

Pseudocodisch könnte es so aussehen:

IocContainer container = new IocContainer();

//Register my impl for the Service Interface, with a Singleton policy
container.RegisterType(Service, ServiceImpl, LifecyclePolicy.SINGLETON);

//Use the container as a factory
Service myService = container.Resolve<Service>();

//Blissfully unaware of the implementation, call the service method.
myService.DoGoodWork();
Hoserdude
quelle
+1 für Punkt 1. Das war sehr klar und verständlich. Punkt 2 ging mir über den Kopf. : P
1
@Sergio: Was hoserdude bedeutet, ist, dass es viele Fälle gibt, in denen Ihr Code nie weiß, was der eigentliche Implementierer des Objekts ist, da er automatisch vom Framework oder einer anderen Bibliothek in Ihrem Namen implementiert wird.
Billy ONeal
Können Sie ein gutes, grundlegendes Beispiel für eine IoC-Containerfabrik geben?
Bulltorious
9

Wenn Sie für eine Schnittstelle programmieren, schreiben Sie Code, der eine Instanz einer Schnittstelle verwendet, keinen konkreten Typ. Beispielsweise könnten Sie das folgende Muster verwenden, das die Konstruktorinjektion enthält. Konstruktorinjektion und andere Teile der Umkehrung der Steuerung sind nicht erforderlich, um gegen Schnittstellen programmieren zu können. Da Sie jedoch aus der TDD- und IoC-Perspektive kommen, habe ich sie auf diese Weise verkabelt, um Ihnen einen Kontext zu geben, den Sie hoffentlich haben vertraut mit.

public class PersonService
{
    private readonly IPersonRepository repository;

    public PersonService(IPersonRepository repository)
    {
        this.repository = repository;
    }

    public IList<Person> PeopleOverEighteen
    {
        get
        {
            return (from e in repository.Entities where e.Age > 18 select e).ToList();
        }
    }
}

Das Repository-Objekt wird übergeben und ist ein Schnittstellentyp. Der Vorteil der Übergabe einer Schnittstelle besteht in der Möglichkeit, die konkrete Implementierung auszutauschen, ohne die Verwendung zu ändern.

Zum Beispiel würde man annehmen, dass der IoC-Container zur Laufzeit ein Repository injiziert, das so verdrahtet ist, dass es die Datenbank erreicht. Während der Testzeit können Sie ein Mock- oder Stub-Repository übergeben, um Ihre PeopleOverEighteenMethode auszuführen.

Michael Shimmins
quelle
3
+1 - Es sollte beachtet werden, dass es keinen Grund gibt, dass Sie wirklich so etwas wie einen IoC-Container benötigen, um Schnittstellen effektiv zu nutzen.
Billy ONeal
Der einzige Vorteil, den ich erhalte, ist die Möglichkeit, ein Mocking-Framework zu verwenden.
Und Erweiterbarkeit. Die Testbarkeit ist ein netter Nebeneffekt eines niedrig gekoppelten, hochkohäsiven Systems. Indem Sie Schnittstellen weitergeben, beseitigen Sie die Besorgnis darüber, was die eigentliche Klasse tut. Sie sind nicht mehr mit , wie sie es tut, sondern nur , dass es behauptet , dass es tut , es zu tun. Dies erzwingt die Trennung von Bedenken und ermöglicht es Ihnen, sich auf die spezifischen Anforderungen der aktuellen Arbeit zu konzentrieren.
Michael Shimmins
@Sergio: Du brauchst so etwas, um irgendeine Art von Mock zu machen. Nicht nur Frameworks. @Michael: Ich dachte "DDD" wäre "Developer Driven Development" ... soll das TDD sein?
Billy ONeal
1
@ Billy - Ich nahm Domain Driven Development an
Michael Shimmins
3

Es bedeutet, generisch zu denken. Unspezifisch.

Angenommen, Sie haben eine Anwendung, die den Benutzer benachrichtigt, der ihm eine Nachricht sendet. Wenn Sie beispielsweise mit einer Schnittstellen-IMessage arbeiten

interface IMessage
{
    public void Send();
}

Sie können pro Benutzer die Art und Weise anpassen, in der er die Nachricht empfängt. Zum Beispiel möchte jemand mit einer E-Mail benachrichtigt werden, und Ihr IoC erstellt eine konkrete EmailMessage-Klasse. Einige andere möchten SMS, und Sie erstellen eine Instanz von SMSMessage.

In all diesen Fällen wird der Code zur Benachrichtigung des Benutzers niemals geändert. Auch wenn Sie eine weitere konkrete Klasse hinzufügen.

Lorenzo
quelle
@ Billy: der "O" Teil von [SOLID] ( en.wikipedia.org/wiki/Solid_(object-oriented_design) , danke!
Lorenzo
StackExchange hat Ihren Link
unterbrochen
@ Billy: Ups ... es hat die letzte Klammer gegessen. Dieser sollte Arbeit sein
Lorenzo
2

Der große Vorteil der Programmierung gegen Schnittstellen bei Unit-Tests besteht darin, dass Sie einen Code von allen Abhängigkeiten isolieren können, die Sie separat testen oder während des Tests simulieren möchten.

Ein Beispiel, das ich hier bereits erwähnt habe, ist die Verwendung einer Schnittstelle für den Zugriff auf Konfigurationswerte. Anstatt ConfigurationManager direkt zu betrachten, können Sie eine oder mehrere Schnittstellen bereitstellen, über die Sie auf Konfigurationswerte zugreifen können. Normalerweise würden Sie eine Implementierung bereitstellen, die aus der Konfigurationsdatei liest, aber zum Testen können Sie eine verwenden, die nur Testwerte zurückgibt oder Ausnahmen oder was auch immer auslöst.

Berücksichtigen Sie auch Ihre Datenzugriffsschicht. Wenn Ihre Geschäftslogik eng an eine bestimmte Datenzugriffsimplementierung gekoppelt ist, ist das Testen schwierig, ohne dass eine gesamte Datenbank mit den benötigten Daten zur Verfügung steht. Wenn Ihr Datenzugriff hinter Schnittstellen verborgen ist, können Sie nur die Daten angeben, die Sie für den Test benötigen.

Die Verwendung von Schnittstellen vergrößert die zum Testen verfügbare "Oberfläche" und ermöglicht feinkörnigere Tests, mit denen einzelne Einheiten Ihres Codes wirklich getestet werden.

Andrew Kennan
quelle
2

Testen Sie Ihren Code wie jemanden, der ihn nach dem Lesen der Dokumentation verwenden würde. Testen Sie nichts basierend auf Ihrem Wissen, da Sie den Code geschrieben oder gelesen haben. Sie möchten sicherstellen, dass sich Ihr Code wie erwartet verhält .

Im besten Fall sollten Sie Ihre Tests als Beispiele verwenden können. Doctests in Python sind hierfür ein gutes Beispiel.

Wenn Sie diese Richtlinien befolgen, sollte das Ändern der Implementierung kein Problem sein.

Auch nach meiner Erfahrung ist es empfehlenswert, jede "Schicht" Ihrer Anwendung zu testen. Sie haben atomare Einheiten, die an sich keine Abhängigkeiten haben, und Sie haben Einheiten, die von anderen Einheiten abhängen, bis Sie schließlich zu der Anwendung gelangen, die an sich eine Einheit ist.

Sie sollten jede Schicht testen und sich nicht darauf verlassen, dass Sie durch Testen von Einheit A auch Einheit B testen, von der Einheit A abhängt (die Regel gilt auch für die Vererbung). Auch dies sollte als Implementierungsdetail behandelt werden obwohl Sie vielleicht das Gefühl haben, sich zu wiederholen.

Beachten Sie, dass sich geschriebene Tests wahrscheinlich nicht ändern, während sich der zu testende Code fast definitiv ändert.

In der Praxis gibt es auch das Problem von E / A und der Außenwelt. Daher möchten Sie Schnittstellen verwenden, damit Sie bei Bedarf Mocks erstellen können.

In dynamischeren Sprachen ist dies kein so großes Problem. Hier können Sie Ententypisierung, Mehrfachvererbung und Mixins verwenden, um Testfälle zu erstellen. Wenn Sie anfangen, Vererbung im Allgemeinen nicht zu mögen, machen Sie es wahrscheinlich richtig.

DasIch
quelle
1

Dieser Screencast erklärt die agile Entwicklung und TDD in der Praxis für c #.

Wenn Sie gegen eine Schnittstelle codieren, können Sie in Ihrem Test ein Scheinobjekt anstelle des realen Objekts verwenden. Mit einem guten Mock-Framework können Sie in Ihrem Mock-Objekt alles tun, was Sie möchten.

BЈовић
quelle