軟件架構設計原則和示例介紹
常見的架構設計原則包括以下幾個方面:
1. 單一職責原則(Single Responsibility Principle,SRP):一個模塊或者類只負責一項功能。
2. 開閉原則(Open-Closed Principle,OCP):軟件實體應該對擴展開放,對修改關閉。
3. 里氏替換原則(Liskov Substitution Principle,LSP):所有引用基類對象的代碼都能夠透明地使用其子類對象。
4. 接口隔離原則(Interface Segregation Principle,ISP):客戶端不應該依賴于它不需要的接口,即一個類對另一個類的依賴應該建立在最小的接口上。
5. 依賴倒置原則(Dependency Inversion Principle,DIP):高層模塊不應該依賴于低層模塊,二者應該依賴于抽象接口。同時,抽象不應該依賴于細節(jié),細節(jié)應該依賴于抽象。
這些原則可以指導我們設計出更加可擴展、可維護、可測試、可復用的架構。
為了更加詳細的說明,以下是各個原則的示例代碼:
1、單一職責原則(SRP)
// 錯誤的示例
public class User {
public void requestBook(string bookName) {
// 做一些請求書籍的事情
// ...
// 做一些記錄用戶行為的事情
logUserAction("request a book");
}
private void logUserAction(string action) {
// 記錄用戶行為到日志中
// ...
}
}
// 正確的示例
public class User {
public void requestBook(string bookName) {
// 做一些請求書籍的事情
// ...
}
}
public class UserActionLogger {
public void logUserAction(string action) {
// 記錄用戶行為到日志中
// ...
}
}
在錯誤的示例中,`User` 類不僅要負責請求書籍的事情,還要負責記錄用戶行為。這不僅讓代碼變得復雜,而且不符合單一職責原則。正確的示例分離了不同的職責,把記錄用戶行為的功能獨立成了一個新的類。
2、開閉原則(OCP)
// 錯誤的示例
public class UserManager {
public void addUser(User user) {
// 添加用戶到數(shù)據(jù)庫中
// ...
}
public void deleteUser(User user) {
// 從數(shù)據(jù)庫中刪除用戶
// ...
}
}
// 新需求:修改用戶信息
public class UserManager {
public void addUser(User user) {
// 添加用戶到數(shù)據(jù)庫中
// ...
}
public void deleteUser(User user) {
// 從數(shù)據(jù)庫中刪除用戶
// ...
}
// 新需求:修改用戶信息
public void updateUser(User user) {
// 修改用戶信息
// ...
}
}
// 正確的示例
public interface IUserManager {
void addUser(User user);
void deleteUser(User user);
}
public class UserManager : IUserManager {
public void addUser(User user) {
// 添加用戶到數(shù)據(jù)庫中
// ...
}
public void deleteUser(User user) {
// 從數(shù)據(jù)庫中刪除用戶
// ...
}
}
public class AdvancedUserManager : IUserManager {
public void addUser(User user) {
// 添加用戶到數(shù)據(jù)庫中
// ...
}
public void deleteUser(User user) {
// 從數(shù)據(jù)庫中刪除用戶
// ...
}
// 新需求:修改用戶信息
public void updateUser(User user) {
// 修改用戶信息
// ...
}
}
在錯誤的示例中,當需要添加新的需求(比如修改用戶信息)時,我們需要修改 `UserManager` 類。這顯然不符合開閉原則。正確的示例使用了接口和不同的實現(xiàn)類分離不同的功能,這樣當我們需要添加新的需求時,只需要創(chuàng)建一個新的實現(xiàn)類即可。
3、里氏替換原則(LSP)
// 錯誤的示例
public class Animal {
public virtual void eat() {
Console.WriteLine("Animal eat");
}
}
public class Dog : Animal {
public override void eat() {
Console.WriteLine("Dog eat");
}
}
public class Cat : Animal {
public override void eat() {
Console.WriteLine("Cat eat");
}
}
public class AnimalFeeder {
public void feed(Animal animal) {
animal.eat();
}
}
static void Main(string[] args) {
AnimalFeeder animalFeeder = new AnimalFeeder();
animalFeeder.feed(new Dog());
animalFeeder.feed(new Cat());
}
// 輸出:
// Dog eat
// Cat eat
// 錯誤的示例,違反了 LSP
public class WildAnimal : Animal {
}
static void Main(string[] args) {
AnimalFeeder animalFeeder = new AnimalFeeder();
animalFeeder.feed(new Dog());
animalFeeder.feed(new WildAnimal()); // 報錯!
}
在錯誤的示例中,我們定義了一個 `WildAnimal` 類繼承自 `Animal` 類,但是這個類并沒有重寫 `eat()` 方法,而是直接繼承了父類的實現(xiàn)。當我們嘗試用 `WildAnimal` 對象來調(diào)用 `AnimalFeeder` 的 `feed()` 方法時,會出現(xiàn)運行時異常,因為 `WildAnimal` 對象沒有正確處理 `eat()` 方法。
4、接口隔離原則(ISP)
// 錯誤的示例
public interface IAnimal {
void eat();
void fly();
}
public class Dog : IAnimal {
public void eat() {
Console.WriteLine("Dog eat");
}
public void fly() {
throw new NotImplementedException(); // 錯誤的設計,狗不能飛行
}
}
public class Bird : IAnimal {
public void eat() {
Console.WriteLine("Bird eat");
}
public void fly() {
Console.WriteLine("Bird fly");
}
}
public class AnimalFeeder {
public void feed(IAnimal animal) {
animal.eat();
}
}
static void Main(string[] args) {
AnimalFeeder animalFeeder = new AnimalFeeder();
animalFeeder.feed(new Dog()); // 報錯!
animalFeeder.feed(new Bird());
}
// 正確的示例
public interface IEatable {
void eat();
}
public interface IFlyable {
void fly();
}
public class Dog : IEatable {
public void eat() {
Console.WriteLine("Dog eat");
}
}
public class Bird : IEatable, IFlyable {
public void eat() {
Console.WriteLine("Bird eat");
}
public void fly() {
Console.WriteLine("Bird fly");
}
}
public class AnimalFeeder {
public void feed(IEatable animal) {
animal.eat();
}
}
static void Main(string[] args) {
AnimalFeeder animalFeeder = new AnimalFeeder();
animalFeeder.feed(new Dog());
animalFeeder.feed(new Bird());
}
在錯誤的示例中,我們定義了一個 `IAnimal` 接口,其中包含了 `eat()` 和 `fly()` 兩個方法。但是不是所有的動物都可以飛行,例如狗就不能飛行。正確的示例把 `IAnimal` 接口拆分成了 `IEatable` 和 `IFlyable` 兩個接口,這樣我們可以根據(jù)實際需要選擇使用哪個接口來表示一個動物的能力。
5、依賴倒置原則(DIP)
// 錯誤的示例
public class UserService {
private readonly UserDAO userDAO;
public UserService() {
this.userDAO = new UserDAO(); // 依賴了具體的實現(xiàn)
}
public void addUser(User user) {
userDAO.addUser(user);
}
}
public class UserDAO {
public void addUser(User user) {
// 添加用戶到數(shù)據(jù)庫中
// ...
}
}
// 正確的示例
public interface IUserDAO {
void addUser(User user);
}
public class UserDAO : IUserDAO {
public void addUser(User user) {
// 添加用戶到數(shù)據(jù)庫中
// ...
}
}
public class UserService {
private readonly IUserDAO userDAO;
public UserService(IUserDAO userDAO) {
this.userDAO = userDAO; // 依賴抽象接口
}
public void addUser(User user) {
userDAO.addUser(user);
}
}
static void Main(string[] args) {
IUserDAO userDAO = new UserDAO();
UserService userService = new UserService(userDAO);
userService.addUser(new User());
}
在錯誤的示例中,`UserService` 類需要訪問數(shù)據(jù)庫添加用戶,但是直接依賴了 `UserDAO` 類。這使得 `UserService` 類不靈活,不能適應變化。正確的示例中,我們把 `UserDAO` 類抽象成了 `IUserDAO` 接口,并且通過構造函數(shù)注入了依賴。這樣做不僅遵循了依賴倒置原則,而且還能夠靈活地替換不同的實現(xiàn)類。