软件架构设计原则

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

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

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

开闭原则示例

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

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

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

  1. public abstract class Shape
  2. {
  3. public abstract void Draw();
  4. }

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

  1. public class Circle : Shape
  2. {
  3. public override void Draw()
  4. {
  5. Console.WriteLine("Drawing a circle");
  6. }
  7. }

  8. public class Rectangle : Shape
  9. {
  10. public override void Draw()
  11. {
  12. Console.WriteLine("Drawing a rectangle");
  13. }
  14. }

  15. public class Triangle : Shape
  16. {
  17. public override void Draw()
  18. {
  19. Console.WriteLine("Drawing a triangle");
  20. }
  21. }

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

  1. public class Ellipse : Shape
  2. {
  3. public override void Draw()
  4. {
  5. Console.WriteLine("Drawing an ellipse");
  6. }
  7. }

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

  1. public class DrawingProgram
  2. {
  3. public void DrawShapes(List<Shape> shapes)
  4. {
  5. foreach (var shape in shapes)
  6. {
  7. shape.Draw();
  8. }
  9. }
  10. }

使用示例:

  1. var shapes = new List<Shape>
  2. {
  3. new Circle(),
  4. new Rectangle(),
  5. new Triangle(),
  6. new Ellipse()
  7. };

  8. var drawingProgram = new DrawingProgram();
  9. drawingProgram.DrawShapes(shapes);

输出结果:

  1. Drawing a circle
  2. Drawing a rectangle
  3. Drawing a triangle
  4. Drawing an ellipse

通过遵循开闭原则,我们可以轻松地扩展应用程序并添加新的图形类型,而无需修改现有代码。这样可以提高代码的可维护性和可扩展性,并支持软件系统的演化和变化。

单一职责示例

单一职责原则(Single Responsibility Principle,SRP)要求一个类应该只有一个引起它变化的原因。换句话说,一个类应该只负责一项职责或功能。

下面是一个使用C#示例来说明单一职责原则的应用:

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

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

  1. public class Student
  2. {
  3. public string Name { get; set; }
  4. public int Age { get; set; }

  5. // 其他与学生相关的属性和方法...
  6. }

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

  1. public class StudentManager
  2. {
  3. private List<Student> students;

  4. public StudentManager()
  5. {
  6. students = new List<Student>();
  7. }

  8. public void AddStudent(Student student)
  9. {
  10. // 将学生信息添加到列表中...
  11. students.Add(student);
  12. Console.WriteLine("Student added successfully.");
  13. }

  14. public void DisplayStudents()
  15. {
  16. // 展示所有学生信息...
  17. foreach (var student in students)
  18. {
  19. Console.WriteLine($"Name: {student.Name}, Age: {student.Age}");
  20. }
  21. }
  22. }

在这个例子中,Student 类负责表示单个学生对象,并封装了与学生相关的属性。而 StudentManager 类负责处理学生信息的管理操作,如添加学生和展示学生信息。

使用示例:

  1. var student1 = new Student { Name = "Alice", Age = 20 };
  2. var student2 = new Student { Name = "Bob", Age = 22 };

  3. var studentManager = new StudentManager();
  4. studentManager.AddStudent(student1);
  5. studentManager.AddStudent(student2);

  6. studentManager.DisplayStudents();

输出结果:

  1. Student added successfully.
  2. Student added successfully.
  3. Name: Alice, Age: 20
  4. Name: Bob, Age: 22

通过将学生对象的表示和管理操作分别封装在不同的类中,我们遵循了单一职责原则。Student 类只负责表示学生对象的属性,而 StudentManager 类只负责处理与学生信息管理相关的操作。这样可以提高代码的可维护性和可扩展性,并使每个类都具有清晰明确的职责。

里式替换

里氏替换原则(Liskov Substitution Principle,LSP)要求子类型必须能够替换其基类型,并且不会破坏程序的正确性。也就是说,子类可以在不影响程序正确性和预期行为的情况下替代父类。

下面是一个使用C#示例来说明里式替换原则的应用:

假设我们正在开发一个图形绘制应用程序,其中包含多种形状(如圆形、矩形等)。我们希望能够根据用户选择的形状类型进行绘制操作。

首先,定义一个抽象基类 Shape 来表示所有形状对象,并声明一个抽象方法 Draw 用于绘制该形状:

  1. public abstract class Shape
  2. {
  3. public abstract void Draw();
  4. }

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

  1. public class Circle : Shape
  2. {
  3. public override void Draw()
  4. {
  5. Console.WriteLine("Drawing a circle...");
  6. }
  7. }

  8. public class Rectangle : Shape
  9. {
  10. public override void Draw()
  11. {
  12. Console.WriteLine("Drawing a rectangle...");
  13. }
  14. }

在这个例子中,每个具体的子类都可以替代其基类 Shape 并实现自己特定的绘制逻辑。这符合里式替换原则,因为无论是 Circle 还是 Rectangle 都可以在不破坏程序正确性和预期行为的情况下替代 Shape

使用示例:

  1. Shape circle = new Circle();
  2. circle.Draw(); // 输出 "Drawing a circle..."

  3. Shape rectangle = new Rectangle();
  4. rectangle.Draw(); // 输出 "Drawing a rectangle..."

通过将具体的子类对象赋值给基类引用变量,并调用相同的方法,我们可以看到不同形状的绘制操作被正确地执行。这证明了里式替换原则的有效性。

总结:里式替换原则要求子类型必须能够替代其基类型,并且不会破坏程序正确性。在C#中,我们可以通过创建具体的子类来表示不同形状,并确保它们能够在继承自抽象基类时正确地实现自己特定的行为。这样可以提高代码的可扩展性和灵活性,并使得代码更易于维护和重用。

依赖倒置

依赖倒置原则(Dependency Inversion Principle,DIP)要求高层模块不应该依赖于低层模块的具体实现,而是应该依赖于抽象。同时,抽象不应该依赖于具体实现细节,而是应该由高层模块定义。

下面是一个使用C#示例来说明依赖倒置原则的应用:

假设我们正在开发一个电子商务系统,其中包含订单处理和支付功能。我们希望能够根据用户选择的支付方式进行订单支付操作。

首先,定义一个抽象接口 IPaymentProcessor 来表示支付处理器,并声明一个方法 ProcessPayment 用于执行订单支付:

  1. public interface IPaymentProcessor
  2. {
  3. void ProcessPayment(decimal amount);
  4. }

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

  1. public class CreditCardPaymentProcessor : IPaymentProcessor
  2. {
  3. public void ProcessPayment(decimal amount)
  4. {
  5. Console.WriteLine($"Processing credit card payment of {amount} dollars...");
  6. // 具体信用卡支付逻辑...
  7. }
  8. }

  9. public class PayPalPaymentProcessor : IPaymentProcessor
  10. {
  11. public void ProcessPayment(decimal amount)
  12. {
  13. Console.WriteLine($"Processing PayPal payment of {amount} dollars...");
  14. // 具体PayPal支付逻辑...
  15. }
  16. }

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

使用示例:

  1. public class OrderProcessor
  2. {
  3. private IPaymentProcessor paymentProcessor;

  4. public OrderProcessor(IPaymentProcessor paymentProcessor)
  5. {
  6. this.paymentProcessor = paymentProcessor;
  7. }

  8. public void ProcessOrder(decimal amount)
  9. {
  10. // 处理订单逻辑...

  11. // 使用依赖注入的方式调用支付处理器
  12. paymentProcessor.ProcessPayment(amount);

  13. // 其他订单处理逻辑...
  14. }
  15. }

  16. // 在应用程序中配置和使用不同的支付方式
  17. var creditCardPayment = new CreditCardPaymentProcessor();
  18. var payPalPayment = new PayPalPaymentProces

