面向对象的最佳实践 – inheritancev合成v接口

我想问一个关于如何处理简单的面向对象devise问题的问题。 我有一些关于解决这个问题的最佳方法,但我希望听到Stack Overflow社区的一些意见。 有关网上文章的链接也赞赏。 我正在使用C#,但问题不是语言特定的。

假设我正在写一个video存储应用程序,其数据库有一个Person表, PersonIdNameDateOfBirthAddress字段。 它还有一个Staff表,它有一个PersonId链接,还有一个Customer表也​​链接到PersonId

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

 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 ,我们是否可以任意select:

 class StaffCustomer : Customer { ... 

 class StaffCustomer : Staff { ... 

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

那么你会怎么做?

  • Person类拥有对StaffDetailsCustomerDetails类的可选引用?
  • 创build一个包含Person的新类,加上可选的StaffDetailsCustomerDetails
  • 使所有的接口(例如IPersonIStaffICustomer )并创build三个类来实现适当的接口?
  • 采取另一种完全不同的方法

马克,这是一个有趣的问题。 你会发现很多意见。 我不相信有一个“正确”的答案。 这是一个很好的例子,说明一个刚性的病媒对象devise在系统build立之后真的可以引起问题。

例如,让我们说你去了“客户”和“职员”类。 你部署你的系统,一切都很开心。 几个星期后,有人指出,他们都是“员工”和“客户”,他们没有收到客户的电子邮件。 在这种情况下,你需要做很多代码修改(重新devise,而不是重新devise)。

我相信如果你试图拥有一组派生类来实现所有人和他们angular色的排列组合,那么过于复杂和难以维护。 鉴于上面的例子非常简单,尤其如此,在大多数实际的应用中,情况将会更加复杂。

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

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

您可能要考虑使用党和问责制模式

这样Person会有一个可能是Customer或Stafftypes的Accountability集合。

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

纯粹的做法是:把所有东西都当成一个接口。 作为实现细节,您可以select使用各种forms的组合或实现inheritance。 由于这些是实现细节,因此它们与公共API无关,因此您可以自由select哪一种方式使您的生活变得最简单。

一个人是一个人,而一个客户只是一个人可能不时采取的angular色。 男人和女人是inheritance人的候选人,但客户是一个不同的概念。

Liskov替代原则指出,我们必须能够使用派生类,在那里我们引用了一个基类,而不知道它。 有客户inheritance人会违反这一点。 客户也许也可能是一个组织所发挥的作用。

让我知道,如果我正确理解Foredecker的答案。 这是我的代码(在Python中,对不起,我不知道C#)。 唯一的区别是,如果一个人“是一个客户”,我不会通知一些事情,如果他的某个angular色“对这个事情感兴趣”,我会这样做。 这足够灵活吗?

 # --------- 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”检查(Java中的“instanceof”)。 一个解决scheme是使用装饰模式 。 您可以创build一个EmailablePerson来装饰Person,其中EmailablePerson使用组合来保存Person的私有实例,并将所有非电子邮件方法委托给Person对象。

我们去年在大学学习这个问题,我们学习了埃菲尔,所以我们使用了多重inheritance。 无论如何,Foredeckerangular色的select似乎是足够灵活的。

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

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

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

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

这里有一些更多的提示:从“甚至不认为这样做”的类别下面是一些代码遇到的不好的例子:

Finder方法返回Object

问题:根据find的发生次数,查找器方法返回一个代表发生次数的数字 – 或! 如果只find一个返回实际的对象。

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

解决scheme:如果需要这样的2个function:计数和获取实例,请创build2个方法,一个返回计数,另一个返回实例,但从来没有一个方法执行。

问题:一个派生的不好的做法是,当一个finder方法返回一个单个事件发现一个发生的数组,如果多个发现。 这种懒惰的编程风格是由一般的程序员完成的。

解决scheme:如果只有一个匹配项,则返回长度为1(一)的数组,如果发现多个匹配项,则返回长度大于1的数组。 而且,完全没有发现将根据应用程序返回null或长度为0的数组。

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

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

解决scheme:使用在接口中定义的相同的超types来定义应该指向返回值的variables。 这使编程保持接口的方式,并保持您的代码清洁。

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

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

你可能不想为此使用inheritance。 试试这个:

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