Programmation pour l'utilisation future des interfaces

42

J'ai un collègue assis à côté de moi qui a conçu une interface comme celle-ci:

public interface IEventGetter {

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

}

Le problème est que, pour le moment, nous n'utilisons pas ce paramètre "end" dans notre code, mais simplement parce que nous aurons peut-être besoin de l'utiliser ultérieurement.

Nous essayons de le convaincre que c’est une mauvaise idée d’introduire des paramètres dans des interfaces qui ne sont d'aucune utilité pour le moment, mais il continue d'insister sur le fait que beaucoup de travail devra être fait si nous implémentons l'utilisation de la "date de fin" quelque temps plus tard et doivent adapter tout le code alors.

Maintenant, ma question est la suivante: existe-t-il des sources traitant d'un sujet comme celui des gourous du code "respectés", vers lesquelles nous pouvons le relier?

Sveri
la source
29
"La prévision est très difficile, surtout pour l'avenir."
Jörg W Mittag
10
Une partie du problème est que ce n'est pas la meilleure interface pour commencer. Obtenir des Foos et les filtrer sont deux problèmes distincts. Cette interface vous oblige à filtrer la liste d’une manière particulière; une approche beaucoup plus générale consiste à passer à une fonction qui décide si le foo doit être inclus. Cependant, ceci n’est élégant que si vous avez accès à Java 8.
Doval
5
Contrer est le point. Assurez-vous simplement que cette méthode acceptant le type personnalisé ne contienne pour l'instant que ce dont vous avez besoin et pourrait éventuellement ajouter le endparamètre à cet objet et même le modifier par défaut afin de ne pas rompre le code
Rémi
3
Avec les méthodes Java 8 par défaut, il n’ya aucune raison d’ajouter des arguments inutiles. Une méthode peut être ajoutée ultérieurement avec une implémentation par défaut utilisant les appels simples définis avec, par exemple null. Les classes d'implémentation peuvent alors remplacer si nécessaire.
Boris l'araignée
1
@Doval Je ne connais pas Java, mais dans .NET, on voit parfois des choses comme éviter d'exposer les bizarreries de IQueryable(on ne peut prendre que certaines expressions) à du code en dehors de la DAL
Ben Aaronson

Réponses:

62

Invitez-le à en savoir plus sur YAGNI . La partie "justification" de la page Wikipedia peut être particulièrement intéressante ici:

Selon ceux qui préconisent l’approche YAGNI, la tentation d’écrire du code qui n’est pas nécessaire pour le moment, mais qui pourrait l’être à l’avenir, présente les inconvénients suivants:

  • Le temps consacré à l'ajout, au test ou à l'amélioration des fonctionnalités nécessaires est pris.
  • Les nouvelles fonctionnalités doivent être déboguées, documentées et prises en charge.
  • Toute nouvelle fonctionnalité impose des contraintes sur ce qui peut être fait dans le futur, ainsi une fonctionnalité inutile peut empêcher les fonctionnalités nécessaires d'être ajoutées à l'avenir.
  • Tant que cette fonctionnalité n’est pas réellement nécessaire, il est difficile de définir complètement ce qu’elle doit faire et de la tester. Si la nouvelle fonctionnalité n'est pas correctement définie et testée, elle risque de ne pas fonctionner correctement, même si cela est éventuellement nécessaire.
  • Cela conduit à un gonflement du code; le logiciel devient plus grand et plus compliqué.
  • À moins de spécifications et d'une sorte de contrôle de révision, les programmeurs pourraient ne pas connaître cette fonctionnalité.
  • L'ajout de la nouvelle fonctionnalité peut suggérer d'autres nouvelles fonctionnalités. Si ces nouvelles fonctionnalités sont également implémentées, cela pourrait se traduire par un effet boule de neige.

