设计良好的系统
注:正文中的引用是直接引用作者的话,两条横线中间的段落的是我自己的观点,其他大约都可以算是笔记了。
本章讲的是「如何设计良好的系统架构」,读起来比较困难,不论是从结构上还是从文字上。结构上作者从「建设一个城市」开讲,之后花了很大的篇幅讲面向切面编程,最后又加上了几个方法论上的东西。文字上本章作者很多说法使用的单词和其他地方看到的的略有不同。但回过头来看,作者的思想是一脉相承的,整个章节其实只讲了一件事情——隔离。
本文会一直提到一个词 「关注」 ,原文中使用的是concern,表达的是在软件开发中对某个问题的解决方案或者某个模块担任的职责(感觉还是不够贴切)。
本章有一个「结论」的小节,就先把她放上来了,内容在后边的小节里慢慢展开。
构建一个系统就像构建一个城市,一个庞大的城市系统之所以能够运转,可以归结为两个原因:
对于一个建筑,建设它的时候可能是各种起重机和钢筋混凝土,在里边工作的工人们戴着安全帽穿着工作服,可是当它建成之后在里边工作的工人们可能会是穿着整洁的西装,完全是另外一番景象。
对于一个软件系统也是如此,「启动过程」要关注的事情和之后的「运行过程」是不同的——「关注点分离」是OOP的一个基本理念——这也是本章首先要探讨的第一个「关注」。
很多的系统里都会有代码10-1这样的懒加载的代码:
//代码10-1 public Service getService() { if (service == null) service = new MyServiceImpl(...); // Good enough default for most cases? return service; }
这种代码可以带来很多好处……但是,不好的地方在于此处接口 Service
依赖于具体的实现类 MyServiceImpl
和所有它的构造器所依赖的资源,同时但它进行单元测试也会出现问题,拥有此方法的类需要关心 MyServiceImpl
的内部情况。另外,偶尔出现一两处这种代码还好,如果整个系统里存在各种各样的这样的初始化代码,那么通常会出现很多的重复性代码。一个好的解决方法就是把初始化的过程从业务逻辑中抽离出来。
一种分离构建过程的方法是简单地把所有初始化的过程放在main里或者main所调用的模块里。那么系统的依赖关系如下图所示。
把构建过程放在main()里
这样程序的业务逻辑就不需要知道main()或者它构造的对象的逻辑,所有它需要的对象都会被main()来装配好。
使用main的方法会导致一个问题,所有程序所需要的对象都会在启动的过程中被创建,这会造成很大程度的资源浪费,那么另一个可以让程序自己来决定 什么时候创建对象 ,但同时又分离了创建过程细节的方法则是使用 抽象工厂方法 ,依赖关系如下图所示
使用抽象工厂来分离
从图中可以看出,当程序(OrderProcessing)需要创建LineItem时,它去调用工厂方法来具体地执行生成对象的过程,程序只需要关系何时调用和生成什么样子的对象。
要实现创建过程和使用过程的分离,一个更强大的方案就是实用依赖注入。依赖注入将生成并装配对象的职责包裹到了一些专用的对象中去,这样就实现了「单一职责」原则。依赖注入的本意是指一些类不直接处理自己的依赖,相反是在类中定义诸如「特定参数的构造器函数」或「getter setter方法」,通过这些方法将依赖 注入 进来。在构建过程中,依赖注入容器初始化所需的对象,然后使用这些方法注入到这个类的实例中去。
如同城市是从小的定居点发展而来一样,任何系统都 一定 是从小系统不断发展成为大系统的,想要「从一开始就把系统设计的足够完善」是不可能的,相反的,我们应该「只关注当下的需求」,然后再不断地对系统进行重构,对系统进行递进式扩展。
在代码层面上,TDD,重构和clean code可以帮助进行这样的工作;在系统层面, 如果 我们适当地对关注点进行隔离,软件系统是可以实现递进式扩展的。
接着作者拿EJB来举了个反例,在EJB1或EJB2中,很多业务逻辑和容器有紧耦合的关系,要实现一个Bean,你必须实现很多容器所提供的生命周期方法,这种紧耦合就导致了分离的单元测试很难进行,同时一个bean也不能通过继承另一个bean来实现功能的扩展,这就导致了OOP概念被破坏,会导致系统中存在冗余数据。
很多关注点(像持久化这样的)通常是跨越不同层次的对象的,原理上说,这些关注应该被封装在一个单独的模块内,但实际中这种逻辑往往是跨越不同层次的对象的。这就引入了AOP的概念,AOP往往是解决像持久化、安全、事务等这种在某个切面上的关注点的通常方法。使用一些AOP的框架可以在实现最大限度的非侵入性的前提下解决切面的关注点的需求,下面将列举Java中3种AOP的解决方案。
在简单的AOP的需求情景里,使用JDK自带的代理来做是比较合适的。使用Java代理,必须使用Interface,不然就要使用CGLIB,ASM或者Javassist等字节码修改库。
Java代理的优点在于简单,缺点则是对于简单的逻辑来说代码稍微有点复杂,你必须去定义一个传出对象的接口,然后实现一个动态代理的接口等等,还有就是实用Java代理不能实现一个全系统的切面。
作者在这一节里讲了Spring的AOP方法,它实现了业务逻辑和切面的解耦。
Spring和Jboss等纯Java的AOP框架可以覆盖百分之80到百分之90的情况,但AspectJ提供了更加丰富和强大的面向切面编程工具,AspectJ的缺点就是你需要使用一些新的工具和学习新的使用方法和语言结构。
使用像AOP这样「隔离关注点」的编程方法可以带来数不尽的好处,它使得我们在代码层面避免了很多紧耦合实现,因为很多逻辑都可以抽象到切面或其他不同的关注点里,留下的逻辑可以在简单的POJO中实现,这就使得真正的使用 测试来驱动 你的系统架构成为可能。
另外,任何系统都是从不完美慢慢发展而来的,永远不要想着在开始就设计好了全部的事情(BDUF, big design up front),BDUF往往会带来很多坏处。对于建筑行业来说,他们必须使用BDUF,因为他们的架构一旦建好就不能更改了,然而软件系统有着自己独特的 物理结构 ,使得去做一些较彻底的改动成为可能。
所以一个软件系统的建设应该是这么一个过程,首先是设计一个「简单的但是良好解耦」的架构,快速的完成客户的需求,然后随着需求的变化不断的进行扩展。许多优秀的网站,有着复杂的数据缓存机制,安全机制,虚拟化机制等等,这些都是因为在每一个抽象层面上它都很「简单」。
模块化和关注点分离使得「去中心化的管理和决策」成为可能。不论是一个软件系统或者一个城市的建设,没有一个人可以对所有的事情都做出有建设性的决策。
很多时候我们习惯于在系统设计的一开始,就想好所有的可能情况,但是最好的做法应该是「把决定放在最后一刻去做」,这并不是懒或者不负责任,这可以使得我们所有的决定都是基于最大可能知道的知识而做出的
就像建筑行业一样,在长久的实践中,在软件系统开发的行业里也积累了很多的标准,如EJB,这些标准使得想法和系统组件得以复用。但是有时候一个标准的使用已经到了 会给开发带来各种问题 的地步,如果你有更好的方法(但是没有遵守现有的标准)来实现客户价值的时候,不需要死板的遵守标准。
合理的使用DSL,可以帮助对系统中的不同概念进行抽象,系统的设计者使用DSL来设计系统,可以摆脱具体实现细节的束缚,能够好地对系统各个模块进行抽象设计。
本文链接地址: 如何设计良好的系统架构(clean code阅读笔记之十)
转载须以超链接形式标明文章原始出处和作者信息及版权声明