设计模式——设计模式理念

发布于:2025-03-31 ⋅ 阅读:(25) ⋅ 点赞:(0)

参考:设计模式——设计模式理念

参考:设计模式——工厂方法模式

参考:设计模式——抽象工厂模式

参考:设计模式——模板方法模式

参考:设计模式——适配器模式

参考:设计模式——装饰器模式

设计模式概念

设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码高内聚、低耦合以及可重用(复用)性、可扩展(维护)性、可读性、可靠性以及安全性的解决方案。

  • 高内聚:模块内部功能紧密相关,职责单一(如 策略模式 中每个策略类只负责一种算法);
  • 低耦合:模块间依赖最小化(如 观察者模式 解耦发布者和订阅者);
  • 可重用性:相同功能的代码可重复使用,避免重复造轮子(如 工厂模式 封装对象创建逻辑,多处复用);
  • 可读性:编程规范性,便于其他程序员的阅读和理解;代码结构符合通用范式(如 单例模式 明确表示全局唯一实例);
  • 可扩展性:当需要增加新的功能时,非常方便;新增功能时无需修改原有代码(如 装饰器模式 动态添加功能);
  • 可靠性:当增加新功能后,对原来的功能没有影响;减少意外错误(如 不可变对象模式 避免状态被篡改);

设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。

设计模式的七大原则

设计模式常用的七大原则(OOP七大原则)有:

  1. 单一职责原则(SRP)
  2. 接口隔离原则(ISP)
  3. 依赖倒转原则(DIP)
  4. 里氏替换原则(LSP)
  5. 开闭原则(OCP)
  6. 迪米特法则(LoD)
  7. 合成复用原则(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 {
        @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 类只需要实现它实际需要的 EatableSwimmable 接口,避免了实现不必要的方法。同样,Bird 类只需要实现 EatableFlyable 接口。这样的设计更加灵活,符合接口隔离原则。

3. 依赖倒转(置)原则(DIP)

思想

依赖倒转(倒置)的中心思想是面向接口编程;

依赖倒转原则包含两个核心要点

  1. 高层模块不应该依赖低层模块,两者都应该依赖抽象:高层模块通常是指负责业务逻辑和整体流程控制的模块,而低层模块则是实现具体功能的细节模块。依赖倒转原则强调,高层模块不应该直接依赖于低层模块的具体实现,而是应该依赖于抽象接口或抽象类。同样,低层模块也应该依赖于抽象,而不是相互依赖具体的实现。

  2. 抽象不应该依赖细节,细节应该依赖抽象:抽象代表着稳定的、通用的概念和规范,而细节则是具体的实现。该原则要求抽象不应该受到具体实现细节的影响,相反,具体的实现细节应该遵循抽象所定义的规范。

    典型应用模式:依赖注入、工厂模式;

    抽象指的是接口或抽象类,细节就是具体的实现类。使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。要面向接口编程,不要面向实现编程。

依赖倒转原则的注意事项和细节:

  1. 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好;
  2. 变量的声明类型尽量是抽象类或接口,这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化;
  3. 继承时遵循里氏替换原则。

示例

假设有一个简单的电商系统,其中有一个 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 类的代码,提高了系统的可扩展性和可维护性。

4. 里氏替换原则(LSP)

思想

里氏替换原则指出:如果S是T的子类型,那么程序中T类型的对象可以被替换为S类型的对象,而不会对程序的正确性产生任何影响。也就是说,所有引用父类的地方必须能透明地使用其子类的对象,一个可以接受父类对象的地方,也应该能够接受其子类对象,并且程序的行为不会因为将基类对象替换为子类对象而发生改变。里氏替换原则强调了继承关系中子类与父类的行为兼容性,确保子类可以无缝替换父类而不引起问题。

更通俗地说:子类必须能够完全替代其父类,而不影响程序的正确性。

  • 典型应用模式:模板方法模式;

核心要点

  • 子类必须完全实现父类的方法:子类是对父类的扩展和细化,因此子类应该实现父类中定义的所有抽象方法和非抽象方法。如果子类没有实现父类的某些方法,那么在使用子类对象替换父类对象时,就可能会导致程序出现错误或异常。
    • 实现抽象类或接口的基本要求;
    • 子类可以覆盖父类的非抽象方法,但覆盖时需保证不改变父类方法的预期行为,确保使用子类对象替换父类对象时程序的正确性;
  • 子类中可以增加自己特有的方法:在满足里氏替换原则的前提下,子类可以添加自己特有的方法和属性,以实现更具体的功能。但这些新增的特性不能影响到子类与父类之间的替换关系,即不能因为子类的特殊行为而破坏了程序中依赖父类的部分的正常运行。
    • 这些新增的特性不会影响子类与父类之间的替换关系,因为在使用父类引用指向子类对象时,不会调用到子类特有的方法,只有当进行类型转换后才能使用这些特有的方法;
  • 覆盖或实现父类的方法时输入参数可以被放大:里氏替换原则允许子类在覆盖或实现父类方法时,将方法的输入参数类型放宽。这意味着子类方法可以接受更广泛的输入参数,而不会影响到使用父类对象的代码。
    • 子类方法的参数类型可以是父类方法参数类型的父类型(即更宽泛的类型);(如父类用Integer,子类可以用Number
    • 子类方法可以接受比父类更宽松的参数值范围;(如父类约束入参>0,子类可以放开入参约束>=0)
  • 覆盖或实现父类的方法时输出参数可以被缩小:与输入参数相反,子类在覆盖或实现父类方法时,输出参数的类型应该是父类方法输出参数类型的子类型。这是因为调用者在使用父类对象时,期望得到的是父类方法所声明的返回类型或其子类型的对象。如果子类方法返回的是父类返回类型的超类型对象,那么可能会导致调用者在处理返回结果时出现错误。
    • 子类方法返回类型可以是父类方法返回类型的子类型(父类返回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 是抽象父类,定义了抽象方法 startCarBicycle 子类都实现了该方法。在 startVehicle 方法中,可传入 CarBicycle 对象,程序正常运行。

  • 不符合里氏替换原则的示例代码

    // 抽象父类:交通工具
    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 子类没有实现父类 Vehiclestart 方法,当调用 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);
        }
    }
    

    父类 ParentprintList 方法接受 List<Integer> 类型参数,子类 ChildprintList 方法接受 List<Number> 类型参数。由于 IntegerNumber 的子类,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> 类型参数
        }
    }
    

    子类 ChildprintList 方法输入参数类型范围比父类小,当使用 Child 对象替换 Parent 对象处理 List<Number> 类型参数时,会出现编译错误。

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);
        }
    }
    

    父类 SuperClassgetNumber 方法返回 Number 类型,子类 SubClassgetNumber 方法返回 Integer 类型,IntegerNumber 的子类。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 类型
        }
    }
    

    子类 SubClassgetNumber 方法返回类型是 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 {
        @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 是抽象类,定义了抽象方法 drawCircleRectangle 是具体的图形类,实现了 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种)

    模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器
    模式、状态模式、策略模式、责任链模式、访问者模式;