7大设计原则学习笔记

7大设计原则

来,跟我背:单衣解开犁河堤

  1. 一职责原则 (Single Responsibility Principle, SRP)
  • 核心思想:一个类应该只有一个引起它变化的原因
  • 关键点:每个类只承担一个职责,功能高度内聚
  • 优势:提高代码可读性、可维护性,降低修改风险

通俗理解:把类尽可能划分开,比如对于表中的数据我们建立了一个实体类,但是当数据在系统中流转的时候又要在造一个dto(数据传输对象),这样即使后来表中新加了字段,我们只需要改变实体类就可以了,而dto则不受影响,因为你新加的字段对于这个dto可能是没用的,它还负责自己原来的工作。

  1. 闭原则 (Open-Closed Principle, OCP)
  • 核心思想:软件实体应对扩展开放,对修改关闭
  • 关键点:通过抽象和接口实现扩展性
  • 优势:系统更稳定,新增功能不影响现有代码

通俗理解:比如你现在有一个计算类,他有一个方法叫做shapeCompute(Shape shape)。那么圆形和三角形的计算方式肯定不一样吧?你是不是要在方法中写个if(shape.name == "圆形")else if(shape.name == "三角形")...之类的代码?现在问题来了,我每增加一个形状,是不是就要再更改一次代码?那么我们可以直接把计算类定义成一个接口,或者是抽象类,然后让其实现类或者子类自己去定义计算逻辑。比如采用接口的形式,直接把Shape拆解成Circle和Trangle,然后Circle implate 计算接口,重写shapeCompute。这样以后你每增加一个形状,就不需要改计算接口了,让新加的形状直接自己重写就好了。

  1. 氏替换原则 (Liskov Substitution Principle, LSP)
  • 核心思想:子类必须能够替换它们的基类而不改变程序的正确性
  • 关键点:继承关系中保持行为一致性
  • 优势:确保继承体系的正确性,避免运行时错误

通俗理解:你爱喝咖啡吗?美食还是卡布奇诺?假设现在有一个基础款的咖啡机(BasicCoffeeMachine),里面只有一个制作美式的方法makeAmericano()。现在要新建一个升级款的咖啡机(要新加上制作卡布奇诺的功能),你要怎么做?重写父类的makeAmericano方法吗?让该方法直接生产出卡布奇诺?当然不!这样你不就只能做卡布奇诺了吗?正确的做法应该是:

scala 复制代码
class AdvancedCoffeeMachine extends BasicCoffeeMachine {
    public void brewCappuccino() {
        super.brewAmericano();  // 先做美式
        this.foamMilk();       
    }
    public void foamMilk(){
        //加奶+打气泡
    }
}

这样AdvancedCoffeeMachine既可以通过调用父类的制作美式咖啡的方法单独制作美式咖啡,又可以结合两种方法来制作卡布奇诺。

  1. 口隔离原则 (Interface Segregation Principle, ISP)
  • 核心思想:客户端不应被迫依赖它们不使用的接口
  • 关键点:接口要小而专,避免臃肿
  • 优势:减少不必要的依赖,提高系统灵活性

通俗理解 :不要出现"瑞士军刀"接口(一个接口过于全能,里面写了太多方法)。接口臃肿导致实现类被迫承载无关方法​,严重时对于程序的运行效率都会带来影响。

  1. 赖倒置原则 (Dependency Inversion Principle, DIP)
  • 核心思想:高层模块不应依赖低层模块,二者都应依赖抽象
  • 关键点:依赖抽象而非具体实现
  • 优势:降低耦合度,提高代码可测试性和可维护性

通俗理解:什么叫做高层依赖于底层呢?这个原则我们直接举一个很实际的例子。Spring中的controller,service和mapper。首先来说他们遵循了单一职责原则,分别负责接收请求、业务代码处理、数据库操作。如果按照高层依赖于底层的方式实现:Controller → Service → Mapper。

java 复制代码
// Controller直接依赖具体Service实现
@Controller
public class UserController {
    private final UserServiceImpl userService; // 直接依赖实现类
    
    public UserController() {
        this.userService = new UserServiceImpl(); // 硬编码依赖
    }
}

// Service直接依赖具体Mapper实现
@Service
public class UserServiceImpl {
    private final UserMapperImpl userMapper = new UserMapperImpl();
}

会有什么问题?

  1. 可维护性差:如果,UserServiceImpl的一个方法名称改变了,或者UserServiceImpl改名了,改成了UserServiceImplv2。那么所有引用了它的Controller是不是全部都要修改?
  2. 耦合度强:过于紧密的结构肯定是不好的,比如你在测试的时候,没有办法将UserMapperImpl替换,也阻止不了它调用真是数据库。而在单元测试controller的时候,这些可能都用不到。
  3. 扩展性差:与1类似,如果UserServiceImpl中需要添加缓存服务,如果直接修改原来的方法,就会导致旧方法不可用。如果写一个子类继承父类,然后手动去替换也可以,但是,是否每一次添加都继承一次呢?会导致类爆炸,也就是过度继承,难以维护。

