关于oop:面向对象的最佳实践 – 继承v组合v接口

Object Oriented Best Practices - Inheritance v Composition v Interfaces

我想问一个关于如何处理一个简单的面向对象设计问题的问题。我对处理这种情况的最佳方法有自己的一些想法,但是我有兴趣听取来自堆栈溢出社区的一些意见。也欢迎链接到相关的在线文章。我用的是C,但这个问题不是特定于语言的。

假设我正在编写一个视频存储应用程序,它的数据库有一个Person表,其中包含PersonIdNameDateOfBirthAddress字段。它还有一个Staff表,它与PersonId有链接,还有一个Customer表,它也与PersonId有链接。

一个简单的面向对象的方法是说,Customer是一个"Person",因此创建的类有点像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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来向所有客户发送电子邮件:

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

这个系统工作得很好,直到我们有了既是客户又是员工的人。假设我们不希望我们的everyone列表中的同一个人出现两次,一次作为Customer一次作为Staff一次,我们是否在以下两种情况之间做出任意选择:

1
class StaffCustomer : Customer { ...

1
class StaffCustomer : Staff { ...

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

那你会怎么做?

  • 使Person类具有对StaffDetailsCustomerDetails类的可选引用?
  • 创建一个包含Person、可选StaffDetailsCustomerDetails的新类?
  • 把所有东西都变成一个接口(如IPersonIStaffICustomer),并创建三个实现适当接口的类?
  • 采取另一种完全不同的方法?

马克,这是个有趣的问题。你会发现关于这一点的意见不计其数。我不相信有"正确"的答案。这是一个很好的例子,说明刚性的继承对象设计在系统构建后确实会导致问题。

例如,假设您参加了"客户"和"员工"课程。你部署了你的系统,一切都很顺利。几周后,有人指出,他们既是"在职人员",又是"客户",而且没有收到客户电子邮件。在这种情况下,您需要进行大量的代码更改(重新设计,而不是重新考虑因素)。

我认为,如果您尝试使用一组派生类来实现人员及其角色的所有排列和组合,那么将过于复杂和难以维护。这一点尤其正确,因为上面的示例非常简单——在大多数实际应用程序中,情况将更加复杂。

对于您在这里的示例,我将使用"另一种完全不同的方法"。我将实现Person类,并在其中包含一个"角色"集合。每个人可以有一个或多个角色,如"客户"、"员工"和"供应商"。

这将使在发现新需求时更容易添加角色。例如,您可能只是拥有一个基本的"角色"类,并从中派生出新的角色。


你可以考虑使用政党和责任模式

通过这种方式,个人将拥有一系列的职责,这些职责可能属于客户或员工类型。

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


纯粹的方法是:把所有东西都变成一个接口。作为实现细节,您可以选择使用任何形式的组合或实现继承。因为这些都是实现细节,所以它们对您的公共API并不重要,所以您可以自由选择使您的生活最简单的内容。


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

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


如果我正确理解Foredecker的答案,请告诉我。这是我的代码(在python中;对不起,我不知道c)。唯一的区别是,如果一个人"是客户",我不会通知他,如果他的角色"对"那件事感兴趣,我会通知他。这个够灵活吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# --------- 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)


我会避免爪哇的"IS"检查。一种解决方案是使用装饰器模式。您可以创建一个emailableperson,它在emailableperson使用组合来保存某个人的私有实例的地方对该人进行修饰,并将所有非电子邮件方法委托给Person对象。


这里还有一些提示:从"不要想这样做"的类别中,可以看到一些遇到的错误代码示例:

finder方法返回对象

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

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

解决方案:如果需要这两个功能:对一个实例进行计数和提取,请创建两个方法:一个返回计数,另一个返回实例,但决不能有一个方法同时执行这两种操作。

问题:派生的错误做法是当finder方法返回一个单独的匹配项时,如果找到多个匹配项,则返回一个匹配项数组。这种懒惰的编程风格通常由执行前一种编程的程序员完成。

解决方案:如果只找到一个匹配项,我将返回长度为1(一)的数组;如果找到更多匹配项,则返回长度大于1的数组。此外,如果根本找不到匹配项,则根据应用程序的不同,将返回空值或长度为0的数组。

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

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

解决方案:使用在接口中定义的相同父类型来定义应该指向返回值的变量。这样可以保持编程到接口方法和代码的整洁。

超过1000行的班级是一个潜在的危险超过100行的方法也是一种潜在的危险!

问题:一些开发人员在一个类/方法中填充了太多的功能,太懒惰而无法破坏功能——这会导致低内聚性,可能导致高耦合——这与OOP中一个非常重要的原则相反!解决方案:避免使用过多的内部/嵌套类——这些类只能根据需要使用,您不必养成使用它们的习惯!使用它们可能会导致更多的问题,比如限制继承。注意代码重复!相同或太相似的代码可能已经存在于某些父类型实现中,或者可能存在于另一个类中。如果它在另一个不是超类型的类中,那么也违反了内聚规则。注意静态方法——也许您需要添加一个实用程序类!< BR>更多在:http://centraladvisor.com/it/oop-what-are-the-best-practices-in-oop


采取另一种完全不同的方法:类StaffCustomer的问题是,您的员工可以从"员工"开始,稍后成为客户,因此您需要将其作为员工删除,并创建一个新的StaffCustomer类实例。也许在"isCustomer"的staff类中有一个简单的布尔值可以让我们的everyone列表(大概是从适当的表中获取所有客户和所有员工而编译的)不获取staff成员,因为它将知道它已经作为客户包括在内。


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


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

如果这些都不能通过大量的重拨来实现,我将给出第一个Foredecker答案,也许你应该为每个人分配一些角色。


我们去年在大学里研究这个问题,我们在学习埃菲尔铁塔,所以我们使用了多重继承。无论如何,Foredecker的角色选择似乎足够灵活。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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; }
}