web-dev-qa-db-fra.com

Dois-je quand même suivre "la programmation vers une interface et non l'implémentation" même si je pense que l'utilisation de membres de classe concrets est la solution la plus simple?

Selon Comprendre la "programmation vers une interface" , si je comprends bien, je pense que je ne devrais dépendre que de la classe abstraite. Cependant, dans certains cas, par exemple, Student:

public class Student {
    private String name;
    private int age;
}

pour le modifier afin qu'il ne dépende que de la classe abstraite (qui MyIString peut être une nouvelle classe abstraite qui enveloppe String):

public class Student {
    private MyIString name;
    private Java.lang.Number age;
}

Je pense que celui modifié est plus complexe. Et un exemple plus "réel", disons Address:

public class Address {
    private ZipCode zipcode;
}

dont j'ai besoin d'un seul type de ZipCode, mais si je le modifie comme:

public class Address {
    private IZipCode zipcode;
}

qui IZipCode est une interface, alors je pense que cela peut induire en erreur d'autres coéquipiers qu'il y aurait d'autres types de ZipCodes.

Je pense que les cas ci-dessus deviennent plus complexes et moins maintenables si on me permettait à une classe d'utiliser uniquement des membres de classe abstraits. Donc ma question est, dois-je quand même suivre "programmation vers une interface pas implémentation" si celle "suivie" devient plus complexe (à mon avis)?

34
aacceeggiikk

Programmation vers une interface signifie que vous devez vous concentrer sur ce que fait le code, pas comment il est réellement implémenté. Voir réponse de Telastyn à la compréhension de la "programmation vers une interface" . Les classes d'interface aident à appliquer cette directive, mais cela ne signifie pas que vous ne devez jamais utiliser de classes concrètes.

J'aime vraiment l'exemple du code postal:

En 1963, le United States Postal Service introduit des codes postaux composés de cinq chiffres. Vous pouvez maintenant avoir la (mauvaise) idée qu'un entier est une représentation suffisante et l'utiliser dans tout votre code. 1983, un nouveau format de code postal est introduit. Il utilise 5 chiffres, un tiret et 4 autres chiffres. Du coup, les codes postaux ne sont plus des entiers et vous devez changer tous les endroits qui utilisent des entiers pour les codes postaux en autre chose, par exemple, en chaînes. Cette refactorisation aurait pu être évitée si une classe ZipCode désignée aurait été utilisée. Ensuite, seule la représentation interne de la classe ZipCode devait être modifiée et ses usages seraient restés les mêmes.

Utiliser une classe ZipCode est déjà programmer sur une interface: Au lieu de programmer contre une implémentation concrète ("entier"/"chaîne"), vous programmez contre une certaine abstraction ("Zip code").

Si nécessaire, vous pouvez ajouter un autre niveau d'abstraction: vous devrez peut-être prendre en charge différents formats de code postal pour différents pays. Par exemple, vous pouvez ajouter une interface IPostalCode avec des implémentations comme PostalCodeUS (qui est juste le ZipCode d'en haut), PostalCodeIreland, PostalCodeNetherlands etc. Cependant, trop de niveaux d'abstraction peuvent également rendre le code plus complexe et plus difficile à raisonner et je pense que cela dépend des exigences de votre application, que vous souhaitiez utiliser une classe ZipCode ou IPostalCode interface. Peut-être qu'un ZipCode qui utilise en interne une chaîne (unicode) est assez bon pour tous les pays et que les éléments spécifiques au pays, par exemple, la validation, peuvent être gérés en dehors de la classe dans certains PostalCodeValidator/IPostalCodeValidator.

76
pschill

"Programmation vers une interface" ne nécessite pas le mot-clé de langue interface. Cela signifie que vous vous souciez de ce que le type promet sur son comportement.

Peu vous importe commentJava.lang.String fait tout ce qu'il fait, seulement que vous pouvez écrire

name = "aacceeggiikk";
age = 42;

"Programmer pour une implémentation" serait utiliser la réflexion pour atteindre les membres privés de String, et les tripoter. Et puis, bouleversé qu'une mise à jour ait cassé votre code parce que vous comptiez sur quelque chose qui a été changé.

47
Caleth

