设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码高内聚、低耦合以及可重用(复用)性、可扩展(维护)性、可读性、可靠性以及安全性的解决方案。
策略模式
中每个策略类只负责一种算法);观察者模式
解耦发布者和订阅者);工厂模式
封装对象创建逻辑,多处复用);单例模式
明确表示全局唯一实例);装饰器模式
动态添加功能);不可变对象模式
避免状态被篡改);设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
设计模式常用的七大原则(OOP七大原则)有:
对类来说的,即一个类应该只负责一项职责;或对方法来说的,保证一个方法尽量做好一件事。如类 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) {
// 邮件发送代码
}
}
通过将不同的职责分离到不同的类中,每个类都只负责一项职责,遵循了单一职责原则。这样的设计使得代码更加清晰、易于维护和扩展。当需要修改某个职责的实现时,只需要在对应的类中进行修改,而不会影响到其他类。
用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。即为各个类建立它们需要的专用接口,提高其内聚性。
按隔离原则应当这样处理:将一个大而全的接口拆分成多个小的、特定的接口。比如类 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 {
@Override
public void eat() {
System.out.println("Dog is eating.");
}
@Override
public void fly() {
// 狗不会飞,这个方法没有实际意义
throw new UnsupportedOperationException("Dogs can't fly.");
}
@Overridejava
public 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 {
@Override
public void eat() {
System.out.println("Dog is eating.");
}
@Override
public void swim() {
System.out.println("Dog is swimming.");
}
}
public class Bird implements Eatable, Flyable {
@Override
public void eat() {
System.out.println("Bird is eating.");
}
@Override
public void fly() {
System.out.println("Bird is flying.");
}
}
通过将大接口拆分成多个小接口,Dog
类只需要实现它实际需要的 Eatable
和 Swimmable
接口,避免了实现不必要的方法。同样,Bird
类只需要实现 Eatable
和 Flyable
接口。这样的设计更加灵活,符合接口隔离原则。
依赖倒转(倒置)的中心思想是面向接口编程;
依赖倒转原则包含两个核心要点:
高层模块不应该依赖低层模块,两者都应该依赖抽象:高层模块通常是指负责业务逻辑和整体流程控制的模块,而低层模块则是实现具体功能的细节模块。依赖倒转原则强调,高层模块不应该直接依赖于低层模块的具体实现,而是应该依赖于抽象接口或抽象类。同样,低层模块也应该依赖于抽象,而不是相互依赖具体的实现。
抽象不应该依赖细节,细节应该依赖抽象:抽象代表着稳定的、通用的概念和规范,而细节则是具体的实现。该原则要求抽象不应该受到具体实现细节的影响,相反,具体的实现细节应该遵循抽象所定义的规范。
典型应用模式:依赖注入、工厂模式;
抽象指的是接口或抽象类,细节就是具体的实现类。使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。要面向接口编程,不要面向实现编程。
依赖倒转原则的注意事项和细节:
假设有一个简单的电商系统,其中有一个 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 {
@Override
public 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
类的代码,提高了系统的可扩展性和可维护性。
里氏替换原则指出:如果S是T的子类型,那么程序中T类型的对象可以被替换为S类型的对象,而不会对程序的正确性产生任何影响。也就是说,所有引用父类的地方必须能透明地使用其子类的对象,一个可以接受父类对象的地方,也应该能够接受其子类对象,并且程序的行为不会因为将基类对象替换为子类对象而发生改变。里氏替换原则强调了继承关系中子类与父类的行为兼容性,确保子类可以无缝替换父类而不引起问题。
更通俗地说:子类必须能够完全替代其父类,而不影响程序的正确性。
核心要点:
Integer
,子类可以用Number
)Number
,子类可以返回Integer
);1、子类必须完全实现父类的方法
子类要实现父类中定义的所有抽象方法和非抽象方法。若子类未实现父类的某些方法,使用子类对象替换父类对象时,程序可能出错。
符合里氏替换原则的示例代码:
// 抽象父类:交通工具
abstract class Vehicle {
// 抽象方法:启动
public abstract void start();
}
// 子类:汽车
class Car extends Vehicle {
@Override
public void start() {
System.out.println("汽车启动");
}
}
// 子类:自行车
class Bicycle extends Vehicle {
@Override
public 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("喵喵叫");
}
@Override
public 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
类型参数,子类 Child
的 printList
方法接受 List
类型参数。由于 Integer
是 Number
的子类,Child
对象可正常处理 List
类型参数。
不符合里氏替换原则的示例代码:
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 类型参数
}
}
子类 Child
的 printList
方法输入参数类型范围比父类小,当使用 Child
对象替换 Parent
对象处理 List
类型参数时,会出现编译错误。
4、覆盖或实现父类的方法时输出参数可以被缩小
子类在覆盖或实现父类方法时,输出参数的类型应是父类方法输出参数类型的子类型。调用者使用父类对象时,期望得到父类方法声明的返回类型或其子类型的对象。
符合里氏替换原则的示例代码:
// 父类
class SuperClass {
public Number getNumber() {
return 1;
}
}
// 子类
class SubClass extends SuperClass {
@Override
public 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 {
@Override
public 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
类型变量会出现编译错误。
对扩展开放,对修改关闭;
解释:扩展原来程序,但尽量不修改原来的程序,即通过扩展(而非修改)增加新功能;
核心思想:通过抽象和继承实现扩展性。开闭原则的核心在于通过抽象和封装,将软件系统中相对稳定的部分和容易变化的部分分离。稳定的部分作为抽象层,定义了系统的基本结构和行为规范;容易变化的部分则通过具体的实现类来体现,当需求发生变化时,只需要添加新的实现类,而不需要修改抽象层和其他已有的实现类。
典型应用模式:装饰器模式、适配器模式、策略模式、模板方法模式;
以一个简单的图形绘制为例,说明开闭原则的应用。
不遵循开闭原则的设计,代码可能如下:
// 图形类
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 {
@Override
public void draw() {
System.out.println("绘制圆形");
}
}
// 矩形类
class Rectangle extends Shape {
@Override
public 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
类的代码,符合开闭原则。
一个对象应尽可能少地了解其他对象,具体来说,一个类对于其他类知道得越少越好,尽量降低类与类之间的耦合;一个类应该只和它的直接朋友通信,而避免和陌生的类直接通信(不要和"陌生人"说话、不要直接操作"朋友的朋友"、不要暴露内部结构给外部)
"直接朋友"包括:
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
类对其他类的了解最少,遵循了迪米特法则,降低了类之间的耦合度。
优先使用对象组合或者聚合等关联关系,其次才考虑使用继承关系来达到复用目的。简单来说,就是在一个新的对象里通过关联关系(组合、聚合)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用功能的目的,而不是通过继承父类来获得已有的功能。
典型应用模式:装饰器模式、桥接模式;
组合与聚合
假设要设计一个学生课程管理系统。
不遵循合成复用原则(使用继承来实现复用),代码可能如下:
// 课程类
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中设计模式:(GoF23)
创建型模式:(5种)跟创建对象有关
单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式;
结构型模式:(7种)
适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式;
行为型模式:(11种)
模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器
模式、状态模式、策略模式、责任链模式、访问者模式;