博尔赫斯说:“写散文体的短文——寓言、神话、短故事——给了我某种神秘的满足。想起这些篇章,就仿佛想到硬币:实在、结实、闪光的小物体,更多的东西的样品。”显然,小物体之美,让博尔赫斯着迷。
同样,在软件设计领域里,小的设计同样让我着迷。这里所谓的“小”,并非绝对的小,而是强调一种恰如其分的设计哲学。在开发过程中,每一次迭代的目标不宜设立过大,需小步前行,避免过度设计。在设计开发时,整个系统最好由松散耦合的细小模块组成。这些细小模块由于功能相对独立而单一,因而更易于理解。
在设计系统架构时,我们要注意克制做大做全的贪婪野心,尽力保证系统的小规模。Unix的缔造者之一Dennis Ritchie就曾遭受过将系统做大做全的滑铁卢。他在贝尔实验室的第一个任务,是参与大项目Multics,即开发一个前所未有的、可以多人使用的、同时运行多个程序的操作系统。该项目由贝尔实验室、麻省理工学院和通用电气公司三方联合研制,但是由于设计过于复杂,迟迟拿不出成果,1969年贝尔实验室宣布退出。
痛定思痛,Dennis Ritchie和同事Ken Thompson之后在设计Unix时,就吸取了Multics设计复杂而导致失败的教训,提出了"保持简单和直接"(Keep it simple stupid)的原则,即所谓KISS原则。
遵循KISS原则,整个Unix系统由许多小程序组成,每个小程序只能完成一个功能,任何复杂的操作都必须分解成一些基本步骤,由这些小程序逐一完成,再组合起来得到最终结果。表面上看,运行一连串小程序很低效。但是事实证明,由于小程序之间可以像积木一样自由组合,所以非常灵活,能够轻易完成大量意想不到的任务。而且,计算机硬件的升级速度非常快,所以性能也不是一个问题。另一方面,当把大程序分解成单一目的的小程序,开发变得容易,Unix在短短几个月内就问世。
我们千万不要轻视小的力量。
专注的小可以保证独立进化。
从“自治”思想看,它需要实现一定程度的自给自足,并保证对外交互的接口足够稳定。这符合老子的“大国小民”的思想。
这种专注的小首要在于分离职责。一种分离的角度是以“内外之分”观察核心与边缘的职责,然后以模块分离的方式分别为它们寻找到“安身立命之所”。在框架设计上,为保证框架的小,我们常常采用这样的观察视角,这就是所谓的“内核模式”,通过识别出整个框架的核心功能,以此作为整个框架的基础,而其余功能皆可视为框架的外围功能,根据关注的域对其进行切分。这些外围功能互相之间应尽量减少耦合,使其能够独立进化。
Spring框架的设计正是遵循了这样的设计理念。除了Spring IoC是整个框架必备的组件之外,Spring MVC、Spring Batch Job、Spring Data等之间没有依赖关系,可以根据项目自身情况酌情裁剪。
那么,如何才能保证设计的系统足够小?首先,在设计思想上要确立“小即是美”的美学观,要清晰地辨别且能够欣赏小的灵活之美,完整之美与轻盈之美。只有在思想上认同它,你才能顺势而为;只有从心理上感受到这种美丽,你才能响应它的召唤。
灵活之美,在于它能快速地响应变化,这种变化可能是局部的,也可以是整个设计方向的改变。例如,在多数企业系统和互联网系统中,都需要分离Online和Offline任务,以指定不同的架构决策。又例如,我们可以设计独立的、具有最小功能子集的Batch Job来承担后台任务。这些Batch Job可以作为一个单独的应用程序执行在单独的进程中。一旦需求要求我们对设计做出改变,我们也能将修改控制在足够小的范围中,从而保证对整个系统不会带来巨大的影响。
若遵循EDA(Event Driven Architecture)模式,我们可以根据业务领域的不同,设计出功能最小完整的自治组件。组件之间的通信通过事件来传播,利用发布者/订阅者的方式解除组件之间的耦合;又或者利用消息传递来处理业务逻辑,例如在AKKA中,我们可以设计出灵活而小的Actor对象;微服务(Micro Service)架构则从服务级别展现了设计的灵活之美。
轻盈之美,体现在它的功能并不臃肿,对外部的依赖较少,既容易在系统中快速引入,又不会使原有系统变得笨重,还能很方便地部署或者启动。
展现了轻盈之美的组件往往具有良好的可测试性。我们可以利用六边形架构将系统分隔为内、外两个边界,凡是系统对外的通信,皆通过端口(Port)和适配器(Adapter)完成,这样就能较好地解除对外部环境的依赖,提高系统的可测试性。而清晰的边界划分也是设计小组件的一种有效手段。
若对于框架或平台而言,则需要尽力降低框架或平台的侵入性。当年Rod Jonson之所以提出J2EE Without EJB,正是因为EJB的侵入性带来了诸多病症。当然,从另一个角度来讲,我们自己研发的产品或项目也要尽可能摆脱对外部资源的依赖,即所谓“稳定依赖原则”。Robert Martin提出的Clean Architecture清晰地勾勒出这样的思想。在Clean Architecture的表述中,他让外部易变的部分依赖于更加稳定的部分,如域模型,而非形成相反的依赖关系。这样还可使实现变得更易于变化;多变的部分依赖于稳定的部分。好架构就要能轻松地改变那些易变的决定。
完整之美,在于它是自足的。完整并不意味着大而全,而在于它足够精简,没有冗余。当然,它同时应该是没有残缺的。残缺,意味着它无法在没有外部支持的情况下,完成自己应该完成的工作。这种美感符合“麻雀虽小,五脏俱全”的标准。Standalone的微服务,正好体现了这种自容器的完整之美。
小的益处还有一点,它可以使得我们在架构决策或技术选型时,可以变得更加从容。
譬如说,因为某些原因我们需要将整个企业系统(Monolithic架构)从WebLogic上迁移到JBoss上,无疑,这是一个艰难的决定,实施起来更是一个漫长的过程。如果系统是基于Micro Service的架构风格进行构建,每个服务根据各自情形选择自己的技术栈。倘若需要对某些服务进行技术栈迁移,相信这个问题不再变得棘手。——大象可以轻盈地跳舞,但付出的努力会百倍于一只敏捷的狐狸。
如今,Java已经发展到Java 8,引入的Lambda表达式等多个特性如此鲜嫩,让人垂涎不已。然而据我所知,国内多数企业的Java项目仍然停滞在JDK 6裹足不前。是JDK 8不够好吗?非也。盖因为求稳的他们仍然心存顾虑。即使Oracle号称这种JDK的迁移多么的平滑,多么的稳健,多数企业仍然不敢轻易做出迁移的决定。若因为迁移而带来未知的缺陷,可谓得不偿失。既然现在项目运行良好,何必冒此风险。
于是,我们这个行业因为系统的庞大而变得守旧老成,亦步亦趋。并非大家没有冒险的精神,实则是庞大的项目难以灵活地改变方向。倘若只是更新系统中的某一个库或框架,形势就截然不同了。记得在没有lambda的时代,当我们让客户看到了Guava的好处时,要引入Guava就轻而易举,真如顺水推舟了。
当我们发现某些功能具备独立和专注的特征时,都是可能做出小系统的机会。这些小系统并不一定是子系统或模块,它还可以是一个独立的应用或服务。
例如在一个税务系统中,需要生成复杂的税务报表。它的整个逻辑是相对独立的,不管是报表的动态生成,格式的转换,数据的查询以及流的处理和PDF文档的生成,都与系统其他部分关联不大。唯一可能与系统存在紧密关联的是数据库。但为了解决高峰期的性能问题,我们可以建立单独的数据提取器,又或引入流处理,定期将数据提取出来,放入内存数据库中。将这样相对独立的功能做成服务,就能够独立演化,并有效支持服务请求的可伸缩。这样的小型服务可以更灵活地应对变化。当我们发现内存数据库不能满足大量请求时,也可以轻而易举地将其迁移到NoSQL上,并根据数据的属性例如按照地域进行分区,支持水平扩展。
若要保证系统的小,我们还可以尝试使用脚本。在开发软件系统时,可以使用一些脚本语言来开发一些小工具,以应对灵活的需求变化,消除重复代码,实现某些步骤的自动化。例如用Groovy编写一些函数,用Ruby编写代码生成工具,又或者使用Gradle、SBT实现系统的自动部署,启动服务器等脚本。脚本语言具有很好的灵活性,而动态语言的特性也使得我们能够编写出短小精悍的超级小工具,甚至可以作为系统模块之间的粘合剂,如机器齿轮上的润滑油一般,让整个系统充满活力。
Russ Miles认为:团队的开发速度经常因编写代码体积和复杂度的增长而放缓。他认为组件结构在简化架构方面非常重要,并提出了Life-Preserver模型。这是一个环形结构,所有的基础设施软件都在环上处理集成,而核心业务组件在环内加入业务价值。这种模型的核心是利用事件来简化架构。
使用事件的一个显著特点是解耦,使得事件的发布者与订阅者都可以独立演化,也可以自由增加事件的订阅者数量。我们可以将事件的发布者与订阅者设计为独立的小程序,就像Unix系统中那些小工具一样,通过管道建立关联。这样的设计思想称之为EDA(Event Driven Architecture)。
当然,要做到系统的“小”,必然也是要付出代价的。奥卡姆剃刀定律认为:“若无必要,勿增实体”。剖析Kent Beck提出的“简单设计”原则,在满足了客户功能、无多余重复、清晰表达设计意图的前提下,需要遵循奥卡姆剃刀定律。盖因为系统分解得约小,就会因为“实体”数量的增加,引入额外的复杂度,包括对实体的管理、实体之间的协作等。显然,任何设计决策都有其两面性,我们需要放入到当下的上下文(Context)中做出正确的判断。正如爱因斯坦所说:“让它尽可能简单,但不要过于简单。”看来,我们对任何事情都需要把握一个“度”,水满则溢,月盈则亏,故而需要损有余而补不足。