當前位置: 妍妍網 > 碼農

C#軟體架構設計原則

2024-01-28碼農

學習設計原則是學習設計模式的基礎。在實際的開發過程中,並不是一定要求所有的程式碼都遵循設計原則,而是要綜合考慮人力、成本、時間、品質,不刻意追求完美,要在適當的場景遵循設計原則。

這體現的是一種平衡取舍,可以幫助我們設計出更加優雅的程式碼結構。

分別用一句話歸納總結軟體設計七大原則,如下表所示。

設計原則 一句話歸納 目的
開閉原則 對擴充套件開放,對修改關閉 降低對維護帶來的新風險
依賴倒置原則 高層不應該依賴底層 更利於程式碼結構的升級擴充套件
單一職責原則 一個類只幹一件事 便於理解,提高程式碼的可讀性
介面隔離原則 一個介面只幹一件事 功能解耦,高聚合、低耦合
迪米特法則 不該知道的不要知道 只和朋友交流,不和陌生人說話,減少程式碼臃腫
芮氏替換原則 子類別重寫方式功能發生改變,不應該影響父類方法的含義 防止繼承泛濫
合成復用原則 盡量使用組合實作程式碼復用,而不使用繼承 降低程式碼耦合
開閉原則範例

當使用C#程式語言時,可以透過以下範例來說明開閉原則的套用:

假設我們正在設計一個圖形繪制應用程式,其中包含不同型別的圖形(如圓形、矩形、三角形等)。

我們希望能夠根據需要輕松地添加新的圖形型別,同時保持現有程式碼的穩定性。

首先,我們定義一個抽象基礎類別 Shape 來表示所有圖形的通用內容和行為:

publicabstract classShape
{
publicabstractvoidDraw();
}

然後,我們建立具體的圖形類,如 Circle Rectangle Triangle ,它們都繼承自 Shape 基礎類別,並實作了 Draw() 方法:

public classCircle : Shape
{
publicoverridevoidDraw()
{
Console.WriteLine("Drawing a circle");
}
}
public classRectangle : Shape
{
publicoverridevoidDraw()
{
Console.WriteLine("Drawing a rectangle");
}
}
public classTriangle : Shape
{
publicoverridevoidDraw()
{
Console.WriteLine("Drawing a triangle");
}
}

現在,如果我們需要添加新的圖形型別(例如橢圓),只需建立一個新的類並繼承自 Shape 類即可。這樣做不會影響現有程式碼,並且可以輕松地擴充套件應用程式。

public classEllipse : Shape
{
publicoverridevoidDraw()
{
Console.WriteLine("Drawing an ellipse");
}
}

在應用程式的其他部份,我們可以使用 Shape 型別的物件來繪制不同的圖形,而無需關心具體的圖形型別。這樣,我們遵循了開閉原則,對擴充套件開放(透過添加新的圖形型別),對修改關閉(不需要修改現有程式碼)。

public classDrawingProgram
{
publicvoidDrawShapes(List<Shape> shapes)
{
foreach (var shape in shapes)
{
shape.Draw();
}
}
}

使用範例:

var shapes = new List<Shape>
{
new Circle(),
new Rectangle(),
new Triangle(),
new Ellipse()
};
var drawingProgram = new DrawingProgram();
drawingProgram.DrawShapes(shapes);

輸出結果:

Drawing a circle
Drawing a rectangle
Drawing a triangle
Drawing an ellipse

透過遵循開閉原則,我們可以輕松地擴充套件應用程式並添加新的圖形型別,而無需修改現有程式碼。這樣可以提高程式碼的可維護性和可延伸性,並支持軟體系統的演化和變化。

單一職責範例

單一職責原則(Single Responsibility Principle,SRP)要求一個類應該只有一個引起它變化的原因。換句話說,一個類應該只負責一項職責或功能。

下面是一個使用C#範例來說明單一職責原則的套用:

假設我們正在開發一個學生管理系統,其中包含學生資訊的錄入和展示功能。我們可以將這個系統分為兩個類: Student StudentManager

首先,定義 Student 類來表示學生物件,並包含與學生相關的內容和方法:

public classStudent
{
publicstring Name { getset; }
publicint Age { getset; }
// 其他與學生相關的內容和方法...
}

然後,建立 StudentManager 類來處理與學生資訊管理相關的操作,如錄入、查詢和展示等:

public classStudentManager
{
private List<Student> students;
publicStudentManager()
{
students = new List<Student>();
}
publicvoidAddStudent(Student student)
{
// 將學生資訊添加到列表中...
students.Add(student);
Console.WriteLine("Student added successfully.");
}
publicvoidDisplayStudents()
{
// 展示所有學生資訊...
foreach (var student in students)
{
Console.WriteLine($"Name: {student.Name}, Age: {student.Age}");
}
}
}

在這個例子中, Student 類負責表示單個學生物件,並封裝了與學生相關的內容。而 StudentManager 類負責處理學生資訊的管理操作,如添加學生和展示學生資訊。

使用範例:

var student1 = new Student { Name = "Alice", Age = 20 };
var student2 = new Student { Name = "Bob", Age = 22 };
var studentManager = new StudentManager();
studentManager.AddStudent(student1);
studentManager.AddStudent(student2);
studentManager.DisplayStudents();

輸出結果:

Student added successfully.
Student added successfully.
Name: Alice, Age: 20
Name: Bob, Age: 22

透過將學生物件的表示和管理操作分別封裝在不同的類中,我們遵循了單一職責原則。 Student 類只負責表示學生物件的內容,而 StudentManager 類只負責處理與學生資訊管理相關的操作。這樣可以提高程式碼的可維護性和可延伸性,並使每個類都具有清晰明確的職責。

裏式替換

芮氏替換原則(Liskov Substitution Principle,LSP)要求子類別型必須能夠替換其基礎類別型,並且不會破壞程式的正確性。也就是說,子類別可以在不影響程式正確性和預期行為的情況下替代父類。

下面是一個使用C#範例來說明裏式替換原則的套用:

假設我們正在開發一個圖形繪制應用程式,其中包含多種形狀(如圓形、矩形等)。我們希望能夠根據使用者選擇的形狀型別進行繪制操作。

首先,定義一個抽象基礎類別 Shape 來表示所有形狀物件,並聲明一個抽象方法 Draw 用於繪制該形狀:

publicabstract classShape
{
publicabstractvoidDraw();
}

然後,建立具體的子類別來表示不同的形狀。例如,建立 Circle 類和 Rectangle 類分別表示圓形和矩形,並實作它們自己特定的繪制邏輯:

public classCircle : Shape
{
publicoverridevoidDraw()
{
Console.WriteLine("Drawing a circle...");
}
}
public classRectangle : Shape
{
publicoverridevoidDraw()
{
Console.WriteLine("Drawing a rectangle...");
}
}

在這個例子中,每個具體的子類別都可以替代其基礎類別 Shape 並實作自己特定的繪制邏輯。這符合裏式替換原則,因為無論是 Circle 還是 Rectangle 都可以在不破壞程式正確性和預期行為的情況下替代 Shape

使用範例:

Shape circle = new Circle();
circle.Draw(); // 輸出 "Drawing a circle..."
Shape rectangle = new Rectangle();
rectangle.Draw(); // 輸出 "Drawing a rectangle..."

透過將具體的子類別物件賦值給基礎類別參照變量,並呼叫相同的方法,我們可以看到不同形狀的繪制操作被正確地執行。這證明了裏式替換原則的有效性。

總結:裏式替換原則要求子類別型必須能夠替代其基礎類別型,並且不會破壞程式正確性。在C#中,我們可以透過建立具體的子類別來表示不同形狀,並確保它們能夠在繼承自抽象基礎類別時正確地實作自己特定的行為。這樣可以提高程式碼的可延伸性和靈活性,並使得程式碼更易於維護和重用。

依賴倒置

依賴倒置原則(Dependency Inversion Principle,DIP)要求高層模組不應該依賴於低層模組的具體實作,而是應該依賴於抽象。同時,抽象不應該依賴於具體實作細節,而是應該由高層模組定義。

下面是一個使用C#範例來說明依賴倒置原則的套用:

假設我們正在開發一個電子商務系統,其中包含訂單處理和支付功能。我們希望能夠根據使用者選擇的支付方式進行訂單支付操作。

首先,定義一個抽象介面 IPaymentProcessor 來表示支付處理器,並聲明一個方法 ProcessPayment 用於執行訂單支付:

publicinterfaceIPaymentProcessor
{
voidProcessPayment(decimal amount);
}

然後,在具體的實作類中分別實作不同的支付方式。例如,建立 CreditCardPaymentProcessor 類和 PayPalPaymentProcessor 類分別表示信用卡和PayPal支付,並實作它們自己特定的支付邏輯:

public classCreditCardPaymentProcessor : IPaymentProcessor
{
publicvoidProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount} dollars...");
// 具體信用卡支付邏輯...
}
}
public classPayPalPaymentProcessor : IPaymentProcessor
{
publicvoidProcessPayment(decimal amount)
{
Console.WriteLine($"Processing PayPal payment of {amount} dollars...");
// 具體PayPal支付邏輯...
}
}

在這個例子中,每個具體的支付處理器都實作了 IPaymentProcessor 介面,並提供了自己特定的支付邏輯。這樣,高層模組(訂單處理模組)就可以依賴於抽象介面 IPaymentProcessor 而不是具體的實作類。

使用範例:

public classOrderProcessor
{
private IPaymentProcessor paymentProcessor;
publicOrderProcessor(IPaymentProcessor paymentProcessor)
{
this.paymentProcessor = paymentProcessor;
}
publicvoidProcessOrder(decimal amount)
{
// 處理訂單邏輯...
// 使用依賴註入的方式呼叫支付處理器
paymentProcessor.ProcessPayment(amount);
// 其他訂單處理邏輯...
}
}
// 在應用程式中配置和使用不同的支付方式
var creditCardPayment = new CreditCardPaymentProcessor();
var payPalPayment = new PayPalPaymentProces 



介面隔離

介面隔離原則(Interface Segregation Principle,ISP)要求客戶端不應該依賴於它們不使用的介面。一個類應該只依賴於它需要的介面,而不是依賴於多余的介面。

下面是一個使用C#範例來說明介面隔離原則的套用:

假設我們正在開發一個檔管理系統,其中包含檔上傳和檔下載功能。我們希望能夠根據使用者需求提供相應的功能。

首先,定義兩個介面 IFileUploadable IFileDownloadable 來表示檔上傳和檔下載功能,並分別聲明相應的方法:

publicinterfaceIFileUploadable
{
voidUploadFile(string filePath);
}
publicinterfaceIFileDownloadable
{
voidDownloadFile(string fileId);
}

然後,在具體的實作類中分別實作這兩個功能。例如,建立 LocalFileManager 類來處理本地檔操作,並實作對應的方法:

public classLocalFileManager : IFileUploadableIFileDownloadable
{
publicvoidUploadFile(string filePath)
{
Console.WriteLine($"Uploading file from local path: {filePath}");
// 具體本地上傳邏輯...
}
publicvoidDownloadFile(string fileId)
{
Console.WriteLine($"Downloading file with ID: {fileId} to local path");
// 具體本地下載邏輯...
}
}

在這個例子中,每個具體的實作類只關註自己需要用到的介面方法,而不需要實作多余的方法。這符合介面隔離原則,因為客戶端可以根據需要依賴於相應的介面。

使用範例:

public classFileManagerClient
{
private IFileUploadable fileUploader;
private IFileDownloadable fileDownloader;
publicFileManagerClient(IFileUploadable fileUploader, IFileDownloadable fileDownloader)
{
this.fileUploader = fileUploader;
this.fileDownloader = fileDownloader;
}
publicvoidUploadAndDownloadFiles(string filePath, string fileId)
{
// 使用檔上傳功能
fileUploader.UploadFile(filePath);
// 使用檔下載功能
fileDownloader.DownloadFile(fileId);
// 其他操作...
}
}
// 在應用程式中配置和使用具體的檔管理類
var localFileManager = new LocalFileManager();
var client = new FileManagerClient(localFileManager, localFileManager);
client.UploadAndDownloadFiles("path/to/file""123456");



透過依賴註入的方式,我們可以將具體的實作類傳遞給客戶端,並根據需要呼叫相應的介面方法。這樣就遵循了介面隔離原則,使得客戶端只依賴於它們所需的介面,並且不會受到多余方法的影響。這提高了程式碼的可維護性和靈活性,並促進了程式碼重用和擴充套件。

迪米特

迪米特法則(Law of Demeter,LoD),也稱為最少知識原則(Principle of Least Knowledge),要求一個物件應該對其他物件有盡可能少的了解。一個類不應該直接與其他類耦合,而是透過中間類進行通訊。

下面是一個使用C#範例來說明迪米特法則的套用:

假設我們正在開發一個社群網路系統,其中包含使用者、好友和訊息等功能。我們希望能夠實作使用者發送訊息給好友的功能。

首先,定義三個類 User Friend Message 來表示使用者、好友和訊息,並在 User 類中實作發送訊息的方法:

