6

C#软件架构设计原则 - 明志德道

 10 months ago
source link: https://www.cnblogs.com/for-easy-fast/p/17762706.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

软件架构设计原则

学习设计原则是学习设计模式的基础。在实际的开发过程中,并不是一定要求所有的代码都遵循设计原则,而是要综合考虑人力、成本、时间、质量,不刻意追求完美,要在适当的场景遵循设计原则。这体现的是一种平衡取舍,可以帮助我们设计出更加优雅的代码结构。

分别用一句话归纳总结软件设计七大原则,如下表所示。

设计原则 一句话归纳 目的
开闭原则 对扩展开放,对修改关闭 降低对维护带来的新风险
依赖倒置原则 高层不应该依赖底层 更利于代码结构的升级扩展
单一职责原则 一个类只干一件事 便于理解,提高代码的可读性
接口隔离原则 一个接口只干一件事 功能解耦,高聚合、低耦合
迪米特法则 不该知道的不要知道 只和朋友交流,不和陌生人说话,减少代码臃肿
里氏替换原则 子类重写方式功能发生改变,不应该影响父类方法的含义 防止继承泛滥
合成复用原则 尽量使用组合实现代码复用,而不使用继承 降低代码耦合

开闭原则示例

当使用C#编程语言时,可以通过以下示例来说明开闭原则的应用:

假设我们正在设计一个图形绘制应用程序,其中包含不同类型的图形(如圆形、矩形、三角形等)。我们希望能够根据需要轻松地添加新的图形类型,同时保持现有代码的稳定性。

首先,我们定义一个抽象基类 Shape 来表示所有图形的通用属性和行为:

public abstract class Shape
{
    public abstract void Draw();
}

然后,我们创建具体的图形类,如 CircleRectangleTriangle,它们都继承自 Shape 基类,并实现了 Draw() 方法:

public class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle");
    }
}
​
public class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a rectangle");
    }
}
​
public class Triangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a triangle");
    }
}

现在,如果我们需要添加新的图形类型(例如椭圆),只需创建一个新的类并继承自 Shape 类即可。这样做不会影响现有代码,并且可以轻松地扩展应用程序。

public class Ellipse : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing an ellipse");
    }
}

在应用程序的其他部分,我们可以使用 Shape 类型的对象来绘制不同的图形,而无需关心具体的图形类型。这样,我们遵循了开闭原则,对扩展开放(通过添加新的图形类型),对修改关闭(不需要修改现有代码)。

