自顶向下设计
当我们开发或者重构一个软件系统时,需要进行很多框架、中间件上的选型。这些选型的粒度有大有小,解决其中一种或几种系统问题。而我们正是通过将这些框架、中间件进行搭配、组合来完成一个系统的骨骼。
如果把软件系统开发比喻成建造一个城市的话,那么对框架与中间件的选择,则对应各种城市的各种职能设施与其建设的配置。我们需要评估我们的需求,然后选择在最合适的位置上摆放最合适的设施,从而用最低的代价满足我们的需求。
城市不是只有市长一个人管理,它有职责上的划分:教育、医疗、交通;也有地域上的划分:区、街道。城市需要在不同的维度中由专人负责,然后依次向下管理。与城市一样,系统中我们也不可能直接由负责人全权负责所有细节,我们会将系统进行各种程度的抽象,将其划分为不同的模块,让其放入到我们选择的框架并用各种中间件进行串联。
如果这种设计是从最宏观的开始设计的话,我们就将其称为自顶向下设计。在自顶向下设计时,我们要关注系统的核心问题,通过框架、中间件、核心代码解决完核心问题后,便是对系统模块进行层级划分,最后才是编码进行具体业务的实现。
我们将模块根据不同的规模进行划分,同时也将人员根据模块的划分情况进行安排,从而完成了软件团队中的分工。这样做的好处便是,开发人员只需要处理当前分层中的业务就可以完成工作。相反,如果层级之间出现交织,就需要开发者们了解跨越层级的内容,而导致所有人需要掌握的细节变多,开发效率就会变低。
所以,我们需要一些手段来保证这样分层的整洁,让自顶向下的分层设计可以发挥最好的效果。
运行态分离
尽管系统项目的启动过程十分的重要,但与按天、月为单位运行的系统运行时间来比,系统的启动过程就不那么重要了。主要是对于启动的过程,我们往往会投入更多的关注,确保启动完成。相比之下,我们更期待程序的运行状态可以稳定地运行。
对于使用Spring框架的后端开发人员来说,Spring已经通过了各种的手段对启动状态进行分离:例如Application上的启动注解、Spring-boot相应的自动装配能力、以及对于Autoware或者Resource的依赖注入等。一方面我们要理解Spring已经很大程度的帮助我们将注意力从启动状态与运行状态分离了,另一方面我们更不应该破坏这种分离状态。
举一个比较常见的例子就是延迟加载。我们可能会通过一个初始值为null的变量来进行延迟加载,当我们需要的时候才对这数据进行创建,同时在重复使用的时候可以直接使用,而不会重复创建。也可以说这是懒加载的单例模式。
延迟加载有其自身的用途,但是会引入新的问题:将初始化代码和程序运行代码耦合在了一起。而这样直接引来的问题就是我们很难对其进行单元测试(用mock类进行替换)。以及很难确定当出现问题的时候,所有这样的代码处于什么状态。
对应的解决办法就是将这种耦合进行分离,利用诸如抽象工厂类、或者Spring的@Lazy注解来实现。这样我们可以通过直接替换测试用的工厂类或者关闭@Lazy来让程序变得可测试。
统一抽象
我们在实际开发过程中总会有如下需求:对所有指定前缀名称的Service都开启事务、对所有接口调用都打印参数日志。如今,我们很容易就能想到可以使用AOP来实现这样的需求,但是我们经常忽略这个做法背后的思想:对模块的统一抽象。
由于我们对模块之间进行了分层,我们可以以经典的三层模型来举例。假设当我们在编写Controller层的,当我们希望对我们的下游调用的所有入口进行入参日志的输出,那我们可以有三种处理方式:
在Controller调用前打印
在Service进入后打印
在出Controller与进入Service之间
但不论我们是在Controller中还是在Service中执行,我们都需要在所有实际出发代码的时候增加新的逻辑。而如果我们选择在Controller与Service之间进行功能的增加,我们就可以将这部分逻辑剥离到两层业务逻辑之外,放到编程框架层面的,而这种方法就需要我们对模块进行统一抽象。
前面说了一种AOP的实现方式,其中我们一般是通过注解或者包路径来描述处理对象,除AOP外常用的还有Java代理类的方式(尽管Spring本身就包含大量代理)。
统一抽象的主要目的在于,将所有一层级的模块抽象成一个概念,就可以统一为其增加功能,从而使得下层逻辑不用特别的对该功能进行关心。事实上,对于可以进行统一抽象的模块功能,它极大概率是不与下层逻辑耦合的,所以即便我们没有对其进行统一的抽象,也应将它们分离出来然后单独的维护。
所以,Aop和代理类只是为了达到统一抽象目的一种实现手段。
延迟决策
显然,在系统的构建时,所有的功能都应该是经过了设计之后再实施的。但我们很难再一开始就将所有的决策确认完毕。一般来说,如果我们需要进行决策时需要收集到足够的信息,例如:根据具体的TPS值确认是否要增加数据缓存。但如果当前的信息、预备知识不足以支撑我们对问题进行的决策,我们可以延迟我们决策的时间。
延迟决策的优点在于,你可以在你获得了足够的信息后,再进行正确性更高的、更有效的决策,这是你可能有更多的用户反馈、或者更完整的测试数据用来对你的决策进行支撑。
在具体的决策前,我们可以先根据“业界方案”来进行设置,因为这样的好处是可能存在更多的相关经验人员,同时出现问题可能有更多的解决方案。
而为了系统能支持延迟决策,我们需要系统进行模块化设计,并已经基于切面地进行了统一抽象设计。这样才能让我们拥有足够的知识后,具备进行决策的时机后,可以快速地进行业务调整。
最后
系统设计中的耦合危害,比方法层级和类层级更加严重。我们是希望系统具备敏捷能力的,这会让我们在多变的业务中快速进行调整。如果系统开始上下层级耦合,则会让测试变得困难、重构没有保证,直接结果就是生产力降低。本文讨论了三种主要的注意点,让系统在层级上保持抽象,让系统具备单独设计模块,最大程度上简化方案的可能性。