Ce n'est qu'une ligne directrice

Tout comme un concept comme KISS est une ligne directrice.

Si vous suivez la directive "programmer vers une interface, pas une implémentation", vous violeriez le principe KISS. Mais en créant un tas de classes simples qui répètent du code ou des actions, vous violeriez le principe SEC .

Il n'y a aucun moyen d'adhérer à chaque directive, principe ou "règle" unique. Vous devez savoir quel équilibre est logique pour l'application votre.

D'après ma propre expérience, essayer de rendre tout à l'échelle de l'entreprise avec des couches abstraites au-dessus des couches abstraites est souvent excessif et inutilement complexe pour de nombreuses applications.

10
Ivo Coumans
  1. La programmation sur une interface concerne des objets, des entités qui peuvent également avoir des propriétés de type primitif comme booléen, int, BigDecimal et String, pour n'en nommer que quelques-uns. A ce niveau la programmation vers une interface ne s'applique plus.

  2. Il existe cependant un autre principe, basé sur l'expérience que les types primitifs sont utilisés là où il devrait y avoir un wrapper pour une classe de valeur plus abstraite. Donc au lieu

    String telephone;
    

    vous pourriez mieux avoir une classe de valeur Téléphone:

    Telephone telephone.
    

    Cela peut prendre en charge le numéro de pays, la forme canonique, l'égalité et autres. Encore une fois, Telephone pourrait être une interface et il pourrait y avoir une classe d'implémentation TelephoneImpl, mais je - heureusement - le vois rarement.

    Encore mieux, lorsque plusieurs champs primitifs peuvent être remplacés par une classe plus abstraite, comme FullName, disons avec int similarityPercentage(FullName other).

  3. Il convient peut-être de mentionner que les objets internes avec des implémentations internes peuvent utiliser des interfaces. Un List possède une interface Iterator iterator() implémentant un objet local avec accès au conteneur de liste. Mais ici, l'interface fait partie de la solution, pas le style de codage.

En résumé:

La programmation par interface dissocie la classe d'implémentation, autorise plusieurs implémentations, a une seule responsabilité en tant qu'API. Cela concerne le comportement, les méthodes. Prenons l'exemple de JDBC.

Les champs sont des propriétés concrètes. Ce sont des données . Ils doivent également être abstraits, masquant l'implémentation à l'aide d'un nom de classe et fournissant des méthodes d'accès, pour une utilisation correcte. Cependant, c'est un niveau différent. Prenez les classes LocalDate, LocalDateTime, YearMonth au lieu de l'ancienne Date pour tous.

7
Joop Eggen

Pour citer un de mes collègues - " Vous pouvez passer des heures à concevoir une voiture sur un ordinateur, mais à un moment donné, les roues doivent effectivement toucher le sol. ".

c'est-à-dire que tout ne peut/ne doit pas être résumé, à un moment donné, vous avez besoin d'un type concret.

Pour votre classe avec votre code postal, la question devrait vraiment être ce que est un code postal (en termes de votre application)?

Answer - c'est une chaîne, donc la propriété doit être une chaîne.

public class Address {
     public String ZipCode...
}

P.S. ne commencez pas à résumer les types de langage sous-jacents - de cette façon réside la folie!

Il y a un certain nombre de réponses/commentaires ici commentant la variété des codes postaux/zip dans différents pays et c'est vrai, cependant, ils sont tous finalement des chaînes.

Espérons que cette classe d'adresse ne se soucie pas de ce qu'il y a dans le code postal (juste que c'est une chaîne) et il y a une autre classe que vous utilisez pour valider votre adresse (et pouvoir désactiver l'implémentation de que classe pourrait bien être utile).

Donc, programmez vers une interface où cela a du sens et particulièrement où l'implémentation peut avoir besoin d'être modifiée ou le comportement injecté. N'utilisez pas d'interface pour tout!

6
Paddy

Dans votre exemple d'adresse/ZipCode:

public class Address {
private ZipCode zipcode;
}