接口隔离

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

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

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

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

  1. public interface IFileUploadable
  2. {
  3. void UploadFile(string filePath);
  4. }

  5. public interface IFileDownloadable
  6. {
  7. void DownloadFile(string fileId);
  8. }

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

  1. public class LocalFileManager : IFileUploadable, IFileDownloadable
  2. {
  3. public void UploadFile(string filePath)
  4. {
  5. Console.WriteLine($"Uploading file from local path: {filePath}");
  6. // 具体本地上传逻辑...
  7. }

  8. public void DownloadFile(string fileId)
  9. {
  10. Console.WriteLine($"Downloading file with ID: {fileId} to local path");
  11. // 具体本地下载逻辑...
  12. }
  13. }

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

使用示例:

  1. public class FileManagerClient
  2. {
  3. private IFileUploadable fileUploader;
  4. private IFileDownloadable fileDownloader;

  5. public FileManagerClient(IFileUploadable fileUploader, IFileDownloadable fileDownloader)
  6. {
  7. this.fileUploader = fileUploader;
  8. this.fileDownloader = fileDownloader;
  9. }

  10. public void UploadAndDownloadFiles(string filePath, string fileId)
  11. {
  12. // 使用文件上传功能
  13. fileUploader.UploadFile(filePath);

  14. // 使用文件下载功能
  15. fileDownloader.DownloadFile(fileId);
  16. // 其他操作...
  17. }
  18. }

  19. // 在应用程序中配置和使用具体的文件管理类
  20. var localFileManager = new LocalFileManager();
  21. var client = new FileManagerClient(localFileManager, localFileManager);
  22. client.UploadAndDownloadFiles("path/to/file", "123456");

通过依赖注入的方式,我们可以将具体的实现类传递给客户端,并根据需要调用相应的接口方法。这样就遵循了接口隔离原则,使得客户端只依赖于它们所需的接口,并且不会受到多余方法的影响。这提高了代码的可维护性和灵活性,并促进了代码重用和扩展。

迪米特

迪米特法则(Law of Demeter,LoD),也称为最少知识原则(Principle of Least Knowledge),要求一个对象应该对其他对象有尽可能少的了解。一个类不应该直接与其他类耦合,而是通过中间类进行通信。

下面是一个使用C#示例来说明迪米特法则的应用:

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

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

  1. public class User
  2. {
  3. private string name;
  4. private List<Friend> friends;

  5. public User(string name)
  6. {
  7. this.name = name;
  8. this.friends = new List<Friend>();
  9. }

  10. public void AddFriend(Friend friend)
  11. {
  12. friends.Add(friend);
  13. }

  14. public void SendMessageToFriends(string messageContent)
  15. {
  16. Message message = new Message(messageContent);

  17. foreach (Friend friend in friends)
  18. {
  19. friend.ReceiveMessage(message);
  20. }
  21. Console.WriteLine($"User {name} sent a message to all friends.");
  22. }
  23. }

  24. public class Friend
  25. {
  26. private string name;

  27. public Friend(string name)
  28. {
  29. this.name = name;
  30. }

  31. public void ReceiveMessage(Message message)
  32. {
  33. Console.WriteLine($"Friend {name} received a message: {message.Content}");
  34. // 处理接收到的消息...
  35. }
  36. }

  37. public class Message
  38. {
  39. public string Content { get; set; }

  40. public Message(string content)
  41. {
  42. Content = content;
  43. }
  44. }

在这个例子中,User 类表示用户,Friend 类表示好友,Message 类表示消息。用户可以添加好友,并通过 SendMessageToFriends 方法向所有好友发送消息。

使用示例:

  1. User user1 = new User("Alice");
  2. User user2 = new User("Bob");

  3. Friend friend1 = new Friend("Charlie");
  4. Friend friend2 = new Friend("David");

  5. user1.AddFriend(friend1);
  6. user2.AddFriend(friend2);

  7. user1.SendMessageToFriends("Hello, friends!");

在这个示例中,用户对象只与好友对象进行通信,并不直接与消息对象进行通信。这符合迪米特法则的要求,即一个对象应该尽可能少地了解其他对象。

