YAZONG 我的开源

设计模式-六大设计原则(一)-单一职责原则

 
0 评论0 浏览

1、1、1 定义

单一职责:Single Responsibility Principle,简称 SRP。
争议之处:对职责的定义,什么是类的职责,以及怎么划分类的职责。
就一个类而言,应该仅有一个引起它变化的原因(There should never be more than one reason for a class to change.)。通俗的说,即一个类只负责一项职责。
案例:使动作主体(用户)与资源的行为(权限)分离。

1、1、2 概述

即使是经验丰富的程序员也回写出违背单一原则的代码,为什么会出现这种情况呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责 P 被划分为粒度更细的职责 P1 和 P2。

比如:类 T 只负责一个职责 P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责 P 细分为粒度更细的职责 P1,P2,这时如果要使程序遵循单一职责原则,需要将类 T 也分解为两个类 T1 和 T2,分别负责 P1、P2 两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类 T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。(这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责 P,在未来可能会扩散为 P1,P2,P3,P4……Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。)

1、1、3 图形结构

#这是项目中经常采用的 SRP 类图。
#SRP 的原话解释是:There should never be more than one reason for a class to change.
单一职责原则的英文名称是 Single Responsibility Principle,简称 SRP。

1、1、4 案例

案例 1

举例说明,用一个类描述动物呼吸这个场景:
public class Client {
public static void main(String[] args) {
Animal animal = new Animal();
animal.breath("牛");	
animal.breath("羊");	
animal.breath("猪");
}
}

class Animal{
public void breath(String animal){
System.out.println(animal+"呼吸空气");
}
}
程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,需要将 Animal 类细分为陆生动物类 Terrestrial,水生动物 Aquatic,代码如下:
public class Client {
public static void main(String[] args){
Terrestrial terrestrial = new Terrestrial();
terrestrial.breathe("牛");
terrestrial.breathe("羊");
terrestrial.breathe("猪");
Aquatic aquatic = new Aquatic();
aquatic.breathe("鱼");
}
}
class Terrestrial{
public void breathe(String animal){
System.out.println(animal+"呼吸空气");
}
}
class Aquatic {
public void breathe(String animal) {
System.out.println(animal + "呼吸水");
}
}

我们会发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。而直接修改类 Animal 来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下:

public class Client {
public static void main(String[] args){
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("猪");
animal.breathe("鱼");
}
}

class Animal{
public void breathe(String animal){
if("鱼".equals(animal)){
System.out.println(animal+"呼吸水");
}else{
System.out.println(animal+"呼吸空气");
}
}
}

可以看到,这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改 Animal 类的 breathe 方法,而对原有代码的修改会对调用“猪”“牛”“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。

public class Client {
public static void main(String[] args){
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("猪");
animal.breathe2("鱼");
}
}
class Animal{
public void breathe(String animal){
System.out.println(animal+"呼吸空气");
}

}

可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别(个人理解还可引申出业务级别、架构级别、类级别、对象级别、属性级别等。)上却是符合单一职责原则的,因为它并没有动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一中呢?其实这真的比较难说,需要根据实际情况来确定。我的原则是:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则;
例如本文所举的这个例子,它太简单了,它只有一个方法,所以,无论是在代码级别上违反单一职责原则,还是在方法级别上违反,都不会造成太大的影响。实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是遵循单一职责原则的好。

案例 2-4 论述接口单一职责

案例 2

image.png

在上述幅图中,用户的属性和用户的行为没有分开,这是一个严重的错误!应该把用户的信息抽取成一个 BO(Business Object,业务对象),把行为抽取成一个 Biz(Business Logic,业务逻辑)。

public interface IUserInfoOld {
    boolean changePassword(String oldPassword);
    boolean deleteUser();
    void mapUser();
    boolean addOrg(int orgId);
    boolean addRole(int roleId);
    void setUserId(String userId);
    String getUserId();
    void setPassword(String password);
    String getPassword();
    void setUserName(String usreName);
    String getUserName();
}

#下述是职责划分后的类图

image.png

public interface IUserBiz {
    boolean changePassword(String oldPassword);
    boolean deleteUser();
    void mapUser();
    boolean addOrg(int orgId);
    boolean addRole(int roleId);
}
public interface IUserBO{
void setUserId(String userId);	
    String getUserId();
    void setPassword(String password);
    String getPassword();
    void setUserName(String usreName);
    String getUserName();
}
public interface IUserInfo extends IUserBO,IUserBiz {
}
public class UserInfo implements IUserInfo {
//略
}
public class Main {
    public static void main(String[] args) {
        IUserInfo userInfo = new UserInfo();
        //纯粹的BO
        IUserBO userBO = userInfo;
        userBO.getPassword();
        //业务逻辑类
        IUserBiz userBiz = userInfo;
        userBiz.mapUser();
    }
}

案例 3

image.png

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

这个接口包含了两个职责:一个是协议管理,一个还是数据传送。
dial 和 hangup 实现的是协议管理,chat 实现的是数据的传送。

image.png

image.png

面向接口编程,对外公布的是接口而不是实现类。如果真要实现类的单一职责,那么就必须使用“职责分明的电话类图”的这种组合模式了,这会引起类间耦合过重、类的数量增加等问题,人为地增加了设计的复杂性。

案例 4

单一职责使用于接口、类,同时也适用于方法,什么意思呢?一个方法尽可能做一件事情,比如一个方法修改用户密码,不要把这个方法放到“修改用户信息”的方法中,这个方法的颗粒度很粗,比如:

image.png

1、1、5 注意事项

软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。其实要去判断是否应该分离出类来,也不难,那就是如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责,就应该考虑类的职责分离。
在编程时,要在类的职责分离上多思考,做到单一职责,这样写出的代码才是真正的易维护、易扩展、易复用、灵活多样。
单一职责提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异(再比如项目中要考虑可变因素和不可变因素,以及相关收益比率)。
接口一定要做到单一职责,类的设计尽可能的做到只有一个原因引起的变化。
需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
单一职责原则很难在项目中得到体现,非常难,为什么?在国内,技术人员的地位和话语权都比较低,因此在项目中需要考虑环境,考虑项目工期,考虑项目成本,考虑工作量,考虑人员的技术水平,考虑硬件的资源情况,考虑网络情况,有时还需要考虑政府政策、垄断协议等等,最终妥协的结果是经常违背单一职责原则。这种国内外文化的差异很难一步改造过来,但是随着技术的深入,单一职责原则必然会深入到项目的设计中。

1、1、6 总结

优点

类的复杂性降低,实现什么职责都有清晰明确的定义;
可读性提高,复杂性降低,那当然可读性提高了;
可维护性提高,可读性提高,那当然更容易维护了;
变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

缺点

如果一类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭到意想不到的破坏。
生搬硬套单一职责原则会引起类的剧增,给维护带来非常多的麻烦,而且过分细分类的职责也会人为地增加系统的复杂性。原则是死的,而人是活的。
文化的差异是很难改造过来的。
单一职责原则最难划分的就是职责。“职责”没有一个量化的标准。


标题:设计模式-六大设计原则(一)-单一职责原则
作者:yazong
地址:https://blog.llyweb.com/articles/2020/03/12/1584023857107.html