Vous attacher à une implémentation concrète au lieu d'une interface pose 3 problèmes:

  • A - Internationalisation - Un exemple Le code postal canadien est quelque chose comme V1C3X8, les adresses des autres pays sont à nouveau différentes. Bonne chance pour stocker les adresses de facturation de vos clients dans une économie mondialisée.
  • B - Un changement dans l'implémentation, c'est pourquoi tout devrait être consommé comme une interface - Vous pourriez décider un jour qu'il est plus efficace de stocker des adresses en tant que GeoCode What Three Words stocké sous forme de nombre et d'étage/numéro d'unité pour un immeuble à plusieurs étages. Par conséquent, l'adresse peut être modifiée à partir de quelque chose comme ceci:
public class Address : IAddress 
{
    public IZipCode ZipCode{get; private set;}
    public string StreetName{get;private set}
}

Pour quelque chose comme:

public class Address : IAddress 
{
    public Address(IGeoCodeService geoCodeService/*Dependency injected*/){........}

    private long What3WordsGeoCodeAsLong {get; set;}

    public IZipCode ZipCode => GetZipCodeFromGeoCode();//calls out to a service to 
    public string StreetName => GetStreetNameFromGeoCode();
}

Lorsqu'elle est sérialisée, la deuxième implémentation est beaucoup plus petite à stocker et à l'abri d'être périmée si un pays décide de déplacer les zones de code postal, ce qui se produit plus souvent que vous ne le pensez (avec un compromis d'avoir à utiliser un service pour rechercher un code postal à chaque fois.), alors que tout ce qui utilise IAddress ne sera même pas "conscient" que la mise en œuvre est maintenant complètement différente!

  • C - Tests unitaires, je vois que vous utilisez une convention de dénomination .Net. Je ne connais aucun framework de test .Net qui vous permettra de se moquer des méthodes sur Address et ZipCode dans toutes les classes où elles sont utilisées, vous devez les consommer comme IAddress et IZipcode.
0
Eugene

C'est mon point de vue sur la "programmation vers une interface". Il y a deux éléments: les entrées et les sorties.

Contributions

Je fais la différence entre les objets qui sont données et les objets qui ont comportement, en particulier effets secondaires.

Dans vos deux exemples, j'utiliserais les classes concrètes: les chaînes et les codes postaux sont des données, IMO. (Je ne ferais PAS simplement du ZipCode une chaîne, comme l'une des réponses suggérées !!! Vous ne pouvez pas simplement passer une ancienne chaîne et la traiter comme un ZipCode. Votre classe ZipCode devrait avoir différents constructeurs pour les différents types de codes postaux que vous pourriez utiliser. Interally vous pouvez stocker une chaîne, mais au moins faire la validation dans les constructeurs!).

Cependant, si j'avais besoin d'un objet qui faisait de l'informatique (un terme vague, je sais), j'utiliserais une interface. Par exemple, si j'avais besoin d'un objet qui me permettait de rechercher des étudiants par nom, je prendrais une interface pour la saisie. L'interface serait quelque chose comme:

interface StudentSearchInterface {
    Optional<Student> search(String query);
}

La raison pour laquelle c'est une bonne idée est que vous pouvez échanger des fournisseurs plus tard, et cela vous permet également d'écrire de bien meilleurs tests où vous pouvez coder des "faux" objets StudentSearchInterface qui renvoient exactement ce que vous voulez pour votre test spécifique. Il peut être difficile de tester la classe de production qui implémente StudentSearchInterface: que faire si elle interroge un serveur sur Internet? Ou lit dans une base de données? Les résultats peuvent changer entre les appels à search(), vous ne pouvez donc pas vraiment le tester.

Les sorties

Vos valeurs de retour doivent être soit des "données", soit une interface également. Par données, je veux dire une classe qui n'a vraiment que des getters et/ou des setters et peut-être quelques méthodes triviales comme toString(), etc. Sinon, faites une interface. Un exemple courant renvoie un List<Foo> Au lieu d'un ArrayList<Foo>. Cela vous donne la liberté de changer la logique à l'intérieur de votre méthode pour utiliser autre chose qui implémente List au lieu d'être coincé avec un implémenteur List spécifique parce que c'est ce que vous avez utilisé en premier.

Tout comme nous marquons certaines méthodes et certains champs comme private pour les encapsuler, nous devons utiliser des interfaces pour fournir des informations uniquement sur la base du "besoin de savoir".

0
R. Agnese