9

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

 2 years ago
source link: https://xiaomozhang.github.io/2021/10/21/software-design-pattern-single-responsibility-principle/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

设计模式六大原则之【单一职则原则】

一、什么是单一职责原则

首先, 我们来看单一职责的定义.

单一职责原则,全称Single Responsibility Principle, 简称SRP.
A class should have only one reason to change
类发生更改的原因应该只有一个

就一个类而言,应该仅有一个引起它变化的原因。应该只有一个职责。如果一个类有一个以上的职责,这些职责就耦合在了一起。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。想要避免这种现象的发生,就要尽可能的遵守单一职责原则。

单一职责原则的核心就是解耦和增强内聚性。

二、为什么要遵守单一职责原则?

通常 , 我们做事情都要知道为什么要这么做, 才回去做. 做的也有底气, 那么为什么我们要使用单一职责原则呢?

1、提高类的可维护性和可读写性
一个类的职责少了,复杂度降低了,代码就少了,可读性也就好了,可维护性自然就高了。

2、提高系统的可维护性
系统是由类组成的,每个类的可维护性高,相对来讲整个系统的可维护性就高。当然,前提是系统的架构没有问题。

3、降低变更的风险
一个类的职责越多,变更的可能性就越大,变更带来的风险也就越大

如果在一个类中可能会有多个发生变化的东西,这样的设计会带来风险, 我们尽量保证只有一个可以变化,其他变化的就放在其他类中,这样的好处就是 ** 提高内聚,降低耦合 **。

三. 单一职责原则应用的范围

单一职责原则适用的范围有接口、方法、类。按大家的说法,接口和方法必须保证单一职责,类就不必保证,只要符合业务就行。

3.1 【方法层面】单一职责原则的应用

现在有一个场景, 需要修改用户的用户名和密码. 就针对这个功能我们可以有多种实现.
第一种:

/**
 * 操作的类型
 */
public enum OperateEnum {
    UPDATE_USERNAME,
    UPDATE_PASSWORD;
}

public interface UserOperate {
    void updateUserInfo(OperateEnum type, UserInfo userInfo);
}

public class UserOperateImpl implements UserOperate{
    @Override
    public void updateUserInfo(OperateEnum type, UserInfo userInfo) {
        if (type == OperateEnum.UPDATE_PASSWORD) {
            // 修改密码
        } else if(type == OperateEnum.UPDATE_USERNAME) {
            // 修改用户名
        }
    }
}

第二种方法:

public interface UserOperate {
    void updateUserName(UserInfo userInfo);

    void updateUserPassword(UserInfo userInfo);
}

public class UserOperateImpl implements UserOperate {
    @Override
    public void updateUserName(UserInfo userInfo) {
        // 修改用户名逻辑
    }

    @Override
    public void updateUserPassword(UserInfo userInfo) {
        // 修改密码逻辑
    }
}

来看看这两种实现的区别:
第一种实现是根据操作类型进行区分, 不同类型执行不同的逻辑. 把修改用户名和修改密码这两件事耦合在一起了. 如果客户端在操作的时候传错了类型, 那么就会发生错误.
第二种实现是我们推荐的实现方式. 修改用户名和修改密码逻辑分开. 各自执行各自的职责, 互不干扰. 功能清晰明了.

由此可见, 第二种设计是符合单一职责原则的. 这是在方法层面实现单一职责原则.

3.2 【接口层面】单一职责原则的应用

我们假设一个场景, 大家一起做家务, 张三扫地, 李四买菜. 李四买完菜回来还得做饭. 这个逻辑怎么实现呢?

/**
 * 做家务
 */
public interface HouseWork {
    // 扫地
    void sweepFloor();

    // 购物
    void shopping();
}

public class Zhangsan implements HouseWork{
    @Override
    public void sweepFloor() {
        // 扫地
    }

    @Override
    public void shopping() {

    }
}

public class Lisi implements HouseWork{
    @Override
    public void sweepFloor() {

    }

    @Override
    public void shopping() {
        // 购物
    }
}

