1. 單一責任原則 SRP Single Responsibility Principle
A class
should have only one
reason to change
一個類別應該只有一個改變的理由!
2. 何謂 “責任” (Responsibility)
責任 = reason to change (改變的理由)
當一個類別擁有多個不同的責任
意味著一個類別負責多項不同的工作
當需求變更時,更動一個類別的理由也可能不只一個
2.1. 隨堂問題 請問以下類別有多少責任?
1 2 3 4 5 6 7 8 9 10 public class OrderManager { public bool LoadOrder () { } }
在這個OrderManager
類別中,LoadOrder
方法承擔了以下四個主要責任:
建立資料庫連線 :負責建立與資料庫的連接,且連接字串是寫死的,這可能會降低靈活性與可維護性。若資料庫連接資訊有所變更,就需要修改這個方法的程式碼。
執行資料存取 :進行 ADO.NET 資料存取操作,並且還包含了資料篩選的邏輯。這使得資料存取和篩選的邏輯與建立連接及其他操作混合在一起,增加了方法的複雜性。
循環取得並轉換資料格式 :透過循環取得資料,並進行資料格式轉換。這又增加了一個不同的功能到此方法中,使得方法的職責更加多樣化。
回傳資料 :最後,還負責將取得的資料回傳出去。 總的來說,這個方法承擔了過多的責任,違反了單一職責原則。理想情況下,這些不同的功能應該分別由不同的方法或類別來處理,以提高程式碼的可維護性、可讀性以及可測試性。
3. 關於SRP的基本精神
一個”類別”負擔太多責任時,意味著該類別可以被切割
可以透過定義一個全新的”類別”輕鬆做到
對類別進行適度的切割,方便日後管理與維護!
SRP
主要精神就是提高內聚力
高內聚力
意味著你可以想到一個清楚的理由
去改它!
4. 常見的設計問題
將所有功能寫在一個類別中
類別複雜度過高
維護時經常找不到應該要改哪裡
發生邏輯問題時找不到Bug在哪裡
使用類別時不知道應該要呼叫哪個方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class ReportGenerator { public void GenerateReport (Data data ) { ProcessData(data); FormatData(data); ExportToPDF(data); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class DataProcessor { public void Process (Data data ) { } } public class DataFormatter { public void Format (Data data ) { } } public class PDFExporter { public void Export (Data data ) { } } var processor = new DataProcessor();var formatter = new DataFormatter();var exporter = new PDFExporter();processor.Process(data); formatter.Format(data); exporter.Export(data);
5. 關於SRP的使用時機
6. SRP討論事項
你怎樣確認一個類別被賦予了過多的責任?
在開發系統時,不可能完全通盤了解需求才開設計系統。雖然SRP精神很簡單,但實現很難…(面對現況)
套用SRP可能會有副作用,因為類別變多導致耦合力增加。
從類別A中,拆出不同責任方法到類別B(新類別)中,使其關注點分離。
7. 關於SRP還需要注意的事
參考YAGNI
(You Ain’t Gonna Need It) 原則
不用急於在第一時間就專注於分離責任
尚未出現的需求(未來的需求)不需要預先分離責任
當需求變更的時候,再進行類別分割即可!
SRP是SOLID中最簡單的,但卻是最難做到的
需要不斷提升你的開發經驗 與重構技術
如果你沒有足夠的經驗去定義一個物件的Responsibility,那麼建議你不要過早進行SRP規劃!
7.1. 隨堂測驗 修改前
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Program { static void Main () { DataAccess.InsertData(); } } class DataAccess { public static void InsertData () { Console.WriteLine("Data inserted into database successfully" ); Console.WriteLine("Logged Time:" + DateTime.Now.ToLongTimeString() + " Log Data insertion completed successfully" ); } }
修改後
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class DataAccess { public static void InsertData () { Console.WriteLine("Data inserted into database successfully" ); Logger.writeLog(); } } class Logger { public static void writeLog () { Console.WriteLine("Logged Time:" + DateTime.Now.ToLongTimeString() + "Log Data insertion completed successfully" ); } }
7.2. 隨堂測驗 修改前
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.Net.Mail;public class UserRegistrationService { public void RegisterUser (string username, string password, string email ) { if (string .IsNullOrWhiteSpace(username)) throw new Exception("Username is not valid" ); if (string .IsNullOrWhiteSpace(password) || password.Length < 6 ) throw new Exception("Password must be at least 6 characters" ); if (string .IsNullOrWhiteSpace(email) || !email.Contains("@" )) throw new Exception("Email is not valid" ); Console.WriteLine("Saving user to database..." ); SmtpClient client = new SmtpClient(); MailMessage mailMessage = new MailMessage("admin@example.com" , email); mailMessage.Subject = "Registration Confirmation" ; mailMessage.Body = "Thank you for registering!" ; client.Send(mailMessage); } }
修改後
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 using System;using System.Net.Mail;public class UserValidator { public void Validate (string username, string password, string email ) { if (string .IsNullOrWhiteSpace(username)) throw new Exception("Username is not valid" ); if (string .IsNullOrWhiteSpace(password) || password.Length < 6 ) throw new Exception("Password must be at least 6 characters" ); if (string .IsNullOrWhiteSpace(email) || !email.Contains("@" )) throw new Exception("Email is not valid" ); } } public class UserRepository { public void SaveUser (string username, string password, string email ) { Console.WriteLine("Saving user to database..." ); } } public class EmailService { public void SendConfirmationEmail (string email ) { SmtpClient client = new SmtpClient(); MailMessage mailMessage = new MailMessage("admin@example.com" , email); mailMessage.Subject = "Registration Confirmation" ; mailMessage.Body = "Thank you for registering!" ; client.Send(mailMessage); } } public class UserRegistrationService { private readonly UserValidator _validator; private readonly UserRepository _repository; private readonly EmailService _emailService; public UserRegistrationService (UserValidator validator, UserRepository repository, EmailService emailService ) { _validator = validator; _repository = repository; _emailService = emailService; } public void RegisterUser (string username, string password, string email ) { _validator.Validate(username, password, email); _repository.SaveUser(username, password, email); _emailService.SendConfirmationEmail(email); } }
7.3. 隨堂測驗 修改前
1 2 3 4 5 6 7 8 9 public class OrderManager { public List<Product> products = new List<Product>(); public void Processing () { } }
修改後
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 public class Product { }public class Customer { }public class Stock { public void CheckAvailability (IEnumerable<Product> products ) { } } public class Payment { public void Processing (Customer customer, IEnumerable<Product> products ) { } } public class Shipment { public void SendProducts (Customer customer, IEnumerable<Product> products ) { } } public class OrderManager { public List<Product> Products { get ; set ; } public Customer Customer { get ; set ; } public OrderManager () { Products = new List<Product>(); } public void Processing () { var stock = new Stock(); stock.CheckAvailability(Products); var payment = new Payment(); payment.Processing(Customer, Products); var shipment = new Shipment(); shipment.SendProducts(Customer, Products); } }
因為現在OrderManager
耦合 Payment
,所以如果後續發生付款方式增加LINE PAY ,則有可能既有其中一個的OrderManager
會被改壞(改A壞B),這時候就需要下一個原則。
8. 補充說明:遇到非必要需求,如何透過SRP抽離與設計 需求情境
假設我們有一個用戶註冊系統,並且需求突然提出希望在用戶註冊完成後額外「記錄用戶行為」(例如記錄註冊時間和 IP 地址)。此功能並非系統核心,僅為輔助紀錄,屬於非必要功能。
修改前
初版的註冊服務可能如下所示:
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 public class UserRegistrationService { private readonly UserValidator _validator; private readonly UserRepository _repository; private readonly EmailService _emailService; public UserRegistrationService (UserValidator validator, UserRepository repository, EmailService emailService ) { _validator = validator; _repository = repository; _emailService = emailService; } public void RegisterUser (string username, string password, string email ) { _validator.Validate(username, password, email); _repository.SaveUser(username, password, email); _emailService.SendConfirmationEmail(email); LogUserActivity(username, email); } private void LogUserActivity (string username, string email ) { Console.WriteLine($"User {username} registered with email {email} at {DateTime.Now} " ); } }
在這個程式碼中,LogUserActivity
被耦合到主業務流程中,增加了 UserRegistrationService
類的複雜度。這違反了 SRP,因為它同時包含了「用戶註冊」和「行為紀錄」兩個職責。
修改後
運用 SRP,我們可以將非必要的用戶行為紀錄功能抽離到一個獨立的類別,並透過依賴注入的方式,在需要時才使用它。
抽離非必要功能
1 2 3 4 5 6 7 public class UserActivityLogger { public void LogActivity (string username, string email ) { Console.WriteLine($"User {username} registered with email {email} at {DateTime.Now} " ); } }
更新 UserRegistrationService
類
可以通過依賴注入將 UserActivityLogger
類作為可選項添加到 UserRegistrationService
中,讓它變成一個可插拔的功能。
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 public class UserRegistrationService { private readonly UserValidator _validator; private readonly UserRepository _repository; private readonly EmailService _emailService; private readonly UserActivityLogger _activityLogger; public UserRegistrationService ( UserValidator validator, UserRepository repository, EmailService emailService, UserActivityLogger activityLogger = null ) { _validator = validator; _repository = repository; _emailService = emailService; _activityLogger = activityLogger; } public void RegisterUser (string username, string password, string email ) { _validator.Validate(username, password, email); _repository.SaveUser(username, password, email); _emailService.SendConfirmationEmail(email); _activityLogger?.LogActivity(username, email); } }
類別或方法的功能過於複雜 :如果一個類別或方法具有多個不相關的功能,這會使得類別變得難以維護、測試和擴展。例如,如果一個類別同時負責數據處理和資料庫儲存,這些職責應分開。
頻繁的變更需求 :當一個類別或方法頻繁因不同的需求而修改,這表示它可能擁有多重職責。例如,假設你有一個「報表生成器」類別,既要處理數據格式化,又要負責文件匯出。若需求變更,你可能只想修改數據格式化部分,但匯出功能也可能受到影響。將不同的功能分離,可以讓變更更專注於單一職責,不會影響其他功能。
難以重用 :當一個類別包含多種職責,這會限制重用性,因為在新環境中,並不總是需要該類別的所有職責。分離不同的功能後,每個類別只專注於一個職責,使得它們更容易重用。
測試困難 :具有多重職責的類別通常難以測試,因為它需要配置不同的依賴關係來涵蓋所有職責。通過實踐SRP,可以使類別更易於單元測試,因為每個類別只需測試其單一職責。
耦合度過高 :當某個類別與多個其他類別有大量的依賴或耦合,這表示它可能包含了多個職責。將不同的職責抽取到不同的類別,可以減少耦合,使系統更具可維護性。