Bonnes pratiques orientées objet-héritage V Composition V Interfaces

Je veux poser une question sur la façon dont vous aborderiez un simple problème de conception orienté objet. J'ai quelques idées sur la meilleure façon d'aborder ce scénario, mais je serais intéressé d'entendre quelques opinions de la communauté Stack Overflow. Les 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 base de données a une table Person, avec PersonId, Name, DateOfBirth et Address champs. Il a également une table Staff, qui a un lien vers un PersonId, et une table Customer qui lie également à PersonId.

Une simple approche orientée objet serait de dire qu'un Customer "est un" Person, et donc de créer des classes un peu comme ceci:

class Person {
    public int PersonId { get; set; }
    public string Name { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string 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 string JobTitle { get; set; }
}

Maintenant, nous pouvons écrire une fonction dire à envoyer des e-mails à tous les clients:

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

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

class StaffCustomer : Customer { ...

Et

class StaffCustomer : Staff { ...

Évidemment, seul le premier de ces deux ne casserait 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 un Person, plus facultatif StaffDetails et CustomerDetails?
  • Faire tout un interface (par exemple IPerson, IStaff, ICustomer) et créer trois classes qui ont implémenté les interfaces appropriées?
  • Prendre une autre approche complètement différente?
39
demandé sur tvanfosson 2008-10-19 19:22:34

12 réponses

Marque, C'est une question intéressante. Vous trouverez autant d'opinions à ce sujet. Je ne crois pas qu'il existe une "bonne" réponse. Ceci est un excellent exemple de l'endroit où une conception d'objet heirarchial 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'ils sont à la fois "personnel" et un "client" et qu'ils ne reçoivent pas les clients mails. Dans ce cas, vous avez beaucoup de modifications de code à faire (re-design, pas re-factor).

Je crois qu'il 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 la combinaison des 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, j'irais avec " prendre un autre approche complètement différente". Je mettrais en œuvre la classe Person et y inclurais une collection de"rôles". Chaque personne pourrait avoir un ou plusieurs rôles tels que "client", "personnel"et " fournisseur".

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

49
répondu Foredecker 2008-10-19 15:34:59

Vous pouvez envisager d'utiliser les modèles parti et Responsabilité

De cette façon, la personne aura une collection 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 relation plus tard.

17
répondu Jorge Villuendas Zapatero 2008-10-19 15:51:32

L'approche pure serait: faire de tout une interface. En tant que détails d'implémentation, vous pouvez éventuellement utiliser l'une des différentes formes de composition ou d'implémentation-héritage. 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 celui qui rend votre vie la plus simple.

10
répondu yfeldblum 2008-10-19 15:29:04

Une Personne est un être humain, alors qu'un Client est tout un Rôle qu'une Personne peut adopter de temps à autre. L'homme et la femme seraient candidats à hériter de la personne, mais le client est un concept différent.

Le principe de substitution de Liskov dit que nous devons pouvoir utiliser des classes dérivées où nous avons des références à une classe de base, sans le savoir. Le fait que le client hérite de la personne violerait cela. Un client peut peut-être aussi être un rôle joué par une Organisation.

7
répondu 2008-10-20 09:34:42

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

# --------- 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 interestedIn(self, subject):
        for role in self.roles:
            if role.interestedIn(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 interestedIn(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.interestedIn(NEW_DVDS):
            person.sendEmail(emailWithTitles)

5
répondu Federico A. Ramponi 2008-10-19 18:43:41

J'éviterais la vérification " is " ("instanceof" en Java). Une solution consiste à utiliser un motif de décorateur . Vous pouvez créer un EmailablePerson qui décore Person où EmailablePerson utilise la composition pour contenir une instance privée d'une personne et délègue toutes les méthodes non-email à L'objet Person.

3
répondu Paul Croarkin 2008-10-19 16:46:41

Nous étudions ce problème au collège l'année dernière, nous apprenions eiffel donc nous avons utilisé l'héritage multiple. Quoi qu'il en soit, l'alternative aux rôles Foredecker semble être assez flexible.

1
répondu 2008-10-19 16:31:45

Qu'y a-t-il de mal à envoyer un e-mail à un client membre du personnel? S'il est un client, il peut être envoyé l'e-mail. Suis-je tort de penser ainsi? Et pourquoi devriez-vous prendre "tout le monde" comme liste d'email? 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 aucun de ces éléments n'est réalisable avec beaucoup de redisgn, Je vais aller avec la première réponse Foredecker et peut-être que vous devriez avoir des rôles assignés à chaque personne.

1
répondu vj01 2008-10-19 16:58:37

Vos classes ne sont que des structures de données: aucune d'entre elles n'a de comportement, juste des getters et des setters. L'héritage est inapproprié ici.

1
répondu fizzer 2008-10-19 18:24:04

Adoptez une autre approche complètement différente: le problème avec la classe StaffCustomer est que votre membre du personnel pourrait commencer en tant que personnel et devenir client plus tard, vous devrez 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 everyone (probablement compilée à partir de tous les clients et de tout le personnel des tables appropriées) de ne pas obtenir le membre du personnel tel qu'il le saura a déjà été inclus en tant que client.

1
répondu Mike D 2010-07-22 07:44:12

Voici quelques conseils: De la catégorie "ne pense même pas à faire cela" voici quelques mauvais exemples de code rencontrés:

La méthode du Finder renvoie L'objet

Problème: Selon le nombre d'occurrences trouvées la méthode de recherche renvoie un nombre représentant le nombre d'occurrences ou! Si un seul trouvé renvoie l'objet réel.

Ne fais pas ça! Ceci est l'une des pires pratiques de codage et il introduit l'ambiguïté et salit le code d'une manière que quand un développeur différent entre en jeu, elle ou il vous haïra pour le faire.

Solution: S'il y a un besoin de ces 2 fonctionnalités: compter et récupérer une instance crée 2 Méthodes une qui renvoie le count et une qui renvoie l'instance, mais jamais une seule méthode faisant les deux sens.

Problème: une mauvaise pratique dérivée est lorsqu'une méthode finder renvoie soit l'une seule occurrence trouvée soit un tableau d'occurrences si plus d'une est trouvée. Cette programmation paresseuse le style est fait beaucoup par les programmeurs qui font le précédent en général.

Solution: ayant ceci entre mes mains, je retournerais un tableau de longueur 1 (un) si une seule occurrence est trouvée et un tableau avec length > 1 si plus d'occurrences sont trouvées. De plus, trouver aucune occurrence du tout retournerait null ou un tableau de longueur 0 en fonction de l'application.

Programmation sur une interface et utilisation des types de retour covariants

Problème: programmation sur une interface et utilisation de covariant types de retour et coulée dans le code appelant.

Solution: utilisez à la place le même supertype défini dans l'interface pour définir la variable qui doit pointer vers la valeur renvoyée. Cela maintient la programmation À une approche d'interface et votre code propre.

Les Classes avec 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 bourrent trop de fonctionnalités dans une classe / méthode, étant trop paresseux pour casser la fonctionnalité-cela conduit à une faible cohésion et peut-être à un couplage élevé-l'inverse d'un principe très important en POO! Solution: évitez d'utiliser trop de classes internes/imbriquées-ces classes ne doivent être utilisées que par besoin, vous n'avez pas besoin de les utiliser! Leur utilisation pourrait conduire à plus de problèmes comme limiter l'héritage. Lookout pour dupliquer le code! Le même code ou trop similaire pourrait déjà exister dans une implémentation de supertype ou peut-être dans une autre classe. Si il est dans une autre classe qui n'est pas un supertype vous avez également violé la règle de cohésion. Méfiez-vous des méthodes statiques-peut-être que vous avez besoin d'une classe utilitaire à ajouter!
Plus à l': http://centraladvisor.com/it/oop-what-are-the-best-practices-in-oop

1
répondu Bogdan 2012-08-03 17:34:32

Vous ne voulez probablement pas utiliser l'héritage pour cela. Essayez ceci à la place:

class Person {
    public int PersonId { get; set; }
    public string Name { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string 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 string JobTitle { get; set; }
}
-1
répondu jeremy 2013-07-16 16:31:47