小钢的架构思考:架构设计

原创文章,转载请注明:转载自Keegan小钢
并标明原文链接:http://keeganlee.me/post/architecture/20160621
微信订阅号:keeganlee_me
写于2016-06-21


小钢的架构思考:什么是架构
小钢的架构思考:架构规划
小钢的架构思考:架构设计


最近一个多月因为忙于工作上的项目重构,所以文章一直没能更新。现在,重构终于暂时告一段落,于是,赶紧抽时间把文章写完更新发布。下面进入正文。


架构规划的结果,整理出一堆不同优先级的需求,尤其是质量需求之后,接下来就要思考如何才能最大限度地实现这些需求,这就是架构设计要解决的问题。那么,如何进行架构设计呢?设计到什么程度才合适呢?我从架构思维和架构原则方面来思考架构设计的问题。

架构思维

这里说的架构思维是指进行架构设计时最高层级的思考方式,比如:面向过程、面向对象、面向切面、面向服务等。

面向过程(Procedure Oriented)

面向过程的设计思路就是将问题分解成一个个步骤,按照步骤一步步执行之后,问题就解决了。每一个步骤就是一个子过程,也可以称为一个模块,子过程还可以继续拆分成更多更细的子过程。因此,面向过程的设计核心就是过程分析、功能分解,一般采用自顶向下、逐步求精的分解方式。一个大的程序可以分解成多个子程序,子程序再分解成多个大模块,大模块再分解成多个小模块,最终分解成一个个函数。

在此我想借用一个象棋对战的例子,例子来源于一篇很老的文章:架构师之路(4)---详解面向对象。以下是采用面向过程的设计思路分解的对战流程图:

将以上每个流程分别用函数实现,问题就解决了。

面向过程的优点主要有两个:一是流程清晰简单;二是性能比较高。尤其是性能,这也是为什么至今很多单片机开发、驱动程序开发、或其他与硬件相关的系统开发等对性能要求很高的软硬件程序依然在用面向过程的方式进行设计和开发。

面向过程的缺点也很明显:一是主程序太重,主程序与模块承担的任务不均衡;二是函数不易扩展,导致其可扩展性、可复用性、可维护性相对都比较差;三是上下层级模块之间的联系太紧密,耦合高,所以模块也难以复用。

面向对象(Object Oriented)

面向过程的思路是“怎么做”,关注于实现细节;而面向对象的思路是“谁来做”,关注于抽象的对象。对象的封装、继承和多态等特性,让我们以更接近现实世界的方式来思考程序设计。面向对象相比面向过程容易实现更好的分离,相应地可扩展性、可复用性、可维护性也会比较高,但同时会牺牲掉一些性能。不过,也因为硬件发展迅猛,所以牺牲的那点性能也不算什么了。

面向对象设计的难点在于抽象,从问题域中抽象出一个个对象,并找出它们之间的关系。好在有SOLID原则和一大堆设计模式指导我们如何更好地设计。也有领域驱动设计的方法论指导我们怎么进行领域建模。

还是象棋对战的例子,用面向对象的设计思路,可以抽象出以下三种对象:

  1. 棋手:负责行棋,红黑两方行为一致。
  2. 棋盘:负责绘制棋盘画面。
  3. 裁判:负责判定吃子、犯规和输赢等。

三者关系如下图:

棋手对象行棋后,棋盘对象根据棋子布局的变化刷新棋盘画面,裁判对象则对棋局进行判定。

面向切面(Aspect Oriented)

面向切面,也就是AOP,是对面向对象的一种扩展,为了弥补面向对象的局限性。面向对象设计主要是对业务领域进行抽象封装,但对于业务领域之外的内容,比如日志记录、权限检查、事务支持等,在没有AOP之前,只能将实现这些功能的代码散布在所有对象层次中,但这些代码与所散布的对象的核心业务功能是没任何关系的。这种做法也导致了大量重复的代码,而且难以复用。AOP就是为了解决这种问题而产生的,将这些与业务领域无关的部分分离出来,以横切面的方式注入系统,从而减少重复代码、减低耦合度、增强扩展性和维护性。

将日志记录、权限检查、事务支持等等使用横切技术分别独立成一个个服务模块,这些模块也称为“横切面”,这样就可以将这些与业务无关的服务从业务核心中解耦出来,就可以将系统划分为两部分:业务核心和通用服务。业务核心依然采用面向对象的思路去设计,而通用服务则可以采用面向切面的思想来实现。

Spring就大量使用了AOP技术,OkHttp的Interceptor也是AOP设计的一种实现。很多场景都可以使用AOP的思想去设计,比如添加统一的Http Request Header,添加统一的登录验证,添加统一的缓存,添加统一的错误处理,等等,只要是通用的功能点基本都可以使用AOP的思想去设计和实现。

面向服务(Service Oriented)

不管是SOA还是现在流行的微服务架构,都是采用面向服务的思维方式。说到面向服务,需要先了解一个概念:Monolith,也称为单体架构。在没有SOA思想之前,软件系统将所有功能整合成一个独立的软件包,然后部署在单一的平台上。比如,在J2EE平台,一个软件系统最终会打成一个包含所有功能的WAR包,然后部署到Web容器中。若要扩展的话,则通过复制这个WAR包部署到多个Web容器来实现。这种方式,如果程序需要改动,不管多么微小的改动,都需要重新打包个新的WAR包,并替换掉所有Web容器的旧WAR包。

面向服务的架构思想则是,将系统的不同功能分离成一个个单独的应用程序或组件,统称为服务,不同服务部署在不同容器中,不同服务之间通过一些轻量级的交互机制来通信,如HTTP,RPC等。这样,相比单体架构,功能服务之间明显是松耦合的,扩展也会灵活很多。而且,不同服务还可以用不同编程语言实现,部署到不同平台。

不管是面向过程,面向对象,面向切面,还是面向服务,最本质的区别还是在于看问题的角度不同。而在实际应用中,也不会只使用一种架构思维,而是综合考虑的,系统的不同方面或不同层级可能会用不同的架构思维去思考。比如,一个庞大的复杂系统,整体上可能用面向服务的架构思维去拆解各种服务,业务核心方面的服务可能再用面向对象的架构思维进行建模,通用功能服务还是用面向切面的架构思维来设计,事务流程当然是采用面向过程的架构思维最直观。

架构原则

架构思维从面向过程,到现在的面向服务,以后也不知道还会出现什么新的思维方式。但无论是何种思维方式,都存在一些共通性的架构原则,可以指导我们如何设计出一个合适的架构。从另一方面来说,架构设计,不管是面向过程、面向对象、面向切面,还是面向服务,无一例外,主要都是在对复杂的系统进行分解。那么,相应地,就需要思考三个问题:分解为哪些?如何分解?分解到什么程度?相对应地,有三个重要原则可以分别为解答这三个问题提供指引。

关注点分离原则

关注点分离原则主要就是为了解决将复杂系统分解为哪些部分的问题,分解出来的部分就是关注点。过程、对象、切面、服务,只是分解的角度(也是关注点)不同而已。将复杂的问题根据不同的关注点分解为多个相对简单的问题,再对每个简单的问题进行分别处理,这就是关注点分离。分离之后,各个关注点相对独立,每个关注点的变化基本不会影响到其他的关注点,即使需要改变,改变的部分也很小。需要扩展时,影响也将会最小化。

关注点分离,最难的在于如何识别出有哪些关注点。要识别出有哪些关注点,需要将复杂系统不同的方方面面抽象成一个个具有清晰明确的边界的概念模型,或为“对象”,或为“组件”,或“切面”,或“服务”,以将复杂问题分解为一个个相对简单的问题。

从不同维度,可以有不同的分离方案。除了上面提到的面向过程、面向对象、面向切面、面向服务等思维角度之外,还有如下图所示的其他几种不同维度,该图引自《软件架构设计》一书中的【2.1.1 关注点分离之道】一节:

上图分别从功能职责、通用性、大小粒度的不同维度进行分离。从职责维度进行分离,就可以分为三层架构:展现层、业务层、数据层,相应的关注点就是:数据展示、数据加工、数据管理。另外,数据层还可以再分离为网络层和缓存层。从通用性维度来看,就可以分离出技术通用部分、领域通用部分、特定应用部分。一般,使用框架技术就可以用于分离各种不同的通用部分。从大小粒度的维度考虑,无非就是将复杂系统分离为各个子系统,再分离为不同模块,再细分到不同类。

在实际应用中,并不会只采用一种维度,而是多种维度综合考虑,不同部分采用不同维度的分离方案。比如,也许,整体上按职责分离为多层架构,然后,在某些层级根据大小粒度再进行分离,例如将业务层按照不同业务模块进行分离。另外,也会将不同的通用部分进行分离,例如可将技术通用部分的日志记录、领域通用部分的权限检查分别分离出来。

《架构就是关注点分离》这篇文章则描述了更多关注点分离的例子。

高内聚低耦合原则

系统应该如何分解?或者说关注点应该如何分离?高内聚低耦合原则就可以为该问题提供设计指引。

内聚是指模块内部的功能和元素之间的紧密程度,而耦合则是指模块与模块之间的关联程度。

内聚可分为好多种:功能内聚、顺序内聚、通信内聚、过程内聚、时间内聚、逻辑内聚、偶然内聚。功能内聚是最强最好的内聚,模块内各元素共同协作完成一个单一的功能,这些元素紧密联系、缺一不可。顺序内聚则是指,模块中各个处理元素和同一个功能密切相关,而且这些处理必须顺序执行,通常前一个处理元素的输出时后一个处理元素的输入。顺序内聚的内聚度也比较高,但相比功能内聚,缺点就是可维护性相对差些。偶然内聚则是最弱的内聚,模块内的各元素之间没有任何联系,只是偶然地被凑到一起。

