面向对象五大设计原则SOLID详解与实战


SOLID五大原则

通常,我们在做软件开发时,会先做架构以及面向对象的设计,SOLID(单一、开闭、里式替换、接口隔离、依赖倒置)五大原则和23种设计模式(常见的单例、构建者、装饰、适配、代理、组合、模板等等)在面向对象的设计中起到了举足轻重的作用。本文将会详细的介绍这五大常用的设计原则。

在这里插入图片描述


单一职责

定义: 单一职责原则认为,一个类应该负责一个功能领域的关注点,而一个关注点指的是导致软件需求发生变化的因素。例如,一个订单管理类应该只负责订单的创建、查询、修改等功能,而不应该包含支付、库存管理等其他业务逻辑。

目的: 使用单一职责原则的目的是为了降低类的复杂度,提高代码的可读性和可维护性。当一个类承担过多的责任时,它的内部逻辑会变得非常复杂,难以理解和测试。同时,当需求变更时,一处改动可能会影响到多个功能,增加了代码的风险和维护成本。

举例:
假设我们要实现一个文档的读取、写入以及格式化等功能。

public class DocumentProcessor {
    public void readDocument(String filename) { /* ... */ }
    public void writeDocument(String filename) { /* ... */ }
    public void formatDocument() { /* ... */ }
}

定义一个类,并实现对应的读取、写入、格式化方法,就可以完成。但是这样的设计明细是不好的,类的功能不清晰、后续新增内容难以维护等。
我们使用单一职责来实现,定义读取、写入和格式化的类,将职责抽象为类来实现。这样的好处是后续支持更多的读取方式、文件类型、格式化方法等,都很好扩展和维护

public class FileReader {
    public String readDocument(String filename) { /* ... */ }
}

public class FileWriter {
    public void writeDocument(String content, String filename) { /* ... */ }
}

public class DocumentFormatter {
    public String formatDocument(String rawContent) { /* ... */ }
}


开闭原则

定义: 开闭原则(Open-Closed Principle, OCP)可以理解为:“软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。” 这意味着当我们需要增加新功能时,应该通过添加新代码来实现,而不是修改现有代码。
对扩展开放: 当应用程序的需求发生变化时,可以轻松地添加新的代码,以适应新的需求。
对修改关闭: 在添加新功能的同时,不应修改已有的代码,以防止引入新的错误或破坏原有的功能。

目的: 保持原有代码的稳定性和可预测性,同时增强系统的灵活性和可扩展性。

举例
假设我们需要设计一个图形绘制系统,支持不同形状的绘制,如圆形、矩形和三角形。

public class ShapeDraw {
    public void draw(String type) {
        switch (type) {
            case "circle":
                System.out.println("Drawing a circle.");
                break;
            case "rectangle":
                System.out.println("Drawing a rectangle.");
                break;
            case "triangle":
                System.out.println("Drawing a triangle.");
        }
    }
}

上面的代码可以完成这个功能,但是每次不利于扩展,后续新增或者修改图像绘制逻辑时,容易对现有功能造成影响,不能很好的适应新需求。
按照开闭原则,我们可以设计如下结构,抽象图形,定义绘制方法,对应的图形实现抽象的图形类,这样新增图形就不会影响现有逻辑。

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

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

public class Triangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a triangle...");
    }
}


里氏替换

定义: 里氏替换原则核心思想是在程序中,子类实例应该能够替换父类实例,而不会影响程序的正确性。换句话说,只要父类能做的事情、子类都能做。

目的:可以确保继承关系的合理性,避免因不当的继承而导致的运行时错误或者异常行为。它帮助我们设计出更加健壮、可扩展的系统架构。

举例
假设我们需要实现一个计算几何形的面积功能,下面我们用满足LSP的思想来实现。

public abstract class Shape {
    public abstract double getArea();
}

public class Rectangle extends Shape { // 子类1
    private double width;
    private double height;

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

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

public class Circle extends Shape { // 子类2
    private double radius;

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

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

// 计算面积的函数
public class AreaCalculator {
    public double calculateArea(Shape shape) {
        return shape.getArea();
    }
}

public class Main { // 实际使用
    public static void main(String[] args) {
        AreaCalculator calculator = new AreaCalculator();
        Shape rectangle = new Rectangle(4, 5); // 可以用子类的实例对象代替父类
        Shape circle = new Circle(3);

        System.out.println("Rectangle area: " + calculator.calculateArea(rectangle));
        System.out.println("Circle area: " + calculator.calculateArea(circle));
    }
}


接口隔离

定义接口隔离原则主张客户端不应该被迫依赖于它不需要的方法。简单来说,就是不要强迫使用者去使用他们并不关心的接口。ISP提倡将臃肿的接口分解成更小、更具体的接口,使得每一个接口都有其特定的作用域,客户端可以选择自己感兴趣的部分接口进行实现或继承。

目的: 减少了类之间的耦合度,提高系统的灵活性和可维护性。当接口过于庞大时,其实现类往往需要实现很多不必要的方法,这不仅增加了代码的复杂性,也可能导致潜在的设计缺陷。通过分离接口,可以让各个组件专注于自身领域内的功能,降低相互间的依赖程度。

举例:
假设我们在设计一个图书馆管理系统,涉及到了书籍、期刊和音像资料的管理。这些资源都需要一些共通的操作,比如获取标题、作者信息等,但也有一些特有的操作,比如书籍可能需要页数信息,而音像资料可能需要时长信息。

我们可以很容易的通过设计一套接口来定义这些共通的操作,但是很明显,部分方法只对一部分实体有意义。

// 错误示范 - 违反ISP
public interface LibraryItem {
    String getTitle();
    String getAuthor();
    int getNumberOfPages();   // 只对书籍有意义
    int getDurationInMinutes();// 只对音像资料有意义
}

class Book implements LibraryItem {
    // 实现所有方法
}

class AudioVisualMaterial implements LibraryItem {
    // 实现所有方法,但实际上不需要getNumberOfPages()
}

接下来我们用满足ISP的思想来设计,把原始的大接口LibraryItem分解成了更细粒度的接口IReadable, IBook, 和IAudioVisualMaterial。这样就减少了不必要的方法实现,提高了系统的灵活性。


public interface IReadable {
    String getTitle();
    String getAuthor();
}

public interface IBook extends IReadable {
    int getNumberOfPages();
}

public interface IAudioVisualMaterial extends IReadable {
    int getDurationInMinutes();
}

class Book implements IBook {
    // 实现IBook接口所需的方法
}

class AudioVisualMaterial implements IAudioVisualMaterial {
    // 实现IAudioVisualMaterial接口所需的方法
}


依赖倒置

定义:依赖倒置原则主要应用于软件工程中的模块间依赖关系。DIP的核心思想是“高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象”。这里的“高层”指的是业务逻辑层,“低层”指的是基础设施层或数据访问层。这么说可能有些难以理解,换句话来说就是模块间都依赖抽象,不进行直接依赖。

**目的:**减少耦合的同时能够提升系统的复用性。通过依赖抽象而非具体实现,可以降低模块间的耦合度,提高系统的可维护性和可扩展性。

举例:
假设我们需要开发一个在线购物平台,其中包含了商品服务、支付服务等多个模块。我们的目标是让支付服务能够独立于具体的支付方式(如信用卡、支付宝、微信支付等),以便于未来的扩展和变更。

快速实现,但似乎并不能满足未来扩展和变更。

public class CreditCardPaymentService {
    public boolean pay(int amount) {
        // 处理信用卡支付的逻辑
        return true; // 假设支付成功
    }
}

public class ShoppingCart {
    private CreditCardPaymentService paymentService;

    public ShoppingCart() {
        paymentService = new CreditCardPaymentService();
    }

    public void checkout(int totalAmount) {
        if (paymentService.pay(totalAmount)) {
            System.out.println("Payment successful.");
        } else {
            System.out.println("Payment failed.");
        }
    }
}

基于DIP的思想实现

// 定义抽象接口
public interface PaymentGateway {
    boolean processPayment(int amount);
}

// 创建实现
public class CreditCardPaymentGateway implements PaymentGateway {
    @Override
    public boolean processPayment(int amount) {
        // 处理信用卡支付的逻辑
        return true; // 假设支付成功
    }
}

public class AlipayPaymentGateway implements PaymentGateway {
    @Override
    public boolean processPayment(int amount) {
        // 处理支付宝支付的逻辑
        return true; // 假设支付成功
    }
}

//修改ShoppingCart类
public class ShoppingCart {
    private PaymentGateway paymentGateway;

    public ShoppingCart(PaymentGateway gateway) {
        this.paymentGateway = gateway;
    }

    public void checkout(int totalAmount) {
        if (paymentGateway.processPayment(totalAmount)) {
            System.out.println("Payment successful.");
        } else {
            System.out.println("Payment failed.");
        }
    }
}
//使用
public class Main {
    public static void main(String[] args) {
        PaymentGateway alipay = new AlipayPaymentGateway();
        ShoppingCart cart = new ShoppingCart(alipay);
        cart.checkout(100); // 输出 "Payment successful."
    }
}

ShoppingCart类不再直接依赖于CreditCardPaymentService,而是依赖于抽象的PaymentGateway接口。这种设计允许我们在不修改ShoppingCart类的情况下,轻松切换支付方式,甚至可以同时支持多种支付方式,极大地提升了系统的灵活性和可扩展性。此外,这种方式也便于进行单元测试,因为我们可以通过构造不同的PaymentGateway实现来进行测试,而无需实际连接到支付网关。

  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java码农杂谈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值