天天看點

寫給自己看的小設計4 - 對象設計通用原則之擴充原則

介紹一些别的對象設計原則。

  除了前面學習的那些核心原則,還有一些衍生的原則,掌握它們,你将更好的面向對象。不妨稱它們為"擴充原則"吧。

迪米特法則:盡量不與無關的類發生關系。

  迪米特法則全稱Law of Demeter,簡稱LoD,也稱為最少知識原則(Least Knowledge Principle,LKP)。這個原則沒什麼固定的定義,大體上有這麼幾種說法:

1. 隻與你的朋友說話
2. 不和陌生人說話
3. 對象應該隻與必須互動的對象通信      

  通俗地講,一個類應該對自己需要調用的類知道得最少,你調用的類的内部是如何實作的,都和我沒關系,那是你的事情,我就知道你提供的接口方法,我就調用這麼多,其他的一概不關心。

  也可以說,不要讓類也染上人們之間的那種神秘的暧昧關系。對象之間聯系越是簡單,則越是容易管理。

  具體的從技術上來說,就是要求對象隻與下列必須互動的各類對象通信:

(1) 目前對象本身(this);
(2) 以參數形式傳入到目前對象方法中的對象;
(3) 目前對象的成員對象;
(4) 如果目前對象的成員對象是一個集合,那麼集合中的元素也都是可以互動的;
(5) 目前對象所建立的對象。      

  此外,還需要适當設定對象的通路權限。

  正面教材太多了,就看一些反面教材吧:

// 方法鍊式調用,此種方式在Web頁面開發中倒是常用,但是靜态語言中似乎不推薦
public void Do()
{
  m_accessor.GetUser().Rename();
}
// 無謂的公開方法
class UI
{
 public void Do()
 {
  WorkHelper();
 }
 
 public void WorkHelper() { }
}      

好萊塢法則:不要調用我,讓我調用你。

  在前面我們分析對象之間互動的時候,直接調用指的就是直接調用對象的方法,間接調用中很重要的一種就是回調,特别是在異步程式設計中,好萊塢法則從某種程度上來說,就是等同于在合适的時候,多使用回調函數。

  下面是C#版本的事件實作:

public class Program
{
 static void Main(string[] args)
 {
  User user = new User();
  View ui = new View(user);
  user.Name = "Hello";
 }
}
 
delegate void OnNameChange(string name);
class View
{
 public View(User user)
 {
  user.onNameChanged += user_onNameChanged;
 }
 
 void user_onNameChanged(string name)
 {
  Console.WriteLine(name);
 }
}
 
class User
{
 private string m_name;
 public string Name
 {
  get { return m_name; }
  set
  {
   m_name = value;
   onNameChanged(m_name);
  }
 }
 public event OnNameChange onNameChanged;
}      

  這也是簡單的MVC模式中的MV之間的互動方式,View作為事件的接收者,隻需要提供好回調函數,當Model部分發生變化時,View自動接收到變化去更新UI(此處隻是列印了出來)。

  如果這裡不使用事件(觀察者模式)來實作,那麼Model必然要儲存View的引用(實際上内部當然還是儲存了相關引用的,但是觀察者合理采用各種抽象手段安排好了引用管理,比如這個例子中delegate的使用,作為C#中相當弱的耦合關系,它遠比直接使用繼承,實作接口的耦合性要弱的多),當Model的資料發生變化時,直接調用View的相關方法去更新UI,這種強烈的互相依賴關系對程式來說并不是什麼好的做法。而且一旦多個未确定的類似于View的角色對Model的改變感興趣的時候,直接應用常常難于處理。

  電影中常說,單線聯系最安全,如此是也。

優先使用組合原則:多使用組合,少使用繼承

  複用的手段除了繼承這種強限制手段,組合這種弱耦合的關系更加靈活。

  看一個小例子:

class User
{
 public virtual void PrintType() { }
}
class Admin : User
{
 public override void PrintType() { Console.WriteLine("Employer"); }
}
class Programmer : User
{
 public override void PrintType() { Console.WriteLine("Employer"); }
}
class Manager : User
{
 public override void PrintType() { Console.WriteLine("Employer"); }
}
class Contractor : User
{
 public override void PrintType() { Console.WriteLine("Temp"); }
}      

  公司的系統中除了Contractor外幾乎全是正式員工,列印類型的時候隻需要列印Employer即可,而隻有Contractor需要列印Temp。

  針對這個功能,如果我們設計一個類層次像上面這樣,工作是完全正常的。而且當有新的正式員工類型的話,也隻需要複制一遍Admin的PrintType方法即可,這裡沒有違背任何的我們前面介紹的基本或者核心原則。但是我們還是發現了不爽的地方,那就是列印正式員工的代碼複制的到處都是,咋辦?還是老套路,抽象加封裝,再傳進來。

public class Program
{
 static void Main(string[] args)
 {
  User admin = new Admin(new EmployerPrintor());
  admin.PrintType();
  User contractor = new Contractor(new TempPrintor());
  contractor.PrintType();
 }
}
 
class User
{
 Printor m_printor;
 public User(Printor printor)
 {
  m_printor = printor;
 }
 
 public virtual void PrintType() { m_printor.PrintType(); }
}
class Admin : User
{
 public Admin(Printor printor)
  :base(printor)
 {
 
 }
}
class Contractor : User
{
 public Contractor(Printor printor)
  : base(printor)
 {
 
 }
}
 
class Printor
{
 public virtual void PrintType() { }
}
class EmployerPrintor : Printor
{
 public override void PrintType() { Console.WriteLine("Employer"); }
}
class TempPrintor : Printor
{
 public override void PrintType() { Console.WriteLine("Temp"); }
}      

  對于這個原則,其實我自己甯願描述為:合理使用繼承與組合。作為複用和描述對象關系的兩種最基本的手段,我想說的是适合繼承的使用場景時候還是得用繼承,适合使用組合的時候就使用組合。

  個人認為:

  繼承的使用場景:滿足嚴格的IS-A關系,也就是說當基類是真正的作為子類的強限制存在時,也即子類完全複用基類的所有資訊的時候,繼承是必須的。

  注意這句話中的"嚴格"和"強限制",繼承作為一種最為沉重的複用關系,使用繼承時要多加考慮,因為現代語言大多數都是單繼承(隻能繼承一個類)、多實作(可以實作多個接口)的使用方式,一旦從類的繼承關系被使用了以後,擴充性其實是被限制在了基類的範圍内了。但是一旦确定需要它,就放下顧慮,直接使用。其實在前面的所有例子中,我們幾乎每個例子中都離不開繼承。

  組合的使用場景:滿足寬松的HAS-A關系,也就是說如果某個類隻是作為另一個類的從屬關系存在的時候,就可以使用組合了。

  注意這句話中的"寬松",組合使用起來就是可以這麼"任性"。

  對于很多的功能,其實純用繼承也是可以實作的,但是總是不完美,要麼有備援成員,要麼複用程度不夠,這個時候基本就說明單純的繼承是不夠的,可以嘗試使用"組合+繼承"的方式。

  好了,大原則中能上台面的也就是這麼多了。

繼續閱讀