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

时间:2008-10-19 15:22:34

标签: inheritance oop composition

我想问一个关于如何处理简单的面向对象设计问题的问题。我对自己处理这种情况的最佳方法有一些想法,但我有兴趣听听Stack Overflow社区的一些意见。还赞赏相关在线文章的链接。我正在使用C#,但问题不是语言特定的。

假设我正在编写一个视频商店应用程序,其数据库有Person表,其中包含PersonIdNameDateOfBirthAddress字段。它还有一个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; }
}

现在我们可以编写一个函数来向所有客户发送电子邮件:

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

这个系统运行正常,直到我们有一个既是客户又是员工的人。假设我们真的不希望我们的everyone列表中有两次同一个人,一次为Customer而另一次为Staff,我们之间是否可以选择: / p>

class StaffCustomer : Customer { ...

class StaffCustomer : Staff { ...

显然,只有这两个中的第一个不会破坏SendEmailToCustomers功能。

那你会怎么做?

  • Person类包含对StaffDetailsCustomerDetails类的可选引用?
  • 创建一个包含Person的新类,以及可选的StaffDetailsCustomerDetails
  • 将所有内容设为界面(例如IPersonIStaffICustomer)并创建三个实现相应界面的类?
  • 采取另一种完全不同的方法?

12 个答案:

答案 0 :(得分:49)

马克,这是一个有趣的问题。你会发现很多意见。我不相信有一个“正确”的答案。这是一个很好的例子,说明在构建系统之后,刚性的层次对象设计确实会导致问题。

例如,假设你选择了“客户”和“员工”课程。部署系统,一切都很愉快。几个星期后,有人指出他们既是“员工”又是“客户”,他们没有收到客户的电子邮件。在这种情况下,您需要进行大量的代码更改(重新设计,而不是重新设计)。

如果您尝试使用一组实现所有排列和人员及其角色组合的派生类,我相信它会过于复杂且难以维护。鉴于以上示例非常简单,这一点尤其正确 - 在大多数实际应用中,事情会更复杂。

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

这样可以在发现新要求时更轻松地添加角色。例如,您可能只有一个基础“角色”类,并从中派生出新角色。

答案 1 :(得分:17)

您可能需要考虑使用Party and Accountability patterns

这样,Person将拥有一组Accountients,可以是Customer或Staff类型。

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

答案 2 :(得分:10)

纯粹的方法是:让一切都成为一个界面。作为实现细节,您可以选择使用各种形式的组合或实现继承。由于这些是实施细节,因此它们与您的公共API无关,因此您可以自由选择最适合您生活的那些。

答案 3 :(得分:7)

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

Liskov替换原则说我们必须能够在不知道它的情况下使用派生类来引用基类。让客户继承Person会违反此规定。客户也许也可能是组织所扮演的角色。

答案 4 :(得分: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)

答案 5 :(得分:3)

我会避免“is”检查(Java中的“instanceof”)。一种解决方案是使用Decorator Pattern。您可以创建一个EmailablePerson来装饰Person,其中EmailablePerson使用组合来保存Person的私有实例,并将所有非电子邮件方法委托给Person对象。

答案 6 :(得分:1)

我们去年在大学学习这个问题,我们学习埃菲尔,所以我们使用了多重继承。无论如何,Foredecker角色替代方案似乎足够灵活。

答案 7 :(得分:1)

向作为工作人员的客户发送电子邮件有什么问题?如果他是客户,那么他可以发送电子邮件。这么想我错了吗? 为什么要把“所有人”作为你的电子邮件列表?因为我们处理的是“sendEmailToCustomer”方法而不是“sendEmailToEveryone”方法,所以最好有一个客户列表? 即使您想使用“所有人”列表,也不能在该列表中允许重复。

如果通过大量重新设置无法实现这些目标,我将会使用第一个Foredecker答案,并且您应该为每个人分配一些角色。

答案 8 :(得分:1)

你的类只是数据结构:它们都没有任何行为,只有getter和setter。继承在这里是不合适的。

答案 9 :(得分:1)

采取另一种完全不同的方法:StaffCustomer类的问题在于,您的员工可以从员工开始,稍后成为客户,因此您需要将其删除为员工并创建StaffCustomer类的新实例。也许在'isCustomer'的Staff类中的一个简单的布尔值将允许我们的每个人列表(可能是从所有客户和所有员工从适当的表中编译而成)得不到工作人员,因为它会知道它已经作为客户包含在内。 / p>

答案 10 :(得分:1)

以下是一些提示: 从“甚至不想这样做”的类别来看,这里遇到了一些不好的代码示例:

Finder方法返回Object

问题:根据发现的次数,finder方法返回一个表示出现次数的数字 - 或者!如果只找到一个返回实际对象。

不要这样做!这是最糟糕的编码实践之一,它引入了歧义,并以某种方式混淆代码,当一个不同的开发人员发挥作用时,她或他会讨厌你这样做。

解决方案:如果需要这样的2个功能:计算和获取实例会创建2个方法,一个返回计数,另一个返回实例,但从不会有一个方法同时执行两种方式。

问题:派生的错误做法是当finder方法返回一个单一事件时,如果找到多个事件,则找到一个出现的数组。这种懒惰的编程风格很大程度上由前一个编程人员完成。

解决方案:如果只发现一次,我将返回一个长度为1(一)的数组,如果找到更多的出现,则返回长度> 1的数组。此外,根本不会发现任何事件将返回null或长度为0的数组,具体取决于应用程序。

编程到接口并使用协变返回类型

问题:编程到接口并使用协变返回类型并在调用代码中强制转换。

解决方案:使用接口中定义的相同超类型来定义应该指向返回值的变量。这使编程保持接口方式并且代码清洁。

超过1000行的类是潜在的危险 超过100行的方法也是潜在的危险!

问题:一些开发人员在一个类/方法中填充太多功能,太懒而不能破坏功能 - 这导致低内聚力和高耦合 - 这是OOP中一个非常重要原则的反转! 解决方案:避免使用太多内部/嵌套类 - 这些类只能在每个需要的基础上使用,您不必习惯使用它们!使用它们可能会导致更多问题,例如限制继承。寻找代码重复!在某些超类型实现中或者在另一个类中可能已存在相同或相似的代码。如果它在另一个不是超类型的类中,你也违反了内聚规则。注意静态方法 - 也许你需要一个实用程序类来添加!
更多信息: http://centraladvisor.com/it/oop-what-are-the-best-practices-in-oop

答案 11 :(得分:-1)

您可能不希望为此使用继承。试试这个:

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; }
}