39 votes

Meilleures 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ée objet. J'ai quelques idées personnelles sur la meilleure façon d'aborder ce scénario, mais je serais intéressé par les 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 au langage.

Supposons que j'écrive une application de vidéothèque dont la base de données comporte un fichier de type Person table, avec PersonId , Name , DateOfBirth et Address champs. Il dispose également d'un Staff qui contient un lien vers une PersonId et un Customer qui renvoie également à PersonId .

Une approche orientée objet simple consisterait à dire qu'une 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 pour envoyer des emails à 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 soit à la fois un client et un membre du personnel. En supposant que nous ne voulons pas vraiment que notre everyone liste d'avoir la même personne deux fois, une fois en tant que Customer et une fois en tant que Staff faisons-nous un choix arbitraire entre.. :

class StaffCustomer : Customer { ...

et

class StaffCustomer : Staff { ...

De toute évidence, seul le premier de ces deux cas n'enfreindrait pas les règles de l'UE. SendEmailToCustomers fonction.

Alors, que feriez-vous ?

  • Faites le Person ont des références facultatives à une classe StaffDetails et CustomerDetails classe ?
  • Créer une nouvelle classe qui contient un Person plus optionnel StaffDetails et CustomerDetails ?
  • Faire de chaque chose une interface (par exemple IPerson , IStaff , ICustomer ) et créer trois classes qui implémentent les interfaces appropriées ?
  • Adopter une autre approche complètement différente ?

49voto

Foredecker Points 5784

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. Il s'agit d'un excellent exemple de cas où une conception hiérarchique rigide des objets peut réellement causer des problèmes après la construction d'un système.

Par exemple, disons que vous avez choisi les classes "Client" et "Personnel". Vous déployez votre système et tout va bien. Quelques semaines plus tard, quelqu'un vous fait remarquer qu'il est à la fois "membre du personnel" et "client" et qu'il ne reçoit pas d'e-mails de clients. Dans ce cas, vous avez beaucoup de modifications à apporter au code (re-design, pas re-factor).

Je pense qu'il serait excessivement complexe et difficile à maintenir si vous essayez d'avoir un ensemble de classes dérivées qui mettent en œuvre toutes les permutations et combinaisons de personnes et de leurs rôles. Ceci est d'autant plus vrai que l'exemple ci-dessus est très simple - dans la plupart des applications réelles, les choses seront plus complexes.

Pour votre exemple, j'opterais pour une approche complètement différente. J'implémenterais la classe Personne et y inclurais une collection de "rôles". Chaque personne pourrait avoir un ou plusieurs rôles tels que "Client", "Personnel", et "Vendeur".

Il sera ainsi plus facile d'ajouter des rôles au fur et à mesure que de nouvelles exigences sont découvertes. Par exemple, vous pouvez simplement avoir une classe de base "Rôle" et en dériver de nouveaux rôles.

17voto

Jorge Villuendas Points 1990

Vous pouvez envisager d'utiliser l'option Modèles de parti et de 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 tard d'autres types de relations.

10voto

yfeldblum Points 42613

L'approche pure serait : Faire de chaque chose une interface. En tant que détails d'implémentation, vous pouvez éventuellement utiliser l'une des diverses formes de composition ou d'héritage d'implémentation. Comme il s'agit de détails d'implémentation, ils n'ont pas d'importance pour votre API publique, vous êtes donc libre de choisir ce qui vous simplifie la vie.

7voto

Une personne est un être humain, tandis qu'un client n'est qu'un rôle qu'une personne peut adopter de temps à autre. L'homme et la femme seraient des candidats à l'héritage de la personne, mais le client est un concept différent.

Le principe de substitution de Liskov stipule que nous devons être en mesure d'utiliser des classes dérivées lorsque nous avons des références à une classe de base, sans le savoir. Le fait que Customer hérite de Person violerait ce principe. Un client pourrait peut-être aussi être un rôle joué par une organisation.

5voto

Federico A. Ramponi Points 23106

Dites-moi si j'ai bien compris la réponse de Foredecker. Voici mon code (en Python ; désolé, je ne connais pas le 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 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)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X