面向对象的最佳实践 - 继承 v 组合 v 接口
-
03-07-2019 - |
题
我想问一个关于如何解决简单的面向对象设计问题的问题。对于解决这种情况的最佳方法,我有一些自己的想法,但我有兴趣听取 Stack Overflow 社区的一些意见。相关在线文章的链接也值得赞赏。我正在使用 C#,但问题不是特定于语言的。
假设我正在编写一个视频商店应用程序,其数据库有一个 Person
表,与 PersonId
, Name
, DateOfBirth
和 Address
字段。它还有一个 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
类有可选的引用StaffDetails
和CustomerDetails
班级? - 创建一个新类,其中包含
Person
, ,加上可选的StaffDetails
和CustomerDetails
? - 让一切都成为接口(例如
IPerson
,IStaff
,ICustomer
)并创建三个实现适当接口的类? - 采取另一种完全不同的方法?
解决方案
马克,这是一个有趣的问题。您会发现对此有很多意见。我不相信有一个“正确”的答案。这是一个很好的例子,说明严格的层次对象设计在系统构建后确实会引起问题。
例如,假设您选择了“客户”和“员工”课程。您部署了系统,一切都很顺利。几周后,有人指出他们既是“员工”又是“客户”,但他们没有收到客户电子邮件。在这种情况下,您需要进行大量代码更改(重新设计,而不是重构)。
我相信,如果您尝试拥有一组派生类来实现人员及其角色的所有排列和组合,那么它会过于复杂且难以维护。考虑到上面的例子非常简单,这一点尤其正确——在大多数实际应用中,事情会更加复杂。
对于这里的例子,我会选择“采取另一种完全不同的方法”。我将实现 Person 类并在其中包含“角色”的集合。每个人都可以拥有一个或多个角色,例如“客户”、“员工”和“供应商”。
这将使在发现新需求时更容易添加角色。例如,您可能只是有一个基“Role”类,并从中派生新角色。
其他提示
纯粹的方法是:让一切都成为界面。作为实现细节,您可以选择使用任何各种形式的组合或实现继承。由于这些是实现细节,因此它们对您的公共 API 并不重要,因此您可以自由选择使您的生活最简单的那个。
人是一个人,而客户只是一个人可能不时采用的角色。Man 和 Woman 都是继承 Person 的候选者,但 Customer 是一个不同的概念。
里氏替换原则指出,我们必须能够在不知道基类引用的情况下使用派生类。让 Customer 继承 Person 会违反这一点。客户也可能是组织扮演的角色。
让我知道我是否正确理解了 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)
我会避免“is”检查(Java 中的“instanceof”)。一种解决方案是使用 装饰模式. 。您可以创建一个装饰 Person 的 EmailablePerson,其中 EmailablePerson 使用组合来保存 Person 的私有实例,并将所有非电子邮件方法委托给 Person 对象。
去年我们在大学研究这个问题,我们正在学习埃菲尔铁塔,所以我们使用了多重继承。无论如何,Foredecker 的角色选择似乎足够灵活。
向身为员工的客户发送电子邮件有什么问题吗?如果他是客户,则可以向他发送电子邮件。我这样想有错吗?为什么你应该把“每个人”作为你的电子邮件列表?既然我们正在处理“sendEmailToCustomer”方法而不是“sendEmailToEveryone”方法,那么拥有一个客户列表不是更好吗?即使您想使用“所有人”列表,您也不能允许该列表中有重复项。
如果通过大量重新设计都无法实现这些目标,我将采用 Foredecker 的第一个答案,也许您应该为每个人分配一些角色。
你的类只是数据结构:它们都没有任何行为,只有 getter 和 setter。继承在这里是不合适的。
采用另一种完全不同的方法:StaffCustomer 类的问题在于,您的员工可能一开始只是员工,后来成为客户,因此您需要将他们作为员工删除并创建 StaffCustomer 类的新实例。也许“isCustomer”的 Staff 类中的一个简单布尔值将允许我们的每个人列表(大概是通过从适当的表中获取所有客户和所有员工来编译的)不获取该员工,因为它会知道它已经作为客户包含在内。
这里还有一些提示:从“甚至不要想这样做”的类别来看,这里有一些遇到的不好的代码示例:
Finder 方法返回对象
问题:根据找到的出现次数,finder 方法返回一个代表出现次数的数字 - 或者!如果只找到一个,则返回实际对象。
不要这样做!这是最糟糕的编码实践之一,它引入了歧义,并以某种方式混乱了代码,当不同的开发人员介入时,她或他会讨厌你这样做。
解决方案:如果需要这2个功能:计数和获取实例确实会创建两种方法,一种返回计数,另一种返回实例,但从来没有一个方法同时执行这两种操作。
问题:一种派生的不良做法是,查找器方法返回找到的单个事件,或者返回一组事件(如果找到多个事件)。这种懒惰的编程风格通常是由从事前一种编程风格的程序员完成的。
解决方案:有了这个,如果只找到一次出现,我将返回一个长度为 1(one) 的数组,如果发现更多出现,则返回一个长度 >1 的数组。此外,如果根本没有找到任何匹配项,则会返回 null 或长度为 0 的数组,具体取决于应用程序。
对接口进行编程并使用协变返回类型
问题:对接口进行编程并在调用代码中使用协变返回类型和转换。
解决方案:使用接口中定义的相同超类型来定义应指向返回值的变量。这可以保持接口方法的编程和代码的干净。
拥有1000多行的课程是一种潜伏的危险方法,其中100行也是潜伏的危险!
问题:一些开发人员在一个类/方法中塞入太多功能,而懒得破坏这些功能——这会导致低内聚性,甚至可能导致高耦合——这与 OOP 中一个非常重要的原则相反!解决方案:避免使用太多内部/嵌套类 - 这些类只能根据需要使用,您不必养成使用它们的习惯!使用它们可能会导致更多问题,例如限制继承。注意代码重复!相同或过于相似的代码可能已经存在于某些超类型实现中,或者可能存在于另一个类中。如果它位于另一个不是超类型的类中,那么您也违反了内聚规则。注意静态方法——也许您需要添加一个实用程序类!
更多内容请见:http://centraladvisor.com/it/oop-what-are-the-best-practices-in-oop
您可能不想为此使用继承。试试这个:
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; }
}