Programmierung für die zukünftige Verwendung von Schnittstellen

42

Neben mir sitzt ein Kollege, der eine Benutzeroberfläche wie diese entworfen hat:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}

Das Problem ist, dass wir diesen "End" -Parameter derzeit nirgendwo in unserem Code verwenden. Er ist nur dort, weil wir ihn möglicherweise irgendwann in der Zukunft verwenden müssen.

Wir versuchen ihn davon zu überzeugen, dass es eine schlechte Idee ist, Parameter in Schnittstellen einzufügen, die momentan nicht von Nutzen sind, aber er besteht weiterhin darauf, dass eine Menge Arbeit erledigt werden muss, wenn wir irgendwann die Verwendung des Enddatums implementieren später und müssen dann den ganzen code anpassen.

Nun, meine Frage ist, gibt es Quellen, die sich mit einem Thema wie diesem "respektierten" Coding-Gurus befassen, mit dem wir ihn verknüpfen können?

sveri
quelle
29
"Vorhersagen sind sehr schwierig, insbesondere in Bezug auf die Zukunft."
Jörg W Mittag
10
Ein Teil des Problems ist, dass es nicht die beste Schnittstelle ist, um damit zu beginnen. Das Erhalten von Foos und das Filtern von Foos sind zwei separate Probleme. Diese Schnittstelle zwingt Sie, die Liste auf eine bestimmte Weise zu filtern. Ein viel allgemeinerer Ansatz ist die Übergabe einer Funktion, die entscheidet, ob das foo einbezogen werden soll. Dies ist jedoch nur elegant, wenn Sie Zugriff auf Java 8 haben.
Doval
5
Kontern ist Punkt. Stellen Sie diese Methode einfach so ein, dass sie benutzerdefinierte Typen akzeptiert, die im Moment nur das enthalten, was Sie benötigen, und fügen Sie dem endObjekt eventuell den Parameter hinzu, und verwenden Sie sogar die Standardeinstellung, um den Code nicht zu beschädigen
Rémi
3
Bei Java 8-Standardmethoden gibt es keinen Grund, unnötige Argumente hinzuzufügen. Eine Methode kann später mit einer Standardimplementierung hinzugefügt werden, die das Definierte mit beispielsweise aufruft null. Implementierende Klassen können dann nach Bedarf überschrieben werden.
Boris die Spinne
1
@Doval Ich weiß nichts über Java, aber in .NET werden Sie manchmal Dinge bemerken, die es vermeiden, die Macken von IQueryable(kann nur bestimmte Ausdrücke annehmen) Code außerhalb des DAL
Ben Aaronson

Antworten:

62

Laden Sie ihn ein, etwas über YAGNI zu erfahren . Der Begründungsteil der Wikipedia-Seite kann hier besonders interessant sein:

Nach Ansicht der Befürworter des YAGNI-Ansatzes hat die Versuchung, Code zu schreiben, der derzeit nicht notwendig ist, aber möglicherweise in der Zukunft liegt, die folgenden Nachteile:

  • Die aufgewendete Zeit wird für das Hinzufügen, Testen oder Verbessern der erforderlichen Funktionalität benötigt.
  • Die neuen Funktionen müssen getestet, dokumentiert und unterstützt werden.
  • Jede neue Funktion unterliegt Einschränkungen für zukünftige Aufgaben. Daher kann eine unnötige Funktion das Hinzufügen erforderlicher Funktionen in der Zukunft verhindern.
  • Bis die Funktion tatsächlich benötigt wird, ist es schwierig, ihre Funktion vollständig zu definieren und zu testen. Wenn die neue Funktion nicht ordnungsgemäß definiert und getestet wurde, funktioniert sie möglicherweise nicht ordnungsgemäß, auch wenn sie eventuell benötigt wird.
  • Das führt dazu, dass der Code aufgebläht wird. Die Software wird größer und komplizierter.
  • Wenn es keine Spezifikationen und Revisionskontrollen gibt, ist die Funktion Programmierern möglicherweise nicht bekannt, die davon Gebrauch machen könnten.
  • Das Hinzufügen der neuen Funktion kann andere neue Funktionen vorschlagen. Wenn diese neuen Funktionen ebenfalls implementiert werden, kann dies zu einem Schneeballeffekt in Richtung Feature Creep führen.