Autres arguments possibles:

  • «80% du coût à vie d'un logiciel est destiné à la maintenance» . Écrire du code juste à temps réduit le coût de la maintenance: il faut maintenir moins de code et se concentrer sur le code réellement nécessaire.

  • Le code source est écrit une fois, mais lu des dizaines de fois. Un argument supplémentaire, non utilisé nulle part, conduirait à une perte de temps en termes de compréhension du pourquoi d’un argument qui n’est pas nécessaire. Étant donné qu'il s'agit d'une interface avec plusieurs implémentations possibles, les choses ne sont que plus difficiles.

  • Le code source devrait être auto-documenté. La signature elle-même est trompeuse, car le lecteur penserait que cela endaffecte le résultat ou l'exécution de la méthode.

  • Les personnes écrivant des implémentations concrètes de cette interface peuvent ne pas comprendre que le dernier argument ne devrait pas être utilisé, ce qui conduirait à des approches différentes:

    1. Je n'ai pas besoin end, alors je vais simplement ignorer sa valeur,

    2. Je n'ai pas besoin end, alors je vais lancer une exception si ce n'est pas le cas null,

    3. Je n'ai pas besoin end, mais je vais essayer de l'utiliser,

    4. J'écrirai beaucoup de code qui pourrait être utilisé plus tard, quand ce endsera nécessaire.

Mais sachez que votre collègue a peut-être raison.

Tous les points précédents sont basés sur le fait que la refactorisation est facile, donc ajouter un argument plus tard ne nécessitera pas beaucoup d'effort. Mais il s’agit d’une interface. Elle peut être utilisée par plusieurs équipes contribuant à d’autres parties de votre produit. Cela signifie que changer une interface peut être particulièrement pénible, auquel cas, YAGNI ne s'applique pas vraiment ici.

La réponse de hjk donne une bonne solution: ajouter une méthode à une interface déjà utilisée n’est pas particulièrement difficile, mais dans certains cas, cela a aussi un coût substantiel:

  • Certains frameworks ne supportent pas les surcharges. Par exemple, et si je me souviens bien (corrigez-moi si je me trompe), le WCF de .NET ne prend pas en charge les surcharges.

  • Si l'interface comporte de nombreuses implémentations concrètes, l'ajout d'une méthode à l'interface nécessiterait de passer en revue toutes les implémentations et d'ajouter la méthode également.

Arseni Mourzenko
la source
4
Je pense qu'il est également important de considérer la probabilité que cette fonctionnalité apparaisse dans le futur. Si vous savez avec certitude qu'il sera ajouté, mais pas pour le moment pour une raison quelconque, je dirais que c'est un bon argument à garder à l'esprit, car il ne s'agit pas simplement d'une fonctionnalité "au cas où".
Jeroen Vannevel
3
@JeroenVannevel: certes, mais c'est très spéculatif. D'après mon expérience, la plupart des fonctionnalités que je pensais certainement implémenter ont été annulées ou retardées à jamais. Cela inclut des fonctionnalités qui ont été à l’origine soulignées comme étant très importantes par les parties prenantes.
Arseni Mourzenko
26

mais il continue d'insister sur le fait qu'il reste beaucoup de travail à faire si nous implémentons l'utilisation de la "date de fin" quelque temps plus tard et si nous devons alors adapter tout le code.

(un peu plus tard)

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
    }
}

C'est tout ce dont vous avez besoin, la surcharge de méthodes. La méthode supplémentaire à l'avenir peut être introduite de manière transparente sans affecter les appels à la méthode existante.

hjk
la source
6
Sauf que votre modification signifie que ce n'est plus une interface et qu'il ne sera plus possible de l'utiliser dans les cas où un objet dérive déjà d'un objet et implémente l'interface. C'est un problème trivial si toutes les classes qui implémentent l'interface sont dans le même projet (chasser les erreurs de compilation). S'il s'agit d'une bibliothèque utilisée par plusieurs projets, l'ajout du champ crée de la confusion et du travail pour d'autres personnes à des moments sporadiques au cours des x prochains mois / années.
Kieveli
1
Si l'équipe d'OP (sauver le collègue) croit vraiment que le endparamètre est inutile maintenant et a continué à utiliser la endméthode -less tout au long du projet, la mise à jour de l'interface est le moindre de leurs soucis car il devient nécessaire de mettre à jour le classes d'implémentation. Ma réponse vise spécifiquement la partie "adapter tout le code", car il semblerait que le collègue pensait devoir mettre à jour les appelants de la méthode d'origine tout au long du projet pour introduire un troisième paramètre default / dummy. .
hjk
18

[Y a-t-il] des gourous du code "respectés" que nous pouvons associer à [pour le persuader]?

