设计之魅:高质量面向对象设计的秘密

2023年 12月 8日 29.3k 0

设计模式是在软件设计中用于解决常见问题的经过验证的解决方案。设计模式并不是代码或库,而是一种解决问题的思考方式。在使用设计模式时,需要考虑一些基本的设计原则,这些原则有助于构建灵活、可维护和可扩展的软件系统。以下是一些常见的设计原则:

单一职责原则(Single Responsibility Principle - SRP):

它指导我们确保一个类只有一个责任。类的责任应该是单一的,即一个类应该只有一个引起它变化的原因。这有助于提高类的内聚性,使得类更加容易理解、修改和维护。

// 违反单一职责原则的例子
class Report {
    private String title;
    private String content;

    public Report(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public void generateReport() {
        // 生成报告的业务逻辑
        System.out.println("Generating report for " + title + " with content: " + content);
    }

    public void saveToFile() {
        // 将报告保存到文件的业务逻辑
        String filename = title.replace(" ", "_") + ".txt";
        // 实际保存到文件的代码略
        System.out.println("Report saved to " + filename);
    }
}

// 遵循单一职责原则的例子
class Report {
    private String title;
    private String content;

    public Report(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public void generateReport() {
        // 生成报告的业务逻辑
        System.out.println("Generating report for " + title + " with content: " + content);
    }
}

class FileSaver {
    public static void saveToFile(Report report) {
        // 将报告保存到文件的业务逻辑
        String filename = report.getTitle().replace(" ", "_") + ".txt";
        // 实际保存到文件的代码略
        System.out.println("Report saved to " + filename);
    }
}

// 上述例子中,Report 类负责生成报告,而 FileSaver 类负责将报告保存到文件。这样,每个类都有一个清晰的责任,遵循了单一职责原则。

在上述例子中,第一个示例中的 Report 类违反了单一职责原则,因为它负责生成报告和保存报告到文件两个不同的责任。在第二个示例中,将这两个责任分别放在 Report 类和 FileSaver 类中,遵循了单一职责原则,使得每个类都更加简单和可维护。这样的设计有助于将系统的不同部分解耦,提高代码的灵活性和可扩展性。

一个类应该只有一个引起变化的原因。换句话说,一个类应该只有一个责任。

开放/封闭原则(Open/Closed Principle - OCP):

由勃兰特·梅耶(Bertrand Meyer)提出。该原则表明一个软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。简而言之,当需要添加新功能时,应该通过扩展而不是修改现有代码来实现。

具体来说,开放/封闭原则的核心思想是:

开放(Open):

软件实体应该可以在不修改它的源代码的情况下进行扩展。

新功能应该通过添加新代码来实现,而不是通过修改已有代码。

封闭(Closed):

已有的软件实体不应该被修改,因为修改可能引入新的错误或影响现有功能的稳定性。

这样的设计使得系统更加稳定,因为不需要修改现有代码,只需要添加新的代码。这也有助于降低代码的耦合性,提高代码的可维护性和可扩展性。

// 违反开放/封闭原则的例子
class Rectangle {
    public double width;
    public double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
}

class AreaCalculator {
    public double calculateRectangleArea(Rectangle rectangle) {
        return rectangle.width * rectangle.height;
    }
}

// 上述代码违反了开放/封闭原则,如果要添加一个新的形状(例如圆形),就需要修改 AreaCalculator 类。

// 遵循开放/封闭原则的例子
interface Shape {
    double calculateArea();
}

class Rectangle implements Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double calculateArea() {
        return width * height;
    }
}

class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

class AreaCalculator {
    public double calculateShapeArea(Shape shape) {
        return shape.calculateArea();
    }
}

// 上述代码遵循了开放/封闭原则,通过引入 Shape 接口和不同的形状类,可以轻松地添加新的形状而无需修改 AreaCalculator 类。

在遵循开放/封闭原则的例子中,通过引入一个 Shape 接口和不同的形状类(例如 Rectangle 和 Circle),可以轻松地添加新的形状而无需修改 AreaCalculator 类。这样,系统的扩展性得到了提高,同时保持了对现有代码的封闭性。

软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着在不修改现有代码的情况下,可以通过添加新的代码来扩展系统的功能。

里氏替换原则(Liskov Substitution Principle - LSP):

由计算机科学家巴巴拉·利斯科夫(Barbara Liskov)提出。该原则指导着子类型(派生类或子类)如何与基类型(基类或父类)进行替换,以确保程序的正确性和一致性。

如果对每一个类型为 S 的对象 o1,都有类型为 T 的对象 o2,使得以 T 定义的所有程序 P 在所有的对象 o1 都替换成 o2 时,程序 P 的行为没有发生变化,那么类型 S 是类型 T 的子类型。

换句话说,如果子类型可以替换父类型而不影响程序的正确性,那么这个子类型是符合里氏替换原则的。

// 违反里氏替换原则的例子

class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int calculateArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(height);
    }
}