Andere mögliche Argumente:

  • „80% der Lebenszykluskosten ein Stück Software geht an Wartung“ . Das Schreiben von Code zur richtigen Zeit reduziert die Wartungskosten: Man muss weniger Code warten und kann sich auf den tatsächlich benötigten Code konzentrieren.

  • Quellcode wird einmal geschrieben, aber Dutzende Male gelesen. Ein zusätzliches Argument, das nirgendwo verwendet wird, würde zu einem zeitraubenden Verständnis führen, warum es ein Argument gibt, das nicht benötigt wird. Da dies eine Schnittstelle mit mehreren möglichen Implementierungen ist, wird die Sache nur schwieriger.

  • Es wird erwartet, dass der Quellcode selbstdokumentierend ist. Die tatsächliche Signatur ist irreführend, da ein Leser annehmen würde, dass dies endentweder das Ergebnis oder die Ausführung der Methode beeinflusst.

  • Personen, die konkrete Implementierungen dieser Schnittstelle schreiben, verstehen möglicherweise nicht, dass das letzte Argument nicht verwendet werden sollte, was zu unterschiedlichen Ansätzen führen würde:

    1. Ich brauche nicht end, also werde ich einfach seinen Wert ignorieren,

    2. Ich brauche nicht end, also werde ich eine Ausnahme auslösen, wenn es nicht ist null,

    3. Ich brauche nicht end, aber ich werde versuchen, es irgendwie zu benutzen,

    4. Ich werde viel Code schreiben, der später verwendet werden kann, wenn ender benötigt wird.

Beachten Sie jedoch, dass Ihr Kollege möglicherweise Recht hat.

Alle vorherigen Punkte basieren auf der Tatsache, dass das Refactoring einfach ist, sodass das spätere Hinzufügen eines Arguments keinen großen Aufwand erfordert. Dies ist jedoch eine Schnittstelle, und als Schnittstelle kann sie von mehreren Teams verwendet werden, die an anderen Teilen Ihres Produkts mitwirken. Das bedeutet, dass das Ändern einer Schnittstelle besonders schmerzhaft sein kann. In diesem Fall trifft YAGNI hier nicht wirklich zu.

Die Antwort von hjk bietet eine gute Lösung: Das Hinzufügen einer Methode zu einer bereits verwendeten Schnittstelle ist nicht besonders schwierig, hat aber in einigen Fällen auch erhebliche Kosten:

  • Einige Frameworks unterstützen keine Überladungen. Zum Beispiel und wenn ich mich gut erinnere (korrigiere mich, wenn ich falsch liege), unterstützt .NETs WCF keine Überladungen.

  • Wenn die Schnittstelle viele konkrete Implementierungen aufweist, müsste zum Hinzufügen einer Methode zur Schnittstelle alle Implementierungen durchlaufen und die Methode auch dort hinzugefügt werden.

Arseni Mourzenko
quelle
4
Ich denke, es ist auch wichtig zu überlegen, wie wahrscheinlich es ist, dass dieses Feature in Zukunft erscheint. Wenn Sie sicher wissen, dass es hinzugefügt wird, aber aus welchem ​​Grund auch immer, dann würde ich sagen, dass dies ein gutes Argument ist, das Sie im Hinterkopf behalten sollten, da es sich nicht nur um eine "für den Fall" -Funktion handelt.
Jeroen Vannevel
3
@JeroenVannevel: sicherlich, aber das ist sehr spekulativ. Nach meiner Erfahrung wurden die meisten Funktionen, von denen ich glaubte, dass sie tatsächlich implementiert werden, entweder für immer gestrichen oder verzögert. Dies schließt Funktionen ein, die ursprünglich von den Stakeholdern als sehr wichtig herausgestellt wurden.
Arseni Mourzenko
26

Er besteht jedoch weiterhin darauf, dass eine Menge Arbeit geleistet werden muss, wenn wir die Verwendung des Enddatums einige Zeit später implementieren und dann den gesamten Code anpassen müssen.

(etwas später)

public class EventGetter implements IEventGetter {

    private static final Date IMPLIED_END_DATE_ASOF_20140711 = new Date(Long.MAX_VALUE); // ???

    @Override
    public List<FooType> getFooList(String fooName, Date start) throws Exception {
        return getFooList(fooName, start, IMPLIED_END_DATE_ASOF_20140711);
    }

