string.IsNullOrEmpty()這個方法算得上是.net中使用頻率最高的方法之一。此方法是string的一個靜態方法,類似的靜態方法在string這個類中還有很多。那么這樣的方法作為靜態方法是否合理呢?如果我們從面向對象的角度出發,我們會發現這種方案不是十分符合面向對象的設計原則。
什么是對象?對象是擁有數據和行為的結合體。如果說string是一個類,那么string message="hello"這句話就定義了一個string的對象,名稱叫做message。
一.讓對象自己說話
對象應該是自治的,它擁有自己的行為和數據。如果把對象當作一個生命體,他是可以自己說話的。比如我們把message當作一個人,就可以出現下面的對話:
A:“hi message,你是空的嗎?"
B:"不是。"
A:"你的長度是多少啊?"
B:"5"
這樣的對話體現了message作為一個對象,擁有自己的行為和數據。
而代碼string.IsNullOrEmpty(message) 則描述了以下對話:
A:“Hi string, message是空的嗎?”
B:“不是”
很顯然,后面的對話借助於string類來完成本應該有message對象自己應該完成的事情。所以我們說這樣的設計並沒有完全符合面向對象的設計原則。如果說這樣的設計還可以說的過去,畢竟string類和message對象是有那么一點關系。那么下面的這種場景則更加不靠譜:
你想對一個字符串實現反轉(reverse),翻開string類查看了一番,發現.net並沒有此方法,於是你創建了一個StringHelper的類,寫下了下面的代碼:
public static string Reverse(string originalString)
{
return something;
}
如果代碼會說話,則會有如下對話:
A:"hi StringHelper, 把message反轉一下"
這樣的對話暴露了兩個問題:
- 耦合了StringHelper類,使用者必須要知道存在這樣的一個類,使用者知道的太多。
- 反轉自己是對象自己的行為,但自己並沒有實現。
經過擴展方法的"補救”,代碼可以寫為:message.IsNullOrEmpty(),這不就是我們想要的結果嗎? Ruby作為一門面向對象的語言,在設計之初就注意到了這個問題:在irb里輸入:String.instance_methods(false)可以看到String的所有實例方法,例如:empty?,:size,:reverse,在Ruby里可以直接寫:message.empty?
實際上,用“補救”一詞並不准確,因為任何人都不能在設計之初考慮到對象的所有行為,擴展方法更多的是提供了一種我們擴展用戶行為的方案。新型的編程語言:諸如F#,Ruby,swift等均提供了對象擴展的能力。
擴展方法可以完美的解決對象行為的擴展問題。你的項目里有沒有諸如**Helper, **Utility之類的類?里面的代碼大多可以成為某個對象的擴展方法。
2.模擬中綴運算符
C#中的+、-、*、/ 等運算符均為中綴表達式,比如要使用運算符"+"連接三個字符串:
"stringA"+"stringB"+"stringC";
如果使用函數則需要寫成:
string.Contact(“stringA”,string.Contact(“stringB”,”stringC”));
后一種寫法的問題在於運算的書寫順序與其實際執行順序相反,因此使用運算符而不使用函數的好處在於“中綴”運算符描述的代碼閱讀起來更為自然。利用擴展方法可以在一定程度上模擬中綴運算符。
Rectangle里有一個Union的靜態方法:public static Rectangle Union(Rectangle a, Rectangle b); 我們很難說Union這個靜態方法應該是Rectangle的一個行為。這個方法更多的表現出了兩個Rectangle在做Union運算,利用擴展方法:
internal static Rectangle Union(this Rectangle @this,Rectangle anotheRectangle)
{
return Rectangle.Union(@this, notheRectangle);
}
可以很自然的寫出:
var unionRegion = r1.Union(r2).Union(r3);
而不是:
var unionRegion = Rectangle.Union(r1, Rectangle.Union(r2, r3));
三、面向語言編程
考慮下面的擴展方法和調用:
public static TimeSpan Days(this int @this)
{
return TimeSpan.FromDays(@this);
}
var timeSpan = 3.Days();
在這種場景下,Days()既不是int的行為,也不是運算,而是具有一點面向語言編程的味道。在面向對象編程環境中,有一種編程風格叫做流暢接口(Fluent API),這樣的編程場景大多用於類庫的API設計,關於Fluent Interface的設計請看使用C#設計Fluent Interface和在C#中使用裝飾器模式和擴展方法實現Fluent Interface。
四、泛型擴展方法
擴展方法在本質上是一個靜態方法,因此也可以寫出泛型擴展,簡單舉兩個例子。
public static T ChangeTo<T>(this object @this)
{
var value = default(T);
value = (T)Convert.ChangeType(@this, typeof (T));
return value;
}
var numberString = 2.ChangeTo<string>();
var numberBool = 2.ChangeTo<bool>();
例2
public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> me, TKey key, Func<TValue> constructor)
{
TValue value;
if (me.TryGetValue(key, out value))
{
return value;
}
value = constructor();
me.Add(key, value);
return value;
}
var books = new Dictionary<int, string>();
var book = books.GetOrAdd(1, () => "book1");
var book2 = books.GetOrAdd(1, () => "book1");
這樣的擴展方法能使代碼更加簡潔。
五、一種新的面向對象設計方案
如果說上面的場景僅僅代表代碼層面的技巧,那么下面的技巧則是體現了擴展方法在面向對象中的一種的新設計方案。
場景:一輛摩托車和一輛自行車。其中鳴笛(Whistle)和讀取實時速度(ReadSpeed)具有相同的實現,而剎車(Brake)和加速(AddSpeed)則各自具有不同的實現方式。
根據這樣的場景我們立刻可以設計出這樣的繼承層次:

