面向对象设计五大原则

2024-08-31 15:27:45 浏览数 (2)

背景

面向对象设计(Object-Oriented Design, OOD)和面向领域设计(Domain-Driven Design, DDD)是两种不同的设计方法论:

OOD:适用于各种类型的软件开发项目,特别是那些需要复杂对象关系和行为的系统。

DDD:适用于具有复杂业务逻辑和领域知识的系统,如企业级应用、金融系统、电子商务平台等。

两者可以结合使用,在复杂业务系统中,DDD 可以帮助构建领域模型和业务逻辑,而 OOD 可以帮助实现具体的技术细节和代码结构。

面向对象设计的五大原子简称为SOLID: 1、S - 单一职责原则(Single Responsibility Principle, SRP)

2、O - 开放封闭原则(Open/Closed Principle, OCP)

3、L - 里氏替换原则(Liskov Substitution Principle, LSP)

4、I - 接口隔离原则(Interface Segregation Principle, ISP)

5、D - 依赖倒置原则(Dependency Inversion Principle, DIP)

一、S单一职责

一个类应该只有一个职责。

不遵守原则的实现

假设我们有一个 User 类,它既负责用户数据的管理,又负责用户数据的持久化。

代码语言:txt复制
class User {
    private String name;
    private String email;

    // 用户数据管理方法
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    // 用户数据持久化方法
    public void saveToDatabase() {
        // 保存用户数据到数据库的逻辑
    }
}

这个类违反了单一职责原则,因为它有两个职责:用户数据管理和用户数据持久化。

遵守原则的实现

我们可以将其拆分为两个类。

代码语言:txt复制
class User {
    private String name;
    private String email;

    // 用户数据管理方法
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

class UserRepository {
    public void saveToDatabase(User user) {
        // 保存用户数据到数据库的逻辑
    }
}

二、O开闭原则

软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。

假设我们有一个简单的图形绘制应用程序,Drawing 类用于输出所有的图形。

不遵守原则的实现

代码语言:txt复制
class Circle {
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

class Rectangle {
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

class Drawing {
    private List<Object> shapes;

    public Drawing() {
        shapes = new ArrayList<>();
    }

    public void addShape(Object shape) {
        shapes.add(shape);
    }

    public void drawShapes() {
        for (Object shape : shapes) {
            if (shape instanceof Circle) {
                ((Circle) shape).draw();
            } else if (shape instanceof Rectangle) {
                ((Rectangle) shape).draw();
            }
        }
    }
}

如果我们想要添加一个新的形状(例如三角形),我们需要修改 Drawing 类的 drawShapes 方法,这违反了开放封闭原则。

遵守原则的实现

代码语言:txt复制
interface Shape {
    void draw();
}

class Circle implements Shape {
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

class Rectangle implements Shape {
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

class Drawing {
    private List<Shape> shapes;

    public Drawing() {
        shapes = new ArrayList<>();
    }

    public void addShape(Shape shape) {
        shapes.add(shape);
    }

    public void drawShapes() {
        for (Shape shape : shapes) {
            shape.draw();
        }
    }
}

增加一个新的类型,只需要实现Shape接口

代码语言:txt复制
class Triangle implements Shape {
    public void draw() {
        System.out.println("Drawing a triangle");
    }
}

如果要将Triangle通过drawShapes输出,则不需要修改drawShapes方法。

三、L里氏替换原则

程序中的对象应该可以在不改变程序正确性的前提下被它们的子类实例替换。【这里是类,不是接口】

假设我们有一个简单的几何图形类层次结构,其中有一个 Rectangle 类和一个 Square 类。

不遵守原则的实现

代码语言:txt复制
class Rectangle {
    private int width;
    private int height;

    public int getWidth() {
        return width;
    }

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

    public int getHeight() {
        return height;
    }

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

    public int getArea() {
        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 类继承了 Rectangle 类,但它违反了里氏替换原则。因为 Square 类改变了 Rectangle 类的行为:在 Square 中,设置宽度会影响高度,反之亦然。

当我们在程序中使用 Rectangle 类时,期望的是宽度和高度可以独立设置:

代码语言:txt复制
Rectangle rect = new Rectangle();
rect.setWidth(5);
rect.setHeight(10);
System.out.println(rect.getArea()); // 输出 50

如果我们用 Square 类替换 Rectangle 类:【替换】

代码语言:txt复制
Rectangle rect = new Square();
rect.setWidth(5);
rect.setHeight(10);
System.out.println(rect.getArea()); // 输出 100

在使用基类 Rectangle 的地方,替换为子类 Square 后,程序的行为发生了变化,导致程序的正确性受到影响。

遵循原则的实现

代码语言:txt复制
abstract class Shape {
    public abstract int getArea();
}

class Rectangle extends Shape {
    private int width;
    private int height;

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

    public int getWidth() {
        return width;
    }

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

    public int getHeight() {
        return height;
    }

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

    @Override
    public int getArea() {
        return width * height;
    }
}

class Square extends Shape {
    private int side;

    public Square(int side) {
        this.side = side;
    }

    public int getSide() {
        return side;
    }

    public void setSide(int side) {
        this.side = side;
    }

    @Override
    public int getArea() {
        return side * side;
    }
}

四、I接口隔离原则

一个类对另一个类的依赖应该建立在最小的接口上。

不遵守原则的实现

假设我们有一个 Worker 接口,它定义了工人可以执行的所有操作。

代码语言:txt复制
interface Worker {
    void work();
    void eat();
}

然后我们有两个实现类:HumanWorkerRobotWorker

代码语言:txt复制
class HumanWorker implements Worker {
    public void work() {
        // 人类工人的工作逻辑
    }

    public void eat() {
        // 人类工人的吃饭逻辑
    }
}

class RobotWorker implements Worker {
    public void work() {
        // 机器人工人的工作逻辑
    }

    public void eat() {
        // 机器人工人不需要吃饭,但必须实现这个方法
    }
}

遵守原则的实现

代码语言:txt复制
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class HumanWorker implements Workable, Eatable {
    public void work() {
        // 人类工人的工作逻辑
    }

    public void eat() {
        // 人类工人的吃饭逻辑
    }
}

class RobotWorker implements Workable {
    public void work() {
        // 机器人工人的工作逻辑
    }
}

五、依赖倒置

  1. 高层模块不应该依赖于低层模块。二者都应该依赖于抽象。
  2. 抽象不应该依赖于细节。细节应该依赖于抽象。

假设我们有一个简单的应用程序,其中有一个 UserService 类需要发送电子邮件通知用户。我们可以有一个 EmailService 类来处理电子邮件的发送。

不遵循原则的实现

代码语言:txt复制
class EmailService {
    public void sendEmail(String message) {
        // 发送电子邮件的逻辑
        System.out.println("Sending email: "   message);
    }
}

class UserService {
    private EmailService emailService;

    public UserService() {
        this.emailService = new EmailService();
    }

    public void notifyUser(String message) {
        emailService.sendEmail(message);
    }
}

如果,邮件服务商终止服务了,或者老板新拉来一个短信服务商,我们需要更换为短信服务商,则UserService需要大改

代码语言:txt复制
class MsgService {
    public void sendMsg(String message) {
        // 发送短信的逻辑
        System.out.println("Sending Msg: "   message);
    }
}

class UserService {
    private EmailService emailService; // 修改
    private MsgService msgService

    public UserService() {
        this.emailService = new EmailService(); // 修改
        this.msgService = new MsgService();
    }

    public void notifyUser(String message) {
        emailService.sendEmail(message); // 修改
        msgService.sendEmail(message);
    }
}

甚至以后短信商服务到期,邮件供应商恢复了合作,又得改一遍。

遵守原则的实现

代码语言:txt复制
// 通知接口,怎么通知不关心
interface NotificationService {
    void sendNotification(String message);
}
// 底层依赖这个接口,具体实现
class EmailService implements NotificationService {
    public void sendNotification(String message) {
        // 发送电子邮件的逻辑
        System.out.println("Sending email: "   message);
    }
}
// 高层依赖这个接口,正式使用
class UserService {
    private NotificationService notificationService;

    public UserService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void notifyUser(String message) {
        notificationService.sendNotification(message);
    }
}

// 扩展一个短信服务商
class MsgService implements NotificationService{
    public void sendNotification(String message) {
        // 发送短信的逻辑
        System.out.println("Sending Msg: "   message);
    }
}

当需要进行信息发送的时候,可以将服务商作为参数传递给UserService

代码语言:txt复制
public class Main {
    public static void main(String[] args) {
        NotificationService emailService = new EmailService();
        UserService userServiceWithEmail = new UserService(emailService);
        userServiceWithEmail.notifyUser("Hello via Email!");

        NotificationService smsService = new SMSService();
        UserService userServiceWithSMS = new UserService(smsService);
        userServiceWithSMS.notifyUser("Hello via SMS!");
    }
}

只需要告诉使用者用什么渠道,而不需要修改使用者。

总结

在现在互联网产品迭代更新频繁的情况下,基本上设计原则很难完全遵守。它们更像是一种道德规范,在这个规范内你可以自由发挥,道德规范一直在这里约束着你不至于在系统实现的时候过于放浪形骸。

0 人点赞