    @Override
    public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception {
        // Final implementation goes here
    }
}

Das ist alles was Sie brauchen, Methodenüberladung. Die zusätzliche Methode in der Zukunft kann transparent eingeführt werden, ohne dass Aufrufe der vorhandenen Methode beeinträchtigt werden.

hjk
quelle
6
Abgesehen davon, dass Ihre Änderung bedeutet, dass sie keine Schnittstelle mehr ist und nicht in Fällen verwendet werden kann, in denen ein Objekt bereits von einem Objekt abgeleitet ist und die Schnittstelle implementiert. Es ist ein triviales Problem, wenn sich alle Klassen, die die Schnittstelle implementieren, im selben Projekt befinden (Chase-Kompilierungsfehler). Wenn es sich um eine Bibliothek handelt, die von mehreren Projekten verwendet wird, führt das Hinzufügen des Feldes in den nächsten x Monaten / Jahren zu Verwirrung und Arbeit für andere Personen zu sporadischen Zeiten.
Kieveli
1
Wenn das OP-Team (außer dem Kollegen) glaubt, dass der endParameter jetzt wirklich unnötig ist, und die endMethode -less während des gesamten Projekts (der Projekte) angewendet hat, ist das Aktualisieren der Schnittstelle die geringste Sorge, da eine Aktualisierung der erforderlich wird Implementierungsklassen. Meine Antwort zielt speziell auf den Teil "Den gesamten Code anpassen" ab, da sich der Kollege so anhielt, als müssten die Aufrufer der ursprünglichen Methode während des gesamten Projekts (der Projekte) aktualisiert werden, um einen dritten Standard- / Dummy-Parameter einzuführen .
hjk
18

[Gibt es] "geachtete" Kodierungsgurus, mit denen wir ihn verknüpfen können [um ihn zu überzeugen]?

Appelle an die Behörde sind nicht besonders überzeugend; Es ist besser, ein Argument vorzulegen, das gültig ist, egal wer es gesagt hat.

Hier ist eine Reduktion ad absurdum , die überzeugen oder demonstrieren soll, dass Ihr Kollege ungeachtet seiner Sensibilität fest davon überzeugt ist, "richtig" zu sein:


Was Sie wirklich brauchen, ist

getFooList(String fooName, Date start, Date end, Date middle, 
           Date one_third, JulianDate start, JulianDate end,
           KlingonDate start, KlingonDate end)

Sie wissen nie, wann Sie für Klingonisch internationalisieren müssen. Deshalb sollten Sie sich jetzt darum kümmern, da die Nachrüstung viel Arbeit erfordert und Klingonen nicht für ihre Geduld bekannt sind.

msw
quelle
22
You never know when you'll have to internationalize for Klingon. Aus diesem Grund sollten Sie einfach ein übergeben Calendarund den Clientcode entscheiden lassen, ob er ein GregorianCalendaroder ein senden möchte KlingonCalendar(ich wette, einige haben bereits eines gesendet). Duh. :-p
SJuan76
3
Was ist mit StarDate sdStartund StarDate sdEnd? Wir können die Föderation schließlich nicht vergessen ...
WernerCD
All dies sind offensichtlich entweder Unterklassen von oder umwandelbar in Date!
Darkhogg
Wenn es nur so einfach wäre .
msw
@msw, ich bin mir nicht sicher, ob er nach einem "Aufruf an die Behörde" gefragt hat, als er nach Links zu "angesehenen Codiergurus" gefragt hat. Wie Sie sagen, braucht er "ein Argument, das gültig ist, egal wer es gesagt hat". Leute vom "Guru" -Typ neigen dazu, viele von ihnen zu kennen. Auch du hast vergessen HebrewDate startund HebrewDate end.
Trysis
12

Aus der Perspektive der Softwareentwicklung glaube ich, dass die richtige Lösung für diese Art von Problemen im Builder-Muster liegt. Dies ist definitiv ein Link von 'Guru'-Autoren für Ihren Kollegen http://en.wikipedia.org/wiki/Builder_pattern .

Im Builder-Muster erstellt der Benutzer ein Objekt, das die Parameter enthält. Dieser Parametercontainer wird dann an die Methode übergeben. Dadurch werden die Parameter, die Ihr Kollege in Zukunft benötigt, nicht mehr erweitert und überlastet, und das Ganze wird sehr stabil, wenn Änderungen vorgenommen werden müssen.