public class DrawingProgram
{
    public void DrawShapes(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#示例来说明单一职责原则的应用:

假设我们正在开发一个学生管理系统,其中包含学生信息的录入和展示功能。我们可以将这个系统分为两个类:StudentStudentManager

首先,定义 Student 类来表示学生对象,并包含与学生相关的属性和方法:

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
​
    // 其他与学生相关的属性和方法...
}

然后,创建 StudentManager 类来处理与学生信息管理相关的操作,如录入、查询和展示等:

public class StudentManager
{
    private List<Student> students;
​
    public StudentManager()
    {
        students = new List<Student>();
    }
​
    public void AddStudent(Student student)
    {
        // 将学生信息添加到列表中...
        students.Add(student);
        Console.WriteLine("Student added successfully.");
    }
​
    public void DisplayStudents()
    {
        // 展示所有学生信息...
        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 用于绘制该形状:

public abstract class Shape
{
    public abstract void Draw();
}

然后,创建具体的子类来表示不同的形状。例如,创建 Circle 类和 Rectangle 类分别表示圆形和矩形,并实现它们自己特定的绘制逻辑:

public class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle...");
    }
}
​
public class Rectangle : Shape
{
    public override void Draw()
    {
        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 用于执行订单支付:

public interface IPaymentProcessor
{
    void ProcessPayment(decimal amount);
}

然后,在具体的实现类中分别实现不同的支付方式。例如,创建 CreditCardPaymentProcessor 类和 PayPalPaymentProcessor 类分别表示信用卡和PayPal支付,并实现它们自己特定的支付逻辑:

public class CreditCardPaymentProcessor : IPaymentProcessor
{
    public void ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Processing credit card payment of {amount} dollars...");
        // 具体信用卡支付逻辑...
    }
}
​
public class PayPalPaymentProcessor : IPaymentProcessor
{
    public void ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Processing PayPal payment of {amount} dollars...");
        // 具体PayPal支付逻辑...
    }
}

在这个例子中,每个具体的支付处理器都实现了 IPaymentProcessor 接口,并提供了自己特定的支付逻辑。这样,高层模块(订单处理模块)就可以依赖于抽象接口 IPaymentProcessor 而不是具体的实现类。

使用示例:

public class OrderProcessor
{
    private IPaymentProcessor paymentProcessor;
​
    public OrderProcessor(IPaymentProcessor paymentProcessor)
    {
        this.paymentProcessor = paymentProcessor;
    }
​
    public void ProcessOrder(decimal amount)
    {
        // 处理订单逻辑...
​
        // 使用依赖注入的方式调用支付处理器
        paymentProcessor.ProcessPayment(amount);
​
        // 其他订单处理逻辑...
    }
}
​
// 在应用程序中配置和使用不同的支付方式
var creditCardPayment = new CreditCardPaymentProcessor();
var payPalPayment = new PayPalPaymentProces 

接口隔离原则(Interface Segregation Principle,ISP)要求客户端不应该依赖于它们不使用的接口。一个类应该只依赖于它需要的接口,而不是依赖于多余的接口。

下面是一个使用C#示例来说明接口隔离原则的应用:

假设我们正在开发一个文件管理系统,其中包含文件上传和文件下载功能。我们希望能够根据用户需求提供相应的功能。

首先,定义两个接口 IFileUploadableIFileDownloadable 来表示文件上传和文件下载功能,并分别声明相应的方法:

public interface IFileUploadable
{
    void UploadFile(string filePath);
}
​
public interface IFileDownloadable
{
    void DownloadFile(string fileId);
}

然后,在具体的实现类中分别实现这两个功能。例如,创建 LocalFileManager 类来处理本地文件操作,并实现对应的方法:

public class LocalFileManager : IFileUploadable, IFileDownloadable
{
    public void UploadFile(string filePath)
    {
        Console.WriteLine($"Uploading file from local path: {filePath}");
        // 具体本地上传逻辑...
    }
​
    public void DownloadFile(string fileId)
    {
        Console.WriteLine($"Downloading file with ID: {fileId} to local path");
        // 具体本地下载逻辑...
    }
}

在这个例子中,每个具体的实现类只关注自己需要用到的接口方法,而不需要实现多余的方法。这符合接口隔离原则,因为客户端可以根据需要依赖于相应的接口。

使用示例:

public class FileManagerClient
{
    private IFileUploadable fileUploader;
    private IFileDownloadable fileDownloader;
​
    public FileManagerClient(IFileUploadable fileUploader, IFileDownloadable fileDownloader)
    {
        this.fileUploader = fileUploader;
        this.fileDownloader = fileDownloader;
    }
​
    public void UploadAndDownloadFiles(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#示例来说明迪米特法则的应用:

假设我们正在开发一个社交网络系统,其中包含用户、好友和消息等功能。我们希望能够实现用户发送消息给好友的功能。

首先,定义三个类 UserFriendMessage 来表示用户、好友和消息,并在 User 类中实现发送消息的方法:

public class User
{
    private string name;
    private List<Friend> friends;
​
    public User(string name)
    {
        this.name = name;
        this.friends = new List<Friend>();
    }
​
    public void AddFriend(Friend friend)
    {
        friends.Add(friend);
    }
​
    public void SendMessageToFriends(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 class Friend
{
   private string name;
​
   public Friend(string name)
   {
       this.name = name;
   }
​
   public void ReceiveMessage(Message message)
   {
       Console.WriteLine($"Friend {name} received a message: {message.Content}");
       // 处理接收到的消息...
   }
}
​
public class Message
{
   public string Content { get; set; }
​
   public Message(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

public abstract class Shape
{
    public abstract void Draw();
}

然后,在具体的子类中分别实现各种形状。例如,创建 Circle 类和 Rectangle 类来表示圆形和矩形,并重写父类中的 Draw 方法:

public class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle");
        // 具体绘制圆形逻辑...
    }
}

public class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a rectangle");
        // 具体绘制矩形逻辑...
    }
}

接下来,创建一个画板类 Canvas 来管理并绘制多个图形。在该类中使用对象组合将多个图形组合在一起:

public class Canvas
{
    private List<Shape> shapes;
​
    public Canvas()
    {
        shapes = new List<Shape>();
    }
​
    public void AddShape(Shape shape)
    {
        shapes.Add(shape);
    }
​
    public void DrawShapes()
    {
        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 方法来绘制所有的图形。

通过使用对象组合而不是继承,我们可以更灵活地实现功能的复用和扩展。例如,可以轻松地添加新的图形类型或修改现有图形类型的行为,而不会影响到画板类。这符合合成复用原则,并提高了代码的可维护性和灵活性。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK