架构思维与方法论
1.修心
1.1 修炼同理心,认同他人的能力
- 在架构设计上多转换视角,明确每个人要做的功能。
- 搞明白我们是在改善系统还是在破坏系统。
- 认同并信任他人的能力,不要在还没有全面理解他人的思想的情况下去调整现有的代码设计逻辑,读懂他人的思想。
- 多积累经验,理解用户核心诉求,保持空杯心态,认同他人。
1.2 大局观念,保持好奇心和韧性
- 理论结合实际,架构不等于框架。
- 明确什么需要迭代,什么不需要,搞明白系统的全貌。
- 持续地学习,保持好奇心,看见新科技与思想,先认同它,体会它,理解它产生的需求背景与技术脉络,融入自己的知识体系。
- 学习需要韧性,不是所有技术都值得深耕,明确自己的兴趣,化腐朽为神奇。
1.3 迭代能力,学会否定自己
- 每一次改代码的过程都是对自己架构能力升华的过程。
- 每一次开发新的任务都是重新审视自身架构合理性的机会。
多思考如下问题:
- 那些潜在需求未来可能需要被满足?
- 如果这些需求在早些被提出,架构如何设计更合理?
- 当前的架构迭代到新架构的成本开销多大?
关于架构调整与迭代:
- 早迭代,小步迭代要比做一个大版本的重构要好。
2. 架构设计准则
架构设计的基本准则:
- KISS: 简单优于复杂;
- Modularity:聚焦模块而不是框架;
Testable:保证可测试性;
Orthogonal Decomposition:正交分解。
- OCP:开闭原则
其中,各项准则解释如下:
KISS 即 Keep it Simple, Stupid,不增加无谓的复杂性。正确理解系统的需求之后才进行设计。
Modularity,强调的是模块化。从架构设计角度来说,模块的规格,也就是模块的接口,比模块的实现机制更重要。框架是易变的。框架是业务流,可复用性相对更低。框架都将经历不断发展演化的过程,逐步得到完善。
Testable,强调的是模块的可测试性。设计应该以可测试性为第一目标。可测试往往意味着低耦合。一个模块可以很方便地进行测试,那么就可以说它是一个设计优良的模块。模块测试的第一步是环境模拟。模块依赖的模块列表、模块的输入输出,这些是模块测试的需要,也是模块耦合度的表征。 只有在模块开发过程中我们就不断积累典型的测试数据,以案例的形式固化所有已知 Bug,才可能在架构调整等最容易引发问题的情形下获得最佳的效果。
Orthogonal Decomposition,中文的意思是 “正交分解”。架构就是不断地对系统进行正交分解的过程。:“优先考虑组合,而不是继承”。如果我们用正交分解的角度来诠释这句话,它本质上是鼓励我们做乘法而不是做加法。组合是乘法,它是让我们用相互正交、完全没有相关性的模块,组合出我们要的业务场景。而继承是加法,通过叠加能力把一个模块改造成另一个模块。
Open Closed Principle,OCP 软件实体(模块,类,函数等)应该对于功能扩展是开放的,但对于修改是封闭的。应尽量通过扩展软件实体的行为来应对变化,满足新的需求,而不是通过修改现有代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。通过引入插件机制把系统分解为 “最小化的核心系统 + 多个彼此正交的周边系统”。事实上回调函数或者接口本质上就是一种事件监听机制,所以它是插件机制的特例。
3. 少谈框架,多谈业务
不要让框架绑架业务。
在架构的两侧,一边是用户需求,一边是技术。接口代表用户需求,代表业务。框架代表技术,是我们满足需求的方法。
框架,体现的是需求泛化的能力。从架构思维角度上来看,它是通过抽象出需求模板,把多个需求场景中变化的部分抽离出来,形成相对稳定的泛化需求。
框架的抽象能力不是一蹴而就的,它既依赖我们抽象系统的能力,也依赖我们对领域需求的理解程度。所以框架会随着时间而迭代,逐步向最理想的状态逼近。不要让代码逃逸出框架,把系统搅得支离破碎。架构层面最好能够提前预测所有可能的需求,以此抑制潜在代码逃逸的风险,坚持不要往核心系统中增加新功能。
4. 不断地重新审视边界
不同的业务模块,分别做什么,它们之间通过什么样的方式耦合在一起。这种耦合方式的需求适应性如何,开发人员实现上的心智负担如何,是我们决策的影响因素。
为了避免留下难以调整的架构缺陷,认真细致做好需求分析,并且在架构设计时,认真细致地过一遍所有的用户故事(User Story),以确认架构适应性。
多梳理自身与外部组件的依赖关系,引用/调用关系
5. 完善中的架构范式
每一个设计模式都应该放到他相应解决的问题领域来看,架构范式也是如此,常见的架构原则如下:
- 接口隔离原则(Interface Segregation Principle,ISP):一个模块与另一个模块之间的依赖性,应该依赖于尽可能小的接口。
- 依赖倒置原则(Dependence Inversion Principle,DIP):高层模块不应该依赖于低层模块,它们应该依赖于抽象接口。
- 无环依赖原则(Acyclic Dependencies Principle,ADP):不要让两个模块之间出现循环依赖。怎么解除循环依赖?见上一条。
- 组合 / 聚合复用原则(Composition/Aggregation Reuse Principle,CARP):当要扩展功能时,优先考虑使用组合,而不是继承。
- 高内聚与低耦合(High Cohesion and Low Coupling,HCLC):模块内部需要做到内聚度高,模块之间需要做到耦合度低。这是判断一个模块是在做一个业务还是多个业务的依据。如果是在做同一个业务,那么它所有的代码都是内聚的,相互有较强的依赖。
- 惯例优于配置(Convention over Configuration,COC):灵活性会增加复杂性,所以除非这个灵活性是必须的,否则应尽量让惯例来减少配置,这样才能提高开发效率。如有可能,尽量做到 “零配置”。
- 命令查询分离(Command Query Separation,CQS):读写操作要分离。在定义接口方法时,要区分哪些是命令(写操作),哪些是查询(读操作),要将它们分离,而不要揉到一起。
- 关注点分离(Separation of Concerns,SOC):将一个复杂的问题分离为多个简单的问题,然后逐个解决这些简单的问题,那么这个复杂的问题就解决了。当然这条说了等于没说,难在如何进行分离,最终还是归结到对业务的理解上。
6. 架构老化与重构
6.1 架构老化的原因
系统添加各种新功能的时候,往往会遇到功能需求的实现方式不在当初框架设定的范围之内,于是很多功能代码逸出框架的范围之外。这些散落在各处的代码,把系统绞得支离破碎。
代码老化的标志,是添加功能越来越难,迭代效率降低,问题却是持续不断,解决了一个问题却又由此生出好几个新问题。
在理想的情况下,如果我们坚持以 “最小化的核心系统 + 多个相互正交的周边系统” 这个指导思想来构建应用,那么代码就很难出现老化。
6.2 如何添加新功能
- 能够少改就少改
- 业务务不依赖。代码独立于既有业务系统,只有少量桥接的代码是耦合的。
6.3 架构的局部优化
- 重写,或者叫局部重构。执行局部重构一定要对重构的代码充分了解,否则会引发严重问题。
- 依赖优化,如果改动不大,代表耦合在合理范围,做到这一步暂时不再往下走是可接受的。如果耦合过多,那就意味着需要站在这个功能本身的业务视角看依赖的合理性了。如果不合理,可以考虑推动局部重构。
6.4 核心系统重构
- 首先确定其边界,也就是使用界面(接口),能够在不修改实现的情况下调整核心系统的使用界面到我们期望的样子是最好的。
- 接着对所有周边模块进行依赖优化的整理,细加分析后可以初步确定核心系统需要暴露的事件集合。
进一步要做的事情是把核心系统的 DOM 接口也抽象出来。包括依赖接口及其合理性,明确期望的接口设计。
重新设计,前置条件是对业务了解有了长足的进步,对周边系统切换有充足的预计。
- 执行重构。
7. 结语
架构的功夫全在平常。无论是在架构范式的不断完善上,还是应对架构老化的经验积累上,都是在日常工作过程中见功夫。不能指望有一天架构水平会突飞猛进。架构能力提升全靠平常一点一滴地不断反思与打磨得来。在应对架构老化这件事情上,不要轻率地选择进行全局性的重构。要把功夫花在平常,让重构在润物细无声中发生。局性的重构比一个全新业务的架构过程要难得多。重构,不只是一个架构的合理性问题。它包含了架构合理性的考量,因为需要知道未来在哪里,迭代方向在哪里。