最近在实践微服务化过程中,对其“单一职责”原则深有体会。那么只有微服务化才可以单一职责,才可以解耦吗?答案是否定的。
单一职责原则是这样定义的:单一的功能,并且完全封装起来。
我们做后端 Java 开发的,应该最熟悉的就是标准的 3 层架构了,尤其是使用 Spring.io 体系的:Controller、Service、Dao/Repository。为什么要分层?就是为了保证单一职责,数据模型的事情交给 Controller,业务逻辑的事情交给 Service,和数据打交道的事情就交给 Dao/Repository。有时候或者有些人会分层分的更多,4 层,5 层,我自己也这样干过,说白了也是为了保证单一职责,3 层不能满足单一职责了,耦合度高了,就分。
我们都知道一个 webapp 在经过一定时间的开发后,就惨不忍睹,即便是有标准的分层,页面或模板文件一大堆,最初的很清晰的 3 层标准架构也变味了,Controller,Service,Dao/Repository 各层之间、Service 之间、Dao/Repository 之间互相调用,一团乱麻。这个时候没改一行代码都有可能一个老鼠害了一锅汤,bug 就如同蚂蚁洞。
这些问题最后就造成:
- 可扩展性灵活性差,出现性能问题
- 业务变更和开发困难,维护成本很高,交付时间长
- 回归测试量很大
- …
为了解决这些问题,就需要时时刻刻清楚的记住“单一职责”,单一职责可以用到软件开发的任何地方。
应该说职责分离来解耦是最常用最有效的架构方法,这能够很大限度的简化一切。
下面就从软件开发、设计、架构,以及重构 / 演进 / 进化,从小到大几个方面来说说单一职责:
类方法 / 函数
这应该是最小的能体现单一职责的程序单元了。最熟悉的最典型的莫过于 Helper/Utils 类方法了,但这种类方法的特征很明显,也很容易遵循单一职责,99% 以上的开发人员都可以做到。但不仅仅这样的类方法要遵循单一职责原则,每一个类方法都应该遵循单一职责原则,尤其是一些处理业务逻辑的类方法更要遵循单一职责原则,处理业务的类方法通常要配合类的单一职责原则进行,下节中讨论。
因此,这也是为什么很多 Team Leader 要求类方法代码行数保持在 20 行左右,其实就是为了保证单一职责,20 行左右是一个经验粗略数字,当然,10 行或者 30 行来完成类方法也是可以的。大部分单一职责的类方法用 20 行左右的代码就够了,如果超过 20 行就要考虑是否保证了单一职责了。那我们在迭代重构的过程中就要考虑拆分这样的类方法来保证单一职责。
类方法的单一职责是最单纯的,很具体的,不掺杂任何额外信息,只关心输入、输出、和职责;一定要明确地定义类方法的职责,保证在迭代中不被错误的扩展,不被调用者错误地使用。
类 / 函数文件
要用面向对象的设计方法,单一职责原则来定义类。开发人员一定要很好地理解“单一职责原则”,具有面向对象的抽象思维能力。
当在迭代中一个类过于庞大或者快速膨胀,说明已经有坏味道了,这时候就需要考虑用单一职责原则或者面向对象的分析方法来重构和重新定义类了,通常就是要抽象和拆分类,否则将来会变成一个方法容器。
把类比作一个人,她的职责就是完成自己职责范围内的事情,如果她什么事情都管,就叫多管闲事,可以想象她多管闲事的后果,会搅得鸡犬不宁。同样,类也是,类如果多管闲事,那会搅得整个应用不稳定,漏洞百出,还很难修复。所以说定义一个类,要明确这个类的职责。使用面向对象的分析和设计方法,能很好地准确定义一个类的职责范围,通常会用到封装、继承、多态和抽象等设计方法。
包结构 / 文件夹
分层就是最常用的架构方法之一,分层具体体现在分包和分类,就是分门别类的意思。俗话说,物以类聚,人以群分。
包结构在单一职责原则上是类的补充,职责范围进一步扩大。如果把一个类叫做一个人,那么包就是一个最小单位的团队,职责就是负责一类特定事情。
如何分包呢?那就要用到分类学的知识了,要以什么特征来分,可能不仅仅只有一种特征,比如,先用公司域名来做基础包名,这里叫一级包名;然后再用一个特定的有意义的标识作为二级子包名;之后按分层(web,dao,service 等等)方法做三级包名,也可以先按照业务再按分层。例如:
域名:tietang.wang 有个项目叫:social 那么我可以这样分: wang.tietang - social - web - service - dao - commons 也可以这样: wang.tietang - commons - user - web - service - dao - relation - web - service - dao
多工程 /module
通常以多 maven module 或者 gradle 多 module 形式存在,来保证单一职责。
当业务量还没有达到服务拆分的火候,通常在一个 APP 发展的太庞大时或者在工程建设初期时,需要规范和整理项目结构。这个时候需要多工程从文件系统上隔离,通过 module 依赖来集成。需要注意的是这样的架构或拆分不是随意的,要以单一职责原则来拆分,更具体一点就是要根据业务、技术框架功能等特性来拆分。
比如,按技术组件拆分,通常会有一些技术组件,可以把她放到 commons module,如果有多种类型的技术组件,就拆分为 commons module 的子 module;也可以直接将这些技术组件拆分为独立的工程,存在于独立的 git/svn 仓库,独立管理,专人负责,其他哪些 module 需要就依赖她。那拆分的这些技术组件的每一个应该遵循单一职责原则,例如数据分片的框架、NIO 基础网络框架等等。
比如,按业务拆分,例如有用户、订单、商品、支付,那么就按照这些业务拆分为子 module,每一个子 module 就只负责自己的业务逻辑,也遵循单一职责。
那每个 module 的职责范围又比类和包更大,这个时候职责也更模糊,有时候很难把握,对于技术组件可能相对清晰,而业务 module 就要熟悉业务,明确业务边界。
多 module 拆分后也是为将来服务化埋下伏笔,同时在物理文件系统比较清晰了,那在依赖管理上也要掌握好保持清晰的依赖逻辑,把握好单一职责原则。
微服务 / 可部署单元
微服务,从运行时隔离,但业务量发展到一定时候,从单体或者多 module 工程拆分或演化出来,可独立打包可独立部署并复合单一原则的 application,当然了微服务所体现的价值不仅仅是隔离和独立部署,还有很多这里可以参考单体应用与微服务优缺点辨析。单一职责在微服务中的价值是最重要的,包含了 app 层面和开发 app 的团队层面,微服务的大部分优点都可以围绕单一职责来展开。
团队
先引用《韩非子·扬权》中的一段文字:
夫物者有所宜,材者有所施,各处其宜,故上下无为。
使鸡司夜,令狸执鼠,皆用其能,上乃无事。
上有所长,事乃不方。
矜而好能,下之所欺:辩惠好生,下因其材。
上下易用,国故不治。
各得其所,各司其职。所以,团队也要遵循单一职责原则,这样才能很好地管理团队成员的时间,提高效率。一个人专注做一件事情的效率远高于同时关注多件事情。同样一个人一直管理和维护同一份代码要比多人同时维护多份代码的效率高很多。每一个人都有自己的个性,他有自己的擅长,让每一个人专注自己擅长的事情,那肯定事半功倍,整个团队绩效肯定也很突出。
总之,引用古文名句说明了所有:
- 物以类聚,人以群分。
- 天下之事,分合交替,分久必合,合久必分!
- 使鸡司夜,令狸执鼠,皆用其能,上乃无事。
参考
感谢魏星对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论