介紹一些别的對象設計原則。
除了前面學習的那些核心原則,還有一些衍生的原則,掌握它們,你将更好的面向對象。不妨稱它們為"擴充原則"吧。
迪米特法則:盡量不與無關的類發生關系。
迪米特法則全稱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關系,也就是說如果某個類隻是作為另一個類的從屬關系存在的時候,就可以使用組合了。
注意這句話中的"寬松",組合使用起來就是可以這麼"任性"。
對于很多的功能,其實純用繼承也是可以實作的,但是總是不完美,要麼有備援成員,要麼複用程度不夠,這個時候基本就說明單純的繼承是不夠的,可以嘗試使用"組合+繼承"的方式。
好了,大原則中能上台面的也就是這麼多了。