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

前言
单一职责原则的定义:应该有且仅有一个原因引起类的变更
单一职责原则的英文名称是: Single Responsibility Principle, 简称 是 SRP。

6大设计原则:

  • 单一职责原则
  • 里氏替换原则
  • 依赖倒置原则
  • 接口隔离原则
  • 迪米特法则
  • 开闭原则

当在项目中,接触到用户、机构、角色管理这些模块,基本使用的都是RBAC 模型( Role- Based Access Control, 基于角色的 访问控制, 通过分配和取消角色来完成用户权限的授予和取消, 使动作主体(用户)与资源的行为(权限)分离)。

示例一:

场景:用户管理、修改用户的信息、增加机构(一个人属于多个机构)、增加角色等,用户有这么多的信息和行为要维护

1. 修改前的版本:

接口设计类图 如图 1- 1 所示。


设计模式 —— 单一职责原则_第1张图片
 

问题:用户的属性和用户的行为没有分开

2. 修改后的版本:
修改: 把用户信息 抽取 成 一个 BO( Business Object, 业务 对象), 把 行为 抽取 成 一个 Biz( Business Logic, 业务 逻辑)。重新 拆封 成 两个 接口, IUserBO 负责 用户 的 属性, 简单 地说, IUserBO 的 职责 就是 收集 和 反馈 用户 的 属性 信息; IUserBiz 负责 用户 的 行为, 完成 用户 信息 的 维护 和 变更。

解析: 我们 现在 是 面向 接口 编程, 所以 产生了 这个 UserInfo 对象 之后, 当然可以 把 它 当 IUserBO 接口 使用。 也可 以当 IUserBiz 接口 使用, 这要 看 你在 什么地方 使用 了。 要 获得 用户 信息, 就 当是 IUserBO 的 实现 类; 要是 希望 维护 用户 的 信息, 就把 它 当作 IUserBiz 的 实现 类 就成 了,类图 如图 1- 2 所示。


设计模式 —— 单一职责原则_第2张图片


代码清单:

class IUserBO
{
public:
    void    setUserID(string userID);
    string  getUserID();
    void    setPassword(string password){}
    string  getPassword();
    void    setUserName(string userName);
    string  getUserName();
};

class IUserBiz
{
public:
    bool    changePassword(string password);
    bool    deleteUser(IUserBO userBO){return true;}
    void    mapUser(IUserBO userBO);
    bool    addOrg(IUserBO userBO, int orgID);
    bool    addRole(IUserBO userBO, int roleID);
};

class IUserInfo:public IUserBO, public IUserBiz
{
    
};

class UserInfo:public IUserInfo
{
    
};


int main ()
{
    //c++中可以使用多继承,java是单继承,这里有区别
    IUserInfo userInfo;
    userInfo.setPassword("abc");
    IUserBO userBO;
    userInfo.deleteUser(userBO);

    return 0;
}

分析:
为什么要把一个接口拆分成两个呢?依赖了单一职责原则。

 

示例二:

场景:电话的使用,电话通话的时候有4个过程发生:拨号、通话、回应、挂机

1. 修改前的版本:类图如图1-4所示


设计模式 —— 单一职责原则_第3张图片
代码 清单:

class Object;

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

问题: IPhone 这个 接口 可不是 只有 一个 职责, 它 包含 了 两个 职责: 一个 是 协议 管理, 一个 是 数据 传送。 dial() 和 hangup() 两个 方法 实现 的 是 协议 管理, 分别 负责 拨号 接通 和 挂 机; chat() 实现 的 是 数据 的 传送, 把 我们 说的 话 转换 成 模拟 信号 或 数字 信号 传递 到 对方, 然后 再把 对方 传递 过来 的 信号 还原 成 我们听得 懂的 语言。

2. 修改一次的版本:
我们 可以 这样 考虑 这个 问题, 协议 接通 的 变化 会 引起 这个 接口 或 实现 类 的 变化 吗? 会的! 那 数据 传送( 想想 看, 电话 不仅仅 可以 通话, 还可以 上网) 的 变化 会 引起 这个 接口 或 实现 类 的 变化 吗? 会的! 那就 很 简单 了, 这里 有两 个 原因 都 引 起了 类 的 变化。 这 两个 职责 会 相互 影响 吗? 电话 拨号, 我 只要 能 接通 就成, 甭管 是 电信 的 还是 网 通 的 协议; 电话 连接 后 还 关心 传递 的 是什么 数据 吗? 通过 这样 的 分析, 我们 发现 类 图上 的 IPhone 接口 包含 了 两个 职责, 而且 这 两个 职责 的 变化 不相 互 影响, 那就 考虑 拆分 成 两个 接口,
职责分明的电话 类 图 如图 1- 5 所示。
设计模式 —— 单一职责原则_第4张图片

3. 修改后的版本
这个 类 图 看上去 有点 复杂 了, 完全 满足 了 单一 职责 原则 的 要求, 每个 接口 职责 分明, 结构 清晰, 但是 我 相信 你在 设计 的 时候 肯定 不会 采用 这种 方式, 一个 手机 类 要把 ConnectionManager 和 DataTransfer 组合 在 一块 才能 使用。 组合 是 一种 强 耦合 关系, 你 和我 都有 共同 的 生命 期, 这样 的 强 耦合 关系 还不 如 使用 接口 实现 的 方式 呢, 而且还 增 加了 类 的 复杂性, 多了 两个 类。 经过 这样 的 思考 后, 我们 再 修改 一下 类 图,
简介清晰、职责分明的电话类图 如图 1- 6 所示。
设计模式 —— 单一职责原则_第5张图片

分析:一个类实现了两个接口,把 两个 职责 融合 在 一个 类 中。 你会 觉得 这个 Phone 有两 个 原因 引起 变化 了 呀, 是的, 但是 别 忘记了 我们 是 面向 接口 编程, 我们 对外 公布 的 是 接口 而 不是 实现 类。 而且, 如 果真 要 实现 类 的 单一 职责, 这个 就必须 使用 上面 的 组合 模式 了, 这 会 引起 类 间 耦合 过重、 类 的 数量 增加 等 问题, 人为 地 增加 了 设计 的 复杂性。

 总结 : 单一职责原则的好处:

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


注: 单一职责原则提出了一个编写程序的标准,用“ 职责” 或“ 变化原因” 来衡量接口或类设计得是否优良,但是“ 职责” 和“ 变化 原因” 都是不可度量的,因项目而异,因环境而异。

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

 

参考文献《秦小波. 设计模式之禅》(第2版) (华章原创精品) (Kindle 位置 308-310). 机械工业出版社

你可能感兴趣的:(设计模式)