通过将消息发送的责任委托给好友对象,在用户类中只需要调用 friend.ReceiveMessage(message) 方法来发送消息给所有好友。这样可以降低类之间的耦合性,并提高代码的可维护性和灵活性。

合成复用

合成复用原则(Composite Reuse Principle,CRP)要求尽量使用对象组合,而不是继承来达到复用的目的。通过将现有对象组合起来创建新的对象,可以更灵活地实现功能的复用和扩展。

下面是一个使用C#示例来说明合成复用原则的应用:

假设我们正在开发一个图形库,其中包含各种形状(如圆形、矩形等)。我们希望能够实现一个可以绘制多个形状的画板。

首先,定义一个抽象基类 Shape 来表示图形,并声明抽象方法 Draw

  1. public abstract class Shape
  2. {
  3. public abstract void Draw();
  4. }

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

  1. public class Circle : Shape
  2. {
  3. public override void Draw()
  4. {
  5. Console.WriteLine("Drawing a circle");
  6. // 具体绘制圆形逻辑...
  7. }
  8. }
  9. public class Rectangle : Shape
  10. {
  11. public override void Draw()
  12. {
  13. Console.WriteLine("Drawing a rectangle");
  14. // 具体绘制矩形逻辑...
  15. }
  16. }

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

  1. public class Canvas
  2. {
  3. private List<Shape> shapes;

  4. public Canvas()
  5. {
  6. shapes = new List<Shape>();
  7. }

  8. public void AddShape(Shape shape)
  9. {
  10. shapes.Add(shape);
  11. }

  12. public void DrawShapes()
  13. {
  14. foreach (Shape shape in shapes)
  15. {
  16. shape.Draw();
  17. }
  18. Console.WriteLine("All shapes are drawn.");
  19. }
  20. }

在这个例子中,Canvas 类通过对象组合的方式将多个图形对象组合在一起,并提供了添加图形和绘制图形的方法。

使用示例:

  1. Canvas canvas = new Canvas();

  2. Circle circle = new Circle();
  3. Rectangle rectangle = new Rectangle();

  4. canvas.AddShape(circle);
  5. canvas.AddShape(rectangle);

  6. canvas.DrawShapes();

在这个示例中,我们创建了一个画板对象 canvas,并向其中添加了一个圆形和一个矩形。然后调用 DrawShapes 方法来绘制所有的图形。

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