Ihr Beispiel wird:

public interface IEventGetter {
    public List<FooType> getFooList(ISearchFooRequest req) {
        throws Exception;
    ....
    }
}

public interface ISearchFooRequest {
        public String getName();
        public Date getStart();
        public Date getEnd();
        public int getOffset();
        ...
    }
}

public class SearchFooRequest implements ISearchFooRequest {

    public static SearchFooRequest buildDefaultRequest(String name, Date start) {
        ...
    }

    public String getName() {...}
    public Date getStart() {...}
    ...
    public void setEnd(Date end) {...}
    public void setOffset(int offset) {...}
    ...
}
InformedA
quelle
3
Dies ist ein guter allgemeiner Ansatz, aber in diesem Fall scheint sich das Problem nur von IEventGetterzu zu verschieben ISearchFootRequest. Ich würde stattdessen so etwas wie public IFooFiltermit member vorschlagen, public bool include(FooType item)das zurückgibt, ob der Artikel enthalten sein soll oder nicht. Anschließend können einzelne Schnittstellenimplementierungen entscheiden, wie diese Filterung durchgeführt wird
Ben Aaronson,
@ Ben Aaronson Ich bearbeite gerade den Code, um zu zeigen, wie das Request-Parameter-Objekt erstellt wird
InformedA
Der Builder ist hier unnötig (und ehrlich gesagt, ein überstrapaziertes / zu empfohlenes Muster im Allgemeinen). Es gibt keine komplexen Regeln für die Einrichtung der Parameter. Daher ist ein Parameterobjekt in Ordnung, wenn berechtigte Bedenken hinsichtlich der Komplexität oder häufigen Änderung der Methodenparameter bestehen. Und ich stimme mit @BenAaronson überein, obwohl der Sinn der Verwendung eines Parameterobjekts darin besteht , die unnötigen Parameter nicht von Anfang an einzuschließen, sondern sie später sehr einfach hinzuzufügen (selbst wenn es mehrere Schnittstellenimplementierungen gibt).
Aaronaught
7

Sie brauchen es jetzt nicht, also fügen Sie es nicht hinzu. Wenn Sie es später benötigen, erweitern Sie die Schnittstelle:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start)
         throws Exception;
    ....

}

public interface IBoundedEventGetter extends IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}
ToddR
quelle
+1, wenn die vorhandene (und wahrscheinlich bereits getestete / gelieferte) Schnittstelle nicht geändert wurde.
Sebastian Godelet
4

Da kein Konstruktionsprinzip absolut ist, stimme ich den anderen Antworten größtenteils zu, dachte aber, ich würde Teufelsanwalt sein und einige Bedingungen diskutieren, unter denen ich die Lösung Ihres Kollegen akzeptieren würde:

  • Wenn es sich um eine öffentliche API handelt und Sie davon ausgehen, dass die Funktion für Drittanbieter nützlich ist, auch wenn Sie sie nicht intern verwenden.
  • Wenn es erhebliche unmittelbare Vorteile hat, nicht nur zukünftige. Wenn das implizite Enddatum lautet Now(), wird durch Hinzufügen eines Parameters ein Nebeneffekt beseitigt, der sich positiv auf Caching und Komponententests auswirkt. Vielleicht ermöglicht es eine einfachere Implementierung. Oder vielleicht ist es konsistenter mit anderen APIs in Ihrem Code.
  • Wenn Ihre Entwicklungskultur eine problematische Vorgeschichte hat. Wenn es aufgrund von Prozessen oder der Territorialität zu schwierig ist, etwas Zentrales wie eine Schnittstelle zu ändern, wurde zuvor festgestellt, dass die Benutzer clientseitige Problemumgehungen implementieren, anstatt die Schnittstelle zu ändern. Dann versuchen Sie, ein Dutzend Ad-hoc-Enddaten beizubehalten Filter anstelle von einem. Wenn so etwas in Ihrem Unternehmen häufig vorkommt, ist es sinnvoll, sich ein wenig mehr um die Zukunftssicherheit zu bemühen. Verstehen Sie mich nicht falsch, es ist besser , Ihre Entwicklungsprozesse zu ändern, aber dies ist in der Regel leichter gesagt als getan.