// 上述代码违反了里氏替换原则,因为在Square类中重写了setWidth和setHeight方法,导致Square对象在替换Rectangle对象时可能会引发意料之外的行为。

// 遵循里氏替换原则的例子

class Shape {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int calculateArea() {
        return width * height;
    }
}

class Rectangle extends Shape {
    // 省略特有的方法或属性
}

class Square extends Shape {
    @Override
    public void setWidth(int side) {
        super.setWidth(side);
        super.setHeight(side);
    }

    @Override
    public void setHeight(int side) {
        super.setHeight(side);
        super.setWidth(side);
    }
}

// 上述代码遵循了里氏替换原则,因为Square类继承自Shape类,没有修改基类的行为,而是通过适当的方式扩展了基类的功能。

在遵循里氏替换原则的例子中,Square类不再继承自Rectangle类,而是继承自一个通用的Shape类,确保子类型可以被替换而不引起意外的行为变化。通过这种方式,程序可以更灵活地使用不同的形状类型,而不必担心替换时可能引发的问题。

子类型必须能够替换其基类型而不改变程序的正确性。如果一个类是某个抽象类的子类,那么它应该能够替代该抽象类的任何地方,并且程序的行为不会改变。

依赖倒置原则(Dependency Inversion Principle - DIP):

由罗伯特·马丁(Robert C. Martin)提出。该原则主要有两个核心观点:

  • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
  • 抽象不应该依赖于细节,细节应该依赖于抽象。

在设计系统时,应该通过依赖于抽象而不是具体实现来减少模块之间的耦合。高层模块和低层模块都应该依赖于通用的抽象,而不是彼此直接依赖。这有助于提高系统的灵活性、可维护性和可扩展性。

// 违反依赖倒置原则的例子

class LightBulb {
    public void turnOn() {
        System.out.println("LightBulb: Bulb turned on...");
    }

    public void turnOff() {
        System.out.println("LightBulb: Bulb turned off...");
    }
}

class Switch {
    private LightBulb bulb;

    public Switch() {
        this.bulb = new LightBulb();
    }

    public void operate() {
        if (bulb != null) {
            if (bulb.isOn()) {
                bulb.turnOff();
            } else {
                bulb.turnOn();
            }
        }
    }
}

// 上述代码违反了依赖倒置原则,因为Switch类直接依赖于具体的LightBulb类。

// 遵循依赖倒置原则的例子

interface Switchable {
    void turnOn();

    void turnOff();
}

class LightBulb implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("LightBulb: Bulb turned on...");
    }

    @Override
    public void turnOff() {
        System.out.println("LightBulb: Bulb turned off...");
    }
}

class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    public void operate() {
        if (device != null) {
            if (device.isOn()) {
                device.turnOff();
            } else {
                device.turnOn();
            }
        }
    }
}

// 上述代码遵循了依赖倒置原则,Switch类依赖于通用的Switchable接口而不是具体的LightBulb类。

在遵循依赖倒置原则的例子中,Switch 类不再直接依赖于 LightBulb 类,而是依赖于通用的 Switchable 接口。这样,如果有其他类实现了 Switchable 接口,可以轻松地替换 LightBulb 类,而不影响 Switch 类的实现。这提高了系统的灵活性和可维护性。

高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。这促使使用接口或抽象类来减少模块之间的耦合。

接口隔离原则(Interface Segregation Principle - ISP):

接口隔离原则(Interface Segregation Principle - ISP)是面向对象设计中的一个原则,它强调一个类不应该被强迫依赖于它不使用的接口。该原则的目标是防止一个类因为实现了不需要的接口而变得庞大臃肿,降低类的内聚性。

接口隔离原则可以通过将大接口拆分成更小的、更具体的接口来实现。具体来说,一个类只应该知道它需要使用的方法,而不需要了解其他不相关的方法。

// 违反接口隔离原则的例子

interface Worker {
    void work();

    void eat();
}

class Robot implements Worker {
    @Override
    public void work() {
        System.out.println("Robot is working...");
    }

    @Override
    public void eat() {
        // 空实现,机器人无需进食
    }
}

class Human implements Worker {
    @Override
    public void work() {
        System.out.println("Human is working...");
    }

    @Override
    public void eat() {
        System.out.println("Human is eating...");
    }
}

// 上述代码违反了接口隔离原则,因为Robot类实现了不需要的eat方法。

// 遵循接口隔离原则的例子

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Robot implements Workable {
    @Override
    public void work() {
        System.out.println("Robot is working...");
    }
}

class Human implements Workable, Eatable {
    @Override
    public void work() {
        System.out.println("Human is working...");
    }

    @Override
    public void eat() {
        System.out.println("Human is eating...");
    }
}

// 上述代码遵循了接口隔离原则,将大接口拆分成Workable和Eatable两个小接口,类只需要实现它们真正需要的接口。