Les appels à l'autorité ne sont pas particulièrement convaincants; préférable de présenter un argument qui est valable, peu importe qui l'a dit.

Voici une reductio ad absurdum qui devrait persuader ou démontrer que votre collègue est bloqué pour avoir "raison" indépendamment de sa sensibilité:


Ce dont tu as vraiment besoin c'est

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

Vous ne savez jamais quand vous devrez internationaliser pour Klingon. Vous devez donc vous en occuper maintenant, car il faudra beaucoup de travail pour le mettre à niveau et les Klingons ne sont pas réputés pour leur patience.

msw
la source
22
You never know when you'll have to internationalize for Klingon. C'est pourquoi vous devriez simplement passer un Calendar, et laisser le code client décider s'il veut envoyer un GregorianCalendarou un KlingonCalendar(je parie que certains l'ont déjà fait). Duh. :-p
SJuan76
3
Qu'en est-il StarDate sdStartet StarDate sdEnd? Nous ne pouvons pas oublier la Fédération après tout ...
WernerCD
Tous ceux sont évidemment soit des sous-classes de ou convertibles en Date!
Darkhogg
Si seulement c'était aussi simple .
msw
@msw, je ne suis pas sûr qu'il ait demandé un "appel à l'autorité" lorsqu'il a demandé des liens vers des "gourous du codage respectés". Comme vous le dites, il a besoin "d'un argument qui soit valable, peu importe qui l'a dit". Les personnes de type "gourou" ont tendance à en connaître beaucoup. Aussi tu as oublié HebrewDate startet HebrewDate end.
Trysis
12

Du point de vue du génie logiciel, je pense que la solution appropriée à ce type de problèmes réside dans le modèle de constructeur. C'est certainement un lien des auteurs de 'gourou' pour votre collègue http://en.wikipedia.org/wiki/Builder_pattern .

Dans le modèle de générateur, l'utilisateur crée un objet contenant les paramètres. Ce conteneur de paramètres sera ensuite passé à la méthode. Cela évitera toute extension et surcharge des paramètres dont votre collègue aurait besoin à l'avenir tout en rendant le tout très stable lorsque des changements doivent être apportés.

Votre exemple deviendra:

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) {...}
    ...
}
InforméA
la source
3
Ceci est une bonne approche générale, mais dans ce cas semble simplement pousser le problème IEventGetterà ISearchFootRequest. Je suggérerais plutôt quelque chose comme public IFooFilteravec membre public bool include(FooType item)qui retourne s'il faut inclure l'élément. Ensuite, les implémentations d’interfaces individuelles peuvent décider de la manière dont elles filtreront
Ben Aaronson
@Ben Aaronson Je viens d'éditer le code pour montrer comment un objet de paramètre de requête est créé
InformedA
Le constructeur est inutile ici (et très franchement, un schéma surutilisé / sur-recommandé en général); il n'y a pas de règles complexes sur la manière dont les paramètres doivent être configurés, donc un objet paramètre convient parfaitement s'il existe une inquiétude légitime quant à la complexité ou à la modification fréquente des paramètres de la méthode. Et je suis d’accord avec @BenAaronson, bien que l’intérêt d’utiliser un objet paramètre ne soit pas d’inclure les paramètres inutiles dès le départ, mais de les rendre très faciles à ajouter plus tard (même s’il existe plusieurs implémentations d’interface).
Aaronaught
7

Vous n'en avez pas besoin maintenant, alors ne l'ajoutez pas. Si vous en avez besoin plus tard, étendez l'interface:

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
la source
+1 pour ne pas modifier l'interface existante (et probablement déjà testée / livrée).
Sebastian Godelet
4