Allerdings ist meiner Erfahrung nach die größte Folge von YAGNI YDKWFYN: Sie wissen nicht, welche Form Sie benötigen (ja, ich habe mir gerade dieses Akronym ausgedacht). Auch wenn die Verwendung eines Grenzwertparameters relativ vorhersehbar ist, kann dies in Form eines Seitengrenzwerts, einer Anzahl von Tagen oder eines Booleschen Werts erfolgen, der angibt, dass das Enddatum aus einer Benutzervoreinstellungstabelle oder einer beliebigen Anzahl von Dingen verwendet werden soll.

Da Sie die Anforderung noch nicht haben, wissen Sie nicht, welcher Typ dieser Parameter sein soll. Oft haben Sie entweder eine umständliche Benutzeroberfläche, die nicht ganz Ihren Anforderungen entspricht, oder Sie müssen sie ohnehin ändern.

Karl Bielefeldt
quelle
2

Es gibt nicht genügend Informationen, um diese Frage zu beantworten. Es kommt darauf an, was getFooListgenau und wie es funktioniert.

Hier ist ein naheliegendes Beispiel für eine Methode, die einen zusätzlichen Parameter unterstützen soll, der verwendet wird oder nicht.

void CapitalizeSubstring (String capitalize_me, int start_index);

Das Implementieren einer Methode für eine Sammlung, bei der Sie den Anfang, aber nicht das Ende der Sammlung angeben können, ist oft albern.

Sie müssen sich wirklich das Problem selbst ansehen und fragen, ob der Parameter im Kontext der gesamten Schnittstelle unsinnig ist und wie viel Belastung der zusätzliche Parameter tatsächlich mit sich bringt.

QuestionC
quelle
1

Ich fürchte, Ihr Kollege hat vielleicht tatsächlich einen sehr gültigen Punkt. Obwohl seine Lösung eigentlich nicht die beste ist.

Aus seiner vorgeschlagenen Schnittstelle geht klar hervor, dass

public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception;

gibt Instanzen zurück, die innerhalb eines bestimmten Zeitintervalls gefunden wurden. Wenn Clients derzeit den End-Parameter nicht verwenden, ändert dies nichts an der Tatsache, dass sie Instanzen erwarten, die innerhalb eines bestimmten Zeitintervalls gefunden wurden. Dies bedeutet nur, dass derzeit alle Clients endlose Intervalle verwenden (von Anfang bis Ewigkeit).

Eine bessere Schnittstelle wäre also:

public List<FooType> getFooList(String fooName, Interval interval) throws Exception;

Wenn Sie das Intervall mit einer statischen Factory-Methode angeben:

public static Interval startingAt(Date start) { return new Interval(start, null); }

Dann haben die Kunden nicht einmal das Gefühl, eine Endzeit angeben zu müssen.

Gleichzeitig vermittelt Ihr Interface korrekter, was es tut, da getFooList(String, Date)nicht mitteilt, dass es sich um ein Intervall handelt.

Beachten Sie, dass mein Vorschlag von dem ausgeht, was die Methode derzeit tut, und nicht von dem, was sie in Zukunft tun sollte oder könnte, und daher das YAGNI-Prinzip (das in der Tat sehr gültig ist) hier nicht gilt.

bowmore
quelle
0

Das Hinzufügen eines nicht verwendeten Parameters ist verwirrend. Die Leute könnten diese Methode aufrufen, vorausgesetzt, diese Funktion wird funktionieren.

Ich würde es nicht hinzufügen. Es ist trivial, es später mit einem Refactoring hinzuzufügen und die Anrufstellen mechanisch zu reparieren. In einer statisch getippten Sprache ist dies einfach zu tun. Es ist nicht notwendig, es eifrig hinzuzufügen.

Wenn es ist ein Grund , dass zu haben Parameter Sie die Verwirrung verhindern: eine Assertion bei der Umsetzung dieser Schnittstelle hinzufügen zu erzwingen , dass dieser Parameter als Standardwert übergeben werden. Wenn jemand zumindest diesen Parameter versehentlich verwendet, merkt er beim Testen sofort, dass diese Funktion nicht implementiert ist. Dies verringert die Gefahr, dass ein Fehler in die Produktion gerät.

usr
quelle