首先定义了一个做家务的接口, 定义两个方法扫地和买菜. 张三扫地, 就实现扫地接口. 李四买菜, 就实现买菜接口. 然后李四买完菜回来还要做饭, 于是就要在接口类中增加一个方法cooking. 张三和李四都重写这个方法, 但只有李四有具体实现.

这样设计本身就是不合理的.
首先: 张三只扫地, 但是他需要重写买菜方法, 李四不需要扫地, 但是李四也要重写扫地方法.
第二: 这也不符合开闭原则. 增加一种类型做饭, 要修改3个类. 这样当逻辑很复杂的时候, 很容易引起意外错误.

上面这种设计不符合单一职责原则, 修改一个地方, 影响了其他不需要修改的地方.

/**
 * 做家务
 */
public interface Hoursework {
}

public interface Shopping extends Hoursework{
    // 购物
    void shopping();
}

public interface SweepFloor extends Hoursework{
    // 扫地
    void sweepFlooring();
}

public class Zhangsan implements SweepFloor{

    @Override
    public void sweepFlooring() {
        // 张三扫地
    }
}

public class Lisi implements Shopping{
    @Override
    public void shopping() {
        // 李四购物
    }
}

上面做家务不是定义成一个接口, 而是将扫地和做家务分开了. 张三扫地, 那么张三就实现扫地的接口. 李四购物, 李四就实现购物的接口. 后面李四要增加一个功能做饭. 那么就新增一个做饭接口, 这次只需要李四实现做饭接口就可以了.

public interface Cooking extends Hoursework{ 
    void cooking();
}

public class Lisi implements Shopping, Cooking{
    @Override
    public void shopping() {
        // 李四购物
    }

    @Override
    public void cooking() {
        // 李四做饭
    }
}

如上, 我们看到张三没有实现多余的接口, 李四也没有. 而且当新增功能的时候, 只影响了李四, 并没有影响张三.
这就是符合单一职责原则. 一个类只做一件事. 并且他的修改不会带来其他的变化.

3.3 【类层面】单一职责原则的应用

从类的层面来讲, 没有办法完全按照单一职责原来来拆分. 换种说法, 类的职责可大可小, 不想接口那样可以很明确的按照单一职责原则拆分. 只要符合逻辑有道理即可.

比如, 我们在网站首页可以注册, 登录, 微信登录.注册登录等操作. 我们通常的做法是:

public interface UserOperate {

    void login(UserInfo userInfo);

    void register(UserInfo userInfo);

    void logout(UserInfo userInfo);
}


public class UserOperateImpl implements UserOperate{
    @Override
    public void login(UserInfo userInfo) {
        // 用户登录
    }

    @Override
    public void register(UserInfo userInfo) {
        // 用户注册
    }

    @Override
    public void logout(UserInfo userInfo) {
        // 用户登出
    }
}

那如果按照单一职责原则拆分, 也可以拆分为下面的形式

public interface Register {
    void register();
}

public interface Login {
    void login();
}

public interface Logout {
    void logout();
}


public class RegisterImpl implements Register{

    @Override
    public void register() {

    }
}

public class LoginImpl implements Login{
    @Override
    public void login() {
        // 用户登录
    }
}

public class LogoutImpl implements Logout{

    @Override
    public void logout() {

    }
}

像上面这样写可不可以呢? 其实也可以, 就是类很多. 如果登录、注册、注销操作代码很多, 那么可以这么写.

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

4.1 合理的职责分解

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

例子:人的行为分析,包括了生活和工作等行为的分析,生活行为包括吃、跑、睡等行为,工作行为包括上下班,开会等行为,如下图所示:

software-design-pattern-single-responsibility-principle-single-responsibility-principle.png
software-design-pattern-single-responsibility-principle-single-responsibility-principle.png

人类的行为分成了两个接口:生活行为接口、工作行为接口,以及两个实现类。如果都用一个实现类来承担这两个接口的职责,就会导致代码臃肿,不易维护,如果以后再加上其他行为,例如学习行为接口,将会产生变更风险(这里还用到了组合模式)。