在遵循接口隔离原则的例子中,将大接口拆分成 Workable 和 Eatable 两个小接口。这样,Robot 类只需实现 Workable 接口,而 Human 类则同时实现了 Workable 和 Eatable 接口。这避免了类实现不需要的方法,提高了系统的灵活性和可维护性。

不应该强迫客户端依赖于它们不使用的接口。一个类不应该被迫实现它用不到的接口。

合成/聚合复用原则(Composition/Aggregation Reuse Principle - CARP):

合成/聚合复用原则(Composition/Aggregation Reuse Principle - CARP)是面向对象设计中的一个原则,它强调在复用时优先使用组合(Composition)和聚合(Aggregation),而不是继承。该原则的核心思想是通过将现有的类组合在一起来创建新的类,而不是通过继承现有类。

合成/聚合复用原则的主要原则有两个:

优先使用合成(Composition):

通过将对象组合在一起来创建新的对象,而不是通过继承现有类。这样可以更灵活地构建对象的行为,而不会产生继承链的问题。

优先使用聚合(Aggregation):

聚合是一种特殊的合成关系,表示一种“整体-部分”的关系,但整体和部分之间的生命周期可以独立存在。这允许部分对象在没有整体对象的情况下存在。与合成一样,聚合也提供了更灵活的复用方式。

// 违反合成/聚合复用原则的例子

class Engine {
    public void start() {
        System.out.println("Engine starting...");
    }
}

class Car extends Engine {
    public void drive() {
        System.out.println("Car is driving...");
    }
}

// 上述代码违反了合成/聚合复用原则,因为Car类通过继承Engine类,导致Car和Engine之间形成了紧耦合的关系。

// 遵循合成/聚合复用原则的例子

class Engine {
    public void start() {
        System.out.println("Engine starting...");
    }
}

class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        engine.start();
        System.out.println("Car is driving...");
    }
}

// 上述代码遵循了合成/聚合复用原则,Car类通过组合引入了Engine类,而不是通过继承。这降低了类之间的耦合性,使得系统更加灵活。

在遵循合成/聚合复用原则的例子中,Car 类通过组合引入了 Engine 类,而不是通过继承。这降低了类之间的耦合性,使得系统更加灵活,更容易进行复用和维护。使用合成和聚合的方式可以避免继承链的问题,并提高系统的灵活性。

首选使用合成/聚合,而不是继承。通过将现有类的实例组合到新的类中,而不是通过继承现有类来实现代码复用。

迪米特法则(Law of Demeter - LoD):

迪米特法则(Law of Demeter - LoD),也被称为最少知识原则,是面向对象设计中的一项原则。迪米特法则强调一个对象应该对其他对象有最少的了解,即一个类不应该直接与其他类过多地发生相互作用。

一个对象应该对其他对象保持最少的了解。只与你的直接朋友通信,而避免和陌生人通信。

这意味着一个类应该尽量减少对其他类的引用,尽量减少依赖关系,以降低类之间的耦合度。通过保持对象之间的关系简单,可以提高系统的灵活性和可维护性。

// 违反迪米特法则的例子

class Teacher {
    public void instruct(Student student) {
        // 教师直接与学生对象发生交互
        student.study();
    }
}

class Student {
    public void study() {
        System.out.println("Student is studying...");
    }
}

// 上述代码违反了迪米特法则,因为Teacher类直接与Student类发生了交互。

// 遵循迪米特法则的例子

class Teacher {
    public void instruct(StudentProxy studentProxy) {
        // 教师只与学生代理对象发生交互,而不直接与学生对象交互
        studentProxy.study();
    }
}

class Student {
    public void study() {
        System.out.println("Student is studying...");
    }
}

class StudentProxy {
    private Student student;

    public StudentProxy(Student student) {
        this.student = student;
    }

    public void study() {
        // 通过代理对象转发请求给学生对象
        student.study();
    }
}

// 上述代码遵循了迪米特法则,Teacher类只与StudentProxy类发生交互,而不直接与Student类发生交互。

在遵循迪米特法则的例子中,Teacher 类只与 StudentProxy 类发生交互,而不直接与 Student 类发生交互。这样,Teacher 类不需要了解 Student 类的内部实现,通过 StudentProxy 类进行间接的交互。这降低了类之间的耦合度,符合迪米特法则的要求。

一个软件实体应当尽可能少地与其他实体发生相互作用。也被称为最少知识原则。

在面向对象设计中,设计原则是指导我们创建灵活、可维护、可扩展软件系统的重要指导方针。每个设计原则都强调特定的方面,例如单一职责原则、开放/封闭原则、里氏替换原则、依赖倒置原则、接口隔离原则和合成/聚合复用原则。这些原则共同构建了一个强大的设计基础,有助于在面对不断变化的需求时更好地应对挑战。

在实际开发中,理解并应用这些设计原则是至关重要的。它们提供了一组指导原则,帮助自己构建出更加健壮和灵活的软件系统。通过不断学习和实践,可以更好地运用这些原则来创建高质量的面向对象设计。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论