当前位置: 首页 > news >正文

悉数六大设计原则

悉数六大设计原则

目录

  • 悉数六大设计原则
    • 前言☕
    • 谁发明了设计模式
    • 设计原则
      • 设计原则与设计模式的关系
    • 单一职责
      • 什么是单一职责
      • 不遵循单一职责原则的设计
      • 遵循单一职责原则的设计
      • 单一职责的优点
      • 示例代码:
    • 里氏替换原则
      • 什么是里氏替换原则
      • 示例代码:
        • 违反里氏替换原则的代码
        • 遵循里氏替换原则的代码
      • 里氏替换原则的优点
    • 依赖倒置原则
      • 什么是依赖倒置原则
      • 依赖倒置原则的核心思想
      • 依赖倒置原则的优点
      • 示例代码:
        • 不遵循依赖倒置原则的设计
        • 遵循依赖倒置原则的设计
      • 实际应用中的优点
    • 接口隔离原则
      • 什么是接口隔离原则
      • 示例代码:
        • 不遵循接口隔离原则的设计
        • 遵循接口隔离原则的设计
      • 实际应用中的好处
      • 示例代码:
    • 迪米特原则
      • 什么是迪米特法则
      • 迪米特法则的规则
      • 示例代码:
        • 违反迪米特法则的代码
        • 遵循迪米特法则的代码
      • 迪米特法则的优点
    • 开闭原则
      • 什么是开闭原则
      • 如何实现开闭原则
      • 示例代码:
        • 场景描述
        • 违反开闭原则的设计
        • 遵循开闭原则的设计
      • 开闭原则的优点
    • 总结🍭

前言☕

大家好,我是Leo哥🫣🫣🫣,今天开始我们来学习一下关于设计模式的内容。提起设计模式,大家肯定不陌生,可能在此之前你也多少了了解过设计模式,但在实际的业务开发中使⽤用却不不多,多数时候都是⼤大⾯面积堆积ifelse 组装业务流程,对于⼀一次次的需求迭代和逻辑补充,只能东拼⻄西凑 Ctrl+C 、 Ctrl+V 。作为一名优秀的程序员,设计模式可谓是必修课,接下来就跟着Leo哥一起来了解了解设计模式吧。

谁发明了设计模式

设计模式的概念最早是由 克⾥里里斯托佛·亚历⼭山⼤大 在其著作 《建筑模式语⾔言》 中⾸首次提出的。 本书介绍了了城市设计的 语⾔言,提供了了253个描述城镇、邻⾥里里、住宅、花园、房间及⻄西部构造的模式, ⽽而此类 语⾔言 的基本单元就是模式。后来, 埃⾥里里希·伽玛 、 约翰·弗利利赛德斯 、 拉尔夫·约翰逊 和 理理查德·赫尔姆 这四位作者接受了了模式的概念。 1994 年年, 他们出版了了 《设计模式: 可复⽤用⾯面向对象软件的基础》 ⼀一书, 将设计模式的概念应⽤用到程序开发领域中。

设计原则

在学习设计模式之前,我们应该先了解一下设计原则,那么什么是设计原则呢。设计原则是指导软件设计的一系列准则和规范,旨在帮助开发人员创建高质量的代码。这些原则强调代码的可维护性、可扩展性和灵活性,减少系统的复杂性和提高代码的可理解性。

设计原则与设计模式的关系

  • 设计原则:设计原则是高层次的指导方针,提供了软件设计的基本框架和标准。这些原则可以应用于任何软件开发项目,以确保代码的高质量和长期可维护性。
  • 设计模式:设计模式是针对特定问题的具体解决方案,是对设计原则的具体应用和实现。设计模式提供了可以复用的代码结构和模板,帮助开发人员解决常见的设计问题。

话不多说,下面我们首先来学习一下经典的六大设计原则吧。

单一职责

首先, 我们来看单一职责的定义。

单一职责原则,全称Single Responsibility Principle, 简称SRP. A class should have only one reason to change 类发生更改的原因应该只有一个 。

什么是单一职责

单一职责原则(Single Responsibility Principle, SRP) 是软件设计中的一种原则,它强调每个类应该只有一个职责,即一个类只负责一项功能或一类功能的逻辑。这个原则是 SOLID 原则中的第一个,它有助于提高代码的可维护性、可读性和可扩展性。

就一个类而言,应该仅有一个引起它变化的原因。应该只有一个职责。如果一个类有一个以上的职责,这些职责就耦合在了一起。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。想要避免这种现象的发生,就要尽可能的遵守单一职责原则。

单一职责原则的核心就是解耦和增强内聚性。

不遵循单一职责原则的设计

public class ReportManager {public String generateReport() {// 生成报告的逻辑return "Report Content";}public void printReport(String report) {// 打印报告的逻辑System.out.println(report);}
}

在上面的代码示例中,ReportManager 类同时负责生成报告和打印报告。这两个职责耦合在一起,如果将来需要修改打印报告的方式,我们需要修改 ReportManager 类,这违反了单一职责原则。

遵循单一职责原则的设计

我们可以将生成报告和打印报告的职责分离到不同的类中:

// 生成报告的类
public class ReportGenerator {public String generateReport() {// 生成报告的逻辑return "Report Content";}
}// 打印报告的类
public class ReportPrinter {public void printReport(String report) {// 打印报告的逻辑System.out.println(report);}
}

现在,ReportGenerator 类只负责生成报告,ReportPrinter 类只负责打印报告。这种设计使得每个类的职责单一,如果将来需要修改打印报告的方式,只需要修改 ReportPrinter 类,不会影响到 ReportGenerator 类。

单一职责的优点

  1. 提高可维护性:职责单一的类更容易理解和维护。每个类的代码量减少,逻辑更加清晰。
  2. 提高可复用性:职责单一的类可以更容易地在不同的上下文中复用,而无需担心未使用的功能带来的负担。
  3. 增强测试性:职责单一的类通常具有较少的依赖,单元测试更容易编写和执行。
  4. 降低耦合度:将不同的职责分离到不同的类中,减少了类之间的耦合,增强了系统的灵活性和可扩展性。

示例代码:

下面我们来写一个一个更完整的代码示例,展示了如何使用单一职责原则设计一个简单的学生管理系统:

// 学生类,负责学生信息
public class Student {private String name;private int age;public Student(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public int getAge() {return age;}
}// 学生数据库操作类,负责与数据库的交互
public class StudentRepository {public void save(Student student) {// 将学生信息保存到数据库的逻辑System.out.println("Saving student: " + student.getName());}public Student findByName(String name) {// 从数据库中查找学生信息的逻辑return new Student(name, 20); // 模拟返回一个学生对象}
}// 学生信息展示类,负责学生信息的展示
public class StudentView {public void displayStudentInfo(Student student) {System.out.println("Student Name: " + student.getName());System.out.println("Student Age: " + student.getAge());}
}// 主类,负责调用其他类完成具体功能
public class Main {public static void main(String[] args) {StudentRepository studentRepository = new StudentRepository();StudentView studentView = new StudentView();Student student = new Student("John Doe", 20);studentRepository.save(student);Student retrievedStudent = studentRepository.findByName("John Doe");studentView.displayStudentInfo(retrievedStudent);}
}
  • Student 类只负责保存学生的基本信息。
  • StudentRepository 类负责与数据库的交互,处理学生信息的保存和查询。
  • StudentView 类负责展示学生信息。

里氏替换原则

什么是里氏替换原则

里氏替换原则(Liskov Substitution Principle,LSP)是由计算机科学家 Barbara Liskov 在 1987 年提出的,是面向对象设计的五大基本原则之一(SOLID 原则中的 L)。里氏替换原则的核心思想是:在一个程序中,如果基类可以被子类替换,而不影响程序的正确性,那么这个子类是正确的。换句话说,子类对象应该能够替换基类对象而不改变程序的行为。

里式替换原则是用来帮助我们在继承关系中进行父子类的设计。

里氏替换原则(Liskov Substitution principle)是对子类型的特别定义的. 为什么叫里式替换原则呢?因为这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的。

里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。

里式替换原则有两层定义:

定义1

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。

如果S是T的子类,则T的对象可以替换为S的对象,而不会破坏程序。

定义2:

Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。

所有引用其父类对象方法的地方,都可以透明的替换为其子类对象

示例代码:

下面是一个违反里氏替换原则的代码示例。

违反里氏替换原则的代码

假设我们有一个基类 Bird 和一个子类 Penguin

class Bird {public void fly() {System.out.println("I can fly");}
}class Penguin extends Bird {@Overridepublic void fly() {throw new UnsupportedOperationException("Penguins can't fly");}
}

这个代码示例中,Penguin 类重写了 fly 方法并抛出异常,这违反了里氏替换原则,因为如果我们使用 Penguin 对象替换 Bird 对象,程序将会抛出异常,导致行为不一致。

遵循里氏替换原则的代码

为了遵循里氏替换原则,我们可以引入一个接口 Flyable,并让 Bird 和其他可以飞的鸟类实现这个接口,而 Penguin 则不实现这个接口:

interface Flyable {void fly();
}class Bird {public void eat() {System.out.println("I can eat");}
}class Sparrow extends Bird implements Flyable {@Overridepublic void fly() {System.out.println("I can fly");}
}class Penguin extends Bird {// 企鹅没有实现Flyable这个接口
}

在这个重构后的设计中,Penguin 类不再需要实现 fly 方法,从而避免了违反里氏替换原则。现在,如果我们有一个方法需要处理所有可以飞的鸟,我们可以使用 Flyable 接口:

public void letBirdFly(Flyable bird) {bird.fly();
}public static void main(String[] args) {Sparrow sparrow = new Sparrow();Penguin penguin = new Penguin();letBirdFly(sparrow); // This works// letBirdFly(penguin); // 这将导致编译时错误
}

通过这种方式,我们确保了替换基类对象不会影响程序的行为,从而遵循了里氏替换原则。

里氏替换原则的优点

  1. 提高代码的可维护性:遵循里氏替换原则,可以确保子类和基类的行为一致,减少代码中的错误,提升代码的可维护性。
  2. 增强代码的可扩展性:通过接口和抽象类的使用,可以更容易地扩展系统,添加新的子类而不影响现有代码。
  3. 增强代码的可重用性:遵循里氏替换原则,可以提高代码的重用性,使得基类和子类之间的关系更加明确和稳固。

依赖倒置原则

什么是依赖倒置原则

**依赖倒置原则(Dependency Inversion Principle,DIP)**是面向对象设计的五个SOLID原则之一。该原则强调:

  1. 高层模块不应该依赖于低层模块。二者都应该依赖于抽象。
  2. 抽象不应该依赖于具体实现。具体实现应该依赖于抽象。

简单来说,依赖倒置原则提倡面向接口编程,而不是面向实现编程。这可以减少高层模块与低层模块之间的耦合,使系统更具灵活性和可扩展性。

依赖倒置原则的核心思想

  1. 依赖于抽象(接口或抽象类),而不是具体类:通过依赖于抽象,可以在不修改高层模块的情况下更换低层模块的实现。
  2. 通过依赖注入来实现依赖倒置:使用构造器注入、方法注入或属性注入的方式,将具体实现传递给高层模块。

依赖倒置原则的优点

  • 降低耦合:高层模块和低层模块之间通过接口或抽象类解耦。
  • 增强可维护性:修改低层模块的实现不会影响高层模块。
  • 提高可扩展性:可以方便地替换或新增实现而不改变现有代码。

示例代码:

不遵循依赖倒置原则的设计

在这个例子中,Light 类和 Switch 类之间有直接的依赖关系:

// 灯类
class Light {public void turnOn() {System.out.println("Light is turned on.");}public void turnOff() {System.out.println("Light is turned off.");}
}// 开关类
class Switch {private Light light;public Switch() {this.light = new Light();}public void operate(String command) {if (command.equalsIgnoreCase("ON")) {light.turnOn();} else if (command.equalsIgnoreCase("OFF")) {light.turnOff();}}
}public class Main {public static void main(String[] args) {Switch lightSwitch = new Switch();lightSwitch.operate("ON");lightSwitch.operate("OFF");}
}

在这个设计中,Switch 类直接依赖于 Light 类,如果需要更换 Light 的实现,需要修改 Switch 类的代码。

遵循依赖倒置原则的设计

在这个例子中,通过引入接口 Switchable,实现依赖倒置原则:

// 开关接口
interface Switchable {void turnOn();void turnOff();
}// 灯类实现开关接口
class Light implements Switchable {public void turnOn() {System.out.println("Light is turned on.");}public void turnOff() {System.out.println("Light is turned off.");}
}// 开关类依赖于开关接口,而不是具体的实现
class Switch {private Switchable device;public Switch(Switchable device) {this.device = device;}public void operate(String command) {if (command.equalsIgnoreCase("ON")) {device.turnOn();} else if (command.equalsIgnoreCase("OFF")) {device.turnOff();}}
}public class Main {public static void main(String[] args) {Switchable light = new Light();Switch lightSwitch = new Switch(light);lightSwitch.operate("ON");lightSwitch.operate("OFF");}
}

在上面的设计中,Switch 类依赖于 Switchable 接口,而不是具体的 Light 类。如果将来需要更换实现,只需实现 Switchable 接口并传递新的实现给 Switch 类。

实际应用中的优点

  1. 增强代码的可测试性:通过依赖注入,可以轻松地将实际实现替换为模拟对象,从而进行单元测试。
  2. 增加代码的灵活性和可扩展性:通过依赖抽象,代码可以适应不同的实现,而不需要修改高层模块。
  3. 提高代码的可维护性:代码的变更只会影响具体实现,不会波及依赖于抽象的高层模块。

接口隔离原则

什么是接口隔离原则

接口隔离原则(Interface Segregation Principle,ISP) 是面向对象设计的五个SOLID原则之一。该原则强调:

