设计模式原则:单一职责原则

Published on 2024-02-19 13:58 in 分类: 博客 with 狂盗一枝梅
分类: 博客

单一职责原则的英文名称是Single Responsibility Principle,简称是SRP,顾名思义:一个类只负责一项职责。它的官方定义是

单一职责原则(Single Responsibility Principle, SRP),There should never be more than one reason for a class to change(有且仅有一个原因引起类的变更

一、单一职责的案例讨论

单单从字面意思上来看,单一职责原则的执行粒度是“类”,也就是一个“类”只负责一项职责,但是很明显我们在实际执行过程中很难做到这点。

1、类的单一职责案例

在《设计模式之禅(第2版)》中,作者举了个例子,有个接口Iphone定义如下

image-20240219100522484

这个接口定义了三个方法

public interface IPhone {
    //拨通电话
    public void dial(String phoneNumber);
    //通话
    public void chat(Object o);
    //通话完毕,挂电话
    public void hangup();
}

拨通电话方法dial和挂断电话方法hangup应该拆到一个类里,通话方法chat应该拆到另外一个单独的类里,最后变成如下图所示

职责分明的电话类图

这是个职责分明的电话类图,但是一个手机类要把ConnectionManager和DataTransfer组合在一块才能使用,组合是一种强耦合关系,你和我都有共同的生命期,这样的强耦合关系还不如使用接口实现的方式呢,而且还增加了类的复杂性,多了两个类,所以作者又优化了下

image-20240219101327973

一个类实现了两个接口,把两个职责融合在一个类中,诶?这不是违反了单一职责原则吗?应该说我们是“半违反”单一职责原则了,因为我们是面向接口编程,我们对外公布的是接口而不是实现类,对使用者来说,Phone这个实例可以是IConnectionManager,也可以是IDataTransfer。

看完这个案例,其实我内心不是很认同。 毕竟,类似于

public interface IPhone {
    //拨通电话
    public void dial(String phoneNumber);
    //通话
    public void chat(Object o);
    //通话完毕,挂电话
    public void hangup();
}

这种代码,是我们面向对象编程第一课就写进教科书里的代码,而且如果非要说,无论是打电话、聊天、挂断电话,这本都是“打电话”这一个过程中发生的事情,是否可以理解为它们统统属于手机的“单一职责”呢,我只能认为是作者举的例子不是很恰当。

但是,紧接着作者又说

看过电话这个例子后,是不是想反思一下了,我以前的设计是不是有点问题了?不,不是的,不要怀疑自己的技术能力,单一职责原则最难划分的就是职责。一个职责一个接口,但问题是“职责”没有一个量化的标准,一个类到底要负责那些职责?这些职责该怎么细化?细化后是否都要有一个接口或类?这些都需要从实际的项目去考虑,从功能上来说,定义一个IPhone接口也没有错,实现了电话的功能,而且设计还很简单,仅仅一个接口一个实现类,实际的项目我想大家都会这么设计

好家伙,这作者自己打脸打的这么快

愣住

这引出了关于单一职责原则最大的争议点“职责”没有一个量化的标准,一千个人眼中有一千个哈姆雷特,都觉得自己是对的,就比如上面举的这个例子,作者觉得拆两个接口才合理,我觉得一开始的封装到一个类中就行(从结果上来看,作者还是最后封装到了一个类里),我们的意见就不一样,其实我还有另外的想法:如果这都不合理,java的多重继承机制干啥用的?

我再举一个实际开发中的例子,我们使用mybatis组件,一定要写一堆的mapper文件,每个mapper文件都可以说是围绕着某个实体类对应的表做的CRUD操作,其形式如下所示:

@Mapper
public interface UserMapper{
    
    public void saveUser(User user);
    
    public void updateUser(User user);
    
    public User getUser(Long id);
    
    public int deleteUser(Long id);
}

我认为这是我们开发时遵守单一职责原则的表现,如果我们在UserMapper中新增了一个方法,叫selectStudentById(),那我们肯定会觉得不合适,这不合适的原因就是破坏了单一职责原则。

2、方法的单一职责案例

单一职责适用于接口、类,同时也适用于方法,**也就是一个方法尽可能做一件事情。**作者举了个例子:一个方法修改用户密码,不要把这个方法放到“修改用户信息”方法中,因为修改用户信息这个方法的颗粒度太粗了。

我在这里也举个例子:我经常在一些复杂业务中看到类似如下这种代码

public User getUserByMobile(String mobile){
    User user = getUserDetail(mobile);
    //获取用户其它信息
    UserCompany compay = getCompany(mobile);
    //如果不为空,则先更新用户的企业信息
    if(Objects.nonNull(company)){
        updateUserCompay(company);
        user.setCompany(company);
    }
    return user;
}

以上代码是一段查询用户信息的方法,这段方法只是看名字谁也想不到,里面竟然还有更新用户信息的逻辑,这实际上违背了单一职责原则,让代码的可维护性变的极差。很明显这段代码需要重构,重构方法要根据业务灵活判断了。

二、总结

1、遵守单一职责原则的好处

单一职责原则有什么好处:

● 类的复杂性降低,实现什么职责都有清晰明确的定义;

● 可读性提高,复杂性降低,那当然可读性提高了;

● 可维护性提高,可读性提高,那当然更容易维护了;

● 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

单一职责原则实际上是“高内聚”的提现

2、如何遵守单一职责原则

其实就是合理的职责分解,相同的职责放到一起,不同的职责分解到不同的接口和实现中去,这个是最容易也是最难运用的原则,关键还是要从业务出发,从需求出发,识别出同一种类型的职责

需要说明的一点是:单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责

还是难在那一句话:“职责”没有一个量化的标准,具体实施肯定会因人而异,《设计模式之禅(第2版)》中给的最佳实践是

接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。

END.


#设计模式
目录