设计模式——设计模式理念
文章目录
- 参考:[设计模式——设计模式理念](https://mp.weixin.qq.com/s/IEduZFF6SaeAthWFFV6zKQ)
- 参考:[设计模式——工厂方法模式](https://mp.weixin.qq.com/s/7tKIPtjvDxDJm4uFnqGsgQ)
- 参考:[设计模式——抽象工厂模式](https://mp.weixin.qq.com/s/QRpn41l4RIJnLPr6EysHpw)
- 参考:[设计模式——模板方法模式](https://mp.weixin.qq.com/s/wbjRs9pFZ_wXa89-y60nrA)
- 参考:[设计模式——适配器模式](https://mp.weixin.qq.com/s/mznNdNSaJ4K85IA_pMDvYA)
- 参考:[设计模式——装饰器模式](https://mp.weixin.qq.com/s/Xb5cc8wJdyW8-byMWvSu5A)
- 设计模式概念
- 设计模式的七大原则
- 1. 单一职责原则(SRP)
- 思想
- 示例
- 2. 接口隔离原则(ISP)
- 思想
- 示例
- 3. 依赖倒转(置)原则(DIP)
- 思想
- 示例
- 4. 里氏替换原则(LSP)
- 思想
- 示例
- 5. 开闭原则(OCP)
- 思想
- 示例
- 6. 迪米特法则(LoD)
- 思想
- 示例
- 7. 合成复用原则(CRP)
- 思想
- 示例
- 23 种设计模式
参考:设计模式——设计模式理念
参考:设计模式——工厂方法模式
参考:设计模式——抽象工厂模式
参考:设计模式——模板方法模式
参考:设计模式——适配器模式
参考:设计模式——装饰器模式
设计模式概念
设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码高内聚、低耦合以及可重用(复用)性、可扩展(维护)性、可读性、可靠性以及安全性的解决方案。
- 高内聚:模块内部功能紧密相关,职责单一(如
策略模式
中每个策略类只负责一种算法); - 低耦合:模块间依赖最小化(如
观察者模式
解耦发布者和订阅者); - 可重用性:相同功能的代码可重复使用,避免重复造轮子(如
工厂模式
封装对象创建逻辑,多处复用); - 可读性:编程规范性,便于其他程序员的阅读和理解;代码结构符合通用范式(如
单例模式
明确表示全局唯一实例); - 可扩展性:当需要增加新的功能时,非常方便;新增功能时无需修改原有代码(如
装饰器模式
动态添加功能); - 可靠性:当增加新功能后,对原来的功能没有影响;减少意外错误(如
不可变对象模式
避免状态被篡改);
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
设计模式的七大原则
设计模式常用的七大原则(OOP七大原则)有:
- 单一职责原则(SRP)
- 接口隔离原则(ISP)
- 依赖倒转原则(DIP)
- 里氏替换原则(LSP)
- 开闭原则(OCP)
- 迪米特法则(LoD)
- 合成复用原则(CRP)
1. 单一职责原则(SRP)
思想
对类来说的,即一个类应该只负责一项职责;或对方法来说的,保证一个方法尽量做好一件事。如类 A 负责两个不同职责:职责1,职责2。当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为A1,A2。
核心思想:高内聚、职责分离
-
职责的单一性:这里的职责是指类所承担的功能或任务。例如,在一个电商系统中,
OrderService
类负责处理订单相关的业务逻辑,如创建订单、查询订单等,而不应该同时负责用户登录、支付等其他与订单无关的功能。每个职责都应该是明确的、独立的,并且能够被清晰地描述和理解。 -
高内聚性:单一职责原则有助于实现类的高内聚性。内聚性是指类中各个元素(方法、属性等)之间的紧密程度。当一个类只负责一项职责时,其内部的方法和属性都与该职责紧密相关,它们之间的内聚性就高。这样的类更容易理解、维护和扩展,因为所有相关的功能都被集中在一个地方。
-
降低耦合度:如果一个类承担了多个职责,那么这些职责之间可能会存在相互依赖关系,这会导致类与其他类之间的耦合度增加。当其中一个职责发生变化时,可能会影响到其他依赖它的类,从而引发连锁反应,增加了系统的复杂性和维护成本。而遵循单一职责原则,将不同的职责分离到不同的类中,可以降低类之间的耦合度,使得各个类可以独立地变化和扩展,互不影响。
典型应用模式:策略模式、命令模式、外观模式;
好处:控制类的粒度大小、将对象解耦、提高其内聚性。
示例
假设有一个 Employee
类,用于处理员工的相关信息和操作。
-
不遵循单一职责原则,代码可能如下:
public class Employee {private String name;private int age;private String department;// 保存员工信息到数据库public void saveToDatabase() {// 数据库操作代码}// 生成员工报表public void generateReport() {// 报表生成代码}// 发送员工邮件public void sendEmail() {// 邮件发送代码} }
在上述代码中,
Employee
类承担了多个职责,包括保存员工信息到数据库、生成员工报表和发送员工邮件。这违反了单一职责原则,因为这些职责之间并没有直接的关联,而且它们的变化原因也不同。 -
遵循单一职责原则,可以将这些职责分离到不同的类中:
// 员工信息类,只负责存储员工的基本信息 public class EmployeeInfo {private String name;private int age;private String department;// 省略getter和setter方法 }// 员工数据存储类,负责将员工信息保存到数据库 public class EmployeeDatabaseHandler {public void saveToDatabase(EmployeeInfo employeeInfo) {// 数据库操作代码} }// 员工报表生成类,负责生成员工报表 public class EmployeeReportGenerator {public void generateReport(EmployeeInfo employeeInfo) {// 报表生成代码} }// 员工邮件发送类,负责发送员工邮件 public class EmployeeEmailSender {public void sendEmail(EmployeeInfo employeeInfo) {// 邮件发送代码} }
通过将不同的职责分离到不同的类中,每个类都只负责一项职责,遵循了单一职责原则。这样的设计使得代码更加清晰、易于维护和扩展。当需要修改某个职责的实现时,只需要在对应的类中进行修改,而不会影响到其他类。
2. 接口隔离原则(ISP)
思想
用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。即为各个类建立它们需要的专用接口,提高其内聚性。
-
按隔离原则应当这样处理:将一个大而全的接口拆分成多个小的、特定的接口。比如类 A 通过接口 Interface1 依赖类B,类 C 通过接口 Interface1 依赖类D,如果接口 Interface1 对于类 A 和类 C 来说不是最小接口,那么类 B 和类 D 也必须去实现他们不需要的方法;所以将接口 Interface1 拆分为独立的几个接口,类 A 和类 C 分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则;接口 Interface1 中出现的方法,根据实际情况拆分为多个接口代码实现。
典型应用模式:适配器模式;
与单一职责原则类似,将接口隔离,系统地指定一系列规则。
示例
假设有一个 Animal
接口,它包含了动物的各种行为方法
-
不遵循接口隔离原则代码示例,代码可能如下:
Animal
接口包含了动物的各种行为方法interface Animal {void eat();void fly();void swim(); }
现在有一个
Dog
类实现这个接口:public class Dog implements Animal {@Overridepublic void eat() {System.out.println("Dog is eating.");}@Overridepublic void fly() {// 狗不会飞,这个方法没有实际意义throw new UnsupportedOperationException("Dogs can't fly.");}@Overridejavapublic void swim() {System.out.println("Dog is swimming.");} }
在这个例子中,
Dog
类实现了Animal
接口,但fly
方法对于狗来说是不需要的,这就导致Dog
类不得不实现一个没有实际意义的方法,违反了接口隔离原则。 -
遵循接口隔离原则代码示例:
将
Animal
接口拆分成多个小接口:public interface Eatable {void eat(); }public interface Flyable {void fly(); }public interface Swimmable {void swim(); }
现在有一个
Dog
类实现它需要的接口,Bird
类实现它需要的接口:public class Dog implements Eatable, Swimmable {@Overridepublic void eat() {System.out.println("Dog is eating.");}@Overridepublic void swim() {System.out.println("Dog is swimming.");} }public class Bird implements Eatable, Flyable {@Overridepublic void eat() {System.out.println("Bird is eating.");}@Overridepublic void fly() {System.out.println("Bird is flying.");} }
通过将大接口拆分成多个小接口,
Dog
类只需要实现它实际需要的Eatable
和Swimmable
接口,避免了实现不必要的方法。同样,Bird
类只需要实现Eatable
和Flyable
接口。这样的设计更加灵活,符合接口隔离原则。
3. 依赖倒转(置)原则(DIP)
思想
依赖倒转(倒置)的中心思想是面向接口编程;
依赖倒转原则包含两个核心要点:
-
高层模块不应该依赖低层模块,两者都应该依赖抽象:高层模块通常是指负责业务逻辑和整体流程控制的模块,而低层模块则是实现具体功能的细节模块。依赖倒转原则强调,高层模块不应该直接依赖于低层模块的具体实现,而是应该依赖于抽象接口或抽象类。同样,低层模块也应该依赖于抽象,而不是相互依赖具体的实现。
-
抽象不应该依赖细节,细节应该依赖抽象:抽象代表着稳定的、通用的概念和规范,而细节则是具体的实现。该原则要求抽象不应该受到具体实现细节的影响,相反,具体的实现细节应该遵循抽象所定义的规范。
典型应用模式:依赖注入、工厂模式;
抽象指的是接口或抽象类,细节就是具体的实现类。使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。要面向接口编程,不要面向实现编程。
依赖倒转原则的注意事项和细节:
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好;
- 变量的声明类型尽量是抽象类或接口,这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化;
- 继承时遵循里氏替换原则。
示例
假设有一个简单的电商系统,其中有一个 OrderService
类(高层模块)负责处理订单业务,PaymentService
类(低层模块)负责处理支付业务。
-
不遵循依赖倒转原则,代码可能如下:
// 具体的支付服务类 public class PaymentService {public void pay() {System.out.println("使用默认支付方式支付");} }// 订单服务类,直接依赖具体的支付服务,OrderService直接依赖于 PaymentService的具体实现, public class OrderService {private PaymentService paymentService;public OrderService() {this.paymentService = new PaymentService();}public void createOrder() {// 处理订单业务逻辑System.out.println("创建订单");// 调用支付服务paymentService.pay();} }
在这个例子中,
OrderService
直接依赖于PaymentService
的具体实现,当需要添加新的支付方式(如支付宝支付、微信支付)时,就需要修改PaymentService
类和OrderService
类,这违反了依赖倒转原则。 -
遵循依赖倒转原则,可以引入一个抽象的支付接口:
// 抽象的支付接口 public interface Payment {void pay(); }// 具体的支付服务类,实现支付接口 public class DefaultPaymentService implements Payment {@Overridepublic void pay() {System.out.println("使用默认支付方式支付");} }// 订单服务类,依赖抽象的支付接口 public class OrderService {private Payment payment;public OrderService(Payment payment) {this.payment = payment;}public void createOrder() {// 处理订单业务逻辑System.out.println("创建订单");// 调用支付服务payment.pay();} }
通过引入
Payment
接口,OrderService
类依赖于抽象的Payment
接口,而不是具体的PaymentService
类。这样,当需要添加新的支付方式时,只需要实现Payment
接口,然后在创建OrderService
对象时传入相应的实现类即可,不需要修改OrderService
类的代码,提高了系统的可扩展性和可维护性。
4. 里氏替换原则(LSP)
思想
里氏替换原则指出:如果S是T的子类型,那么程序中T类型的对象可以被替换为S类型的对象,而不会对程序的正确性产生任何影响。也就是说,所有引用父类的地方必须能透明地使用其子类的对象,一个可以接受父类对象的地方,也应该能够接受其子类对象,并且程序的行为不会因为将基类对象替换为子类对象而发生改变。里氏替换原则强调了继承关系中子类与父类的行为兼容性,确保子类可以无缝替换父类而不引起问题。
更通俗地说:子类必须能够完全替代其父类,而不影响程序的正确性。
- 典型应用模式:模板方法模式;
核心要点:
- 子类必须完全实现父类的方法:子类是对父类的扩展和细化,因此子类应该实现父类中定义的所有抽象方法和非抽象方法。如果子类没有实现父类的某些方法,那么在使用子类对象替换父类对象时,就可能会导致程序出现错误或异常。
- 实现抽象类或接口的基本要求;
- 子类可以覆盖父类的非抽象方法,但覆盖时需保证不改变父类方法的预期行为,确保使用子类对象替换父类对象时程序的正确性;
- 子类中可以增加自己特有的方法:在满足里氏替换原则的前提下,子类可以添加自己特有的方法和属性,以实现更具体的功能。但这些新增的特性不能影响到子类与父类之间的替换关系,即不能因为子类的特殊行为而破坏了程序中依赖父类的部分的正常运行。
- 这些新增的特性不会影响子类与父类之间的替换关系,因为在使用父类引用指向子类对象时,不会调用到子类特有的方法,只有当进行类型转换后才能使用这些特有的方法;
- 覆盖或实现父类的方法时输入参数可以被放大:里氏替换原则允许子类在覆盖或实现父类方法时,将方法的输入参数类型放宽。这意味着子类方法可以接受更广泛的输入参数,而不会影响到使用父类对象的代码。
- 子类方法的参数类型可以是父类方法参数类型的父类型(即更宽泛的类型);(如父类用
Integer
,子类可以用Number
) - 子类方法可以接受比父类更宽松的参数值范围;(如父类约束入参>0,子类可以放开入参约束>=0)
- 子类方法的参数类型可以是父类方法参数类型的父类型(即更宽泛的类型);(如父类用
- 覆盖或实现父类的方法时输出参数可以被缩小:与输入参数相反,子类在覆盖或实现父类方法时,输出参数的类型应该是父类方法输出参数类型的子类型。这是因为调用者在使用父类对象时,期望得到的是父类方法所声明的返回类型或其子类型的对象。如果子类方法返回的是父类返回类型的超类型对象,那么可能会导致调用者在处理返回结果时出现错误。
- 子类方法返回类型可以是父类方法返回类型的子类型(父类返回
Number
,子类可以返回Integer
); - 子类方法可以承诺比父类更精确的返回值特性(父类返回任意集合,子类返回排序集合);
- 子类方法可以抛出比父类更少的异常或更具体的异常类型;
- 子类方法返回类型可以是父类方法返回类型的子类型(父类返回
示例
1、子类必须完全实现父类的方法
子类要实现父类中定义的所有抽象方法和非抽象方法。若子类未实现父类的某些方法,使用子类对象替换父类对象时,程序可能出错。
-
符合里氏替换原则的示例代码:
// 抽象父类:交通工具 abstract class Vehicle {// 抽象方法:启动public abstract void start(); }// 子类:汽车 class Car extends Vehicle {@Overridepublic void start() {System.out.println("汽车启动");} }// 子类:自行车 class Bicycle extends Vehicle {@Overridepublic void start() {System.out.println("自行车蹬起来启动");} }// 测试类 public class LSPExample1 {public static void main(String[] args) {Vehicle car = new Car();Vehicle bicycle = new Bicycle();startVehicle(car);startVehicle(bicycle);}public static void startVehicle(Vehicle vehicle) {vehicle.start();} }
Vehicle
是抽象父类,定义了抽象方法start
。Car
和Bicycle
子类都实现了该方法。在startVehicle
方法中,可传入Car
或Bicycle
对象,程序正常运行。 -
不符合里氏替换原则的示例代码:
// 抽象父类:交通工具 abstract class Vehicle {// 抽象方法:启动public abstract void start(); }// 子类:汽车 class Car extends Vehicle {// 未实现 start 方法 }// 测试类 public class LSPViolationExample1 {public static void main(String[] args) {Vehicle car = new Car();startVehicle(car);}public static void startVehicle(Vehicle vehicle) {vehicle.start(); // 编译错误,Car 类未实现 start 方法} }
Car
子类没有实现父类Vehicle
的start
方法,当调用startVehicle
方法时,会出现编译错误,无法正常使用子类对象替换父类对象。
2、子类中可以增加自己特有的方法
在满足里氏替换原则的基础上,子类可添加自身特有的方法和属性,但不能影响子类与父类的替换关系。
-
符合里氏替换原则的示例代码:
// 父类:动物 class Animal {public void eat() {System.out.println("动物进食");} }// 子类:猫 class Cat extends Animal {public void meow() {System.out.println("喵喵叫");} }// 测试类 public class LSPExample2 {public static void main(String[] args) {Animal cat = new Cat();cat.eat();if (cat instanceof Cat) {Cat realCat = (Cat) cat;realCat.meow();}} }
Cat
类继承自Animal
类,添加了meow
方法。可将Cat
对象赋值给Animal
类型变量并调用eat
方法,若要调用meow
方法,需进行类型转换。 -
不符合里氏替换原则的示例代码:
// 父类:动物 class Animal {public void eat() {System.out.println("动物进食");} }// 子类:猫 class Cat extends Animal {public void meow() {System.out.println("喵喵叫");}@Overridepublic void eat() {throw new UnsupportedOperationException("猫拒绝进食");} }// 测试类 public class LSPViolationExample2 {public static void main(String[] args) {Animal cat = new Cat();try {cat.eat(); // 调用时抛出异常,破坏了原有行为} catch (UnsupportedOperationException e) {System.out.println("出现异常:" + e.getMessage());}} }
Cat
类重写eat
方法时抛出异常,改变了父类方法的正常行为。当使用Cat
对象替换Animal
对象调用eat
方法时,程序出现异常,破坏了程序的正确性。
3、覆盖或实现父类的方法时输入参数可以被放大
子类在覆盖或实现父类方法时,可放宽方法的输入参数类型,使子类方法能接受更广泛的输入参数,且不影响使用父类对象的代码。
-
符合里氏替换原则的示例代码:
import java.util.ArrayList; import java.util.List;// 父类 class Parent {public void printList(List<Integer> list) {for (Integer num : list) {System.out.println(num);}} }// 子类 class Child extends Parent {public void printList(List<Number> list) {for (Number num : list) {System.out.println(num);}} }// 测试类 public class LSPExample3 {public static void main(String[] args) {Parent parent = new Parent();Parent child = new Child();List<Integer> intList = new ArrayList<>();intList.add(1);intList.add(2);parent.printList(intList);child.printList(intList);} }
父类
Parent
的printList
方法接受List<Integer>
类型参数,子类Child
的printList
方法接受List<Number>
类型参数。由于Integer
是Number
的子类,Child
对象可正常处理List<Integer>
类型参数。 -
不符合里氏替换原则的示例代码:
import java.util.ArrayList; import java.util.List;// 父类 class Parent {public void printList(List<Number> list) {for (Number num : list) {System.out.println(num);}} }// 子类 class Child extends Parent {public void printList(List<Integer> list) {for (Integer num : list) {System.out.println(num);}} }// 测试类 public class LSPViolationExample3 {public static void main(String[] args) {Parent parent = new Parent();Parent child = new Child();List<Number> numberList = new ArrayList<>();numberList.add(1.0);numberList.add(2.0);parent.printList(numberList);// child.printList(numberList); 编译错误,Child 类的 printList 方法不能接受 List<Number> 类型参数} }
子类
Child
的printList
方法输入参数类型范围比父类小,当使用Child
对象替换Parent
对象处理List<Number>
类型参数时,会出现编译错误。
4、覆盖或实现父类的方法时输出参数可以被缩小
子类在覆盖或实现父类方法时,输出参数的类型应是父类方法输出参数类型的子类型。调用者使用父类对象时,期望得到父类方法声明的返回类型或其子类型的对象。
-
符合里氏替换原则的示例代码:
// 父类 class SuperClass {public Number getNumber() {return 1;} }// 子类 class SubClass extends SuperClass {@Overridepublic Integer getNumber() {return 2;} }// 测试类 public class LSPExample4 {public static void main(String[] args) {SuperClass superClass = new SuperClass();SuperClass subClass = new SubClass();Number num1 = superClass.getNumber();Number num2 = subClass.getNumber();System.out.println(num1);System.out.println(num2);} }
父类
SuperClass
的getNumber
方法返回Number
类型,子类SubClass
的getNumber
方法返回Integer
类型,Integer
是Number
的子类。SubClass
对象可正常赋值给SuperClass
类型变量并调用getNumber
方法。 -
不符合里氏替换原则的示例代码:
// 父类 class SuperClass {public Integer getNumber() {return 1;} }// 子类 class SubClass extends SuperClass {@Overridepublic Number getNumber() {return 2.0;} }// 测试类 public class LSPViolationExample4 {public static void main(String[] args) {SuperClass superClass = new SuperClass();SuperClass subClass = new SubClass();Integer num1 = superClass.getNumber();// Integer num2 = subClass.getNumber(); 编译错误,无法将 Number 类型赋值给 Integer 类型} }
子类
SubClass
的getNumber
方法返回类型是Number
,比父类的返回类型范围大。当使用SubClass
对象替换SuperClass
对象时,将返回值赋值给Integer
类型变量会出现编译错误。
5. 开闭原则(OCP)
思想
对扩展开放,对修改关闭;
解释:扩展原来程序,但尽量不修改原来的程序,即通过扩展(而非修改)增加新功能;
-
核心思想:通过抽象和继承实现扩展性。开闭原则的核心在于通过抽象和封装,将软件系统中相对稳定的部分和容易变化的部分分离。稳定的部分作为抽象层,定义了系统的基本结构和行为规范;容易变化的部分则通过具体的实现类来体现,当需求发生变化时,只需要添加新的实现类,而不需要修改抽象层和其他已有的实现类。
典型应用模式:装饰器模式、适配器模式、策略模式、模板方法模式;
示例
以一个简单的图形绘制为例,说明开闭原则的应用。
-
不遵循开闭原则的设计,代码可能如下:
// 图形类 class Shape {String type;public Shape(String type) {this.type = type;} }// 图形绘制类 class Drawing {public void drawShape(Shape shape) {if ("circle".equals(shape.type)) {drawCircle();} else if ("rectangle".equals(shape.type)) {drawRectangle();}}private void drawCircle() {System.out.println("绘制圆形");}private void drawRectangle() {System.out.println("绘制矩形");} }
在这个设计中,如果需要添加新的图形(如三角形),就需要修改
Drawing
类的drawShape
方法,添加新的if-else
分支,这违反了开闭原则。 -
遵循开闭原则的设计
// 抽象图形类 abstract class Shape {public abstract void draw(); }// 圆形类 class Circle extends Shape {@Overridepublic void draw() {System.out.println("绘制圆形");} }// 矩形类 class Rectangle extends Shape {@Overridepublic void draw() {System.out.println("绘制矩形");} }// 图形绘制类 class Drawing {public void drawShape(Shape shape) {shape.draw();} }
在这个设计中,
Shape
是抽象类,定义了抽象方法draw
。Circle
和Rectangle
是具体的图形类,实现了draw
方法。Drawing
类的drawShape
方法通过调用Shape
对象的draw
方法来绘制图形。当需要添加新的图形(如三角形)时,只需要创建一个新的类继承自Shape
,并实现draw
方法,而不需要修改Drawing
类的代码,符合开闭原则。
6. 迪米特法则(LoD)
思想
一个对象应尽可能少地了解其他对象,具体来说,一个类对于其他类知道得越少越好,尽量降低类与类之间的耦合;一个类应该只和它的直接朋友通信,而避免和陌生的类直接通信(不要和"陌生人"说话、不要直接操作"朋友的朋友"、不要暴露内部结构给外部)
"直接朋友"包括:
- 当前对象本身(
this
):对象自身的属性和方法可以直接访问。 - 以参数形式传入到当前对象方法中的对象:在方法内部可以直接使用这些参数对象。
- 当前对象的成员变量(属性):如果当前对象包含其他对象作为成员变量,那么这些成员对象也是朋友;
- 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友。
- 当前对象的方法所创建或实例化的对象:通过
new
关键字创建的对象,可在当前对象中直接使用。
典型应用模式:外观模式、中介者模式;
示例
假设有一个学校管理系统,包含 School
类、Teacher
类和 Student
类。School
类需要统计所有学生的数量。
-
不遵循迪米特法则的设计,代码可能如下:
// 学生类 class Student {// 学生相关属性和方法 }// 教师类 class Teacher {private Student[] students;public Teacher(Student[] students) {this.students = students;}public Student[] getStudents() {return students;} }// 学校类 class School {private Teacher[] teachers;public School(Teacher[] teachers) {this.teachers = teachers;}public int getTotalStudents() {int total = 0;for (Teacher teacher : teachers) {Student[] students = teacher.getStudents();total += students.length;}return total;} }
在这个设计中,
School
类通过Teacher
类获取了Student
类的信息,这使得School
类与Student
类之间产生了不必要的交互,违反了迪米特法则。School
类知道了太多关于Student
类的信息,增加了类之间的耦合度。 -
遵循迪米特法则的设计:
// 学生类 class Student {// 学生相关属性和方法 }// 教师类 class Teacher {private Student[] students;public Teacher(Student[] students) {this.students = students;}public int getStudentCount() {return students.length;} }// 学校类 class School {private Teacher[] teachers;public School(Teacher[] teachers) {this.teachers = teachers;}public int getTotalStudents() {int total = 0;for (Teacher teacher : teachers) {total += teacher.getStudentCount();}return total;} }
在这个设计中,
School
类只与Teacher
类进行交互,通过调用Teacher
类的getStudentCount
方法来获取学生数量,而不需要了解Student
类的具体信息。这样,School
类对其他类的了解最少,遵循了迪米特法则,降低了类之间的耦合度。
7. 合成复用原则(CRP)
思想
优先使用对象组合或者聚合等关联关系,其次才考虑使用继承关系来达到复用目的。简单来说,就是在一个新的对象里通过关联关系(组合、聚合)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用功能的目的,而不是通过继承父类来获得已有的功能。
典型应用模式:装饰器模式、桥接模式;
组合与聚合
- 组合:是一种强 “拥有” 关系,体现了严格的部分和整体的关系,部分和整体的生命周期是一致的。例如,汽车和发动机的关系,发动机是汽车的一部分,没有汽车,发动机通常没有独立的意义,并且发动机的生命周期和汽车的生命周期紧密相关。
- 聚合:是一种弱 “拥有” 关系,体现的是 A 对象可以包含 B 对象,但 B 对象不是 A 对象的一部分。比如,公司和员工的关系,员工是公司的一部分,但员工可以独立于公司存在,有自己独立的生命周期。
示例
假设要设计一个学生课程管理系统。
-
不遵循合成复用原则(使用继承来实现复用),代码可能如下:
// 课程类 class Course {private String courseName;private String teacher;public Course(String courseName, String teacher) {this.courseName = courseName;this.teacher = teacher;}public void showCourseInfo() {System.out.println("课程名: " + courseName + ", 授课教师: " + teacher);} }// 学生选课类,继承自课程类 class StudentCourse extends Course {private String studentName;public StudentCourse(String courseName, String teacher, String studentName) {super(courseName, teacher);this.studentName = studentName;}public void showStudentInfo() {System.out.println("选课学生: " + studentName);} }
在这个设计中,
StudentCourse
类继承了Course
类。然而,继承是一种强耦合关系。要是Course
类发生改变,例如添加或修改方法,可能会对StudentCourse
类产生影响。而且,从逻辑上来说,学生选课并非是课程的一种特殊形式,这种继承关系在语义上不太合适。 -
遵循合成复用原则(使用组合):
// 课程类 class Course {private String courseName;private String teacher;public Course(String courseName, String teacher) {this.courseName = courseName;this.teacher = teacher;}public void showCourseInfo() {System.out.println("课程名: " + courseName + ", 授课教师: " + teacher);} }// 学生类 class Student {private String studentName;private Course[] selectedCourses;public Student(String studentName, Course[] selectedCourses) {this.studentName = studentName;this.selectedCourses = selectedCourses;}public void showStudentAndCourses() {System.out.println("学生姓名: " + studentName);for (Course course : selectedCourses) {course.showCourseInfo();}} }
在这个设计里,
Student
类通过组合的方式持有Course
对象的引用。Student
类和Course
类是松耦合关系,当Course
类的实现发生变化时,只要其接口(如showCourseInfo
方法)保持不变,就不会对Student
类产生影响。同时,这种设计更符合实际逻辑,学生可以选择多门课程,并且能灵活地对课程进行管理。
23 种设计模式
23中设计模式:(GoF23)
-
创建型模式:(5种)跟创建对象有关
单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式;
-
结构型模式:(7种)
适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式;
-
行为型模式:(11种)
模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器
模式、状态模式、策略模式、责任链模式、访问者模式;
相关文章:
设计模式——设计模式理念
文章目录 参考:[设计模式——设计模式理念](https://mp.weixin.qq.com/s/IEduZFF6SaeAthWFFV6zKQ)参考:[设计模式——工厂方法模式](https://mp.weixin.qq.com/s/7tKIPtjvDxDJm4uFnqGsgQ)参考:[设计模式——抽象工厂模式](https://mp.weixin.…...
解析 ID 数组传参的解决方案:基于 Axios 的实现
解析 ID 数组传参的解决方案:基于 Axios 的实现 在实际开发中,经常需要将一个 ID 数组作为参数传递给后端接口。然而,不同的后端框架和前端库对数组参数的处理方式可能有所不同。通过一个具体的例子,在前端使用 Axios 框架发送 I…...
C语言快速入门-C语言基础知识
这个c语言入门,目标人群是有代码基础的,例如你之前学过javaSE,看此文章可能是更有帮助,会让你快速掌握他们之间的差异,文章内容大部分都是泛谈,详细的部分我会在之后时间发布,我也在慢慢学习&am…...
Ubuntu 22.04 上安装 VS Code
在 Ubuntu 22.04 上安装 VS Code 的方法如下: 方法 1:通过 APT 包管理器安装 更新系统包索引: 打开终端并执行以下命令: sudo apt update安装依赖项: 执行以下命令以安装所需的依赖项: sudo apt install s…...
AI人工智能-PyCharm的介绍安装应用
下载与安装 创建python项目 项目路径:C:\Users\miloq\Desktop\python_project 配置环境 提前找到conda配置的python-base路径 配置conda环境 运行项目 运行结果...
Todesk介绍
文章目录 ToDesk 软件介绍1. 软件概述2. ToDesk 的功能特点2.1 简单易用2.2 高质量的图像与流畅的操作2.3 跨平台支持2.4 多屏显示与协作2.5 文件传输功能2.6 实时聊天与语音通话2.7 远程唤醒与自动启动2.8 多种权限设置与安全性2.9 无需公网 IP 3. ToDesk 的应用场景3.1 个人使…...
【JavaEE】springMVC返回Http响应
目录 一、返回页面二、Controller和ResponseBody与RestController区别三、返回HTML代码⽚段四、返回JSON五、HttpServletResponse设置状态码六、设置Header6.1 HttpServletResponse设置6.2 RequestMapping设置 一、返回页面 步骤如下: 我们先要在static目录下创建…...
青少年编程与数学 02-011 MySQL数据库应用 02课题、MySQL数据库安装
青少年编程与数学 02-011 MySQL数据库应用 02课题、MySQL数据库安装 一、安装Windows系统Linux系统(以Ubuntu 20.04为例)macOS系统 二、配置(一)Windows系统1. 创建配置文件2. 初始化数据库3. 启动MySQL服务4. 登录MySQL5. 修改ro…...
springboot441-基于SpringBoot的校园自助交易系统(源码+数据库+纯前后端分离+部署讲解等)
💕💕作者: 爱笑学姐 💕💕个人简介:十年Java,Python美女程序员一枚,精通计算机专业前后端各类框架。 💕💕各类成品Java毕设 。javaweb,ssm…...
【安全运营】关于攻击面管理相关概念的梳理(一)
目录 一、ASM 介绍ASM 是“Attack Surface Management”(攻击面管理)的缩写【框架视角,广义概念】1. 介绍2. 兴起的原因3. 工作流程3.1 资产发现3.2 分类和优先级排序3.3 修复3.4 监控 二、EASM 介绍EASM 是 "External Attack Surface M…...
IPv6 网络访问异常 | 时好时坏 / 部分访问正常
注:本文为 “ IPv6 间接性连接异常” 相关文章合辑。 略作重排,未去重。 如有内容异常,请看原文。 IPv6 间接性连接异常?尝试调整路由器的 MTU 设置 Nero978 2024-1-29 17:54 背景 2024 年 1 月 29 日,因寒假返家…...
Unity编辑器功能及拓展(1) —特殊的Editor文件夹
Unity中的Editor文件夹是一个具有特殊用途的目录,主要用于存放与编辑器扩展功能相关的脚本和资源。 一.纠缠不清的UnityEditor 我们Unity中进行游戏构建时,我们经常遇到关于UnityEditor相关命名空间丢失的报错,这时候,只得将报错…...
LLMs之PE:《Tracing the thoughts of a large language model》翻译与解读
LLMs之PE:《Tracing the thoughts of a large language model》翻译与解读 导读:这篇论文的核心贡献在于提出了一种新颖的、基于提示工程的LLMs推理过程追踪技术——“Tracing Thoughts”。该技术通过精心设计的提示,引导LLMs生成其推理过程的…...
[Python] 贪心算法简单版
贪心算法-简单版 贪心算法的一般使用场景是给定一个列表ls, 让你在使用最少的数据的情况下达到或超过n. 我们就来使用上面讲到的这个朴素的例题来讲讲贪心算法的基本模板: 2-1.排序 既然要用最少的数据, 我们就要优先用大的数据拼, 为了实现这个效果, 我们得先给列表从大到小…...
游戏引擎学习第191天
回顾并制定今天的计划 最近几天,我们有一些偏离了原计划的方向,主要是开始了一些调试代码的工作。最初我们计划进行一些调试功能的添加,但是随着工作的深入,我们开始清理和整理调试界面的呈现方式,以便能够做一些更复…...
Git撤回操作全场景指南:未推送与已推送,保留和不保留修改的差异处理
一、未推送到远程仓库的提交(仅本地存在) 特点:可直接修改本地提交历史,不会影响他人 1. 保留修改重新提交 git reset --soft HEAD~1 # 操作效果: # - 撤销最后一次提交 # - 保留工作区所有修改 # - 暂存区内容保持…...
Java 贪吃蛇游戏
这段 Java 代码实现了一个经典的贪吃蛇游戏。玩家可以使用键盘的上下左右箭头键控制蛇的移动方向,蛇会在游戏面板中移动并尝试吃掉随机生成的食物。每吃掉一个食物,蛇的身体会变长,玩家的得分也会增加。如果蛇撞到自己的身体或者撞到游戏面板…...
QT图片轮播器(QT实操学习2)
1.项目架构 1.UI界面 2.widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget>#define TIMEOUT 1 * 1000 QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent n…...
mac m1/m2/m3 pyaudio的安装
google了很多方法,也尝试了 issue68的方法, 但是均失败了,但是问deepseek竟然成功了,下面是deepseek r1给出的方法。在M3 pro芯片上可以成功运行. 安装homebrew /bin/bash -c "$(curl -fsSL https://raw.githubusercontent…...
Appium中元素定位的注意点
应用场景 了解这些注意点可以以后在出错误的时候,更快速的定位问题原因。 示例 使用 find_element_by_xx 或 find_elements_by_xx 的方法,分别传入一个没有的“特征“会是什么结果呢? 核心代码 driver.find_element_by_id("xxx") drive…...
《深入探索 Python 数据分析:用 Pandas 高效处理与可视化大型数据集》
《深入探索 Python 数据分析:用 Pandas 高效处理与可视化大型数据集》 引言:从零到分析高手 数据是当代社会最宝贵的资源,而数据分析技能是现代职业人不可或缺的一部分。在数据科学的领域中,Python 已成为当之无愧的“首选语言”,其强大的生态系统和简洁的语法让人如虎添…...
[GWCTF 2019]我有一个数据库1 [CVE phpMyAdmin漏洞]
扫出来一些东西 访问/phpmyadmin 发现界面 这里用到了CVE-2018-12613,光速学习 出现漏洞的代码是: $target_blacklist array (import.php, export.php );// If we have a valid target, lets load that script instead if (! empty($_REQUEST[targe…...
spring 常用注解区别及使用场景
1. 组件注册注解 Bean 作用:用于方法上,表示该方法返回的对象由Spring容器管理。通常用于配置类(Configuration)中,注册第三方库或自定义的Bean。 使用场合: 当你需要将非Spring管理的类(如第…...
【后端】【Django】信号使用详解
Django post_save 信号使用详解(循序渐进) 一、信号的基本概念 Django 的 信号(Signal) 允许不同部分的代码在发生某些事件时进行通信,而不需要直接调用。这种机制可以解耦代码,让不同的模块独立工作。 …...
ML算法数学概念
交叉熵损失(Cross-Entropy Loss) 是机器学习和深度学习中常用的一种损失函数,主要用于衡量两个概率分布之间的差异。它在分类问题中(尤其是多分类问题)被广泛使用,因为它能够有效地评估模型预测的概率分布与…...
wps 怎么显示隐藏文字
wps 怎么显示隐藏文字 》文件》选项》视图》勾选“隐藏文字” wps怎么设置隐藏文字 wps怎么设置隐藏文字...
Vue3 其它API Teleport 传送门
Vue3 其它API Teleport 传送门 在定义一个模态框时,父组件的filter属性会影响子组件的position属性,导致模态框定位错误使用Teleport解决这个问题把模态框代码传送到body标签下...
亚马逊玩具品类技术驱动型选品策略:从趋势洞察到合规基建
一、全球玩具电商技术演进趋势 (技术化重构原市场背景) 数据可视化分析:通过亚马逊SP-API抓取2023年玩具品类GMV分布热力图 监管技术升级: 美国CPSC启用AI质检系统(缺陷识别准确率92.7%) 欧盟EPR合规接口…...
SpringBoot3+EasyExcel通过WriteHandler动态实现表头重命名
方案简介 为了通过 EasyExcel 实现动态表头重命名,可以封装一个方法,传入动态的新表头名称列表(List<String>),并结合 WriteHandler 接口来重命名表头。同时,通过 EasyExcel 将数据直接写入到输出流…...
PHY——LAN8720A 寄存器读写 (二)
文章目录 PHY——LAN8720A 寄存器读写 (二)工程配置引脚初始化代码以太网初始化代码PHY 接口实现LAN8720 接口实现PHY 接口测试 PHY——LAN8720A 寄存器读写 (二) 工程配置 这里以野火电子的 F429 开发板为例,配置以太网外设 这里有一点需要注意原理图 RMII_TXD0…...
HTML5和CSS3的一些特性
HTML5 和 CSS3 是现代网页设计的基础技术,它们引入了许多新特性和功能,极大地丰富了网页的表现力和交互能力。 HTML5 的一些重要特性包括: 新的语义化标签: HTML5 引入了一些重要的语义化标签如 <header>, <footer>, <articl…...
sass报错,忽略 Sass 弃用警告,降级版本
最有效的方法是创建一个 .sassrc.json 文件来配置 Sass 编译器。告诉 Sass 编译器忽略来自依赖项的警告消息。 解决方案: 1. 在项目根目录创建 .sassrc.json 文件: {"quietDeps": true }这个配置会让 Sass 编译器忽略所有来自依赖项&#x…...
DeepSeek+Kimi:PPT制作的效率革命
摘要:传统PPT制作面临模板选择困难、内容逻辑混乱、设计排版能力有限以及反复修改等问题。DeepSeek和Kimi两款AI工具的组合为PPT制作提供了全新的解决方案。DeepSeek擅长内容生成与逻辑推理,能够快速生成高质量的PPT大纲和内容;Kimi则专注于长…...
transformers中学习率warmup策略具体如何设置
在使用 get_linear_schedule_with_warmup(如 Hugging Face Transformers 库中的学习率调度器)时,参数的合理设置需要结合 数据量(dataset size)、批次大小(batch size) 和 训练轮数(…...
linux实现rsync+sersync实时数据备份
1.概述 rsync(Remote Sync) 是一个Unix/linux系统下的文件同步和传输工具 2.端口和运行模式 tcp/873 采用C/S模式(客户端/服务器模式) 3.特点 可以镜像保存整个目录和文件第一次全量备份(备份全部的文件),之后是增量备份(只备份变化的文件) 4. 数…...
CTF类题目复现总结-[MRCTF2020]ezmisc 1
一、题目地址 https://buuoj.cn/challenges#[MRCTF2020]ezmisc二、复现步骤 1、下载附件,得到一张图片; 2、利用010 Editor打开图片,提示CRC值校验错误,flag.png应该是宽和高被修改了,导致flag被隐藏掉;…...
『Linux』 第十一章 线程同步与互斥
1. 线程互斥 1.1 进程线程间的互斥相关背景概念 临界资源:多线程执行流共享的资源就叫做临界资源临界区:每个线程内部,访问临界资源的代码,就叫做临界区互斥:任何时刻,互斥保证有且只有一个执行流进入临界…...
【数据结构】队列
目录 一、队列1、概念与结构2、队列的实现3、队列的初始化4、打印队列数据5、入队6、销毁队列7、队列判空8、出队9、取队头、队尾数据10、队列中有效元素个数 二、源码 个人主页,点击这里~ 数据结构专栏,点击这里~ 一、队列 1、概念与结构 概念&#x…...
【导航定位】GNSS数据协议-RINEX OBS
RINEX协议 RINEX(Receiver INdependent EXchange format,与接收机无关的交换格式)是一种在GPS测量应用中普遍采用的标准数据格式,该格式采用文本文件形式(ASCII码)存储数据数据记录格式与接收机的制造厂商和具体型号无关。目前RINEX版本已经发布到了4.x…...
Qt中的事件循环
Qt的事件循环是其核心机制之一,它是一种消息处理机制,负责处理各种事件(如用户输入、定时器、网络请求等)的分发和处理。Qt中的事件循环是一个持续运行的循环,负责接收事件并将它们分发给相应的对象进行处理。当没有事件需要处理时࿰…...
Android并发编程:线程池与协程的核心区别与最佳实践指南
1. 基本概念对比 特性 线程池 (ThreadPool) 协程 (Coroutine) 本质 Java线程管理机制 Kotlin轻量级并发框架 最小执行单元 线程(Thread) 协程(Coroutine) 创建开销 较高(需分配系统线程资源) 极低(用户态调度) 并发模型 基于线程的抢占式调度 基于协程的协作式调度 2. 核心差异…...
吴恩达深度学习复盘(2)神经网络的基本原理轮廓
笔者注 这两节课主要介绍了神经网络的大的轮廓。而神经网络基本上是在模拟人类大脑的工作模式,有些仿生学的意味。为了便于理解,搜集了一些脑神经的资料,这部分是课程中没有讲到的。 首先要了解一下大脑神经元之间结构。 细胞体࿱…...
【redis】集群 数据分片算法:哈希求余、一致性哈希、哈希槽分区算法
文章目录 什么是集群数据分片算法哈希求余分片搬运 一致性哈希扩容 哈希槽分区算法扩容相关问题 什么是集群 广义的集群,只要你是多个机器,构成了分布式系统,都可以称为是一个“集群” 前面的“主从结构”和“哨兵模式”可以称为是“广义的…...
计算机组成原理笔记(六)——2.2机器数的定点表示和浮点表示
计算机在进行算术运算时,需要指出小数点的位置,根据小数点的位置是否固定,在计算机中有两种数据格式:定点表示和浮点表示。 2.2.1定点表示法 一、基本概念 定点表示法是一种小数点的位置固定不变的数据表示方式,用于表示整数或…...
将树莓派5当做Ollama服务器,C#调用generate的API的示例
其实完全没这个必要,性能用脚后跟想都会很差。但基于上一篇文章的成果,来都来了就先简单试试吧。 先来看看这个拼夕夕上五百多块钱能达到的效果: 只要对速度没要求,那感觉就还行。 Ollama默认只在本地回环(127.0.0…...
MYSQL数据库(一)
一.数据库的操作 1.显示数据库 show databases; 2.创建数据库 create database 数据库名; 3.使用数据库 use 数据库名; 4.删除数据库 drop database 数据库名; drop database if exists 数据库名; 二.表的操作 1.显示所有表 show tables; 2.查看表结构 des…...
Python Cookbook-4.15 字典的一键多值
任务 需要一个字典,能够将每个键映射到多个值上。 解决方案 正常情况下,字典是一对一映射的,但要实现一对多映射也不难,换句话说,即一个键对应多个值。你有两个可选方案,但具体要看你怎么看待键的多个对…...
IDEA 终端 vs CMD:为什么 java -version 显示的 JDK 版本不一致?
前言:离谱的 JDK 版本问题 今天遇到了一个让人抓狂的现象:在 Windows 的 CMD 里输入 java -version 和在 IntelliJ IDEA 终端输入 java -version,居然显示了不同的 JDK 版本! 本以为是环境变量、缓存或者 IDEA 设置的问题&#x…...
Flask登录页面后点击按钮在远程CentOS上自动执行一条命令
templates文件夹和app.py在同一目录下。 templates文件夹下包括2个文件:index.html login.html app.py代码如下: import os import time from flask import Flask, render_template, request, redirect, session, make_response import mysql.con…...
深度解析:文件夹变白色文件的数据恢复之道
在数字化时代,数据的重要性不言而喻。然而,当我们在使用计算机时,偶尔会遇到一些棘手的问题,其中“文件夹变白色文件”便是一个令人困惑且亟待解决的难题。这一现象不仅影响了文件的正常访问,更可能隐藏着数据丢失的风…...