C#软件架构设计原则的更多相关文章

  1. 大型Java进阶专题(三) 软件架构设计原则(下)

    前言 ​ 今天开始我们专题的第二课了,本章节继续分享软件架构设计原则的下篇,将介绍:接口隔离原则.迪米特原则.里氏替换原则和合成复用原则.本章节参考资料书籍<Spring 5核心原理>中的 ...

  2. 大型Java进阶专题(二) 软件架构设计原则(上)

    前言 ​ 今天开始我们专题的第一课了,也是我开始进阶学习的第一天,我们先从经典设计思想开始,看看大牛市如何写代码的,提升技术审美.提高核心竞争力.本章节参考资料书籍<Spring 5核心原理&g ...

  3. SoC嵌入式软件架构设计之三:代码分块(Bank)设计原则

    上一节讲述了在没有MMU的CPU(如80251.MIPS M控制器系列.ARM cortex m系列)上实现虚拟内存管理的集成硬件设计方法.新设计的内存管理管理单元要实现虚拟内存管理还须要操作系统.代 ...

  4. Unity3D设计原则

    原则1:单一职责 原则2:里氏替换原则(子类扩展但不改变父类功能) 原则3:依赖倒置原则 原则4:接口隔离原则 原则5:迪米特法则(最少知道原则) 原则6:开闭原则 原则1:单一职责原则 说到单一职责 ...

  5. SOA 的基本概念及设计原则浅议

    SOA是英文词语"Service Oriented Architecture"的缩写,中文有多种翻译,如"面向服务的体系结构"."以服务为中心的体系结 ...

  6. SoC嵌入式软件架构设计II:没有MMU的CPU虚拟内存管理的设计和实现方法

    大多数的程序代码是必要的时,它可以被加载到内存中运行.手术后,可直接丢弃或覆盖其它代码. 我们PC然在同一时间大量的应用,地址空间差点儿能够整个线性地址空间(除了部分留给操作系统或者预留它用).能够觉 ...

  7. SoC嵌入式软件架构设计II:否MMU的CPU虚拟内存管理的设计与实现方法

    大多数的程序代码是必要的时,它可以被加载到内存中运行.手术后,可直接丢弃或覆盖其他代码.我们PC然在同一时间大量的应用,能够整个线性地址空间(除了部分留给操作系统或者预留它用),能够觉得每一个应用程序 ...

  8. JAVA设计模式总结之六大设计原则

    从今年的七月份开始学习设计模式到9月底,设计模式全部学完了,在学习期间,总共过了两篇:第一篇看完设计模式后,感觉只是脑子里面有印象但无法言语.于是决定在看一篇,到9月份第二篇设计模式总于看完了,这一篇 ...

  9. SoC嵌入式软件架构设计

    内存是SoC(System on Chip,片上系统)集成设计的重要模块,是SoC中成本比重较大的部分.内存管理的软硬件设计是SoC软件架构设计的重要一环,架构设计师必须要在成本和效率中取得平衡,做到 ...

  10. 面象对象设计原则之七:合成复用原则(Composition/Aggregate Reuse Principle, CARP)

    合成复用原则又称为组合/聚合复用原则(Composition/Aggregate Reuse Principle, CARP),其定义如下: 合成复用原则(Composite Reuse Princi ...

随机推荐

  1. 使用 nuxt3 开发简约优雅的个人 blog

    起因 很早前我就有过搭建个人博客的想法,但是我希望使用纯前端实现,这样就不需要付出额外的后端维护成本,维护成本又低,而且更加安全.网上也有很多博客框架但是也不符合我的需求,所以我使用了nuxt3 + ...

  2. 《Among Us》火爆全球,实时语音助力派对游戏开启第二春

    今年在全球"宅经济"的影响下,社交派对类游戏意外的迎来了爆发. 8月份,<糖豆人:终极淘汰赛>突然爆火,创造了首日150万玩家.首周Steam 200万销量.单周Twi ...

  3. 【阅读笔记】RAISR

    RAISR: RAISR: Rapid and Accurate Image Super Resolution --Yaniv Romano, 2017(211 Citations) 核心思想 LR ...

  4. Python 学习笔记:基础篇

    ! https://zhuanlan.zhihu.com/p/644232952 Python 学习笔记:基础篇 承接之前在<[[Python 学习路线图]]>一文中的规划,接下来,我将会 ...

  5. Go语言中指针详解

    指针在 Go 语言中是一个重要的特性,它允许你引用和操作变量的内存地址.下面是指针的主要作用和相关示例代码: 1. 引用传递 在 Go 中,所有的变量传递都是值传递,即复制变量的值.如果你想在一个函数 ...

  6. pandas 格式化日期

    output_data["ShipDate"] = output_data["ShipDate"].dt.strftime("%Y/%m/%d&quo ...

  7. PHP插件

  8. ois七层模型与数据封装过程

    一,ois七层模型 一,ois七层模型1 为什么要分层2 七层模型3 七层总结二,协议,端口,的作用2.1协议作用2.2tcp/udp的区别2.3ARP 协议的作用2.4客户端与服务端的作用2.5ic ...

  9. redux的三个概念与三大核心

    1.什么是redux?一个组件里可能会有很多的状态,比如控制某个内容显示的flag,从后端获取的展示数据,那么这些状态可以在自己的单个页面进行管理,也可以选择别的管理方式,redux就是是一种状态管理 ...

  10. 事务,不只ACID

    1. 什么是事务? 应用在运行时可能会发生数据库.硬件的故障,应用与数据库的网络连接断开或多个客户端端并发修改数据导致预期之外的数据覆盖问题,为了提高应用的可靠性和数据的一致性,事务应运而生. 从概念 ...