当前位置 博文首页 > 盛开的太阳:设计模式六大原则(一)----单一职责原则

    盛开的太阳:设计模式六大原则(一)----单一职责原则

    作者:盛开的太阳 时间:2021-06-03 18:25

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

    一、什么是单一职责原则

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

    单一职责原则,全称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 合理的职责分解

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

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

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

    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);
        }
    

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

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

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

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

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

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

    bk