J'ai vu cela mentionné à quelques reprises et je ne comprends pas ce que cela signifie. Quand et pourquoi feriez-vous cela?
Je sais ce que font les interfaces, mais le fait que je ne sois pas clair là-dessus me fait penser que je manque de les utiliser correctement.
Est-ce juste si vous deviez faire:
IInterface classRef = new ObjectWhatever()
Vous pouvez utiliser n'importe quelle classe qui implémente IInterface
? Quand auriez-vous besoin de faire ça? La seule chose à laquelle je peux penser, c’est que si vous avez une méthode et que vous ne savez pas quel objet sera transmis, à moins qu’il implémente IInterface
. Je ne peux pas penser à combien de fois vous auriez besoin de faire ça.
Aussi, comment pourriez-vous écrire une méthode qui prend un objet qui implémente une interface? Est-ce possible?
Il existe de très bonnes réponses ici à ces questions qui abordent toutes sortes de détails concernant les interfaces et le couplage lâche de code, l'inversion de contrôle, etc. Il y a des discussions assez houleuses, alors j'aimerais profiter de l'occasion pour expliquer un peu mieux pourquoi une interface est utile.
Quand j'ai commencé à être exposé aux interfaces, j'étais également confus quant à leur pertinence. Je n'ai pas compris pourquoi tu avais besoin d'eux. Si nous utilisons un langage tel que Java ou C #, nous avons déjà un héritage et j’ai considéré les interfaces comme une forme d’héritage plus faible et je me suis dit «pourquoi s’embêter? Dans un sens, j’avais raison, vous pouvez considérer les interfaces comme une sorte d’héritage faible, mais au-delà, j’ai enfin compris leur utilisation en tant que construction de langage en les considérant comme un moyen de classer des traits ou des comportements communs potentiellement de nombreuses classes d'objets non liées.
Par exemple, supposons que vous avez un jeu sur la carte SIM et que vous avez les classes suivantes:
class HouseFly inherits Insect {
void FlyAroundYourHead(){}
void LandOnThings(){}
}
class Telemarketer inherits Person {
void CallDuringDinner(){}
void ContinueTalkingWhenYouSayNo(){}
}
Il est clair que ces deux objets n’ont rien de commun en termes d’héritage direct. Mais, vous pourriez dire qu'ils sont tous les deux ennuyeux.
Disons que notre jeu doit avoir une sorte de chose aléatoire qui gêne le joueur quand il dîne. Cela peut être une HouseFly
ou une Telemarketer
ou les deux - mais comment autorisez-vous les deux avec une seule fonction? Et comment demandez-vous à chaque type d'objet de "faire ce qui est ennuyeux" de la même manière?
La clé à comprendre est qu’une Telemarketer
et une HouseFly
partagent un comportement commun mal interprété, même s’ils ne ressemblent en rien à leur modélisation. Alors, créons une interface que les deux peuvent implémenter:
interface IPest {
void BeAnnoying();
}
class HouseFly inherits Insect implements IPest {
void FlyAroundYourHead(){}
void LandOnThings(){}
void BeAnnoying() {
FlyAroundYourHead();
LandOnThings();
}
}
class Telemarketer inherits Person implements IPest {
void CallDuringDinner(){}
void ContinueTalkingWhenYouSayNo(){}
void BeAnnoying() {
CallDuringDinner();
ContinueTalkingWhenYouSayNo();
}
}
Nous avons maintenant deux classes qui peuvent être ennuyeuses à leur manière. Et ils n'ont pas besoin de dériver de la même classe de base et de partager des caractéristiques inhérentes communes - ils doivent simplement satisfaire le contrat de IPest
- ce contrat est simple. Vous devez juste BeAnnoying
. À cet égard, nous pouvons modéliser les éléments suivants:
class DiningRoom {
DiningRoom(Person[] diningPeople, IPest[] pests) { ... }
void ServeDinner() {
when diningPeople are eating,
foreach pest in pests
pest.BeAnnoying();
}
}
Nous avons ici une salle à manger qui accepte un certain nombre de convives et un certain nombre d'organismes nuisibles - notez l'utilisation de l'interface. Cela signifie que dans notre petit monde, un membre du tableau pests
pourrait en réalité être un objet Telemarketer
ou un objet HouseFly
.
La méthode ServeDinner
est appelée lorsque le dîner est servi et que les personnes présentes dans la salle à manger sont censées manger. Dans notre petit jeu, c’est à ce moment-là que nos parasites font leur travail - chaque nuisible a pour instruction d’être ennuyeux via l’interface IPest
. De cette façon, nous pouvons facilement avoir à la fois Telemarketers
et HouseFlys
ennuyeux dans chacune de leurs manières - nous nous soucions seulement que nous avons quelque chose dans l'objet DiningRoom
qui est un parasite, nous ne nous soucions pas vraiment de quoi il s'agit et ils pourraient n'ont rien en commun avec les autres.
Cet exemple très artificiel de pseudo-code (qui a duré beaucoup plus longtemps que je ne l'aurais prévu) vise simplement à illustrer le genre de chose qui a finalement allumé la lumière pour moi quant au moment où nous pourrions utiliser une interface. Je m'excuse par avance pour la sottise de l'exemple, mais j'espère que cela vous aidera à comprendre. Et, bien sûr, les autres réponses que vous avez reçues ici couvrent vraiment toute la gamme des interfaces utilisées aujourd'hui dans les modèles de conception et les méthodologies de développement.
L’exemple spécifique que j’avais l'habitude de donner aux étudiants est qu'ils devraient écrire
List myList = new ArrayList(); // programming to the List interface
au lieu de
ArrayList myList = new ArrayList(); // this is bad
Celles-ci ont exactement la même apparence dans un programme court, mais si vous continuez à utiliser myList
100 fois dans votre programme, vous constaterez une différence. La première déclaration garantit que vous appelez uniquement les méthodes myList
qui sont définies par l'interface List
(donc, aucune méthode spécifique à ArrayList
). Si vous avez programmé cette interface de cette manière, vous pourrez décider plus tard que vous avez vraiment besoin
List myList = new TreeList();
et il vous suffit de changer votre code à cet endroit. Vous savez déjà que le reste de votre code ne fait rien qui puisse être cassé en modifiant le implementation car vous avez programmé pour le interface.
Les avantages sont encore plus évidents (je pense) lorsque vous parlez de paramètres de méthode et de valeurs de retour. Prenons ceci par exemple:
public ArrayList doSomething(HashMap map);
Cette déclaration de méthode vous lie à deux implémentations concrètes (ArrayList
et HashMap
). Dès que cette méthode est appelée à partir d'un autre code, toute modification apportée à ces types signifie probablement que vous devrez également modifier le code d'appel. Il serait préférable de programmer pour les interfaces.
public List doSomething(Map map);
Maintenant, le type de List
que vous retournez ou le type de Map
transmis en tant que paramètre importent peu. Les modifications que vous apportez dans la méthode doSomething
ne vous obligeront pas à modifier le code d'appel.
Programmer sur une interface, c'est dire: "J'ai besoin de cette fonctionnalité et peu importe d'où elle vient."
Considérons (en Java) l'interface List
par rapport aux classes concrètes ArrayList
et LinkedList
. Si tout ce qui m'importe, c'est que j'ai une structure de données contenant plusieurs éléments de données auxquels je devrais accéder par itération, je choisirais une List
(et ce sera à 99% du temps). Si je sais que j'ai besoin d'insérer/supprimer à temps constant à l'une des extrémités de la liste, je pourrais choisir l'implémentation concrète LinkedList
(ou plus probablement, utiliser la File d'attente interface). Si je sais que j'ai besoin d'un accès aléatoire par index, je choisirais la classe concrète ArrayList
.
L'utilisation d'interfaces est un facteur clé pour rendre votre code facilement testable, en plus de supprimer les couplages inutiles entre vos classes. En créant une interface qui définit les opérations sur votre classe, vous permettez aux classes qui souhaitent utiliser cette fonctionnalité de l'utiliser sans dépendre directement de votre classe d'implémentation. Si plus tard, vous décidez de changer et d'utiliser une implémentation différente, il vous suffira de changer la partie du code où l'implémentation est instanciée. Le reste du code n'a pas besoin de changer, car cela dépend de l'interface et non de la classe d'implémentation.
Ceci est très utile pour créer des tests unitaires. Dans la classe sous test, cela dépend de l'interface et injecte une instance de l'interface dans la classe (ou une fabrique qui lui permet de créer des instances de l'interface selon les besoins) via le constructeur ou un paramètre de propriété. La classe utilise l'interface fournie (ou créée) dans ses méthodes. Lorsque vous écrivez vos tests, vous pouvez simuler ou simuler l’interface et fournir une interface qui répond aux données configurées dans votre test unitaire. Vous pouvez le faire car votre classe sous test traite uniquement de l'interface, pas de votre implémentation concrète. Toute classe implémentant l'interface, y compris votre classe fictive ou fausse, fera l'affaire.
EDIT: Ci-dessous un lien vers un article dans lequel Erich Gamma discute de sa citation, "Programmez une interface, pas une implémentation".
Vous devriez examiner Inversion of Control:
Dans un tel scénario, vous n'écririez pas ceci:
IInterface classRef = new ObjectWhatever();
Vous écririez quelque chose comme ceci:
IInterface classRef = container.Resolve<IInterface>();
Cela irait dans une configuration basée sur des règles dans l'objet container
et construirait l'objet réel pour vous, qui pourrait être ObjectWewise. L'important est que vous puissiez remplacer cette règle par quelque chose qui utilise un autre type d'objet et que votre code fonctionne toujours.
Si nous laissons IoC hors de la table, vous pouvez écrire du code sachant qu'il peut parler à un objet qui fait quelque chose de spécifique, mais pas quel type d'objet ni comment il le fait.
Cela serait utile lors du passage de paramètres.
En ce qui concerne votre question entre parenthèses "Et comment pouvez-vous écrire une méthode qui prend un objet qui implémente une interface? Est-ce possible?", Vous utiliseriez simplement le type d'interface pour le type de paramètre, comme ceci:
public void DoSomethingToAnObject(IInterface whatever) { ... }
Ceci se branche directement dans la "conversation avec un objet qui fait quelque chose de spécifique". La méthode définie ci-dessus sait ce à quoi s'attendre de l'objet, qu'il implémente tout dans IInterterface, mais il se fiche de son type d'objet, mais seulement du fait qu'il adhère au contrat, qui est ce qu'une interface est.
Par exemple, vous connaissez probablement les calculatrices et vous en avez probablement déjà utilisé plusieurs, mais la plupart du temps, elles sont toutes différentes. D'autre part, vous savez comment une calculatrice standard devrait fonctionner, vous pouvez donc toutes les utiliser, même si vous ne pouvez pas utiliser les fonctions spécifiques que chaque calculatrice possède.
C'est la beauté des interfaces. Vous pouvez écrire un morceau de code, sachant qu'il obtiendra des objets auxquels il peut s'attendre d'un certain comportement. Peu importe le type d'objet, peu importe le type de comportement recherché.
Laissez-moi vous donner un exemple concret.
Nous avons un système de traduction sur mesure pour les formulaires Windows. Ce système parcourt les contrôles d'un formulaire et traduit le texte dans chacun d'eux. Le système sait comment gérer les contrôles de base, tels que la propriété-type-de-contrôle-qui-a-un-texte, et des éléments de base similaires, mais pour tout élément de base, cela ne donne rien.
Maintenant, puisque les contrôles héritent de classes prédéfinies sur lesquelles nous n’avons aucun contrôle, nous pouvons faire l’une des trois choses suivantes:
Donc nous avons fait nr. 3. Tous nos contrôles implémentent ILocalizable, qui est une interface qui nous donne une méthode, la possibilité de traduire "lui-même" dans un conteneur de traduction texte/règles. En tant que tel, le formulaire n'a pas besoin de savoir quel type de contrôle il a trouvé, il doit seulement implémenter l'interface spécifique et savoir qu'il existe une méthode permettant d'appeler pour localiser le contrôle.
La programmation sur une interface n'a absolument rien à voir avec des interfaces abstraites comme on le voit en Java ou .NET. Ce n'est même pas un concept OOP.
Cela signifie vraiment que vous ne devez pas vous occuper des éléments internes d'un objet ou d'une structure de données. Utilisez l'interface de programme abstrait (API) pour interagir avec vos données. En Java ou en C #, cela signifie qu’il faut utiliser des propriétés et des méthodes publiques au lieu d’un accès au champ brut. Pour C, cela signifie utiliser des fonctions plutôt que des pointeurs bruts.
EDIT: Et avec les bases de données, cela signifie l’utilisation de vues et de procédures stockées au lieu d’un accès direct à une table.
Code pour l'interface Pas l'implémentation n'a rien à faire avec Java, ni sa construction d'interface.
Ce concept a été mis en avant dans les livres Patterns/Gang of Four, mais il existait probablement bien avant cela. Le concept existait certainement bien avant Java.
La construction d'interface Java a été créée pour aider à cette idée (entre autres), et les gens sont devenus trop concentrés sur la construction en tant que centre de la signification plutôt que l'intention originale. Cependant, c’est la raison pour laquelle nous avons des méthodes et des attributs publics et privés en Java, C++, C #, etc.
Cela signifie simplement interagir avec l'interface publique d'un objet ou d'un système. Ne vous inquiétez pas et ne prévoyez même pas comment il fait ce qu'il fait en interne. Ne vous inquiétez pas de la façon dont cela est mis en œuvre. Dans le code orienté objet, c'est pourquoi nous avons des méthodes/attributs publics et privés. Nous sommes destinés à utiliser les méthodes publiques car les méthodes privées ne sont là que pour une utilisation interne, au sein de la classe. Ils constituent l'implémentation de la classe et peuvent être modifiés au besoin sans changer l'interface publique. Supposons qu'en ce qui concerne la fonctionnalité, une méthode sur une classe effectuera la même opération avec le même résultat attendu chaque fois que vous l'appelez avec les mêmes paramètres. Cela permet à l'auteur de changer le fonctionnement de la classe, son implémentation, sans casser la façon dont les gens l'interagissent.
Et vous pouvez programmer l'interface, pas l'implémentation sans jamais utiliser une construction d'interface. Vous pouvez programmer sur l'interface et non sur l'implémentation en C++, qui n'a pas de construction d'interface. Vous pouvez intégrer deux systèmes d'entreprise énormes de manière beaucoup plus robuste, à condition qu'ils interagissent via des interfaces publiques (contrats) plutôt que d'appeler des méthodes sur des objets internes aux systèmes. Les interfaces doivent toujours réagir de la même manière attendue avec les mêmes paramètres d'entrée. si implémenté à l'interface et non l'implémentation. Le concept fonctionne dans de nombreux endroits.
Secouez la pensée que les interfaces Java ont quelque chose à voir avec le concept de «programme pour l'interface, pas l'implémentation». Ils peuvent aider à appliquer le concept, mais ils sont pas le concept.
Il semble que vous compreniez le fonctionnement des interfaces mais ne sachiez pas quand les utiliser et quels avantages elles offrent. Voici quelques exemples de cas où une interface aurait un sens:
// if I want to add search capabilities to my application and support multiple search
// engines such as google, yahoo, live, etc.
interface ISearchProvider
{
string Search(string keywords);
}
je pourrais alors créer GoogleSearchProvider, YahooSearchProvider, LiveSearchProvider, etc.
// if I want to support multiple downloads using different protocols
// HTTP, HTTPS, FTP, FTPS, etc.
interface IUrlDownload
{
void Download(string url)
}
// how about an image loader for different kinds of images JPG, GIF, PNG, etc.
interface IImageLoader
{
Bitmap LoadImage(string filename)
}
puis créez JpegImageLoader, GifImageLoader, PngImageLoader, etc.
La plupart des compléments et systèmes de plugins fonctionnent avec des interfaces.
Une autre utilisation populaire est le modèle de référentiel. Dites que je veux charger une liste de codes postaux de différentes sources
interface IZipCodeRepository
{
IList<ZipCode> GetZipCodes(string state);
}
je pouvais ensuite créer un XMLZipCodeRepository, un SQLZipCodeRepository, un CSVZipCodeRepository, etc. Pour mes applications Web, je crée souvent des référentiels XML à un stade précoce, de manière à pouvoir utiliser quelque chose avant la préparation de la base de données SQL. Une fois que la base de données est prête, j'écris un SQLRepository pour remplacer la version XML. Le reste de mon code reste inchangé puisqu'il ne fonctionne que sur les interfaces.
Les méthodes peuvent accepter des interfaces telles que:
PrintZipCodes(IZipCodeRepository zipCodeRepository, string state)
{
foreach (ZipCode zipCode in zipCodeRepository.GetZipCodes(state))
{
Console.WriteLine(zipCode.ToString());
}
}
Cela rend votre code beaucoup plus extensible et plus facile à gérer lorsque vous avez des ensembles de classes similaires. Je suis un programmeur débutant, donc je ne suis pas un expert, mais je viens de terminer un projet nécessitant quelque chose de similaire.
Je travaille sur un logiciel côté client qui communique avec un serveur exécutant un dispositif médical. Nous développons actuellement une nouvelle version de cet appareil avec de nouveaux composants que le client doit parfois configurer. Il existe deux types de nouveaux composants et ils sont différents, mais ils sont également très similaires. En gros, je devais créer deux formulaires de configuration, deux classes de listes, deux de tout.
J'ai décidé qu'il serait préférable de créer une classe de base abstraite pour chaque type de contrôle qui contiendrait la quasi-totalité de la logique réelle, puis des types dérivés pour prendre en compte les différences entre les deux composants. Cependant, les classes de base n'auraient pas été en mesure d'effectuer des opérations sur ces composants si je devais me soucier des types tout le temps (eh bien, elles auraient pu, mais il y aurait eu une instruction "if" ou un commutateur dans chaque méthode) .
J'ai défini une interface simple pour ces composants et toutes les classes de base parlent à cette interface. Maintenant, lorsque je change quelque chose, cela "fonctionne" presque partout et je n'ai aucune duplication de code.
Si vous programmez en Java, JDBC est un bon exemple. JDBC définit un ensemble d'interfaces mais ne dit rien sur la mise en œuvre. Vos applications peuvent être écrites sur cet ensemble d'interfaces. En théorie, vous choisissez un pilote JDBC et votre application fonctionnerait simplement. Si vous découvrez qu'il existe un pilote JDBC plus rapide, "meilleur" ou moins cher ou pour quelque raison que ce soit, vous pouvez de nouveau théoriquement reconfigurer votre fichier de propriétés et, sans aucune modification de votre application, celle-ci fonctionnerait toujours.
La programmation aux interfaces est géniale, elle favorise le couplage lâche. Comme @lassevk l'a mentionné, l'inversion de contrôle en est une excellente utilisation.
De plus, examinez les SOLID principes. voici une série de vidéos
Il passe par un code codé (exemple fortement couplé) puis examine les interfaces, puis passe à un outil IoC/DI (NInject).
J'arrive tardivement à cette question, mais je tiens à mentionner ici que la ligne "Programmer une interface, pas une implémentation" a eu une discussion intéressante dans le livre GoF (Gang of Four) Design Patterns.
Il a déclaré, à la p. 18:
Programmer une interface, pas une implémentation
Ne déclarez pas que les variables sont des instances de classes concrètes spécifiques. Au lieu de cela, commettez uniquement sur une interface définie par une classe abstraite. Vous constaterez qu'il s'agit d'un thème commun aux modèles de conception de ce livre.
et au-dessus, cela a commencé par:
Il y a deux avantages à manipuler des objets uniquement en termes d'interface définie par des classes abstraites:
- Les clients ne sont pas conscients des types d'objets qu'ils utilisent, tant qu'ils adhèrent à l'interface attendue par les clients.
- Les clients ne sont pas conscients des classes qui implémentent ces objets. Les clients ne connaissent que les classes abstraites définissant l'interface.
En d'autres termes, n'écrivez pas vos classes de manière à ce qu'elles aient une méthode quack()
pour les canards, puis une méthode bark()
pour les chiens, car elles sont trop spécifiques pour une implémentation particulière d'une classe (ou sous-classe). Ecrivez plutôt la méthode en utilisant des noms suffisamment généraux pour être utilisés dans la classe de base, tels que giveSound()
ou move()
, de sorte qu'ils puissent être utilisés pour des canards, des chiens ou même des voitures, puis le client de vos classes peut simplement dire .giveSound()
plutôt que de penser à utiliser quack()
ou bark()
ou même à déterminer le type avant d'envoyer le message correct à envoyer à l'objet.
Beaucoup d’explications, mais pour simplifier encore les choses… __. Prenons par exemple un List
On peut implémenter une liste avec comme:
En construisant une interface, disons List
. Vous codez uniquement sur la définition de liste ou sur ce que List
signifie en réalité.
Vous pouvez utiliser n'importe quel type d'implémentation en interne, à savoir une implémentation array
. Mais supposons que vous souhaitiez modifier l’implémentation pour une raison quelconque, par exemple un bug ou une performance. Ensuite, il vous suffit de changer la déclaration List<String> ls = new ArrayList<String>()
en List<String> ls = new LinkedList<String>()
.
Nulle part ailleurs dans le code, vous ne devrez rien changer d'autre; Parce que tout le reste a été construit sur la définition de List
.
En plus de la réponse déjà sélectionnée (et des différents messages informatifs ici), je vous recommande vivement de vous procurer un exemplaire de Head First Design Patterns . C'est une lecture très facile. Elle répondra directement à votre question, vous expliquera pourquoi c'est important et vous montrera de nombreux modèles de programmation que vous pouvez utiliser pour utiliser ce principe (et d'autres).
Pour ajouter aux publications existantes, il est parfois utile de coder les interfaces pour les grands projets lorsque les développeurs travaillent simultanément sur des composants distincts. Tout ce dont vous avez besoin est de définir les interfaces en amont et de leur écrire du code pendant que d'autres développeurs en écrivent sur l'interface que vous implémentez.
C'est également bon pour les tests unitaires, vous pouvez injecter vos propres classes (qui répondent aux exigences de l'interface) dans une classe qui en dépend
Donc, pour que cela soit correct, l’avantage d’une interface est que je peux séparer l’appel d’une méthode de toute classe. Au lieu de cela, créez une instance de l'interface, où l'implémentation est donnée à partir de la classe que j'ai choisie et qui implémente cette interface. Cela me permet donc d’avoir beaucoup de classes, qui ont des fonctionnalités similaires mais légèrement différentes et, dans certains cas (les cas liés à l’intention de l’interface), peu importe l’objet.
Par exemple, je pourrais avoir une interface de mouvement. Une méthode qui fait bouger quelque chose et tout objet (Personne, Voiture, Chat) qui implémente l'interface de mouvement peut être passé et demandé de se déplacer. Sans la méthode, tout le monde connaît le type de classe.
Explication C++.
Pensez à une interface en tant que méthodes publiques de vos classes.
Vous pouvez ensuite créer un modèle qui "dépend" de ces méthodes publiques afin de remplir sa propre fonction (les appels de fonction sont définis dans l'interface publique de classes). Disons que ce modèle est un conteneur, comme une classe Vector, et que son interface est un algorithme de recherche.
Toute classe d’algorithmes définissant les fonctions/l’interface avec laquelle Vector fait appel satisfera le «contrat» (comme l’a expliqué une personne dans la réponse initiale). Les algorithmes n'ont même pas besoin d'être de la même classe de base; la seule exigence est que les fonctions/méthodes dont dépend le vecteur (interface) soient définies dans votre algorithme.
L'intérêt de tout cela est que vous pouvez fournir n'importe quel algorithme/classe de recherche aussi longtemps que l'interface fournie dont Vector dépend (recherche par bulle, recherche séquentielle, recherche rapide).
Vous pouvez également concevoir d'autres conteneurs (listes, files d'attente) exploitant le même algorithme de recherche que Vector en les faisant remplir l'interface/le contrat dont dépendent vos algorithmes de recherche.
Cela vous fait gagner du temps (principe de «réutilisation du code» de la programmation orientée objet), car vous êtes capable d'écrire un algorithme spécifique à chaque nouvel objet que vous créez, sans trop compliquer le problème avec un arbre d'héritage trop étendu.
Quant à «rater» le fonctionnement des choses; big-time (au moins en C++), car c’est ainsi que fonctionne la majeure partie de l’infrastructure de la bibliothèque TEMPLATE standard.
Bien sûr, lors de l'utilisation d'héritage et de classes abstraites, la méthodologie de programmation pour une interface change; mais le principe est le même, vos fonctions/méthodes publiques sont l'interface de vos classes.
C’est un sujet énorme et l’un des principes fondamentaux de Design Patterns.
Imaginez que vous ayez un produit appelé 'Zebra' qui peut être étendu par des plugins. Il trouve les plugins en recherchant des DLL dans certains répertoires. Il charge toutes ces DLL et utilise la réflexion pour rechercher les classes qui implémentent IZebraPlugin
, puis appelle les méthodes de cette interface pour communiquer avec les plug-in.
Cela le rend complètement indépendant de toute classe de plugin spécifique - peu importe ce que sont les classes. Ils se soucient seulement qu'ils répondent aux spécifications de l'interface.
Les interfaces sont un moyen de définir des points d'extensibilité comme celui-ci. Le code qui communique avec une interface est plus faiblement couplé - en fait, il n'est pas du tout couplé à un autre code spécifique. Il peut interagir avec des plugins écrits des années plus tard par des personnes qui n'ont jamais rencontré le développeur d'origine.
Vous pourriez plutôt utiliser une classe de base avec des fonctions virtuelles - tous les plugins seraient dérivés de la classe de base. Mais ceci est beaucoup plus contraignant car une classe ne peut avoir qu'une seule classe de base, alors qu'elle peut implémenter un nombre quelconque d'interfaces.
En Java, ces classes concrètes implémentent toutes l'interface CharSequence:
CharBuffer, String, StringBuffer, StringBuilder
Ces classes concrètes n'ont pas de classe parente commune autre que Object. Par conséquent, rien ne les relie, hormis le fait qu'elles ont chacune à voir avec des tableaux de caractères représentant ceux-ci ou manipulant ceux-ci. Par exemple, les caractères de String ne peuvent plus être changés une fois qu'un objet String est instancié, alors que les caractères de StringBuffer ou StringBuilder peuvent être édités.
Cependant, chacune de ces classes est capable d'implémenter correctement les méthodes d'interface CharSequence:
char charAt(int index)
int length()
CharSequence subSequence(int start, int end)
String toString()
Dans certains cas, les classes de bibliothèque de classes Java qui acceptaient String ont été révisées pour accepter désormais l'interface CharSequence. Ainsi, si vous avez une instance de StringBuilder, au lieu d'extraire un objet String (ce qui signifie instancier une nouvelle instance d'objet), vous pouvez simplement transmettre le StringBuilder lui-même lorsqu'il implémente l'interface CharSequence.
L'interface appendable implémentée par certaines classes présente à peu près les mêmes avantages pour toutes les situations dans lesquelles des caractères peuvent être ajoutés à une instance de l'instance d'objet de classe concrète sous-jacente. Toutes ces classes concrètes implémentent l'interface appendable:
BufferedWriter, CharArrayWriter, CharBuffer, FileWriter, FilterWriter, LogStream, OutputStreamWriter, PipedWriter, PrintStream, PrintWriter, StringBuffer, StringBuilder, StringWriter, Writer
Il peut être avantageux de programmer des interfaces, même lorsque nous ne dépendons pas d'abstractions.
Programmation pour interfaces nous oblige à utiliser un sous-ensemble d'objet approprié au contexte. Cela aide parce que ça:
Par exemple, considérons une classe Person
qui implémente les interfaces Friend
et Employee
.
class Person implements AbstractEmployee, AbstractFriend {
}
Dans le contexte de l'anniversaire de la personne, nous programmons l'interface Friend
pour éviter de la traiter comme une variable Employee
.
function party() {
const friend: Friend = new Person("Kathryn");
friend.HaveFun();
}
Dans le contexte du travail de la personne, nous programmons l'interface Employee
pour éviter que les limites du lieu de travail ne soient floues.
function workplace() {
const employee: Employee = new Person("Kathryn");
employee.DoWork();
}
Génial. Nous nous sommes comportés de manière appropriée dans différents contextes et notre logiciel fonctionne bien.
Loin dans l'avenir, si notre activité évolue pour travailler avec des chiens, nous pouvons changer le logiciel assez facilement. Tout d'abord, nous créons la classe Dog
qui implémente à la fois Friend
et Employee
. Ensuite, nous changeons en toute sécurité new Person()
en new Dog()
. Même si les deux fonctions ont des milliers de lignes de code, cette édition simple fonctionnera, car nous savons que les conditions suivantes sont vraies:
party
utilise uniquement le sous-ensemble Friend
de Person
. workplace
utilise uniquement le sous-ensemble Employee
de Person
. Dog
implémente les interfaces Friend
et Employee
. D'autre part, si party
ou workplace
devait avoir programmé contre Person
, il y aurait un risque que les deux aient un code spécifique à Person
. Pour passer de Person
à Dog
, il serait nécessaire de parcourir le code afin d'extirper tout code spécifique à Person
que Dog
ne prend pas en charge.
La morale : la programmation aux interfaces aide notre code à se comporter correctement et à se préparer au changement. Cela prépare également notre code à dépendre d'abstractions, ce qui apporte encore plus d'avantages.
En termes simples ... Si j'écris une nouvelle classe Swimmer pour ajouter la fonctionnalité swim () et doit utiliser un objet de la classe say Dog, et cette classe Dog implémente l'interface Animal qui déclare nager () [Pour mieux comprendre ... vous pouvez dessiner un diagramme illustrant ce dont je parle]. Au sommet de la hiérarchie (Animal), c'est très abstrait alors qu'au fond (Chien), c'est très concret. La façon dont je pense à la "programmation aux interfaces" est que, au moment d'écrire à la classe Swimmer, je veux écrire mon code par rapport à l'interface qui est aussi avancée dans la hiérarchie, qui dans ce cas est un objet Animal. Une interface est exempte de détails d'implémentation et rend donc votre code faiblement couplé. Les détails de l'implémentation peuvent être modifiés avec le temps, mais cela n'affectera pas le code restant car toutes vos interactions interagissent avec l'interface et non l'implémentation. Vous ne vous souciez pas de la façon dont l’implémentation ressemble… tout ce que vous savez, c’est qu’il y aura une classe qui implémenterait l’interface.
nouvelle: Postman est invité à rentrer chez lui à la maison et à recevoir les jaquettes (lettres, documents, chèques, carte-cadeau, application, lettre d'amour) avec l'adresse écrite à sa livraison.
Supposons qu’il n’y ait pas de couverture et demande au postier de rentrer chez lui chez lui et de recevoir toutes les choses et de le livrer à une autre personne que le facteur peut confondre,
donc mieux enveloppez-le avec la couverture (dans notre histoire c'est l'interface) alors il fera son travail bien.
Maintenant, le travail de facteur consiste à recevoir et à livrer les couvertures seulement .. (il ne s'est pas soucié de ce qui est à l'intérieur de la couverture).
Créez un type de type interface
pas réel, mais implémentez-le avec le type réel.
Create to interface signifie que vos composants obtiennent s’intègre facilement au reste du code
Je vous donne un exemple.
vous avez une interface AirPlane comme ci-dessous.
interface Airplane{
parkPlane();
servicePlane();
}
Supposons que vous ayez des méthodes dans votre classe de contrôleurs comme
parkPlane(Airplane plane)
et
servicePlane(Airplane plane)
mis en œuvre dans votre programme. Il ne sera pasCASS&EACUTE;votre code . Je veux dire, il n’a pas besoin de changer tant qu’il accepte les arguments en tant que AirPlane
.
Parce qu'il acceptera n'importe quel avion malgré le type actuel, flyer
, highflyr
, fighter
, etc.
Aussi, dans une collection:
List<Airplane> plane;
// Prendra tous vos avions.
L'exemple suivant clarifiera votre compréhension.
Vous avez un avion de combat qui le met en œuvre, donc
public class Fighter implements Airplane {
public void parkPlane(){
// Specific implementations for fighter plane to park
}
public void servicePlane(){
// Specific implementatoins for fighter plane to service.
}
}
La même chose pour HighFlyer et d’autres groupes:
public class HighFlyer implements Airplane {
public void parkPlane(){
// Specific implementations for HighFlyer plane to park
}
public void servicePlane(){
// specific implementatoins for HighFlyer plane to service.
}
}
Pensez maintenant à vos classes de contrôleur en utilisant AirPlane
plusieurs fois,
Supposons que votre classe de contrôleurs soit ControlPlane comme ci-dessous,
public Class ControlPlane{
AirPlane plane;
// so much method with AirPlane reference are used here...
}
voici la magie vient comme
vous pouvez créer autant de fois que vous le souhaitez vos nouvelles instances de type AirPlane
et vous ne changez pas
code de la classe ControlPlane
.
vous pouvez ajouter une instance ..
JumboJetPlane // implementing AirPlane interface.
AirBus // implementing AirPlane interface.
vous pouvez également supprimer des instances de types créés précédemment.
Une interface est comme un contrat, dans lequel vous voulez que votre classe d'implémentation implémente des méthodes écrites dans le contrat (interface). Puisque Java ne fournit pas l'héritage multiple, la "programmation vers l'interface" est un bon moyen de réaliser l'héritage multiple.
Si vous avez une classe A qui étend déjà une autre classe B, mais que vous voulez que cette classe respecte également certaines directives ou implémente un certain contrat, vous pouvez le faire en appliquant la stratégie de "programmation à interface".
Q: - ... "Vous pouvez utiliser n'importe quelle classe qui implémente l'interface?"
A: - Oui.Q: -... "Quand auriez-vous besoin de faire ça?"
A: - Chaque fois que vous avez besoin d'une classe qui implémente une interface.
Note: _ nous ne pouvions pas instancier une interface non implémentée par une classe - True.
AnIntf anInst = new Aclass ();
// nous pourrions faire ceci seulement si Aclass implémente AnIntf.
// anInst aura une référence Aclass.
Remarque:
Maintenant, nous pourrions comprendre ce qui se passera si Bclass et Cclass implémentent le même Dintf.
Dintf bInst = new Bclass();
// now we could call all Dintf functions implemented (defined) in Bclass.
Dintf cInst = new Cclass();
// now we could call all Dintf functions implemented (defined) in Cclass.
Ce que nous avons:
mêmes prototypes d’interface (noms de fonctions dans l’interface) et appellent différentes implémentations.
Bibliographie: _
Prototypes - wikipedia
Je ne retiens pas interface
s sont la chose la plus importante dans un langage: il est plus communément utilisé la classe héritant. Mais de toute façon, ils sont importants!
Par exemple (ceci est un code Java
, mais il peut simplement être adapté à C#
ou à de nombreuses autres langues):
interface Convertable<T> {
T convert();
}
public class NumerableText implements Convertable<Integer> {
private String text = "";
public NumerableText() { }
public NumerableText(String text) {
this.text = text;
}
public String getText() {
return this.text;
}
public void setText(String text) {
this.text = text;
}
public Integer convert() {
return this.text.hashCode();
}
}
public class NumerableTextArray implements Convertable<Integer> {
private String[] textArray = "";
public NumerableTextArray() { }
public NumerableTextArray(String[] textArray) {
this.textArray = textArray;
}
public String[] getTextArray() {
return this.textArray;
}
public void setTextArray(String[] text) {
this.textArray = textArray;
}
public Integer convert() {
Integer value = 0;
for (String text : textArray)
value += text.hashCode();
return value;
}
}
public class Foo {
public static void main() {
Convertable<Integer> num1 = new NumerableText("hello");
Convertable<Integer> num2 = new NumerableTextArray(new String[] { "test n°1", "test n°2" });
System.out.println(String.valueOf(num1.convert()));
System.out.println(String.valueOf(num2.convert()));
//Here are you two numbers generated from two classes of different type, but both with the method convert(), which allows you to get that number.
}
}
Commençons par quelques définitions:
Interface _ {n. L'ensemble de toutes les signatures définies par les opérations d'un objet est appelé l'interface avec l'objet.
Type _ {n. Une interface particulière
Un exemple simple de interface telle que définie ci-dessus serait l'ensemble des méthodes d'objet PDO telles que query()
, commit()
, close()
etc., dans leur ensemble, et non séparément. Ces méthodes, c’est-à-dire son interface, définissent l’ensemble complet des messages, requêtes pouvant être envoyées à l’objet.
Un type tel que défini ci-dessus est une interface particulière. Je vais utiliser l'interface de forme créée pour démontrer: draw()
, getArea()
, getPerimeter()
etc.
Si un objet est du type base de données, cela signifie qu'il accepte les messages/requêtes de l'interface de base de données, query()
, commit()
etc. Les objets peuvent être de plusieurs types. Vous pouvez avoir un objet de base de données de type forme tant qu'il implémente son interface, auquel cas il s'agirait de sous-typage _.
De nombreux objets peuvent être de types/interfaces différents et implémenter cette interface différemment. Cela nous permet de substituer des objets, nous permettant de choisir lequel utiliser. Aussi connu sous le polymorphisme.
Le client sera uniquement au courant de l'interface et non de la mise en œuvre.
Donc, essentiellement, programmer une interface impliquerait de créer un type de classe abstraite telle que Shape
avec l’interface uniquement spécifiée, par exemple draw()
, getCoordinates()
, getArea()
etc. Classe de triangle. Donc programme pour une interface pas une implémentation.
"Programme à interface" signifie que le code matériel n'est pas fourni correctement, ce qui signifie que votre code doit être étendu sans casser les fonctionnalités précédentes. Juste des extensions, pas l'édition du code précédent.
Je crois fermement que la question difficile devrait être expliquée avec une réponse facile du monde réel. Et dans le domaine de la conception de logiciels, c'est très important.
Regardez n'importe quel porte dans votre maison, votre école, votre église ... tout bâtiment .
Imaginez des portes avez le périls right-bottom (vous devez donc vous incliner pour interagir avec la porte ouverte ou la fermer),
Ou d’autres qui viennent juste dans en haut à gauche (ainsi, certains nains, personnes handicapées ou Kevin Hart ne trouveront pas de telles portes très amusantes et utilisables).
Si design est le mot clé, créez des programmes pour les autres humains pouvez développez/utilisez-le.
Ce que Interfaces
fait, c’est de faciliter les choses aux autres développeurs juniors/seniors dans le cadre de projets colosses [1] , afin que tout le monde sache ce qu’ils font avec peu d’aide des autres pour que vous puissiez travailler aussi sans heurt que possible ).
[1] Comment ?. en exposant la forme de la valeur. Vous n'avez donc pas besoin de documentation, car le code lui-même est explicite (Awesome).
Cette réponse n'était pas destinée aux langues spécifiques mais aux concepts (Après tout, humains créer des outils en écrivant du code).
De plus, je vois ici beaucoup de bonnes réponses explicatives, je tiens donc à donner ici mon point de vue, y compris des informations supplémentaires sur ce que j'ai remarqué en utilisant cette méthode.
Tests unitaires
Au cours des deux dernières années, j'ai écrit un projet de loisir et je n'ai pas écrit de tests unitaires pour celui-ci. Après avoir écrit environ 50 000 lignes, j'ai découvert qu'il serait vraiment nécessaire d'écrire des tests unitaires ... Je n'ai pas utilisé d'interfaces (ou avec parcimonie) ... et lorsque j'ai effectué mon premier test unitaire, j'ai découvert que c'était compliqué. Pourquoi?
Parce que je devais créer beaucoup d'instances de classe, utilisées pour les entrées en tant que variables de classe et/ou paramètres. Les tests ressemblent donc davantage à des tests d'intégration (il faut créer un «cadre» complet de classes puisque tout était lié entre eux).
Peur des interfaces Alors j'ai décidé d'utiliser des interfaces. Ma crainte était que je devais mettre en œuvre toutes les fonctionnalités partout (dans toutes les classes utilisées) plusieurs fois. D'une certaine manière, cela est vrai, cependant, en utilisant l'héritage, il peut être beaucoup réduit.
Combinaison d'interfaces et d'héritage J'ai découvert que la combinaison est très bonne pour être utilisée. Je donne un exemple très simple.
public interface IPricable
{
int Price { get; }
}
public interface ICar : IPricable
public abstract class Article
{
public int Price { get { return ... } }
}
public class Car : Article, ICar
{
// Price does not need to be defined here
}
De cette façon, la copie de code n'est pas nécessaire, tout en offrant l'avantage d'utiliser une voiture comme interface (ICar).