面向对象的最佳实践 - 继承vs组合vs接口

38
我想问一个有关如何解决简单面向对象设计问题的问题。我自己对应对这种情况有一些想法,但我很想听听来自 Stack Overflow 社区的意见。相关在线文章的链接也会受到赞赏。我使用的是 C#,但这个问题并不特定于某种语言。
假设我正在编写一个视频店应用程序,其数据库具有 Person 表,其中包含 PersonId、Name、DateOfBirth 和 Address 字段。它还有一个 Staff 表,其中有一个指向 PersonId 的链接,以及一个 Customer 表,它也链接到 PersonId。
一个简单的面向对象方法是说一个 Customer "是" 一个 Person,因此创建类可能会像这样:
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; }
}

现在我们可以编写一个名为“say”的函数,用于向所有客户发送电子邮件:

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

这个系统在没有既是客户又是员工的人时运作良好。假设我们不想让everyone名单中同一个人以客户和员工两种身份出现,我们应该在以下两者之间做出任意选择吗:

class StaffCustomer : Customer { ...

并且。
class StaffCustomer : Staff { ...

显然,这两种方法中只有第一种不会破坏SendEmailToCustomers函数。

那你会怎么做呢?

  • 使Person类具有对StaffDetailsCustomerDetails类的可选引用?
  • 创建一个包含Person以及可选StaffDetailsCustomerDetails的新类?
  • 将所有内容都变成接口(例如,IPersonIStaffICustomer),并创建三个实现适当接口的类?
  • 采取另一种完全不同的方法?
12个回答

49

马克,这是一个有趣的问题。你会发现很多人对此有不同的看法。我认为没有一个“正确”的答案。这是一个很好的例子,说明在系统建成之后,过于死板的层次化对象设计可能会导致问题。

例如,假设你选择了“客户”和“员工”类。你部署了系统,一切都很顺利。几周后,有人指出他们既是“员工”又是“客户”,但他们没有收到客户的电子邮件。在这种情况下,你需要进行许多代码更改(重新设计而不是重构)。

我认为,如果你试图拥有一组派生类来实现人员及其角色的所有排列组合,那么这将过于复杂和难以维护。在大多数实际应用中,情况会更加复杂,上述示例只是非常简单的情况。

对于你在这里的示例,我会采取“采取完全不同的方法”。我将实现Person类,并在其中包含“角色”集合。每个人可以拥有一个或多个角色,例如“客户”、“员工”和“供应商”。

这将使添加新要求时更容易。例如,你可以简单地拥有一个基本的“角色”类,并从它们派生新的角色。


17

你可能需要考虑使用党派和问责模式

这样,人员将拥有一组账户,其中可能是客户或员工类型。

如果以后添加更多关系类型,该模型也会更简单。


10

最纯粹的方法是:将所有内容都设计为接口。作为实现细节,您可以选择使用各种形式的组合或实现继承。由于这些都是实现细节,对公共API不重要,因此您可以自由选择任何使您的生活最简单的方式。


是的,您现在可以选择一种实现方式,稍后更改想法而不会破坏其他代码。 - Jason Cohen

7

一个人是一个人类,而客户只是一个人可能会从时间到时间采用的角色。男人和女人将成为继承人的候选人,但客户是一个不同的概念。

Liskov替换原则指出,我们必须能够在引用基类的情况下使用派生类,而不知道它。让客户继承人会违反这一点。客户可能也是组织扮演的角色。


一个组织通常被视为一种人,即法人。 - ProfK

5

请告诉我是否正确理解了Foredecker的答案。这是我的代码(使用Python编写;抱歉,我不会C#)。唯一的区别是,如果一个人“有兴趣”于某件事情,我会通知他,而不是只通知他“是客户”。这样做是否足够灵活?

# --------- 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)


是的,这看起来是一个不错的解决方案,并且非常易于扩展。 - Mark Heath

4
我会避免使用"instanceof"检查(在Java中的实现)。其中一种解决方案是使用装饰者模式。您可以创建一个EmailablePerson,将其作为装饰器应用于Person对象,其中EmailablePerson使用组合来持有Person的私有实例,并将所有非电子邮件方法委托给Person对象。

1
采用完全不同的方法:StaffCustomer类的问题在于,您的员工可能一开始只是员工,后来成为客户,因此您需要将他们从员工中删除,并创建一个新的StaffCustomer类的实例。也许,在Staff类中添加一个简单的布尔值'isCustomer'会使得我们的everyone列表(可能是从适当的表中获取所有客户和所有员工编制的)不会将该员工作为员工成员而包含进去,因为它已经知道该员工已经被包含为客户了。

1

我们去年在大学里研究了这个问题,当时我们正在学习 Eiffel 语言,所以采用了多继承的方式。不管怎样,Foredecker 角色替代方案似乎足够灵活。


1
发送电子邮件给一个客户兼员工有什么问题吗?如果他是客户,那么可以向他发送电子邮件。我这样想错了吗? 为什么要将“每个人”作为您的电子邮件列表?既然我们正在处理“sendEmailToCustomer”方法而不是“sendEmailToEveryone”方法,那么拥有客户列表会更好,不是吗? 即使您想使用“everyone”列表,也不能在该列表中允许重复项。 如果没有大量重新设计,我将采用第一个Foredecker答案,也许您应该为每个人分配一些角色。

在给定的例子中,一个人不能既是顾客又是员工。这就是问题所在。 - OregonGhost
嗨, 我认为这个问题更多地涉及“如果一个人既是客户又是员工,我不想发送多封电子邮件”的问题。 为了解决这个问题, 1)“所有人”不应允许重复 2)如果它允许重复,则Person类应该像Foredecker指出的那样定义“角色”。 - vj01

1

你的类只是数据结构:它们没有任何行为,只有获取器和设置器。在这里继承是不合适的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接