耦合也分为好多种:非直接耦合、数据耦合、标记耦合、控制耦合、外部耦合、公共耦合、内容耦合。非直接耦合表示两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的,其耦合度是最弱的,模块独立性最强。数据耦合表示调用模块和被调用模块之间只传递简单的数据项参数,相当于高级语言中的值传递。标记耦合也称为特征耦合,表示调用模块和被调用模块之间传递的不是简单数据,而是数据结构,像高级语言中的数据名、记录名和文件名等数据结果,这些名字即为标记,其实传递的是地址。控制耦合则表示模块之间传递的不是数据信息,而是控制信息例如标志、开关等,一个模块控制了另一个模块的功能。外部耦合则是指一组模块都访问同一全局简单变量,而且不通过参数表传递该全局变量的信息。内容耦合则是一个模块直接访问另一模块的内容,这是最强的耦合。

高内聚的设计原则是说:一个模块只完成一个单一的功能,尽可能使模块达到功能内聚。
低耦合的设计原则是说:若模块间必须存在耦合,应尽量使用数据耦合,少用控制耦合,慎用或有控制地使用公共耦合,并限制公共耦合的范围,尽量避免内容耦合。

适度设计

适度设计原则关注的就是系统分解到什么程度的问题。适度设计就是指设计不要过度,也不要不足。那么,怎样才算设计过度?怎样才算设计不足?一句话,设计过度就是想太多,设计不足就是想太少。感觉好虚,是吧?我也这么觉得。因为,如何判断一个设计是否过度或不足,并没有标准的可量化指标。因此,设计是否适度,更多在于主观的判断。而如何避免设计过度或不足,更多的也在于个人经验积累所形成的直觉。

设计不足相对还比较容易判断,导致设计不足的原因主要有两个:一是因为新手的设计经验不足而导致;二是因为一味追求快速实现产品功能而跳过或大幅度减少了设计而导致。

也有些设计过度比较明显的例子,比如Uncle Bob提出的Clean架构,每个关注点都有着清晰明确的边界,架构真的很清晰,可维护性、可测试性都非常不错,高内聚低耦合。但是,如果将其应用到一个只有两三个开发人员的小团队的小项目中,就会明显发现代码量大而且复杂,每需要添加一个小功能,却需要编写大量代码。这对一个小团队小项目来说,明显不适合。Clean架构比较适用于人员较多的团队,和中大型项目。

因此,判断设计是否适度,不能脱离团队和项目的现状。另外,还有其他现状因素,包括各种商业需求、功能需求和质量需求。大部分情况下,形成过度设计的原因在于:一是过多地考虑了未来可能发生的变化;二是为了追求设计而设计。适度设计,首先应该着眼于当下,当下的需求、当下的开发成本、当下的人员和项目现状;其次才是适当考虑如何应对未来的变化。对于未来的变化,也不是任何可能都要考虑,只需考虑在可预见的未来里有非常大的几率会发生的变化即可,这个非常大的几率可以达到90%以上。比如,已经确定要实现的需求,只是因为优先级问题而稍微延后;比如,已经确定的人员扩充计划;比如,双11要搞活动,交易量将会激增;等等。

也就是说,适度设计的原则,可以总结为:设计应该优先满足当前确定的需求,再满足可预见未来里几乎可以确定会发生的需求。只满足当前需求而不考虑未来,就容易导致设计不足;而过多地考虑未来可能发生的需求,就容易导致设计过度。因此,适度设计需要在当前需求和未来需求之间做好平衡,而我觉得只考虑当前需求和未来几乎确定会发生的需求是最好的平衡点。

写在最后

本来计划还想再谈谈架构风格,比如分层架构、MVC\MVP\MVVM、RESTFul、Clean架构等等。但因为这篇文章的进度已经拖了一个多月,不能再拖了。因此,决定本文就此收手,那些架构风格以后有机会再讲吧。


扫描以下二维码即可关注订阅号。

Comments
Write a Comment
  • 有态度网友06MY8Q reply

    可以补充点DDD(领域驱动设计)的内容

  • 有态度网友06MY5Z reply

    DDD在前一篇文章就有提到了,不过没有深入

  • 有态度网友06MY8R reply

    光思考有什么用

  • 有态度网友06MY5Z reply

    没有思考,哪来的升华

  • 有态度网友06MY8R reply

    升华一定要有落地的方案与事实实行才会产生

  • 有态度网友06MY5Z reply

    落地方案是下个阶段的事,要一步一步来,步子大了容易扯到蛋

  • 有态度网友06MY9I reply

    适度设计好难把握啊

  • 有态度网友06MY5Z reply

    嗯嗯,适度设计是很难把握的,需要经验积累才行