4.2 来看看简单的代码实现

第一步: 定义一个行为接口
/**
 * 人的行为
 * 人的行为包括两种: 生活行为, 工作行为
 */
public interface IBehavior {

}

这里面定义了一个空的接口, 行为接口. 具体这个行为接口下面有哪些接口呢?有生活和工作两方面的行为.

第二步: 定义生活和工作接口, 并且他们都是行为接口的子类

生活行为接口:

public interface LivingBehavior extends IBehavior{
    /** 吃饭 */
    void eat();

    /** 跑步 */
    void running();

    /** 睡觉 */
    void sleeping();
}

工作行为接口:

public interface WorkingBehavior extends IBehavior{

    /** 上班 */
    void goToWork();

    /** 下班 */
    void goOffWork();

    /** 开会 */
    void meeting();
}

第三步: 定义工作行为接口和生活行为接口的实现类

生活行为接口实现类:

public class LivingBehaviorImpl implements LivingBehavior{
    @Override
    public void eat() {
        System.out.println("吃饭");
    }

    @Override
    public void running() {
        System.out.println("跑步");
    }

    @Override
    public void sleeping() {
        System.out.println("睡觉");
    }
}

工作行为接口实现类:

public class WorkingBehaviorImpl implements WorkingBehavior{

    @Override
    public void goToWork() {
        System.out.println("上班");
    }

    @Override
    public void goOffWork() {
        System.out.println("下班");
    }

    @Override
    public void meeting() {
        System.out.println("开会");
    }
}

第四步: 行为组合调用.

行为接口定义好了. 接下来会定义一个行为集合. 不同的用户拥有的行为是不一样 , 有的用户只用生活行为, 有的用户既有生活行为又有工作行为.ewgni

我们并不知道具体用户到底会有哪些行为, 所以,通常使用一个集合来接收用户的行为. 用户有哪些行为, 就往里面添加哪些行为.

1. 行为组合接口BehaviorComposer
public interface BehaviorComposer {
    void add(IBehavior behavior);
}
2. 行为组合接口实现类IBehaviorComposerImpl
public class IBehaviorComposerImpl implements BehaviorComposer {

    private List<IBehavior> behaviors = new ArrayList<>();
    @Override
    public void add(IBehavior behavior) {
        System.out.println("添加行为");
        behaviors.add(behavior);
    }

    public void doSomeThing() {
        behaviors.forEach(b->{
            if(b instanceof LivingBehavior) {
                LivingBehavior li = (LivingBehavior)b;
                // 处理生活行为
            } else if(b instanceof WorkingBehavior) {
                WorkingBehavior wb = (WorkingBehavior) b;
                // 处理工作行为
            }

        });
    }
}
第五步: 客户端调用

用户在调用的时候, 根据实际情况调用就可以了, 比如下面的代码: 张三是全职妈妈, 只有生活行为, 李四是职场妈妈, 既有生活行为又有工作行为.

public static void main(String[] args) {
        //  张三--全职妈妈
        LivingBehavior zslivingBehavior = new LivingBehaviorImpl();
        BehaviorComposer zsBehaviorComposer = new IBehaviorComposerImpl();
        zsBehaviorComposer.add(zslivingBehavior);

        // 李四--职场妈妈
        LivingBehavior lsLivingBehavior = new LivingBehaviorImpl();
        WorkingBehavior lsWorkingBehavior = new WorkingBehaviorImpl();

        BehaviorComposer lsBehaviorComposer = new IBehaviorComposerImpl();
        lsBehaviorComposer.add(lsLivingBehavior);
        lsBehaviorComposer.add(lsWorkingBehavior);
    }

可以看出单一职责的好处.

五、单一职责原则的优缺点

  • 类的复杂性降低: 一个类实现什么职责都有清晰明确的定义了, 复杂性自然就降低了

  • 可读性提高: 复杂性降低了,可读性自然就提高了

  • 可维护性提高: 可读性提高了,代码就更容易维护了

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

四、最佳实践
百家号-设计模式:单一职责原则


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK