单一职责原则的思考
Posted on 2018-11-15 | Edited on 2019-01-09 | Comments: 2 | Views: 5
单一职责原则是软件开发过程中经常被用到的一个原则,易于理解,且十分受用。
什么是单一职责原则
单一职责原则,是面向对象设计的基本原则之一,易于理解,且十分受用。官方对此理解为:一个类应该有且只有一个变化的原因。当有多个原因导致类发生变化时,这意味着可以通过设计新的类来体现这种变化,而不是让一个已有的类变得臃肿。重新思考我们的类,让已有的类尽可能的简单化,是迭代开发中一个必要的环节。
单一职责,追求的是简单明确的设计,强调的是分工明确,职责简单。这种设计,不局限于类的设计,可以扩展到任何一个程序逻辑单元的设计,如函数的设计,模块的设计,工程的设计,项目的设计,集群的设计等。
单一职责的核心可以理解为:化整为零,将整体分解为局部的逻辑单元,不同的逻辑单元之间尽可能的相对独立,并且职责明确。例如,一个项目,可以分解为多个工程;一个工程,可以分解为多个模块;一个模块,可以分解为多个包和类;一个类,可以分解为多个属性和方法行为。设计层面的分解,可以对应到具体实现层面的分离。下面以Java Web项目为例,从分离的角度,分享对单一职责原则的思考。
前后端分离
在早起的Java Web开发中,MVC是一种经典的分层设计思路。通过将模型、控制和视图进行分层,不同的开发者可以专注于他们擅长的领域,如前端开发者可以专注于视图,后端开发者可以专注于逻辑控制,数据库设计者可以专注于底层数据模型的设计,层与层之间只要约定好接口和规范,就可以顺利的进行协作开发。但这样也有一个问题,模型、控制和视图这三者必须同时运行起来,整个Web工程才可以正常的启动,这对于开发调试和部署的代价都是很大的。本质上讲,视图层的职责就是负责数据的展示,不关心数据从哪里来以及数据如何加工处理。而控制层的职责是处理外部的请求并给出响应,专注于内容而不是内容的表现形式。至于模型层,其职责主要是管理持久化的数据,而并不关心数据要如何被使用。
因此从职责上讲,这三者可以认为是独立的,既然是独立的,那么能否进行独立的部署呢?答案是可以的,独立的部署,意味着在开发前端视图时,并不依赖于后端的服务,只需要专注于界面的开发即可。对于后端,只需要专注于数据服务和业务逻辑。在具体实现上,前端可以基于Node.js,Webpack或者React 组件进行开发,后端可以基于Spring Boot的微服务架构进行开发,两边通过Restful API 进行数据的交互,且独立开发和部署。这样,将一个Java Web 项目分拆为两个工程,通过前后端分离,将给开发和维护带来很大的便利。
接口与实现分离
接口和实现的分离,是Java语言倡导的基本设计哲学,通过interface和class关键字就可以搞定。从单一职责的角度来讲,接口的职责就是定义对外的服务,既然是对外,那么所有的常量和方法都是public的,因此在定义接口的服务时,建议不需要带上public关键字,而专注于服务的名称和参数设计。接口可以理解为一种契约,一种协议,在此协议下,针对不同的场景,可以有不同的实现方式。协议,即签名,它是通过接口的方式定义的,接口要做的,是保证提高稳定可靠的服务,而具体的实现,交给实现类去完成就好了。从单一职责的角度,实现类的职责就是专注于接口的方法实现,换言之,如果实现类中有公共的public方法,那么最好都是接口中声明的,即@Override注解。不建议在实现类中暴露额外的public方法,如果不得不这么做,可以重新思考接口的设计,而不是实现类本身。
实现类的职责很简单,就是为了实现而实现。如果不遵从接口的定义而开放过多的public方法,这将使得类的设计变得混乱,后期不利于扩展和维护。接口和实现分离的案例有很多,如Web开发中经常遇见的service层和对应的ServiceImpl实现类,当需要新增一个action去处理请求时,首先考虑的不是action如何写,而是应该如何通过接口去定义服务。
业务与系统分离
业务与系统的分离,是基于这样的一个事实:如果把一个Java Web工程看作一个应用系统,那么在面向业务的同时,它还有一个职责是服务于系统自身。简单的说,应用系统既要面向复杂灵活的业务,又得保持自身的稳定性、扩展性,可维护性等。从开发的角度,这意味着有些代码是用于自身配置的,如权限管控,公共工具,服务器配置,路由网关,负载均衡,RPC等,而有些代码是用于处理业务的,如查询第三方的业务库获取数据,如按照业务规则进行文件的转换等。如果不留意,随着工程代码量的增加,渐渐的会发现,业务相关的代码和系统相关的代码都糅合在一起,这将严重影响代码的质量,以至于变得不可维护。从本质上来讲,业务相关的东西是与系统本身无关的,是外界的,是不可控的,这意味这类代码需要设计的尽可能灵活。保证灵活性的一个有效途径就是让代码尽可能的职责单一,即无侵入式设计。有一个简单的方法可以检测这种设计:理想情况下将所有业务相关的代码放在一个包或一个模块中,如果直接移除这个包或模块而不影响整个工程的运行,说明满足了业务与系统的分离。
在具体的工程实现中,与业务相关的代码可以放在独立的包或模块中,而对于易变的业务,可以采用脚本语言编写,如Python,Shell,然后运行时调用即可。编程语言都有着各自的优势和不足,在处理易变的业务规则方面,脚本语言相比于强编译型语言,显得更灵活更便捷。
公共与逻辑分离
公共与逻辑的分离,讲究的是不要把本可以抽离出的公共对象,放在逻辑处理的代码中。从单一职责的角度讲,公共对象就好比日常中的小工具一样,它们彼此间相互独立,它的职责就是提供逻辑处理上的帮助。对于公共的东西,往往会多个地方会用到,即拥有全局的作用域。从实现的角度,静态类,静态方法,外部配置文件等,都可以用于描述公共对象。而逻辑,通常指的是为了实现某个功能的代码片段,对于逻辑代码,无权也并不需要维护公共对象的定义和值域,只要使用好即可。
在一个Java Web工程中,往往会涉及许多的逻辑代码,久而久之产生了许多的配置项和公共工具类,如处理字符编码,中文转拼音,Json返回值模板,邮件正文模板,数据库配置,缓存配置,Web服务器配置,本地路径配置等。
开发与生产分离
单一职责原则,还体现在开发与生产的分离。在开发环境下,我们首先考虑的是方便开发,方便代码调试。在生产环境下,我们需要尽量保持代码简洁,加载性能最优,稳定性和可靠性显得尤为重要。为此,对于开发者,在后台服务方面,比如可以引入Maven来管理依赖,可以引入内嵌的Tomcat来充当Web服务器,可以引入H2这样的内存数据库来做数据的持久化,可以引入devtools进行应用程序的自动重启。在前端开发方面,比如可以引入webpack-dev-server进行热部署,可以引入Babel进行ES6语法的转译,可以引入Eslint进行JS语法检查,等等。当进行生产环境部署时,可能需要一些额外的参数的配置,这些配置不同于开发环境下的配置,如数据库连接信息,并发控制,网络,IP等。
Spring Boot的自动配置非常的灵活,可以针对开发和生产环境进行相应的条件配置。在前端的开发中,如果使用了Webpack打包工具,也可以独立的配置开发环境和生产环境的参数,比如在开发环境,可以相应的配置模块热替换,代码映射等功能,便于开发和调试;在生产环境,可以相应的配置代码压缩、模块拆分等,尽可能的减少前端文件的大小,从而减轻浏览器加载的压力。总之,从开发到生产,是十分严肃的事情,开发和生产的分离显得尤为必要。
数据模型和处理分离
数据模型和数据处理,几乎在所有的Java Web应用中都有所涉及。数据模型,侧重于数据的定义和组织,而数据处理,侧重的是数据的使用和交互。至于数据的管理,则需要同时关注数据模型和数据处理。数据模型层面,涉及日常的数据更新和维护;数据处理层面,涉及读写操作的控制。读写控制,可以有两种思路实现,一种是代码级别做处理,具体可以表现为,将涉及读和写操作的业务代码做进一步分解,分别处理读和写。另一种思路是在数据库层进行一定的约束,提供不同读写权限的数据库用户给应用层使用。
在现有的主流框架体系中,数据模型和数据处理大体上都是分离的,这种分离的处理方式不单单指的是Bean层和DAO层的分离,还可以用到其他的POJO对象实体中。业务对象(BO)往往对应着业务数据库的实体,持久化对象(PO)的范围更大,不限于业务对象,还可以是系统本身配置用到的对象等,值对象(VO)通常用于业务层之间的数据传递,如可以用来定义RESTful API的JSON响应实体,可以用来定义不同业务之间的消息实体等。在数据处理过程中,不涉及数据模型等定义,如确实有需要,考虑到其他类不使用的情况下,可以通过内部类的方式实现。总之,模型和处理的职责都很明确,一个专注于数据结构的定义,一个专注于优雅的算法处理。
最后,用一句话来诠释单一职责原则:专注成就卓著。欢迎一起交流探讨。