设计模式——设计模式理念
文章目录
- 参考:[设计模式——设计模式理念](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.…...
Kubernetes对象基础操作
基础操作 文章目录 基础操作一、创建Kubernetes对象1.使用指令式命令创建Deployment2.使用指令式对象配置创建Deployment3.使用声明式对象配置创建Deployment 二、操作对象的标签1.为对象添加标签2.修改对象的标签3.删除对象标签4.操作具有指定标签的对象 三、操作名称空间四、…...
Java与代码审计-Java基础语法
Java基础语法 package com.woniuxy.basic;public class HelloWorld {//入口函数public static void main(String[] args){System.out.println("Hello World");for(int i0;i< args.length;i){System.out.println(args[i]);}} }运行结果如下: 但是下面那个没有参数…...
Xenium | 细胞邻域(Cellular Neighborhood)分析(fixed radius)
上节我们介绍了空间转录组数据分析中常见的细胞邻域分析,CN计算过程中定义是否为细胞邻居的方法有两种,一种是上节我们使用固定K最近邻方法(fixed k-nearest neighbors)定义细胞Neighborhood,今天我们介绍另外一种固定半径范围内(fixed radiu…...
Python:爬虫概念与分类
网络请求: https://www.baidu.com url——统一资源定位符 请求过程: 客户端,指web浏览器向服务器发送请求 请求:请求网址(request url);请求方法(request methods);请求头(request header)&…...
[Linux实战] Linux设备树原理与应用详解
Linux设备树原理与应用详解 一、设备树概述 1.1 什么是设备树 设备树(Device Tree,简称DT)是一种描述硬件资源的数据结构,它通过一种树状结构来描述系统硬件配置,包括CPU、内存、总线、外设等硬件信息。设备树最初在…...
用Nginx实现负载均衡与高可用架构(整合Keepalived)
前言 在分布式架构中,负载均衡和高可用是保障系统稳定性的两大核心能力。本文将深入讲解如何通过Nginx实现七层负载均衡,并结合Keepalived构建无单点故障的高可用架构。文末附完整配置模板! 一、Nginx负载均衡实现方案 1. 核心原理 Nginx通…...
SQLMesh调度系统深度解析:内置调度与Airflow集成实践
本文系统解析SQLMesh的两种核心调度方案:内置调度器与Apache Airflow集成。通过对比两者的适用场景、架构设计和操作流程,为企业构建可靠的数据分析流水线提供技术参考。重点内容包括: 内置调度器的轻量级部署与性能优化策略Airflow集成的端到…...
Excel 中 INDEX 和 VLOOKUP 的对比
INDEX 和 VLOOKUP 都是 Excel 中常用的查找函数,但它们的用途和灵活性有所不同。 1. 相同点 均可用于查找数据:都能根据某个条件返回目标值。 支持精确匹配:均可使用 0 或 FALSE 进行精确匹配。 2. 不同点 特性VLOOKUPINDEX MATCH查找方向…...
Multism TL494仿真异常
仿真模型如下:开关频率少了一半,而且带不动负载,有兄弟知道为什么吗 这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码…...
基于 Trae 的超轻量级前端架构设计与性能优化实践
一、技术背景与选型动因 在单页应用(SPA)复杂度指数级增长的今天,传统框架在千级列表渲染场景下普遍存在首屏延迟(>1.5s)、内存占用过高(>200MB)等问题。基于对 Webpack Bundle Analyzer 的长期观察,我们发现核心问题集中在: • 类组件…...
算法练习(队列)
队列 单向队列 1. 定义一个队列 Queue<Integer> q new LinkedList<>(); Queue<Character> q new LinkedList<>();2. 入队列 q.offer(1); q.offer(2); // 从队尾入队列 q.add();3. 出队列 q.poll() // 从队头出队列,并将删除的元素…...
HarmonyOS NEXT开发进阶(十五):日志打印 hilog 与 console.log 的区别
文章目录 一、前言二、两者区别对比三、HiLog 详解四、拓展阅读 一、前言 在日常开发阶段,日志打印是调试程序非常常用的操作,在鸿蒙的官方文档中介绍了hilog这种方式,前端转过来的开发者发现console.log也可以进行日志打印,而且…...
【差分隐私相关概念】差分隐私中的稀疏向量技术
差分隐私中的稀疏向量技术(Sparse Vector Technique, SVT) 稀疏向量技术(SVT)是差分隐私中的一种高效机制,专用于处理稀疏高影响查询的场景。其核心思想是:当面对大量查询时,仅对其中“显著超过…...
快速幂算法还有用吗?——从内置函数到高性能计算的深度解析
博主在学习过程中遇到了一个疑问,既然C语言中有内置函数pow,那为什么还需要算法思想中的快速幂算法呢?下面将会讲解快速幂算法在特定场景下依然非常有用,具体原因如下: 目录 1. 精度与整数运算 2. 性能对比 3. 应用场…...
开源测试用例管理平台
不可错过的10个开源测试用例管理平台: PingCode、TestLink、Kiwi TCMS、Squash TM、FitNesse、Tuleap、Robot Framework、SpecFlow、TestMaster、Nitrate。 开源测试用例管理工具提供了一种透明、灵活的解决方案,使团队能够在不受限的情况下适应具体的测…...
vue 权限应用
目录 一、系统菜单栏权限 二、系统页面按钮权限 在企业开发中,不同的用户所扮演的角色不一样,角色拥有权限,所以用户拥有角色,就会有角色对应的权限。例如,张三是系统管理员角色,登录后就拥有整个系统的…...
鸿蒙HarmonyOS NEXT设备升级应用数据迁移流程
数据迁移是什么 什么是数据迁移,对用户来讲就是本地数据的迁移,终端设备从HarmonyOS 3.1 Release API 9及之前版本(单框架)迁移到HarmonyOS NEXT(双框架)后保证本地数据不丢失。例如,我在某APP…...
利用 PCI-Express 交换机实现面向未来的推理服务器
在数据中心系统的历史上,没有比被 Nvidia 选为其 AI 系统的组件供应商更高的赞誉了。 这就是为什么新兴的互连芯片制造商 Astera Labs 感到十分高兴,因为该公司正在 PCI-Express 交换机、PCI-Express 重定时器和 CXL 内存控制器方面与 Broadcom 和 Marv…...
Python调用手机摄像头检测火焰烟雾的三种方法
方法1:使用IP摄像头应用 OpenCV 1. 在手机上安装IP摄像头应用(如IP Webcam for Android) 2. 配置应用并启动服务器 3. 在Python中使用OpenCV连接 import cv2 import numpy as np # 手机IP摄像头URL(替换为你的手机IP和端口…...
Python if else while for 学习笔记
一.if,else if语句用于根据条件执行代码块 else语句可与if语句结合,当if判断为假时执行else语句 x10 if x>5:print("x大于5") y3 if y>5:print("y大于5") else:print("y小于等于5")结果: 二.while循环…...
正则化是什么?
正则化(Regularization)是机器学习中用于防止模型过拟合(Overfitting)的一种技术,通过在模型训练过程中引入额外的约束或惩罚项,降低模型的复杂度,从而提高其泛化能力(即在未见数据上…...
搜索-BFS
马上蓝桥杯了,最近刷了广搜,感觉挺有意思的,广搜题类型都差不多,模板也一样,大家写的时候可以直接套模板 这里给大家讲一个比较经典的广搜题-迷宫 题目问问能否走到 (n,m) 位置,假设最后一个点是我们的&…...
《边缘计算风云录:FPGA与MCU的算力之争》
点击下面图片带您领略全新的嵌入式学习路线 🔥爆款热榜 88万阅读 1.6万收藏 文章目录 **第一章:边城烽烟——数据洪流压境****第二章:寒铁剑匣——FPGA的千机变****第三章:枯木禅杖——MCU的至简道****第四章:双生契…...
R-GCN-Modeling Relational Data with GraphConvolutional Networks(论文笔记)
CCF等级:B 发布时间:2018年6月 25年3月31日交 目录 一、简介 二、原理 1.整体 2.信息交换与更新 2.1基分解 2.2块对角矩阵 3.实体分类或链接预测 3.1实体分类 3.2链接预测 三、结论和未来工作 一、简介 RGCN通过允许不同关系类型之间的信息…...
蓝桥杯第十六届模拟赛——基础细节考频分析
文章目录 前言一、STL函数二、日期问题三、质数与约数四、基本常识总结 前言 一、STL函数 #include< cmath > 详解floor函数、ceil函数和round函数 1.floor() 功能:把一个小数向下取整如果数是2.2 ,那向下取整的结果就为2.000000如果数是-2.2 &…...
【C++初阶】----模板初阶
1.泛型函数 泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。 2.函数模板 2.1函数模板的概念 函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型…...
PyCharm操作基础指南
一、安装与配置 1. 版本选择 专业版:支持 Web 开发(Django/Flask)、数据库工具、科学计算等(需付费)。 社区版:免费,适合纯 Python 开发。 2. 安装步骤 访问 JetBrains 官网 下载对应版本。…...
Pycharm(七):几个简单案例
一.剪刀石头布 需求:和电脑玩剪刀石头布游戏 考察点:1.随机数;2.判断语句 import random # numrandom.randint(1,3) # print(num) # print(**30) #1.录入玩家手势 playerint(input(请输入手势:(1.剪刀 2.石头 3&…...
Android并发编程:线程池与协程的核心区别与最佳实践指南
1. 基本概念对比 特性 线程池 (ThreadPool) 协程 (Coroutine) 本质 Java线程管理机制 Kotlin轻量级并发框架 最小执行单元 线程(Thread) 协程(Coroutine) 创建开销 较高(需分配系统线程资源) 极低(用户态调度) 并发模型 基于线程的抢占式调度 基于协程的协作式调度 2. 核心差异…...
