1. 開放封閉原則 OCP Open Closed Principle
Software entities(classes,modules,functions,etc.)
should be open for extension
but closed for modification
軟體實體(類別、模組、函式等)應該開放擴充
但封閉修改
藉由增加新的程式碼
來擴充系統的功能,而不是藉由修改原本已經存在的程式碼
來擴充系統。
2. 關於OCP的基本精神
2.1. 補充說明:擴充方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| using System; using System.Globalization;
public static class StringExtensions { public static string ToTitleCase(this string str) { if (string.IsNullOrEmpty(str)) { return str; }
TextInfo textInfo = CultureInfo.CurrentCulture.TextInfo; return textInfo.ToTitleCase(str.ToLower()); } }
public class Program { public static void Main() { string text = "hello world"; Console.WriteLine(text.ToTitleCase()); } }
|
3. 常見的設計問題
4. 關於OCP的實作方式
- 採用分離與相依的技巧(相依於抽象)
- 抽象型別可以是
一般類別
、抽象類別
、介面
- 類別A相依於這個抽象型別,原本寫的code還是在類別B、C、D,只是再相依於這個抽象型別
- 如果你在過去專案有看到大量的interface是因為他希望你做到更好的擴充
- 缺點:需要針對原有程式碼進行重構
5. 關於OCP的C#範例
- 透過
抽象類別
限制其修改,並透過繼承開放擴充不同實作
1 2 3 4 5
| public abstract class DataProvider{ public abstract int OpenConnection(); public abstract int CloseConnection(); public abstract int ExecuteCommand(); }
|
5.1. 隨堂測驗
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| void Main() { DataProvider DataProviderObject = new SqlDataProvider(); DataProviderObject.OpenConnection(); DataProviderObject.ExecuteCommand(); DataProviderObject.CloseConnection(); DataProviderObject = new OracleDataProvider(); DataProviderObject.OpenConnection(); DataProviderObject.ExecuteCommand(); DataProviderObject.CloseConnection(); }
abstract class DataProvider { public abstract int OpenConnection(); public abstract int CloseConnection(); public abstract int ExecuteCommand(); }
class SqlDataProvider : DataProvider { public SqlDataProvider() { Console.WriteLine("\nCreate SqlDataProvider"); } public override int OpenConnection() { Console.WriteLine("\nSql Connection opened successfully"); return 1; } public override int CloseConnection() { Console.WriteLine("\nSql Connection closed successfully"); return 1; } public override int ExecuteCommand() { Console.WriteLine("\nSql Connection executed successfully"); return 1; } }
class OracleDataProvider : DataProvider { public OracleDataProvider() { Console.WriteLine("\nCreate OracleDataProvider"); } public override int OpenConnection() { Console.WriteLine("\nSql Connection opened successfully"); return 1; } public override int CloseConnection() { Console.WriteLine("\nSql Connection closed successfully"); return 1; } public override int ExecuteCommand() { Console.WriteLine("\nSql Connection executed successfully"); return 1; } }
class OledbDataProvider : DataProvider { public override int OpenConnection() { Console.WriteLine("\nSql Connection opened successfully"); return 1; } public override int CloseConnection() { Console.WriteLine("\nSql Connection closed successfully"); return 1; } public override int ExecuteCommand() { Console.WriteLine("\nSql Connection executed successfully"); return 1; } }
|
改為interface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| void Main() { DataProvider DataProviderObject = new SqlDataProvider(); DataProviderObject.OpenConnection(); DataProviderObject.ExecuteCommand(); DataProviderObject.CloseConnection();
DataProviderObject = new OracleDataProvider(); DataProviderObject.OpenConnection(); DataProviderObject.ExecuteCommand(); DataProviderObject.CloseConnection(); } interface DataProvider { public int OpenConnection(); public int CloseConnection(); public int ExecuteCommand(); }
class SqlDataProvider : DataProvider { public SqlDataProvider() { Console.WriteLine("\nCreate SqlDataProvider"); } public int OpenConnection() { Console.WriteLine("\nSql Connection opened successfully"); return 1; } public int CloseConnection() { Console.WriteLine("\nSql Connection closed successfully"); return 1; } public int ExecuteCommand() { Console.WriteLine("\nSql Connection executed successfully"); return 1; } }
class OracleDataProvider : DataProvider { public OracleDataProvider() { Console.WriteLine("\nCreate OracleDataProvider"); } public int OpenConnection() { Console.WriteLine("\nSql Connection opened successfully"); return 1; } public int CloseConnection() { Console.WriteLine("\nSql Connection closed successfully"); return 1; } public int ExecuteCommand() { Console.WriteLine("\nSql Connection executed successfully"); return 1; } }
class OledbDataProvider : DataProvider { public int OpenConnection() { Console.WriteLine("\nSql Connection opened successfully"); return 1; } public int CloseConnection() { Console.WriteLine("\nSql Connection closed successfully"); return 1; } public int ExecuteCommand() { Console.WriteLine("\nSql Connection executed successfully"); return 1; } }
|
6. 關於OCP的使用時機
6.1. 範例程式:強調穩定
假設我們有一個Animal
基礎類別,並希望在不修改此類別的前提下擴展不同的動物。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public abstract class Animal { public abstract string MakeSound(); }
public class Dog : Animal { public override string MakeSound() { return "Bark"; } }
public class Cat : Animal { public override string MakeSound() { return "Meow"; } }
|
當我們需要加入新動物時,例如鳥類,可以通過擴展Animal
類別來實現,而不修改現有的類別。
1 2 3 4 5 6 7
| public class Bird : Animal { public override string MakeSound() { return "Chirp"; } }
|
6.2. 範例程式:加入新需求的屬性或方法
假設現在需要在Animal
類別中加入Move
(移動)行為,我們可以透過新的基礎類別來擴展,而不是修改原本的Animal
類別。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public abstract class Animal { public abstract string MakeSound(); }
public abstract class MovingAnimal : Animal { public abstract string Move(); }
public class Dog : MovingAnimal { public override string MakeSound() { return "Bark"; }
public override string Move() { return "Run"; } }
|
此例中,我們使用新的MovingAnimal
類別,讓具有移動行為的動物繼承此類別,保持Animal
類別的封閉性,並對擴展開放。
6.3. 範例程式:擔心修改既有程式破壞現有系統運作
假設原來的系統有一個PaymentProcessor
類別,只支持信用卡支付:
1 2 3 4 5 6 7
| public class PaymentProcessor { public void ProcessCreditCard(decimal amount) { } }
|
如果需要支持其他支付方式(如支付寶或PayPal),可以透過設計接口來擴展各種支付方式,而不修改PaymentProcessor
的原始代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public interface IPaymentMethod { void Pay(decimal amount); }
public class CreditCardPayment : IPaymentMethod { public void Pay(decimal amount) { Console.WriteLine("Processing credit card payment of " + amount); } }
public class AlipayPayment : IPaymentMethod { public void Pay(decimal amount) { Console.WriteLine("Processing Alipay payment of " + amount); } }
public class PaymentProcessor { public void ProcessPayment(IPaymentMethod paymentMethod, decimal amount) { paymentMethod.Pay(amount); } }
|
7. OCP討論事項
7.1. 隨堂測試
起初只是一個簡單的需求,因此程式開發時,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class AppEvent { public void GenerateEvent(string message) { Logger fooLogger = new Logger(); fooLogger.Log(message); } }
public class Logger { public void Log(string message) { Console.WriteLine(message); } }
|
這樣的程式碼並沒有問題,需求僅此。
但後來客戶想要增加Log輸出到檔案的功能,你會如何改寫程式碼?
沒學過SOLID的開發者,可能會這樣寫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class AppEvent { public void GenerateEvent(string message) { Logger fooLogger = new Logger("Console"); fooLogger.Log(message); } }
public class Logger { private readonly string Target;
public Logger(string target) { Target = target; }
public void Log(string message) { if (Target == "Console") Console.WriteLine(message); else if (Target == "File") File.WriteAllText("MyLog", message); else throw new NotImplementedException(); } }
|
這樣的寫法是否有符合SRP精神?現在已經有兩個理由以上改這段(一個是console、一個是file),這樣的寫法已經不符合SRP
接下來,採用分離與相依的技巧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| public interface ILogger { void Log(string message); }
public class ConsoleLogger : ILogger { public void Log(string message) { Console.WriteLine(message); } }
public class FileLogger : ILogger { public void Log(string message) { File.WriteAllText("MyLog", message); } }
public class AppEvent { private readonly ILogger _logger;
public AppEvent(string loggerType) { this._Logger = LoggerFactory.CreateLogger(loggerType); }
public void GenerateEvent(string message) { _Logger.Log(message); } }
public class LoggerFactory { public static ILogger CreateLogger(string loggerType) { if (loggerType == "Console") return new ConsoleLogger(); else if (loggerType == "File") return new FileLogger(); else throw new NotImplementedException(); } }
|
如果我們再加上Web API Log
要改的只有LoggerFactory
,很符合SRP精神。