2008-10-19 85 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; } 
}