通俗易懂的设计模式七大原则

725 阅读5分钟

hi 大家好,我是 DHL。就职于美团、快手、小米。公众号:ByteCode,分享有用的原创文章,涉及鸿蒙、Android、Java、Kotlin、性能优化、大厂面经。

来自微信小程序「猿面试」当中的一道面试题,更多大厂面试题 Java、Android、鸿蒙和ArkTS设计模式算法和数据结构 欢迎前往查看「猿面试」。


理解设计模式的七大原则,能够帮助开发者写出更好的代码,因此在面试中也是问的比较多的问题,这篇文章将会配合示例,简要说明设计模式的六大原则,帮助你在面试中更好的回答这些问题。

1. 单一职责原则 (Single Responsibility Principle, SRP)

每个类应该只负责一项职责,不应该成为一个多功能的“大杂烩”。例如一个 Logger 类应该只负责日志记录的职责,而不应该同时处理文件操作等其他工作。

// 符合单一职责原则的Logger类
public class Logger {
    public void log(String message) {
        // 只负责日志逻辑
        System.out.println(message);
    }
}

2. 开放封闭原则 (Open/Closed Principle, OCP)

软件实体(如类、模块、函数等)应该对扩展开放,对修改封闭,这意味着在不修改现有代码的基础上,可以增加新的功能。

// 绘图类接口
public interface Shape {
    void draw();
}

// 圆形类
public class Circle implements Shape {
    public void draw() {
        // 绘制圆形
    }
}

// 画布类
public class Canvas {
    public void drawShape(Shape shape) {
        shape.draw(); // 使用Shape接口绘制各种形状,支持多种扩展形状,无需修改此类
    }
}

使用抽象类或接口允许类进行扩展,而无需修改类本身的代码。

3. 里氏替换原则 (Liskov Substitution Principle, LSP)

子类应该能够替换它们的父类,并且替换之后不改变程序的正确性。也就是说一个函数如果使用的是一个基类对象,那么它应该能够使用这个基类的任何一个子类对象,而程序依然能正确运行

// 鸟类
public class Bird {
    public void fly() {
        // 实现飞行
    }
}

// 燕子类
public class Swallow extends Bird {
    // 可以直接使用 Bird 类的飞行功能
}

// 使用鸟类对象的客户端代码
public class BirdUser {
    public void makeBirdFly(Bird bird) {
        bird.fly();
    }
}

4. 依赖倒置原则 (Dependency Inversion Principle, DIP)

这个原则告诉我们,我们的代码应该依赖于接口和抽象类,而不是具体的类。这可以使我们更容易地替换组件,而不用去修改依赖于这些组件的其他代码。

// 抽象层高级的存储接口
public interface Storage {
    void save(Object data);
}

// 实现了 Storage 的文件存储类
public class FileStorage implements Storage {
    public void save(Object data) {
        // 将数据保存到文件
    }
}

// 高层模块
public class DataProcessor {
    private Storage storage;

    public DataProcessor(Storage storage) {
        this.storage = storage;
    }

    public void process(Object data) {
        // 处理数据...
        storage.save(data);
    }
}

5. 接口隔离原则 (Interface Segregation Principle, ISP)

这个原则告诉我们,我们应该设计细小的、专门的接口,而不是设计大而全的接口。这个原则鼓励我们把大的接口拆分成一组更小的和更具体的接口,只需要知道和使用它们真正需要的接口。

// 细分接口
public interface Printer {
    void print(Document d);
}

public interface Scanner {
    Document scan();
}

// 组合设备实现两个接口
public class ComboDevice implements Printer, Scanner {
    public void print(Document d) {
        // 实现打印功能
    }

    public Document scan() {
        // 实现扫描功能
        return new Document();
    }
}

6. 迪米特法则 (Law of Demeter, LoD) 或最小知识原则

对象应该尽可能少地了解其他对象。换言之,一个对象应该对其他对象有最小的了解,并且只与其直接的朋友通信。在这里,“朋友”指的是那些直接的成员变量、方法参数或者是方法内部创建的对象。

// 一个具体的例子:顾客、服务员和厨师
class Customer {
    void dine() {
        Waiter waiter = new Waiter();
        waiter.takeOrder();
    }
}

class Waiter {
    void takeOrder() {
        Chef chef = new Chef();
        chef.cook();
    }
}

class Chef {
    void cook() {
        // cooking the dish
    }
}

// 在这里,Customer 类并不直接和 Chef 类交流,
// 它通过 Waiter 类来让厨师开始烹饪。这样,Customer 类就遵循了迪米特法则。

在这个例子中,Customer 不需要知道 Chef 接口的细节。它只需要与 Waiter 交流,这样就减少了类之间的直接依赖关系。

依此,Customer 关心的是结果,而过程由 WaiterChef 负责,这样做有助于减少系统中各部分的耦合,使得代码更容易理解和维护。

合成复用原则

合成复用原则强调优先使用对象组合(通过组合已有的类来构成新的类)而不是继承来达到复用的目的,这样可以使系统更加灵活和可维护。

如果修改父类的方法会影响子类的方法,那么这个父类和子类就不应该采用继承,而应该使用组合。

合成复用原则的错误示例:通过继承来复用代码

class Vehicle {
    void startEngine() {
        // ...启动发动机...
    }
}

class Car extends Vehicle {
    // Car 继承了 Vehicle,自动获得了 startEngine 方法
}

合成复用原则的正确示例:通过组合来复用代码

class Engine {
    void start() {
        // ...启动发动机...
    }
}

class Car {
    private Engine engine; // Car 类包含一个 Engine 类型的对象

    Car(Engine engine) {
        this.engine = engine; // 通过构造器传入 Engine 实例
    }

    void startCar() {
        engine.start(); // 调用 Engine 类的 start 方法来启动车辆
    }
}

在后者的例子中,Car 类并不是继承了 Engine 类,而是有了一个 Engine 类型的对象,它可以调用 Engine 对象的 start 方法来启动发动机。这样做的好处是可以灵活地更换 Engine,而不会影响到 Car 类本身,同时也避免了通过继承而造成的紧耦合问题。


全文到这里就结束了,感谢你的阅读,如果文章对你有帮助,欢迎在看、点赞、分享给身边的小伙伴,你的点赞是我持续更新的动力。

推荐阅读


Hi 大家好,我是 DHL,大厂程序员,公众号:ByteCode ,在美团、快手、小米工作过。分享有用的原创文章,涉及鸿蒙、Android、Java、Kotlin、性能优化、大厂面经。