Meilleures pratiques orientées object – Héritage v Composition v Interfaces

Je veux poser une question sur la façon dont vous aborderiez un problème de conception simple orienté object. J’ai quelques idées sur la meilleure façon d’aborder ce scénario, mais je serais intéressé à entendre certaines opinions de la communauté Stack Overflow. Des liens vers des articles en ligne pertinents sont également appréciés. J’utilise C #, mais la question n’est pas spécifique à la langue.

Supposons que j’écris une application de magasin vidéo dont la firebase database a une table Person , avec les PersonId , Name , DateOfBirth et Address . Il a également une table Staff , qui contient un lien vers PersonId , et une table Customer qui est également liée à PersonId .

Une approche simple orientée object consisterait à dire qu’un Customer “est une” Person et donc à créer des classes un peu comme ceci:

 class Person { public int PersonId { get; set; } public ssortingng Name { get; set; } public DateTime DateOfBirth { get; set; } public ssortingng Address { get; set; } } class Customer : Person { public int CustomerId { get; set; } public DateTime JoinedDate { get; set; } } class Staff : Person { public int StaffId { get; set; } public ssortingng JobTitle { get; set; } } 

Maintenant, nous pouvons écrire une fonction pour envoyer des emails à tous les clients:

 static void SendEmailToCustomers(IEnumerable everyone) { foreach(Person p in everyone) if(p is Customer) SendEmail(p); } 

Ce système fonctionne bien jusqu’à ce que nous ayons quelqu’un à la fois client et membre du personnel. En supposant que nous ne voulons pas vraiment que notre liste de everyone les everyone ait la même personne deux fois, une fois en tant que Customer et une fois en tant que membre du Staff , faisons-nous un choix arbitraire entre:

 class StaffCustomer : Customer { ... 

et

 class StaffCustomer : Staff { ... 

De toute évidence, seul le premier de ces deux ne briserait pas la fonction SendEmailToCustomers .

Alors, que feriez-vous?

  • Faire en sorte que la classe Person ait des références facultatives à une classe StaffDetails et CustomerDetails ?
  • Créer une nouvelle classe contenant une Person , plus facultatif StaffDetails et CustomerDetails ?
  • Tout faire une interface (par exemple IPerson , IStaff , ICustomer ) et créer trois classes qui implémentent les interfaces appropriées?
  • Prendre une autre approche complètement différente?

Mark, c’est une question intéressante. Vous trouverez autant d’opinions à ce sujet. Je ne crois pas qu’il y ait une “bonne” réponse. Ceci est un excellent exemple de la façon dont une conception d’object hiérarchique rigide peut vraiment causer des problèmes après la construction d’un système.

Par exemple, disons que vous êtes allé avec les classes “Client” et “Personnel”. Vous déployez votre système et tout est heureux. Quelques semaines plus tard, quelqu’un fait remarquer qu’il est à la fois «membre du personnel» et «client» et qu’il ne reçoit pas les courriels des clients. Dans ce cas, vous avez beaucoup de changements de code à effectuer (re-conception, pas re-facteur).

Je pense que ce serait trop complexe et difficile à maintenir si vous essayez d’avoir un ensemble de classes dérivées qui implémentent toutes les permutations et combinaisons de personnes et de leurs rôles. Cela est particulièrement vrai étant donné que l’exemple ci-dessus est très simple – dans la plupart des applications réelles, les choses seront plus complexes.

Pour votre exemple ici, je voudrais aller avec “Prendre une autre approche complètement différente”. J’implémenterais la classe Person et y inclurais une collection de “rôles”. Chaque personne peut avoir un ou plusieurs rôles tels que “Client”, “Personnel” et “Vendeur”.

Cela facilitera l’ajout de rôles à mesure que de nouvelles exigences seront découvertes. Par exemple, vous pouvez simplement avoir une classe de base “Role” et en tirer de nouveaux rôles.

Vous voudrez peut-être envisager d’utiliser les modèles Party et Accountability

De cette façon, la personne aura un ensemble de responsabilités qui peuvent être de type client ou personnel.

Le modèle sera également plus simple si vous ajoutez plus de types de relations ultérieurement.

L’approche pure serait la suivante: Faire de toute chose une interface. En tant que détails d’implémentation, vous pouvez éventuellement utiliser l’une des différentes formes de composition ou d’inheritance d’implémentation. Comme ce sont des détails d’implémentation, ils n’ont pas d’importance pour votre API publique, vous êtes donc libre de choisir celle qui vous simplifie le plus la vie.

Une personne est un être humain, alors qu’un client est simplement un rôle qu’une personne peut adopter de temps à autre. L’homme et la femme seraient des candidats à l’inheritance de la personne, mais le concept du client est différent.

Le principe de substitution de Liskov dit que nous devons pouvoir utiliser des classes dérivées dans lesquelles nous avons des références à une classe de base, sans le savoir. Avoir un client héritant de la personne violerait ceci. Un client peut aussi être un rôle joué par une organisation.

Faites-moi savoir si j’ai bien compris la réponse de Foredecker. Voici mon code (en Python; désolé, je ne connais pas C #). La seule différence est que je ne notifierais pas quelque chose si une personne “est un client”, je le ferais si un de ses rôles “est intéressé” par cette chose. Est-ce assez flexible?

 # --------- PERSON ---------------- class Person: def __init__(self, personId, name, dateOfBirth, address): self.personId = personId self.name = name self.dateOfBirth = dateOfBirth self.address = address self.roles = [] def addRole(self, role): self.roles.append(role) def interestdIn(self, subject): for role in self.roles: if role.interestdIn(subject): return True return False def sendEmail(self, email): # send the email print "Sent email to", self.name # --------- ROLE ---------------- NEW_DVDS = 1 NEW_SCHEDULE = 2 class Role: def __init__(self): self.interests = [] def interestdIn(self, subject): return subject in self.interests class CustomerRole(Role): def __init__(self, customerId, joinedDate): self.customerId = customerId self.joinedDate = joinedDate self.interests.append(NEW_DVDS) class StaffRole(Role): def __init__(self, staffId, jobTitle): self.staffId = staffId self.jobTitle = jobTitle self.interests.append(NEW_SCHEDULE) # --------- NOTIFY STUFF ---------------- def notifyNewDVDs(emailWithTitles): for person in persons: if person.interestdIn(NEW_DVDS): person.sendEmail(emailWithTitles) 

J’éviterais le “is” check (“instanceof” en Java). Une solution consiste à utiliser un motif de décorateur . Vous pouvez créer un EmailablePerson qui décore Person lorsque EmailablePerson utilise la composition pour contenir une instance privée d’une personne et délègue toutes les méthodes non-email à l’object Person.

Nous avons étudié ce problème à l’université l’année dernière, nous apprenions eiffel et nous avons utilisé l’inheritance multiple. Quoi qu’il en soit, les rôles de Foredecker semblent être assez souples.

Qu’est-ce qui ne va pas dans l’envoi d’un courriel à un client qui est membre du personnel? S’il est un client, il peut alors recevoir le courrier électronique. Est-ce que j’ai tort de penser ainsi? Et pourquoi devriez-vous prendre “Tout le monde” comme votre liste de courrier électronique? Ne serait-il pas préférable d’avoir une liste de clients puisque nous avons affaire à la méthode “sendEmailToCustomer” et non à la méthode “sendEmailToEveryone”? Même si vous souhaitez utiliser la liste “Tout le monde”, vous ne pouvez pas autoriser les doublons dans cette liste.

Si rien de tout cela n’est réalisable avec beaucoup de redisgn, je vais aller avec la première réponse de Foredecker et peut-être que vous devriez avoir des rôles assignés à chaque personne.

Vos classes ne sont que des structures de données: aucune d’entre elles n’a de comportement, mais seulement des getters et des setters. L’inheritance est inapproprié ici.

Adoptez une autre approche complètement différente: le problème avec la classe StaffCustomer est que votre membre du personnel peut commencer par être simplement un membre du personnel et devenir un client ultérieurement. Vous devez donc les supprimer en tant que personnel et créer une nouvelle instance de la classe StaffCustomer. Peut-être qu’un simple booléen dans la classe Staff de «isCustomer» permettrait à notre liste de personnes (probablement compilée à partir de tous les clients et de tous les membres du personnel des tables appropriées) de ne pas trouver le membre du personnel.

Voici quelques astuces supplémentaires: Dans la catégorie «ne même pas penser à le faire», voici quelques exemples de code rencontrés:

La méthode Finder renvoie un object

Problème: Selon le nombre d’occurrences trouvées, la méthode finder renvoie un nombre représentant le nombre d’occurrences – ou! Si un seul trouvé renvoie l’object réel.

Ne fais pas ça! C’est l’une des pires pratiques de codage et cela introduit une ambiguïté et perturbe le code d’une manière qui, lorsqu’un autre développeur entre en jeu, vous déteste pour cela.

Solution: Si vous avez besoin de ces deux fonctionnalités: compter et extraire une instance, créez deux méthodes, l’une qui renvoie le nombre et l’autre qui renvoie l’instance, mais jamais une seule méthode dans les deux sens.

Problème: Une mauvaise pratique dérivée est la suivante: lorsqu’une méthode de recherche renvoie soit la seule occurrence trouvée soit un tableau d’occurrences si plusieurs ont été trouvées. Ce style de programmation paresseux est fait beaucoup par les programmeurs qui font le précédent en général.

Solution: En ayant ceci sur la main, je retournerais un tableau de longueur 1 (un) si une seule occurrence est trouvée et un tableau de longueur> 1 si plus d’occurrences sont trouvées. De plus, ne trouver aucune occurrence renverrait null ou un tableau de longueur 0 selon l’application.

Programmation vers une interface et utilisation de types de retour covariants

Problème: Programmation vers une interface et utilisation de types de retour covariants et conversion dans le code appelant.

Solution: Utilisez plutôt le même supertype défini dans l’interface pour définir la variable qui doit pointer sur la valeur renvoyée. Cela maintient la programmation à une approche d’interface et votre code propre.

Les classes de plus de 1000 lignes sont un danger caché Les méthodes avec plus de 100 lignes sont également un danger!

Problème: Certains développeurs utilisent trop de fonctionnalités dans une classe / méthode, trop paresseux pour rompre la fonctionnalité – cela conduit à une faible cohésion et peut-être à un couplage élevé – l’inverse d’un principe très important dans la POO! Solution: évitez d’utiliser trop de classes internes / nestedes – ces classes doivent être utilisées UNIQUEMENT au besoin, vous n’avez pas besoin de les utiliser! Leur utilisation pourrait entraîner plus de problèmes, comme la limitation de l’inheritance. Cherchez le code dupliqué! Le même code ou un code similaire pourrait déjà exister dans certaines implémentations de supertypes ou peut-être dans une autre classe. Si c’est dans une autre classe qui n’est pas un supertype, vous avez également violé la règle de cohésion. Attention aux méthodes statiques – peut-être avez-vous besoin d’une classe d’utilitaires à append!
Plus à: http://centraladvisor.com/it/oop-what-are-the-best-practices-in-oop

Vous ne voulez probablement pas utiliser l’inheritance pour cela. Essayez plutôt ceci:

 class Person { public int PersonId { get; set; } public ssortingng Name { get; set; } public DateTime DateOfBirth { get; set; } public ssortingng Address { get; set; } } class Customer{ public Person PersonInfo; public int CustomerId { get; set; } public DateTime JoinedDate { get; set; } } class Staff { public Person PersonInfo; public int StaffId { get; set; } public ssortingng JobTitle { get; set; } }