39

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

假设我正在编写一个视频商店应用程序,其数据库有一个Person表,其中包含、PersonIdName字段。它还有一个表,它有一个链接到 a ,还有一个表也链接到。DateOfBirthAddressStaffPersonIdCustomerPersonId

一个简单的面向对象的方法是说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列表中有两次相同的人,一次作为 aCustomer一次作为 a Staff,我们是否可以在以下之间做出任意选择:

class StaffCustomer : Customer { ...

class StaffCustomer : Staff { ...

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

那你会怎么做?

  • 使类具有对 a和类Person的可选引用?StaffDetailsCustomerDetails
  • 创建一个新类,其中包含一个Person,加上可选StaffDetailsCustomerDetails
  • 让所有东西都成为一个接口(例如IPerson, IStaff, ICustomer)并创建三个实现相应接口的类?
  • 采取另一种完全不同的方法?
4

12 回答 12

49

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

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

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

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

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

于 2008-10-19T15:34:59.433 回答
17

您可能需要考虑使用Party 和 Accountability 模式

这样,Person 将拥有一个 Accountabilities 的集合,其类型可能是 Customer 或 Staff。

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

于 2008-10-19T15:51:32.647 回答
10

纯粹的方法是:让一切都成为接口。作为实现细节,您可以选择使用任何各种形式的组合或实现继承。由于这些是实现细节,它们对您的公共 API 无关紧要,因此您可以自由选择让您的生活更简单的方法。

于 2008-10-19T15:29:04.453 回答
7

一个人是一个人,而客户只是一个人可能不时采用的角色。Man 和 Woman 将是继承 Person 的候选人,但 Customer 是一个不同的概念。

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

于 2008-10-20T09:34:42.670 回答
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)

于 2008-10-19T18:18:46.670 回答
3

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

于 2008-10-19T16:46:41.597 回答
1

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

于 2008-10-19T16:31:45.467 回答
1

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

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

于 2008-10-19T16:50:35.397 回答
1

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

于 2008-10-19T18:24:04.380 回答
1

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

于 2010-07-22T07:44:12.553 回答
1

这里还有一些提示:从“甚至不想这样做”的类别中,这里有一些遇到的不良代码示例:

Finder 方法返回对象

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

不要这样做!这是最糟糕的编码实践之一,它会引入歧义并以某种方式使代码混乱,以至于当不同的开发人员参与进来时,她或他会因为这样做而讨厌你。

解决方案:如果需要这样的 2 个功能:计数和获取实例确实会创建 2 个方法,一个返回计数,一个返回实例,但从来没有一个方法可以双向使用。

问题:一个衍生的不好的做法是,当查找器方法返回找到的一个单一事件时,或者如果找到多个事件,则返回一个事件数组。这种懒惰的编程风格通常由使用前一种的程序员完成。

解决方案:如果只找到一次,我将返回一个长度为 1(one) 的数组,如果找到更多的匹配项,则返回一个长度 >1 的数组。此外,根据应用程序,根本找不到任何事件将返回 null 或长度为 0 的数组。

对接口进行编程并使用协变返回类型

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

解决方案:使用接口中定义的相同超类型来定义应该指向返回值的变量。这使编程保持接口方法和您的代码干净。

超过 1000 行的类是一个潜伏的危险 超过 100 行的方法也是一个潜伏的危险!

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

于 2012-08-03T17:34:32.073 回答
-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; }
}
于 2013-07-16T16:31:47.377 回答