原文地址:http://www.dotnetcurry.com/dotnet/1092/dotnet-design-patterns
軟件開發有許多設計模式。其中一些模式非常受歡迎。說幾乎所有的模式都可以被接受,而不管我們選擇的編程語言如何。我們將看到如何在C#中使用一些設計模式。
在這篇文章中,我們不會只關注一組設計模式。我們很好的重新觀察一些已有的問題,看看我們如何能夠將它們用于現實世界的困境和疑慮。
.NET設計模式一個小背景
有些開發人員討厭設計模式是事實。這主要是因為分析、決定和實施一個特定的模式可能是一個頭痛的問題。我敢肯定,我們都遇到過開發者花費無數個小時,甚至幾天的時間來討論使用的模式類型的情況。更不用說,最好的方法和實施它的方式。這是一個非常糟糕的方式來開發軟件。
這種困境往往是由于他們的代碼可以適用于一組設計模式而引起的。這是一件難以置信的事情。但是,如果我們認為這些模式是一套工具,可以讓我們做出正確的決定,并且可以用作有效的溝通工具; 那么我們正在以正確的方式接近它。
本文主要關注使用C#作為編程語言的.NET設計模式。一些模式也可以應用于非基于.NET的編程語言。重復我剛才所說的,大多數模式可以被接受,而不管我們選擇的編程語言。
抽象工廠模式
維基百科定義:“ 抽象工廠模式提供了一種方法來封裝一組具有共同主題的單個工廠,而不指定具體的類。”
雖然這個定義是正確的,但是這種模式的真實用法可能是不同的。它可以基于現實生活中的問題和人們可能遇到的問題。以最簡單的形式,我們將創建具有相關對象的實例,而不必指定具體的實現。請參考下面的例子:
public class Book
{
public string Title { get; set; }
public int Pages { get; set; }
public override string ToString()
{
return string.Format("Book {0} - {1}", Title, Pages);
}
}
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine(CreateInstance("ConsoleApplication1.Book", new Dictionary));
}
}
根據上面的例子,實例的創建被委托給一個名為CreateInstances()的方法。
這個方法接受一個類名和屬性值作為參數。
乍一看,這似乎是很多代碼只是為了創建一個實例,并為其屬性添加一些值。但是當我們想要根據參數動態創建實例時,這種方法變得非常強大。例如,基于用戶輸入在運行時創建實例。這對于 依賴注入(DI) 也是非常重要的。上面的例子只是演示了這種模式的基礎。但是展示抽象工廠模式的最好方法就是看看現實世界的例子。介紹一些已經存在的東西是多余的。因此,如果你有興趣,請看這個堆棧溢出問題,它有一些很好的信息。
附加說明: Activator.CreateInstance不以抽象工廠模式為中心。它只是允許我們基于類型參數以便捷的方式創建實例。在某些情況下,我們只是通過新建(即新的Book())來創建實例,并仍然使用抽象工廠模式。這一切都取決于用例及其各種應用程序。
級聯模式
public class MailManager
{
public void To(string address) { Console.WriteLine("To");}
public void From(string address) { Console.WriteLine("From"); }
public void Subject(string subject) { Console.WriteLine("Subject"); }
public void Body(string body) { Console.WriteLine("Body"); }
public void Send() { Console.WriteLine("Sent!"); }
}
public class Program
{
public static void Main(string[] args)
{
var mailManager = new MailManager();
mailManager.From("alan@developer.com");
mailManager.To("jonsmith@developer.com");
mailManager.Subject("Code sample");
mailManager.Body("This is an the email body!");
mailManager.Send();
}
}
這是一個非常簡單的代碼示例。但是讓我們把注意力集中在類Program的MailManager類的客戶端上。如果我們看這個類,它創建一個MailManager實例并調用例如.To(),.From(),.Body() .Send()等方法。
如果我們仔細看一下代碼,就像我們剛剛看到的那樣編寫代碼有幾個問題。 一個是注意變量“mailManager”已經重復了好幾次了。所以我們覺得編寫多余的寫代碼有些尷尬。其次如果我們想發送另外一封郵件?我們應該創建一個新的MailManager實例,還是應該重用現有的“ mailManager ”實例?我們首先遇到這些問題的原因是API(應用程序編程接口)對于使用者來說是不清楚的。
讓我們看看更好的方式來表示這個代碼。
首先,我們對MailManager類進行一個小改動,如下所示。我們修改代碼,以便返回MailManager的當前實例,而不是返回類型void。
注意Send()方法不返回MailManager。我將解釋為什么我們這樣做是在下一節。
修改后的代碼顯示在這里。
public class Mailmanager
{
public MailManager To(string address) { Console.WriteLine("To"); return this; }
public MailManager From(string address) { Console.WriteLine("From"); return this; }
public MailManager Subject(string subject) { Console.WriteLine("Subject"); return this;}
public MailManager Body(string body) { Console.WriteLine("Body"); return this; }
public void Send() { Console.WriteLine("Sent!"); }
}
為了使用新的MailManager實現,我們將修改程序如下。
public static void Main(string[] args)
{
new MailManager()
.From("alan@developer.com")
.To("jonsmith@developer.com")
.Subject("Code sample")
.Body("This is an the email body!")
.Send();
}
代碼的重復和冗長已被刪除。我們還介紹了一個很好的流暢風格的API。我們稱之為級聯模式。您可能已經在許多流行的框架(如FluentValidation)中看到過這種模式。我最喜歡的是NBuilder。
Builder<product>.CreateNew().With(x => x.Title = "some title").Build(); </product>
級聯Lambda模式
這就是我們開始為Cascade Pattern添加一些風味的地方。我們再來擴展一下這個例子。基于前面的例子,這是我們最終編寫的代碼。
new MailManager()
.From("alan@developer.com")
.To("jonsmith@developer.com")
.Subject("Code sample")
.Body("This is an the email body!")
.Send();
請注意,Send()方法是從MailManager的一個實例中調用的。這是方法鏈的最后一個例程。因此它不需要返回一個實例。這也意味著API隱含地指出,如果我們想發送另一個郵件,我們將不得不創建一個新的MailManager實例。然而,在調用.Send()之后,用戶還不清楚應該怎么做。
這是我們可以利用lambda表達式的優勢,并明確向API的使用者展示意圖。首先,我們將Send()方法轉換為一個Static方法,并將其簽名更改為接受一個Action委托。此委托將MailManager作為參數。我們在Send()方法中調用這個動作,如下所示:
public class MailManager
{
public MailManager To(string address) { Console.WriteLine("To"); return this; }
public MailManager From(string address) { Console.WriteLine("From"); return this; }
public MailManager Subject(string subject) { Console.WriteLine("Subject"); return this;
}
public MailManager Body(string body) { Console.WriteLine("Body"); return this; }
public static void Send(Action<mailmanager> action)
{
action(new MailManager());
Console.WriteLine("Sent!");
} //</mailmanager>
為了使用MailManager類,我們可以改變程序,如下所示:
Mailmanager.Send((mail) => mail
.From("alan@developer.com")
.To("jonsmith@developer.com")
.Subject("Code sample")
.Body("This is an the email body!"));
正如我們在代碼示例中看到的那樣,由委托指定的動作作為Send()方法的參數,清楚地表明動作與構造郵件有關。因此它可以通過調用Send()方法發送出去。這種方法更加優雅,因為它消除了前面描述的Send()方法的困惑。
插件模式
描述可插拔行為的最好方法是使用示例。以下代碼示例計算給定數字數組的總數。
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine(GetTotal(new [] {1, 2, 3, 4, 5, 6}));
Console.Read();
}
public static int GetTotal(int[] numbers)
{
int total = 0;
foreach (int n in numbers)
{
total += n;
}
return total;
}
}
假設我們有一個新的要求。盡管我們不想改變GetTotal()方法,但是我們也想計算偶數。我們大多數人會添加另一個方法,比如GetEvenTotalNumbers,如下所示。
public class Program
{
public static int GetTotal(int[] numbers)
{
int total = 0;
foreach (int n in numbers)
{
total += n;
}
return total;
}
public static int GetTotalEvenNumbers(int[] numbers)
{
int total = 0;
foreach (int n in numbers)
{
if (n%2 == 0)
{
total += n;
}
}
return total;
}
public static void Main(string[] args)
{
Console.WriteLine(GetTotal(new [] {1, 2, 3, 4, 5, 6}));
Console.WriteLine(GetTotalEvenNumbers(new[] { 1, 2, 3, 4, 5, 6 }));
Console.Read();
}
}
我們只是復制/粘貼現有??的函數,并添加了需要計算偶數的唯一條件。那很容易!假設有另外一個要求計算奇數的總數,它就像復制/粘貼一個較早的方法一樣簡單,并稍微修改它來計算奇數。
public static int GetTotalOddNumbers(int[] numbers)
{
int total = 0;
foreach (int n in numbers)
{
if (n % 2 != 0)
{
total += n;
}
}
return total;
}
在這個階段,你可能會意識到,這不是我們編寫軟件的方法。這幾乎是復制粘貼不可維護的代碼。為什么不可維護?比方說,如果我們必須改變我們計算總數的方式。這意味著我們將不得不對3種不同的方法進行修改。如果我們仔細分析這三種方法,它們在實施上是非常相似的。if條件只有不同。為了刪除代碼重復,我們可以引入可插入行為。我們可以將差異外化并將其注入方法。通過這種方式,API的使用者可以控制傳入該方法的內容。這被稱為可插入行為。
public class Program
{
public static int GetTotal(int[] numbers, Predicate<int> selector)
{
int total = 0;
foreach (int n in numbers)
{
if (selector(n)) {total += n;}
}
return total;
}
public static void Main(string[] args)
{
Console.WriteLine(GetTotal(new [] {1, 2, 3, 4, 5, 6}, i => true));
Console.WriteLine(GetTotal(new[] { 1, 2, 3, 4, 5, 6 }, i => i % 2 == 0));
Console.WriteLine(GetTotal(new[] { 1, 2, 3, 4, 5, 6 }, i => i % 2 != 0));
Console.Read();
}
}
正如我們在上面的例子中看到的一個委托已被注入該方法。這使我們能夠將選擇標準外化。代碼重復已被刪除,我們有更多的可維護代碼。
除此之外,假設我們要擴展選擇器的行為。例如,選擇基于多個參數。為此,我們可以使用一個Func委托。您可以指定多個參數給選擇器并返回您所需的結果。有關如何使用Func委托的更多信息,請參閱Func。
用Lambda表達式執行模式
這個模式允許我們使用lambda表達式來執行一段代碼。現在聽起來很簡單,這就是lambda表達式所做的。然而,這種模式是關于使用lambda表達式并實現一種編碼風格,這將增強現有流行的設計模式之一。我們來看一個例子。
假設我們要清理對象中的資源。我們將編寫類似于以下的代碼:
public class Database
{
public Database()
{
Debug.WriteLine("Database Created..");
}
public void Query1()
{
Debug.WriteLine("Query1..");
}
public void Query2()
{
Debug.WriteLine("Query2..");
}
~Database()
{
Debug.WriteLine("Cleaned-Up");
}
}
public class Program
{
public static void Main(string[] args)
{
var db = new Database();
db.Query1();
db.Query2();
}
}
這個程序的輸出是:
Database Created..
Query1..
Query2..
Cleaned Up!
請注意,Finalizer/Destructor隱式調用,它將清理資源。與上面的代碼的問題是,我們沒有在控制時終結被調用。
讓我們看看for循環中的相同代碼并執行它幾次:
public class Program
{
public static void Main(string[] args)
{
for (int i = 0; i < 4; i++)
{
var db = new Database();
db.Query1();
db.Query2();
}
}
}
它的執行結果將如下:
Database Created..
Query1..
Query2..
Database Created..
Query1..
Query2..
Database Created..
Query1..
Query2..
Database Created..
Query1..
Query2..
Cleaned Up!
Cleaned Up!
Cleaned Up!
Cleaned Up!
每個數據庫創建的所有清理操作發生在循環的結尾!如果我們想要明確地釋放資源,這樣可能并不理想,因此在被垃圾收集之前他們不會在托管堆中生活太久。在現實世界的例子中,可能有太多的對象有一個大的對象圖,試圖創建數據庫連接和超時。顯而易見的解決辦法是盡快明確清理資源。
我們來介紹一個Cleanup()方法,如下所示:
public class Database
{
public Database()
{
Debug.WriteLine("Database Created..");
}
public void Query1()
{
Debug.WriteLine("Query1..");
}
public void Query2()
{
Debug.WriteLine("Query2..");
}
public void Cleanup()
{
Debug.WriteLine("Cleaned Up!");
}
~Database()
{
Debug.WriteLine("Cleaned Up!");
}
}
public class Program
{
public static void Main(string[] args)
{
for (int i = 0; i < 4; i++)
{
var db = new Database();
db.Query1();
db.Query2();
db.Cleanup();
}
}
}
輸出結果:
Database Created..
Query1..
Query2..
Cleaned Up!
Database Created..
Query1..
Query2..
Cleaned Up!
Database Created..
Query1..
Query2..
Cleaned Up!
Database Created..
Query1..
Query2..
Cleaned Up!
Cleaned Up!
Cleaned Up!
Cleaned Up!
Cleaned Up!
請注意,我們還沒有刪除Finalizer 。對于每個數據庫創建Cleanup()將顯式執行。正如我們在循環的第一個例子中看到的那樣,在循環結束時,資源將被垃圾收集。
這種方法的一個問題是,如果在其中一個查詢操作中出現異常,則清理操作將永遠不會被調用。
正如我們許多人所做的那樣,我們將查詢操作封裝在try {}塊和finally {}塊中,并執行清理操作。此外,我們捕捉異常并做一些事情,但為了簡化代碼,我忽略了這一點。
public class Program
{
public static void Main(string[] args)
{
for (int i = 0; i < 4; i++)
{
var db = new Database();
try
{
db.Query1();
db.Query2();
}
finally
{
db.Cleanup();
}
}
}
}
從技術上講,這解決了這個問題。由于清理操作總是被調用,不管是否有例外。
但是這個方法還有其他一些問題。例如,每當我們實例化Db并調用查詢操作時,作為開發人員,我們必須記住將它包含在try {}和finally塊中。更糟的是,在更復雜的情況下,我們甚至可以在不知道要調用哪個操作的情況下引入錯誤。
那么我們該如何處理這種情況呢?
這是我們大多數人會使用有名的 [Dispose模式] (http://msdn.microsoft.com/en-us/library/b1yfkh5e(v=vs.110).aspx)的地方。使用Dispose模式{}和{}} 不再需要{} 。如IDisposable.Dispose()方法在操作結束清除資源。這包括查詢操作期間的任何異常情況。
public class Database : IDisposable
{
//More code..
public void Dispose()
{
Cleanup();
GC.SuppressFinalize(this);
}
}
public class Program
{
public static void Main(string[] args)
{
for (int i = 0; i < 4; i++)
{
using (var db = new Database())
{
db.Query1();
db.Query2();
}
}
}
}
這絕對是編寫代碼更好的方法。在使用塊抽象了對象的處置。它確保使用Dispose()方法進行清理。我們大多數人會用這種方法來解決。你會看到這個模式在很多應用程序中使用。
但是,如果我們仔細觀察,使用模式本身仍然存在問題。從技術上說,明確地清理資源是正確的。但是,不能保證API的客戶端將使用使用塊來清理資源。例如,任何人都可以編寫下面的代碼:
var db = new Database();
db.Query1();
db.Query2();
對于資源密集型應用程序,如果此代碼已被提交而未被注意到,則可能對應用程序產生不利影響。所以我們回到原點。正如我們已經注意到的那樣,沒有立即進行處理或清理操作。
丟失Dispose()方法是一個嚴重的問題。更何況,我們還面臨著一個新的挑戰,要確保我們正確實施Dispose方法/邏輯。不是每個人都知道如何正確地實現Dispose方法/邏輯。大多數會采取一些其他資源,如博客/文章。這是所有不必要的麻煩。
public class Database
{
private Database()
{
Debug.WriteLine("Database Created..");
}
public void Query1()
{
Debug.WriteLine("Query1..");
}
public void Query2()
{
Debug.WriteLine("Query2..");
}
private void Cleanup()
{
Debug.WriteLine("Cleaned Up!");
}
public static void Create(Action<database> execution)
{
var db = new Database();
try
{
execution(db);
}
finally
{
db.Cleanup();
}
}
} </database>
這里發生了一些有趣的事情。IDisposable的實現已被刪除。這個類的構造函數變成私有的。所以這個設計被強制執行,用戶不能直接實例化數據庫實例。同樣,Cleanup()方法也是私有的。有一個新的Create()方法,它將Action委托(它接受數據庫的一個實例)作為參數。這個方法的實現將執行Action指定的動作參數。重要的是,這個動作的執行已經封裝在一個try {}塊中,允許清理操作,就像我們前面看到的那樣。
這是客戶端/用戶如何使用這個API:
public class Program
{
public static void Main(string[] args)
{
Database.Create(database =>
{
database.Query1();
database.Query2();
});
}
}
與以前的方法的主要區別在于,現在我們正在從客戶端/用戶抽象清理操作,而是引導用戶使用特定的API。由于所有樣板代碼都已經從客戶端抽象出來,所以這種方法變得非常自然。這樣很難想象開發者會犯錯。
用Lambda表達式實現更多的方法模式的實際應用
顯然這種模式不限于管理數據庫的資源。它有很多其他的潛力。這里有一些應用程序。
- 在創建事務的事務代碼中,檢查事務是否完成,然后在需要時進行提交或回滾。
- 如果我們有很大的外部資源,我們要盡快處理,而不必等待.NET垃圾收集。
- 為了解決一些框架限制 - 下面更多的內容。這很有趣。請看下面。
解決框架限制
我相信大部分人都熟悉單元測試。我是單元測試的忠實擁躉。在.NET平臺中,如果你使用了MSTest框架,我相信你已經看到了ExpectedException屬性。這個屬性的使用有一個限制,我們不能在測試執行過程中指定引發異常的確切的調用。
例如,請參閱這里的測試。
[TestClass]
public class UnitTest1
{
[TestMethod][ExpectedException(typeof(Exception))]
public void SomeTestMethodThrowsException()
{
var sut = new SystemUnderTest("some param");
sut.SomeMethod();
}
}
此代碼演示ExpectedException屬性的典型實現。請注意,我們期望sut.SomeMethod()會拋出異常。
以下是SUT(被測系統)的外觀。請注意,我已經刪除了代碼簡潔的詳細實現。
public class SystemUnderTest
{
public SystemUnderTest(string param)
{
}
public void SomeMethod()
{
//more code
//throws exception
}
}
在執行測試期間,如果有異常被拋出,它將被捕獲并且測試會成功。然而,測試不會確切知道拋出異常的位置。例如,它可能在創建SytemUnderTest的過程中。
我們可以使用Lambda表達式執行周圍方法模式來解決這個限制。這是通過創建一個輔助方法,它接受一個行動 參數作為委托。
public static class ExceptionAssert
{
public static T Throws<t>(Action action) where T : Exception
{
try
{
action();
}
catch (T ex)
{
return ex;
}
Assert.Fail("Expected exception of type {0}.", typeof(T));
return null;
}
}
調用方法如下:
[TestMethod]
public void SomeTestMethodThrowsException()
{
var sut = new SystemUnderTest("some param");
ExceptionAssert.Throws<exception>(() => sut.SomeMethod());
} </exception>
上面的ExceptionAssert.Throws()可用于顯式調用拋出異常的方法。
題外話...
在其他一些單元測試框架(如NUnit,xUnit)中我們不會有這個限制。這些框架已經有內置的幫助方法(使用這種模式實現)來定位導致異常的確切操作。
例如xUnit.NET有:
public static T Throws<t>(Assert.ThrowsDelegate testCode) where T : Exception </t>
總結
在這篇文章中,我們研究了C#中的各種.NET設計模式。設計模式是好的,但只有正確使用它們才能生效。我們希望將設計模式視為一套工具,使我們能夠在代碼基礎上做出更好的決策。我們也希望把它們當作通訊工具,這樣我們可以改善代碼庫的溝通。
我們已經看了 抽象工廠模式 和 級聯模式 。我們還研究了使用lambda表達式對現有設計模式采用稍微不同的方法。這包括 級聯Lambda模式,插件模式,最后是 Lambda表達式執行模式 。在本文中,我們看到Lambda表達式是增強某些著名軟件模式的強大功能的好方法。
本文采用 知識共享署名-非商業性使用-相同方式共享 3.0 中國大陸許可協議
轉載請注明:作者 張蘅水