  1. 客户不应该被迫依赖他们不使用的方法。
  2. 多个特定客户端接口要好于一个宽泛用途的接口。

Clients should not be forced to depend upon interfaces that they don’t use. 客户端只依赖于它所需要的接口;它需要什么接口就提供什么接口,把不需要的接口剔除掉。

The dependency of one class to another one should depend on the smallest possible interface. 类间的依赖关系应建立在最小的接口上。

换句话说,接口隔离原则提倡将大接口拆分为多个小接口,使得接口更具针对性和灵活性。这样,客户端只需依赖它们真正需要的接口,避免了冗余和不必要的依赖。

也就是说: 接口尽量细化,接口中的方法尽量少

示例代码:

不遵循接口隔离原则的设计

在这个例子中,Worker 接口包含了所有工作者可能需要的方法,但具体的工作者类可能只需要其中的一部分:

// 工作者接口
public interface Worker {void work();void eat();
}// 开发者类
public class Developer implements Worker {@Overridepublic void work() {System.out.println("Developer is working.");}@Overridepublic void eat() {System.out.println("Developer is eating.");}
}// 机器人类
public class Robot implements Worker {@Overridepublic void work() {System.out.println("Robot is working.");}@Overridepublic void eat() {// 机器人不需要吃饭,但必须实现这个方法}
}

在这个设计中,Robot 类被迫实现了 eat 方法,这违反了接口隔离原则。

遵循接口隔离原则的设计

通过将 Worker 接口拆分为更细化的接口,可以避免上述问题:

// 工作接口
public interface Workable {void work();
}// 吃饭接口
public interface Eatable {void eat();
}// 开发者类实现了工作和吃饭接口
public class Developer implements Workable, Eatable {@Overridepublic void work() {System.out.println("Developer is working.");}@Overridepublic void eat() {System.out.println("Developer is eating.");}
}// 机器人类只实现了工作接口
public class Robot implements Workable {@Overridepublic void work() {System.out.println("Robot is working.");}
}

在这个设计中,Developer 类实现了 WorkableEatable 接口,而 Robot 类只实现了 Workable 接口,遵循了接口隔离原则。

实际应用中的好处

  1. 提高灵活性:将大接口拆分为多个小接口,使得类可以选择实现自己需要的接口,增加了系统的灵活性。
  2. 减少冗余:客户端只依赖它们实际需要的接口,减少了不必要的方法实现。
  3. 增强可维护性:接口的细化使得系统更易于理解和维护,修改和扩展时影响范围更小。
  4. 提高可测试性:小接口更容易进行单元测试,因为每个接口只包含了特定的功能方法。

示例代码:

// 工具使用接口
public interface ToolUsable {void useTool();
}// 吃饭接口
public interface Eatable {void eat();
}// 工人类实现了工具使用和吃饭接口
public class Worker implements ToolUsable, Eatable {@Overridepublic void useTool() {System.out.println("Worker is using a tool.");}@Overridepublic void eat() {System.out.println("Worker is eating.");}
}// 机器人类只实现了工具使用接口
public class Robot implements ToolUsable {@Overridepublic void useTool() {System.out.println("Robot is using a tool.");}
}public class Main {public static void main(String[] args) {ToolUsable workerToolUser = new Worker();Eatable workerEater = new Worker();ToolUsable robotToolUser = new Robot();workerToolUser.useTool();workerEater.eat();robotToolUser.useTool();}
}

在这个示例中,我们将 Worker 类和 Robot 类的接口细化,使得它们只实现自己需要的接口,遵循了接口隔离原则。

迪米特原则

什么是迪米特法则

迪米特法则(Law of Demeter, LoD),又称为最少知识原则(Principle of Least Knowledge),是一种软件设计原则,其主要思想是:一个对象应该对其他对象有尽可能少的了解,即一个对象不应该知道太多不属于它直接责任的对象细节。

如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。

朋友圈的确定“朋友”条件:

  1. 当前对象本身(this)
  2. 以参数形式传入到当前对象方法中的对象. 方法入参是一个对象, 这是这个对象和当前类是朋友
  3. 当前对象的实例变量直接引用的对象 定一个一个类, 里面的属性引用了其他对象, 那么这个对象的实例和当前实例是朋友
  4. 当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友 如果属性是一个对象, 那么属性和对象里的元素都是朋友
  5. 当前对象所创建的对象

任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”;否则就是“陌生人”。

狭义的迪米特法则的缺点:

在系统里造出大量的小方法,这些方法仅仅是传递间接的调用,与系统的业务逻辑无关。 遵循类之间的迪米特法则会是一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联。但是,这也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。

迪米特法则的规则

  1. 只调用自己的方法:对象只能调用自己方法,或者调用由自身创建的对象的方法。
  2. 只调用直接朋友的方法:对象只能调用作为参数传递给它的对象的方法,或是它的成员变量、全局变量的方法。

示例代码:

违反迪米特法则的代码

假设我们有一个 Car 类,它包含一个 Engine 对象,而 Engine 对象包含一个 Oil 对象:

class Oil {public void checkOilLevel() {System.out.println("Checking oil level");}
}class Engine {private Oil oil;public Engine() {this.oil = new Oil();}public Oil getOil() {return oil;}
}class Car {private Engine engine;public Car() {this.engine = new Engine();}public Engine getEngine() {return engine;}
}public class Main {public static void main(String[] args) {Car car = new Car();// 违反迪米特法则的代码:直接访问内部对象的内部对象的方法car.getEngine().getOil().checkOilLevel();}
}

在上面的例子中,Main 类通过 Car 对象访问 Engine 对象,再通过 Engine 对象访问 Oil 对象,最终调用 checkOilLevel 方法。这违反了迪米特法则,因为 Main 类知道了太多关于 EngineOil 的细节。

遵循迪米特法则的代码

我们可以通过在 Car 类中添加一个方法,来避免直接访问内部对象的内部对象:

class Oil {public void checkOilLevel() {System.out.println("Checking oil level");}
}class Engine {private Oil oil;public Engine() {this.oil = new Oil();}public void checkOilLevel() {oil.checkOilLevel();}
}class Car {private Engine engine;public Car() {this.engine = new Engine();}public void checkOilLevel() {engine.checkOilLevel();}
}public class Main {public static void main(String[] args) {Car car = new Car();// 遵循迪米特法则的代码:只调用直接对象的方法car.checkOilLevel();}
}

在这个重构后的例子中,Main 类只调用了 Car 对象的方法 checkOilLevelCar 对象内部处理了所有与 EngineOil 对象的交互。这遵循了迪米特法则,降低了对象之间的耦合度。

迪米特法则的优点

  1. 降低耦合度:减少对象之间的依赖关系,使得系统更容易维护和扩展。
  2. 提高内聚性:每个对象只关注自身的职责,增强了代码的内聚性。
  3. 增强可读性:减少了代码的复杂性,使代码更容易理解和阅读。

开闭原则

什么是开闭原则

开闭原则(Open-Closed Principle, OCP)是面向对象设计中的重要原则之一,由 Bertrand Meyer 于 1988 年提出。它是 SOLID 原则中的第二个,指的是软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着一个系统在需要改变的时候,应该通过扩展已有代码的行为来实现,而不是修改已有的代码。

如何实现开闭原则

“需求总是变化”、“世界上没有一个软件是不变的”。这里投射出的意思是:需求总是变化的, 可是对于软件设计者来说, 如何才能做到不对原有系统修改的前提下, 实现灵活的扩展. 这就是开闭原则要实现的。

我们在设计系统的时候, 不可能设想一次性把需求确定后, 后面就不改变了.这不科学也不现实的. 既然需求是一定会变化的, 那么我们要如何优雅的面对这种变化呢? 如何设计可以使软件相对容易修改, 不至于需求一变, 就要把整个程序推到重来?

开封-封闭原则. 设计软件要容易维护且不容易出问题的最好办法, 就是多扩展, 少修改。

开闭原则通常通过使用抽象和多态性来实现。具体来说,可以通过以下几种方法:

  1. 使用接口或抽象类:定义一个接口或抽象类,并通过不同的实现类来扩展功能。
  2. 使用设计模式:策略模式、装饰器模式、工厂模式等设计模式都可以帮助实现开闭原则。

示例代码:

场景描述

假设我们要开发一个简单的绘图应用程序,该程序可以绘制不同的形状(如圆形、矩形)。我们希望能够在不修改现有代码的情况下,轻松添加新的形状。

违反开闭原则的设计

以下代码在添加新形状时需要修改 ShapeDrawer 类,违反了开闭原则:

class ShapeDrawer {public void draw(String shapeType) {if (shapeType.equals("circle")) {System.out.println("Drawing a circle");} else if (shapeType.equals("rectangle")) {System.out.println("Drawing a rectangle");}// 添加新形状时需要修改此处代码}
}public class Main {public static void main(String[] args) {ShapeDrawer drawer = new ShapeDrawer();drawer.draw("circle");drawer.draw("rectangle");}
}
遵循开闭原则的设计

我们可以使用策略模式,通过抽象类或接口来扩展新形状,而不需要修改现有的代码:

// 定义抽象类 Shape
abstract class Shape {public abstract void draw();
}// 圆形类
class Circle extends Shape {@Overridepublic void draw() {System.out.println("Drawing a circle");}
}// 矩形类
class Rectangle extends Shape {@Overridepublic void draw() {System.out.println("Drawing a rectangle");}
}// 使用策略模式的绘图类
class ShapeDrawer {private Shape shape;public void setShape(Shape shape) {this.shape = shape;}public void drawShape() {shape.draw();}
}public class Main {public static void main(String[] args) {ShapeDrawer drawer = new ShapeDrawer();drawer.setShape(new Circle());drawer.drawShape();drawer.setShape(new Rectangle());drawer.drawShape();// 可以通过添加新类来扩展新形状,而无需修改 ShapeDrawer 类class Triangle extends Shape {@Overridepublic void draw() {System.out.println("Drawing a triangle");}}drawer.setShape(new Triangle());drawer.drawShape();}
}

在这个示例代码中,我们定义了一个抽象类 Shape,并为每种形状创建一个具体实现类。ShapeDrawer 类通过组合一个 Shape 对象来绘制形状。在需要添加新形状时,只需创建一个新的形状类并实现 Shape 抽象类,而无需修改 ShapeDrawer 类的代码。这就实现了开闭原则。

开闭原则的优点

  1. 提高可维护性:减少了对已有代码的修改,降低了引入新错误的风险。
  2. 提高可扩展性:通过扩展现有代码来实现新功能,而不是修改现有代码,增加了系统的灵活性。
  3. 提高复用性:抽象和实现分离,使得代码更易于复用。

总结🍭

在软件设计中,遵循设计原则有助于提高代码的可维护性、可扩展性和复用性。

  1. 单一职责原则(SRP):
    1. 核心思想:每个类应当只有一个引起其变化的原因,即一个类只负责一项职责。
    2. 优势:提高代码的可读性和可维护性,降低类之间的耦合度,增强系统的灵活性。
    3. 示例:将生成报告和打印报告的功能分离到不同的类中。
  2. 里氏替换原则(LSP):
    1. 核心思想:子类必须能够替换基类而不影响程序的正确性。
    2. 优势:确保子类能够正确扩展基类功能,提高代码的稳定性和可扩展性。
    3. 示例:通过接口和抽象类实现多态性,避免子类破坏基类的行为。
  3. 迪米特法则(LoD):
    1. 核心思想:一个对象应当尽可能少地了解其他对象,即只与直接的朋友通信。
    2. 优势:降低对象之间的耦合度,提高系统的内聚性和可维护性。
    3. 示例:通过在 Car 类中添加方法来避免直接访问内部对象的内部对象。
  4. 开闭原则(OCP):
    1. 核心思想:软件实体应当对扩展开放,对修改关闭。
    2. 优势:通过扩展现有代码来实现新功能,而不是修改已有代码,减少引入新错误的风险,提高系统的灵活性和可扩展性。
    3. 示例:使用策略模式,通过抽象类或接口来扩展新形状,而不修改现有代码。

在实际开发中,这些设计原则通常是相辅相成的。 例如,通过遵循单一职责原则,可以提高类的内聚性和可维护性,而结合里氏替换原则和开闭原则,可以设计出更灵活和可扩展的系统。此外,迪米特法则可以进一步降低类之间的耦合度,提高系统的健壮性。

通过理解和应用这些设计原则,可以构建出更高质量的软件系统,减少后期维护的复杂性,并提高开发效率和代码复用性。这些原则不仅适用于面向对象编程,也同样适用于其他编程范式,是软件开发过程中不可或缺的指导思想。

相关文章:

悉数六大设计原则

悉数六大设计原则 目录 悉数六大设计原则前言☕谁发明了设计模式设计原则设计原则与设计模式的关系 单一职责什么是单一职责不遵循单一职责原则的设计遵循单一职责原则的设计单一职责的优点示例代码: 里氏替换原则什么是里氏替换原则示例代码:违反里氏替…...

hdfs复习

一.hadoop概述 1.4高(优势) 1).高可靠性:hadoop底层多个数据副本,即使某个计算节点存储出现故障,不会导致数据丢失。 2).高扩展性:可以动态增加服务器节点。 3).高效…...

css-Ant-Menu 导航菜单更改为左侧列表行选中

1.Ant-Menu导航菜单 导航菜单是一个网站的灵魂&#xff0c;用户依赖导航在各个页面中进行跳转。一般分为顶部导航和侧边导航&#xff0c;顶部导航提供全局性的类目和功能&#xff0c;侧边导航提供多级结构来收纳和排列网站架构。 2.具体代码 html <!-- 左侧切换 --><…...

02-CSS3基本样式

目录 1. CSS3简介 1.1 CSS3的兼容情况 1.2 优雅降级和渐进增强的开发思想 2. 新增选择器 2.1 选择相邻兄弟 2.2 匹配选择器 2.3 属性选择器(重点) 2.4 结构性伪类选择器&#xff08;重点&#xff09; 2.4.1 整体结构类型 2.4.2 标签结构类型 2.4.3 指定子元素的序号&…...

USART串口外设

USART介绍 USART&#xff1a;另外我们经常还会遇到串口&#xff0c;叫UART&#xff0c;少了个S&#xff0c;就是通用异步收发器&#xff0c;一般我们串口很少使用这个同步功能&#xff0c;所以USART和UART使用起来&#xff0c;也没有什么区别。 其实这个STM32的USART同步模式&a…...

大模型应用之基于Langchain的测试用例生成

一 用例生成实践效果 在组内的日常工作安排中&#xff0c;持续优化测试技术、提高测试效率始终是重点任务。近期&#xff0c;我们在探索实践使用大模型生成测试用例&#xff0c;期望能够借助其强大的自然语言处理能力&#xff0c;自动化地生成更全面和高质量的测试用例。 当前…...

C++之map

1、标准库的map类型 2、插入数据 #include <map> #include <string> #include <iostream>using namespace std;int main() {map<string, int> mapTest;// 插入到map容器内部的元素是默认按照key从小到大来排序// key类型一定要重载小于号<运算符map…...

【量算分析工具-方位角】GeoServer改造Springboot番外系列六

【量算分析工具-概述】GeoServer改造Springboot番外系列三-CSDN博客 【量算分析工具-水平距离】GeoServer改造Springboot番外系列四-CSDN博客 【量算分析工具-水平面积】GeoServer改造Springboot番外系列五-CSDN博客 【量算分析工具-方位角】GeoServer改造Springboot番外系列…...

【机器学习】机器学习与大模型在人工智能领域的融合应用与性能优化新探索

文章目录 引言机器学习与大模型的基本概念机器学习概述监督学习无监督学习强化学习 大模型概述GPT-3BERTResNetTransformer 机器学习与大模型的融合应用自然语言处理文本生成文本分类机器翻译 图像识别自动驾驶医学影像分析 语音识别智能助手语音转文字 大模型性能优化的新探索…...

上传图片并显示#Vue3#后端接口数据

上传图片并显示#Vue3#后端接口数据 效果&#xff1a; 上传并显示图片 代码&#xff1a; <!-- 上传图片并显示 --> <template><!-- 上传图片start --><div><el-form><el-form-item><el-uploadmultipleclass"avatar-uploader&quo…...

音视频开发14 FFmpeg 视频 相关格式分析 -- H264 NALU格式分析

H264简介-也叫做 AVC H.264&#xff0c;在MPEG的标准⾥是MPEG-4的⼀个组成部分–MPEG-4 Part 10&#xff0c;⼜叫Advanced Video Codec&#xff0c;因此常常称为MPEG-4 AVC或直接叫AVC。 原始数据YUV,RGB为什么要压缩-知道就行 在⾳视频传输过程中&#xff0c;视频⽂件的传输…...

Qt学习记录(15)数据库

目录 前言&#xff1a; 数据库连接 项目文件加上sql 打印查看Qt支持哪些数据库驱动 QMYSQL [static] QSqlDatabase QSqlDatabase::addDatabase(const QString &type, const QString &connectionName QLatin1String(defaultConnection)) 数据库插入 头文件.h 源…...

c++常用设计模式

1、单例模式(Singleton)&#xff1a;保证一个类只有一个实例&#xff0c;提供一个全局访问点&#xff1b; class Singleton { private:static Singleton* instance;Singleton() {}public:static Singleton* getInstance() {if (instance nullptr) {instance new Singleton()…...

【动手学深度学习】softmax回归从零开始实现的研究详情

目录 &#x1f30a;1. 研究目的 &#x1f30a;2. 研究准备 &#x1f30a;3. 研究内容 &#x1f30d;3.1 softmax回归的从零开始实现 &#x1f30d;3.2 基础练习 &#x1f30a;4. 研究体会 &#x1f30a;1. 研究目的 理解softmax回归的原理和基本实现方式&#xff1b;学习…...

MySQL:MySQL执行一条SQL查询语句的执行过程

当多个客户端同时连接到MySQL,用SQL语句去增删改查数据,针对查询场景,MySQL要保证尽可能快地返回客户端结果。 了解了这些需求场景,我们可能会对MySQL进行如下设计: 其中,连接器管理客户端的连接,负责管理连接、认证鉴权等;查询缓存则是为了加速查询,命中则直接返回结…...

解决Python导入第三方模块报错“TypeError: the first argument must be callable”

注意以下内容只对导包时遇到同样的报错会有参考价值。 问题描述 当你尝试导入第三方模块时&#xff0c;可能会遇到如下报错信息&#xff1a; TypeError: the first argument must be callable 猜测原因 经过仔细检查代码&#xff0c;我猜测这个错误的原因是由于变量名冲突所…...

在python中连接了数据库后想要在python中通过图形化界面显示数据库的查询结果,请问怎么实现比较好? /ttk库的treeview的使用

在Python中&#xff0c;你可以使用图形用户界面&#xff08;GUI&#xff09;库来显示数据库的查询结果。常见的GUI库包括Tkinter&#xff08;Python自带&#xff09;、PyQt、wxPython等。以下是一个使用Tkinter库来显示数据库查询结果的简单示例。 首先&#xff0c;你需要确保…...

OZON的选品工具,OZON选品工具推荐

在电商领域&#xff0c;选品一直是决定卖家成功与否的关键因素之一。随着OZON平台的崛起&#xff0c;越来越多的卖家开始关注并寻求有效的选品工具&#xff0c;以帮助他们在这个竞争激烈的市场中脱颖而出。本文将详细介绍OZON的选品工具&#xff0c;并推荐几款实用的辅助工具&a…...

营销方案撰写秘籍:包含内容全解析,让你的方案脱颖而出

做了十几年品牌&#xff0c;策划出身&#xff0c;混迹过几个知名广告公司&#xff0c;个人经验供楼主参考。 只要掌握以下这些营销策划案的要点&#xff0c;你就能制作出既全面又专业的策划案&#xff0c;让你的工作成果不仅得到同事的认可&#xff0c;更能赢得老板的赏识&…...

如何制作一本温馨的电子相册呢?

随着科技的不断发展&#xff0c;电子相册已经成为了一种流行的方式来记录和分享我们的生活。一张张照片&#xff0c;一段段视频&#xff0c;都能让我们回忆起那些温馨的时光。那么&#xff0c;如何制作一本温馨的电子相册呢&#xff1f; 首先&#xff0c;选择一款合适的电子相册…...

485通讯网关

在工业自动化与智能化的浪潮中&#xff0c;数据的传输与交互显得尤为重要。作为这一领域的核心设备&#xff0c;485通讯网关凭借其卓越的性能和广泛的应用场景&#xff0c;成为了连接不同设备、不同协议之间数据转换和传输的桥梁。在众多485通讯网关中&#xff0c;HiWoo Box以其…...

Anaconda中的常用科学计算工具

Anaconda中的常用科学计算工具 Anaconda是一个流行的Python科学计算环境&#xff0c;它提供了大量的科学计算工具&#xff0c;这些工具可以帮助用户进行数据分析、机器学习、深度学习等任务。以下是一些常见的Anaconda中的科学计算工具&#xff1a; NumPy&#xff1a;一个用于…...

Java 中BigDecimal传到前端后精度丢失问题

1.用postman访问接口&#xff0c;返回的小数点精度正常 2.返回到页面里的&#xff0c;小数点丢失 3.解决办法&#xff0c;在字段上加注解 JsonFormat(shape JsonFormat.Shape.STRING) 或者 JsonSerialize(using ToStringSerializer.class) import com.fasterxml.jackson.a…...

在Linux/Ubuntu/Debian上安装TensorFlow 2.14.0

在Ubuntu上安装TensorFlow 2.14.0&#xff0c;可以遵循以下步骤。请注意&#xff0c;由于TensorFlow的版本更新可能很快&#xff0c;这里提供的具体步骤可能需要根据你的系统环境和实际情况进行微调。 准备工作 检查系统要求&#xff1a;确保你的Ubuntu系统满足TensorFlow的运…...

多语言for循环遍历总结

多语言for循环遍历总结 工作中经常需要遍历对象&#xff0c;但不同编程语言之间存在一些细微差别。为了便于比较和参考&#xff0c;这里对一些常用的遍历方法进行了总结。 JAVA 数组遍历 Test void ArrayForTest() {String[] array {"刘备","关羽", &…...

python API自动化(Jsonpath断言、接口关联及加密处理)

JsonPath应用及断言 重要 自动化要解决的核心问题 &#xff1a;进行自动测试-自动校验&#xff08;进行结果的校验 主要能够通过这个方式提取数据业务场景&#xff1a;断言 、接口关联 {key:value}网址&#xff1a;附&#xff1a;在线解析 JSONPath解析器 - 一个工具箱 - 好用…...

C++入门5——C/C++动态内存管理(new与delete)

目录 1. 一图搞懂C/C的内存分布 2. 存在动态内存分配的原因 3. C语言中的动态内存管理方式 4. C内存管理方式 4.1 new/delete操作内置类型 4.2 new/delete操作自定义类型 1. 一图搞懂C/C的内存分布 说明&#xff1a; 1. 栈区&#xff08;stack&#xff09;&#xff1a;在…...

leetcode 743.网络延时时间

思路&#xff1a;迪杰斯特拉最短路径 总结起来其实就两件事&#xff1a; 1.从所给起点开始能不能到达所有点&#xff1b; 2.如果能够到达所有点&#xff0c;那么这个时候需要判断每一个点到源点的最短距离&#xff0c;然后从这些点中求出最大值。 所以用最小路径求解是最划…...

MATLAB导入导出Excel的方法|读与写Excel的命令|附例程的github下载链接

前言 前段时间遇到一个需求&#xff1a;导出变量到Excel里面&#xff0c;这里给出一些命令&#xff0c;同时给一个示例供大家参考。 MATLAB读/写Excel的命令 在MATLAB中&#xff0c;可以使用以下命令来读写Excel文件&#xff1a; 读取Excel文件&#xff1a; xlsread(filen…...

【第4章】SpringBoot实战篇之登录优化(含redis使用)

文章目录 前言一、整合redis1. 引入库2. 配置 二、登录优化1.登录2.拦截器3. 登出4. 修改密码 总结 前言 上一章的登录接口,我们将用户登录信息放置于Map中,存在一个问题,集群部署无法共享以及应用停止用户登录信息即丢失,接下来我们整合redis来整合这个问题。 一、整合redis …...