Aucun principe de conception n'est absolu. Par conséquent, bien que je sois généralement d'accord avec les autres réponses, je pensais que je deviendrais l'avocat du diable et discuterais de certaines conditions dans lesquelles j'accepterais d'accepter la solution de votre collègue:

  • S'il s'agit d'une API publique et que vous pensez que cette fonctionnalité sera utile aux développeurs tiers, même si vous ne l'utilisez pas en interne.
  • S'il présente des avantages immédiats considérables, pas seulement ceux à venir. Si la date de fin implicite est Now(), l'ajout d'un paramètre supprime un effet secondaire, ce qui présente des avantages pour la mise en cache et les tests unitaires. Peut-être que cela permet une implémentation plus simple. Ou peut-être est-il plus cohérent avec les autres API de votre code.
  • Si votre culture de développement a une histoire problématique. Si les processus ou la territorialité rendent trop difficile la modification d'un élément central tel qu'une interface, ce que j'ai déjà vu se passe avant que des personnes implémentent des solutions de contournement côté client au lieu de modifier l'interface, vous essayez de conserver une douzaine de dates de fin ad hoc. filtres au lieu d'un. Si ce genre de chose se produit souvent dans votre entreprise, il est logique de consacrer un peu plus d'effort à la mise à l'épreuve du futur. Ne vous méprenez pas, il est préférable de modifier vos processus de développement, mais c'est généralement plus facile à dire qu'à faire.

Cela étant dit, selon mon expérience, le plus grand corollaire de YAGNI est YDKWFYN: Vous ne savez pas de quelle forme vous aurez besoin (oui, je viens de créer cet acronyme). Même si le besoin d' un paramètre limite peut être relativement prévisible, il peut prendre la forme d'une limite de pages, d'un nombre de jours ou d'un booléen spécifiant l'utilisation de la date de fin à partir d'un tableau de préférences utilisateur, ou un nombre important d'éléments.

Étant donné que vous n'avez pas encore l'exigence, vous n'avez aucun moyen de savoir quel type doit être ce paramètre. Vous vous retrouvez souvent avec une interface délicate qui ne correspond pas tout à fait à votre exigence, ou devant la modifier de toute façon.

Karl Bielefeldt
la source
2

Il n'y a pas assez d'informations pour répondre à cette question. Cela dépend de ce que getFooListfait réellement et de la façon dont il le fait.

Voici un exemple évident de méthode qui devrait supporter un paramètre supplémentaire, utilisé ou non.

void CapitalizeSubstring (String capitalize_me, int start_index);

Implémenter une méthode qui opère sur une collection où vous pouvez spécifier le début mais pas la fin de la collection est souvent ridicule.

Il faut vraiment examiner le problème lui-même et se demander si le paramètre est absurde dans le contexte de toute l'interface et quel est le fardeau imposé par le paramètre supplémentaire.

QuestionC
la source
1

J'ai bien peur que votre collègue ait un argument très valable. Bien que sa solution ne soit en réalité pas la meilleure.

De son interface proposée, il est clair que

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

renvoie des instances trouvées dans un intervalle de temps. Si les clients n'utilisent pas actuellement le paramètre end, cela ne change rien au fait qu'ils s'attendent à des instances trouvées dans un intervalle de temps. Cela signifie simplement que tous les clients utilisent actuellement des intervalles ouverts (du début à la fin).

Donc, une meilleure interface serait:

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

Si vous fournissez intervalle avec une méthode fabrique statique:

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

alors les clients ne ressentent même plus le besoin de spécifier une heure de fin.

Dans le même temps, votre interface indique plus correctement ce qu’elle fait, car getFooList(String, Date)elle ne communique pas qu’elle traite d’un intervalle.

Notez que ma suggestion découle de ce que la méthode fait actuellement, et non de ce qu'elle devrait ou pourrait faire à l'avenir, et en tant que tel, le principe de YAGNI (qui est en effet très valable) ne s'applique pas ici.

Bowmore
la source
0

Ajouter un paramètre inutilisé est déroutant. Les gens peuvent appeler cette méthode en supposant que cette fonctionnalité fonctionnera.

Je ne l'ajouterais pas. Il est trivial de l'ajouter plus tard en utilisant une refactorisation et en réparant mécaniquement les sites d'appels. Dans un langage typé statiquement, cela est facile à faire. Pas nécessaire de l'ajouter avec empressement.

S'il est une raison d'avoir ce paramètre , vous pouvez éviter la confusion: Ajouter une assertion dans la mise en œuvre de cette interface pour faire respecter que ce paramètre soit passé comme la valeur par défaut. Si quelqu'un utilise accidentellement ce paramètre au moins, il remarquera immédiatement lors des tests que cette fonctionnalité n'est pas implémentée. Cela supprime le risque de glisser un bogue dans la production.

usr
la source