2008-10-19 65 views
37

我想问一个关于如何处理简单的面向对象设计问题的问题。我有一些关于解决这种情况的最佳方法,但我希望听到Stack Overflow社区的一些意见。有关网上文章的链接也表示赞赏。我使用C#,但问题不是语言特定的。面向对象的最佳实践 - 继承v组合v接口

假设我写一个视频商店应用程序,其数据库中有一个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,我们之间做出任意选择:

class StaffCustomer : Customer { ... 

class StaffCustomer : Staff { ... 

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

那么你会怎么做?

  • 充分利用Person类有一个StaffDetailsCustomerDetails类可选的参考?
  • 创建一个新类,其中包含一个Person,加上可选的StaffDetailsCustomerDetails
  • 使所有的接口(例如IPersonIStaff,ICustomer)并创建三个类来实现适当的接口?
  • 采取另一种完全不同的方法?

回答

47

马克,这是一个有趣的问题。你会发现很多意见。我不相信有一个“正确的”答案。这是一个很好的例子,说明在系统建成之后,僵硬的甲状旁腺对象设计会真正引起问题。

例如,让我们说你去了“客户”和“职员”类。你部署你的系统,一切都很开心。几周后,有人指出,他们都是“在职人员”和“客户”,他们没有收到客户的电子邮件。在这种情况下,您需要对代码进行大量更改(重新设计,而不是重新考虑因素)。

我相信如果您尝试拥有一组派生类来实现所有人员及其角色的组合,派生类就会过于复杂且难以维护。考虑到上述例子非常简单,尤其如此,在大多数实际应用中,情况会更加复杂。

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

随着新需求的发现,这将更容易添加角色。例如,您可以简单地拥有一个基础“角色”类,并从中派生新的角色。

10

纯粹的方法是:使所有的接口。作为实现细节,您可以选择使用各种形式的组合或实现继承。由于这些是实现细节,因此它们与公共API无关,因此您可以自由选择哪种方式使您的生活变得最简单。

+0

是的,你现在可以选择一个实现,稍后改变你的想法,而不会破坏其他代码。 – 2008-10-19 15:33:14

16

你可能要考虑使用人Party and Accountability patterns

这样就会有问责的集合,它可以是类型客户或员工。

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

1

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

3

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

1

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

如果这些都不能用大量的redisgn实现,我会用第一个Foredecker答案去做,也许你应该为每个人分配一些角色。

+0

在给出的例子中,一个人不能既是客户又是职员。这就是问题所在。 – OregonGhost 2008-10-19 16:57:34

+0

嗨, 我认为这个问题更多的是“如果一个人既是客户又是员工,我不想发送多封电子邮件”。 要解决这个问题, 1)“每个人”不应该允许重复 2)如果它允许重复,那么Person类应该有“角色”定义为Foredecker – vj01 2008-10-19 17:07:15

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) 

+0

指出是的,这看起来是一个很好的解决方案,并且是非常可扩展的。 – 2008-10-19 18:32:22

1

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

7

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

Liskov替代原则说,我们必须能够使用派生类,在那里我们引用基类,而不知道它。有客户继承人会违反此规定。顾客也许也可能是一个组织发挥的作用。

+0

一个组织通常有资格成为一种人,即司法人员。 – ProfK 2011-10-15 17:53:04

1

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

1

这里的一些技巧: 从类别“想都不要想做到这一点”这里遇到的代码一些不好的例子:

Finder方法返回Object

问题:根据不同的号码的发现找到方法返回一个数字代表发生的次数 - 或!如果只找到一个返回实际的对象。

不要这样做!这是最糟糕的编码实践之一,它引入了模糊和混淆代码的方式,当不同的开发人员进场时,他或他会恨你这样做。

解决方法:如果有需要这样2个功能:计数,取一个实例做创建2种方法中的一种,它返回的计数和一个返回的实例,但从来没有一个单一的方法做的两种方式。

问题:A衍生不好的做法是,当取景器方法将返回的一个单次出现发现任一发生的阵列如果发现不止一个。这种懒惰的编程风格通常由做前一个的程序员完成。

解决方案:有了这个在我的手上我会回来的长度1(一)的数组,如果只有一个发生被发现,长度为数组> 1,如果发现了更多的事件。此外,完全没有发现将根据应用返回null或长度为0的数组。

面向接口编程和使用协变返回类型

问题:面向接口编程和使用协变返回类型和调用代码铸造。

解决方案:使用而不是在接口中定义的相同的超类型用于限定其应指向返回值的变量。这可以使编程保持接口方式和您的代码清洁。

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

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

0

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

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