public classUser
{
privatestring name;
private List<Friend> friends;
publicUser(string name)
{
this.name = name;
this.friends = new List<Friend>();
}
publicvoidAddFriend(Friend friend)
{
friends.Add(friend);
}
publicvoidSendMessageToFriends(string messageContent)
{
Message message = new Message(messageContent);
foreach (Friend friend in friends)
{
friend.ReceiveMessage(message);
}
Console.WriteLine($"User {name} sent a message to all friends.");
}
}
public classFriend
{
privatestring name;
publicFriend(string name)
{
this.name = name;
}
publicvoidReceiveMessage(Message message)
{
Console.WriteLine($"Friend {name} received a message: {message.Content}");
// 處理接收到的訊息...
}
}
public classMessage
{
publicstring Content { getset; }
publicMessage(string content)
{
Content = content;
}
}








在這個例子中, User 類表示使用者, Friend 類表示好友, Message 類表示訊息。使用者可以添加好友,並透過 SendMessageToFriends 方法向所有好友發送訊息。

使用範例:

User user1 = new User("Alice");
User user2 = new User("Bob");
Friend friend1 = new Friend("Charlie");
Friend friend2 = new Friend("David");
user1.AddFriend(friend1);
user2.AddFriend(friend2);
user1.SendMessageToFriends("Hello, friends!");

在這個範例中,使用者物件只與好友物件進行通訊,並不直接與訊息物件進行通訊。這符合迪米特法則的要求,即一個物件應該盡可能少地了解其他物件。

透過將訊息發送的責任委托給好友物件,在使用者類中只需要呼叫 friend.ReceiveMessage(message) 方法來發送訊息給所有好友。這樣可以降低類之間的耦合性,並提高程式碼的可維護性和靈活性。

合成復用

合成復用原則(Composite Reuse Principle,CRP)要求盡量使用物件組合,而不是繼承來達到復用的目的。透過將現有物件組合起來建立新的物件,可以更靈活地實作功能的復用和擴充套件。

下面是一個使用C#範例來說明合成復用原則的套用:

假設我們正在開發一個圖形庫,其中包含各種形狀(如圓形、矩形等)。我們希望能夠實作一個可以繪制多個形狀的畫板。

首先,定義一個抽象基礎類別 Shape 來表示圖形,並聲明抽象方法 Draw

publicabstract classShape
{
publicabstractvoidDraw();
}

然後,在具體的子類別中分別實作各種形狀。例如,建立 Circle 類和 Rectangle 類來表示圓形和矩形,並重寫父類中的 Draw 方法:

public classCircle : Shape
{
publicoverridevoidDraw()
{
Console.WriteLine("Drawing a circle");
// 具體繪制圓形邏輯...
}
}
public classRectangle : Shape
{
publicoverridevoidDraw()
{
Console.WriteLine("Drawing a rectangle");
// 具體繪制矩形邏輯...
}
}

接下來,建立一個畫板類 Canvas 來管理並繪制多個圖形。在該類中使用物件組合將多個圖形組合在一起:

public classCanvas
{
private List<Shape> shapes;
publicCanvas()
{
shapes = new List<Shape>();
}
publicvoidAddShape(Shape shape)
{
shapes.Add(shape);
}
publicvoidDrawShapes()
{
foreach (Shape shape in shapes)
{
shape.Draw();
}
Console.WriteLine("All shapes are drawn.");
}
}


在這個例子中, Canvas 類透過物件組合的方式將多個圖形物件組合在一起,並提供了添加圖形和繪制圖形的方法。

使用範例:

Canvas canvas = new Canvas();
Circle circle = new Circle();
Rectangle rectangle = new Rectangle();
canvas.AddShape(circle);
canvas.AddShape(rectangle);
canvas.DrawShapes();

在這個範例中,我們建立了一個畫板物件 canvas ,並向其中添加了一個圓形和一個矩形。然後呼叫 DrawShapes 方法來繪制所有的圖形。

透過使用物件組合而不是繼承,我們可以更靈活地實作功能的復用和擴充套件。例如,可以輕松地添加新的圖形型別或修改現有圖形型別的行為,而不會影響到畫板類。這符合合成復用原則,並提高了程式碼的可維護性和靈活性。

轉自:明誌德道

連結:cnblogs.com/for-easy-fast/p/17762706.html

技術群: 添加小編微信並備註進群

小編微信:mm1552923

公眾號:dotNet編程大全