实际标准:Controller → Service接口 ← ServiceImpl → Mapper接口 ← MapperImpl。 Controller依赖的Service接口只是一个抽象,需要替换Impl时更为方便。mapper也是一个接口,当和数据库变更时,完全也不影响上层。也就是说,Service接口和Mapper接口就是原则中所说的抽象。它即被上层模块注入,又约束了下层的实现,所以说上层下层都依赖于抽象。

  1. 米特法则/最少知识原则 (Law of Demeter, LoD)
  • 核心思想:一个对象应该对其他对象保持最少的了解
  • 关键点:减少对象间的直接交互
  • 优势:降低耦合度,提高模块独立性

通俗理解:不要和陌生人说话,只和直接的朋友(对象自身,成员变量,方法参数,内部创建的对象)进行通讯。假设我们现在的几个类结构是这样的:

css 复制代码
Address->[city, street, zipCode]
Profile->[email, *address*]
User->[id,name, *profile*]
其中被*包围的代表引用数据类型

那么User和Profile就是直接的朋友,Address和Profile也是直接的朋友,User和Address就是间接的朋友。 如果在外部代码中写user.getProfile().getAddress().getCity()就违反了迪米特法则,因为调用方需要完整的知道所有的关系。 正确的写法应该是在User中写一个getCity()方法,通过profile.getCity()来返回。这样就可以user.getCity()了。

  1. /聚合复用原则 (Composite/Aggregate Reuse Principle, CARP)
  • 核心思想:优先使用组合/聚合,而不是继承来达到复用的目的
  • 关键点:对象间关系更灵活
  • 优势:避免继承的缺点,系统更灵活易扩展

通俗理解: 这块我自己都快忘了,啥是组合啥是聚合了。 让AI帮我列了一下:

维度 聚合(Aggregation) 组合(Composition)
​生命周期​ 部分可以独立于整体存在 部分与整体共存亡
​关系强度​ 弱关联("has-a") 强关联("contains-a")
​UML表示​ 空心菱形箭头(指向整体) 实心菱形箭头(指向整体)
​代码表现​ 通过setter或方法参数注入 在构造函数/声明时创建
​可共享性​ 部分可被多个整体共享 部分专属某个整体
​设计意图​ 表示松散的包含关系 表示紧密的组成关系

简单记忆:"你们这帮人聚在一起干什么"(分开每个人都是个个体,但是聚在一起就变成了一个集合)。"大家好,我们是,糖果超甜~"(如果少了某个糖糖,就不够甜了捏,组合就要面临解散捏)。

继承复用:

  • 优点:简单方便,子类直接得到父类全部的方法。
  • 缺点:破环封装,耦合度高

组合/聚合复用:

  • 优点:降低耦合度,维护封装
  • 缺点:产生的对象比较多。

举个例子:

scala 复制代码
public class RedEleCar extends EleCar{
    @Override
    public void move() {
        System.out.println("红色电动车启动");
    }
}
scala 复制代码
public class BlueEleCar extends EleCar{
    @Override
    public void move() {
        System.out.println("蓝色电动车启动");
    }
}

那么此时如果要再加上黄色汽油车,黑色汽油车呢?就是不断地继承,然后重写方法。 来看下组合方式怎么实现:

arduino 复制代码
public class Car extends EleEngine {
    private Color color;
    private Engine engine;
    public Car(Color color, Engine engine) {
        this.color = color;
        this.engine = engine;
    }
    public void move() {
        System.out.println(color.getColor() + engine.getEngin() + "启动");
    }

    public static void main(String[] args) {
        new Car(new RedColor(), new EleEngine()).move();
    }

}
typescript 复制代码
public class RedColor implements Color {

    @Override
    public String getColor() {
        return "红色";
    }
}
typescript 复制代码
public class GasEngine implements Engine{
    @Override
    public String getEngin() {
        return "汽油";
    }
}

在同样都只创建了一种类型的车的情况下,来看下我的文件数量:

在继承复用的情况下,如果要再加一种车,只增加一个子类就好。但是组合复用则要再加两个,一个颜色的实现类,一个引擎的实现类。听上去貌似多了一点哈。但是设想一种情况,现在有九种颜色的车,和九种引擎。要创建每种颜色和引擎的对象各一个,你要写几个子类?81个!可是如果是组合的方式呢?是不是只要写九个Color实现类和九个Engine实现类,然后循环遍历一下就有了?体会一下吧嘿嘿。

首尾呼应:单衣解开犁河堤!!

感谢你能看到这里,下一篇再见!

相关推荐
秋千码途1 小时前
小架构step系列17:getter-setter-toString
java·开发语言·架构
ん贤1 小时前
如何加快golang编译速度
后端·golang·go
吃西瓜不吐籽_1 小时前
Mac 安装及使用sdkman指南
java·笔记
晨启AI1 小时前
Trae IDE:打造完美Java开发环境的实战指南
java·环境搭建·trae
C雨后彩虹2 小时前
行为模式-策略模式
java·设计模式·策略模式
Ashlee_code2 小时前
美联储降息趋缓叠加能源需求下调,泰国证券交易所新一代交易系统架构方案——高合规、强韧性、本地化的跨境金融基础设施解决方案
java·算法·金融·架构·系统架构·区块链·需求分析
西奥_2 小时前
【JVM】运行时数据区域
java·jvm
lgx0406051122 小时前
Maven详细解
java·maven
玩代码2 小时前
模板方法设计模式
java·开发语言·设计模式·模板方法设计模式
都叫我大帅哥3 小时前
Spring Cloud LoadBalancer:微服务世界的“吃货选餐厅”指南 🍜
java·spring cloud