我想问一个关于如何处理简单的面向对象设计问题的问题.我对自己处理这种情况的最佳方法有一些想法,但我有兴趣听听Stack Overflow社区的一些意见.还赞赏相关在线文章的链接.我正在使用C#,但问题不是语言特定的.
假设我写一个视频商店应用程序,其数据库中有一个Person
表,其中PersonId
,Name
,DateOfBirth
和Address
领域.它还有一个Staff
表,其中包含指向a的链接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(IEnumerableeveryone) { foreach(Person p in everyone) if(p is Customer) SendEmail(p); }
这个系统运行正常,直到我们有一个既是客户又是员工的人.假设我们真的不希望我们的everyone
列表在两次中有同一个人,一次作为a Customer
,一次作为a Staff
,我们之间做出任意选择:
class StaffCustomer : Customer { ...
和
class StaffCustomer : Staff { ...
显然,只有这两个中的第一个不会破坏SendEmailToCustomers
功能.
那你会怎么做?
使Person
类具有对a StaffDetails
和CustomerDetails
类的可选引用?
创建一个包含a的新类Person
,加上可选StaffDetails
和CustomerDetails
?
使所有的接口(例如IPerson
,IStaff
,ICustomer
),并创建实施适当的接口三类?
采取另一种完全不同的方法
Foredecker.. 49
马克,这是一个有趣的问题.你会发现很多意见.我不相信有一个"正确"的答案.这是一个很好的例子,说明在构建系统之后,刚性的层次对象设计确实会导致问题.
例如,假设您选择了"客户"和"员工"类.部署系统,一切都很愉快.几个星期后,有人指出他们既是"员工"又是"客户",他们没有收到客户的电子邮件.在这种情况下,您需要进行大量的代码更改(重新设计,而不是重新设计).
我相信如果你试图拥有一组实现所有排列和人员及其角色组合的派生类,那将会过于复杂和难以维护.鉴于以上示例非常简单,这一点尤其正确 - 在大多数实际应用中,事情会更复杂.
对于你的例子,我会选择"采取另一种完全不同的方法".我将实现Person类并在其中包含"角色"的集合.每个人都可以拥有一个或多个角色,例如"客户","员工"和"供应商".
这样可以在发现新需求时更轻松地添加角色.例如,您可能只有一个基础"角色"类,并从中派生出新角色.
马克,这是一个有趣的问题.你会发现很多意见.我不相信有一个"正确"的答案.这是一个很好的例子,说明在构建系统之后,刚性的层次对象设计确实会导致问题.
例如,假设您选择了"客户"和"员工"类.部署系统,一切都很愉快.几个星期后,有人指出他们既是"员工"又是"客户",他们没有收到客户的电子邮件.在这种情况下,您需要进行大量的代码更改(重新设计,而不是重新设计).
我相信如果你试图拥有一组实现所有排列和人员及其角色组合的派生类,那将会过于复杂和难以维护.鉴于以上示例非常简单,这一点尤其正确 - 在大多数实际应用中,事情会更复杂.
对于你的例子,我会选择"采取另一种完全不同的方法".我将实现Person类并在其中包含"角色"的集合.每个人都可以拥有一个或多个角色,例如"客户","员工"和"供应商".
这样可以在发现新需求时更轻松地添加角色.例如,您可能只有一个基础"角色"类,并从中派生出新角色.
您可能需要考虑使用Party和Accountability模式
这样,Person将拥有一组Accountients,可以是Customer或Staff类型.
如果稍后添加更多关系类型,模型也会更简单.
纯粹的方法是:让一切都成为一个界面.作为实现细节,您可以选择使用各种形式的组合或实现继承.由于这些是实现细节,因此它们与您的公共API无关,因此您可以自由选择最简单的生活方式.
一个人是一个人,而一个人只是一个人可能不时采用的角色.男人和女人会成为继承人的候选人,但顾客是一个不同的概念.
Liskov替换原则说我们必须能够在不知道它的情况下使用派生类来引用基类.让客户继承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)