為了抽取鳴笛(Whistle)和讀取實時速度(ReadSpeed)這兩個行為並且公用,我們抽象了一個Vehicle作為抽象類。這樣的設計也許是正確的,但是作為一個有經驗的OO開發者也許不會馬上贊同這個方案。原因有3:
- 因為要公用代碼就立即設計類的繼承關系不具有說服力,特別是在設計初期,抽象出的Vehicle並不一定准確,很多公用的行為放在Vehicle里久而久之違反了SRP(單一職責),進一步違反OCP(開放封閉原則)
- 面向對象編程中有一個指導性的原則叫做:使用組合而非繼承。這個原則告訴我們組合比繼承更加靈活,沒有十足的把握不要使用繼承。
- 只有非常確定繼承關系,並且繼承關系符合LSP(里氏替換原則)時,才會認為這個抽象類設計的沒有問題。
利用擴展方法的方案如下:抽象出接口ICanRun,用來實現剎車(Brake)和加速(AddSpeed)。將鳴笛(Whistle)和讀取實時速度(ReadSpeed)兩個公共的實現擴展在了ICanRun接口上,整個實現非常松耦合。

internal interface ICanRun
{
void Brake();
void AddSpeed();
}
internal class Bicycle : ICanRun
{
public void Brake()
{
}
public void AddSpeed()
{
}
}
internal class Motor : ICanRun
{
public void Brake()
{
}
public void AddSpeed()
{
}
}
internal static class VehicleState
{
internal static void Whistle(this ICanRun @this)
{
}
internal static void ReadSpeed(this ICanRun @this)
{
}
}
六:利用擴展方法寫出混搭風格的代碼
我們現在看微軟對擴展方法的定義:擴展方法使您能夠向現有類型“添加”方法,而無需創建新的派生類型、重新編譯或以其他方式修改原始類型。這里的“添加”之所以使用引號,是因為並沒有真正地向指定類型添加方法。
這個定義更多的是在說明擴展方法的實現方式,但是也會帶來一個誤解:在有源代碼的情況下無需使用擴展方法。通過上面幾種場景我們可以看出擴展方法的存在不僅僅是為了“添加”方法,靈活使用擴展方法有助於寫出簡潔、富有表達力的代碼。比較典型的案例為:owin katana,asp.net core 1.0,這兩個項目中大量使用了擴展方法,這種混搭風格的代碼提高了代碼的擴展性,易於閱讀。
舉個例子,剛開始我們有這樣的一個設計:
public interface ISprite
{
void Move();
void Stop();
void Speak();
}
public class Sprite:ISprite
{
public void Move()
{
throw new NotImplementedException();
}
public void Stop()
{
throw new NotImplementedException();
}
public void Speak()
{
throw new NotImplementedException();
}
}
接口ISprite定義的一塵不染,定義了Sprite應該具有的核心能力,這樣的代碼對於閱讀者而言一目了然。不過隨着業務的發展,我們需要對Sprite序列化和反序列化,還要給Sprite添加一些屬性等待各種需求。這些能力應該是Sprite對象應該具備的,定義在Sprite類中無可厚非,但是這樣一來Sprite類急劇膨脹,閱讀性也變的差了起來,閱讀序列化和反序列化這樣的代碼幾乎不會增加閱讀者對Sprite類的理解,所以我們完全可以將這些能力分別歸類擴展在Sprite上:
public static class SpriteSerilization
{
public static byte[] Serilize(this ISprite sprite)
{
return null;
}
public static ISprite Deserilize(this byte[] bytes, string name)
{
return null;
}
}
public static class SpriteClassifier
{
public static bool IsBad(this ISprite sprite)
{
return true;
}
public static bool IsGood(this ISprite sprite)
{
return false;
}
}
這兩個擴展類幫我們對Sprite不太核心的能力做了分類,SpriteSerilization和SpriteClassifier對能力做了歸類。在我來看這樣的設計更利於閱讀和維護,你覺得呢?
