关于架构的一些理解

101 阅读10分钟

最近在处理项目中一些基础组件的重构和优化,遇到了一些问题,通过反复的试验和思考,找到了一些解决办法。在这个过程中,得到了一些启发和感悟,特此记录下来。

【一,对外接口中,聚合接口的理解和维护成本要远远大于功能单一的接口】

这里的聚合接口是指:将单一化的多个接口的实现代码,放在一个接口中,并且对外提供某个功能。

在实际的代码维护中,我们时常会遇到一个函数长达50行甚至100行。当这种函数是Private的时候,其维护的成本主要在于多种状态和数据的耦合。但把其作为Public给到外面使用时,那对于维护来讲,就是一场严重的车祸现场。原因在于两点:1,对外接口,与其他模块耦合,一旦有变动,其影响是巨大的。2,方法可以明确抛出状态数据(如果聚合接口是将多种状态偶合在一起的,那就是核爆现场了),但该状态的得出,会存在一系列的处理判断。这些处理逻辑的耦合,会对代码的维护造成困扰。

所以,我们的结论:

1,对外接口,是两个模块耦合的部分,如何管控这种耦合,远远要比模块本身的代码重要。我的观点是,对外接口的职能必须是单一的,且可以通过函数名字自描述的。注释的内容与函数结果无关,而与运算重要节点相关。当然,如果重要节点就是结果,那注释就应该去掉。

2,对外接口是协议,而不是函数。协议是用来定义规则的,而方法才是用来实现具体计算的。协议的实现,必须是简单的,大部分情况下仅仅是对Private方法的调用而已,或者几个Private方法的简单聚合而已。注意,这里的聚合是调用聚合,而不是实现聚合。方法调用链中的最终实现,必须是对某一运算的简单描述。组合优于继承。方法的合理拆分和组合调用是一个程序员最简单和最重要的基本功之一。

3,如果想把组件提供的多种单一能力聚合起来,使得业务方可以获得通用的定制能力。则需要单独做一层中间件,而不是由组件本身做聚合。

【二,协议的主要作用是针对某一类问题制定统一规则,匿名函数的主要作用是针对某一问题的处理】

首先声明,我这一条的总结不是绝对的。

在数据流动和状态传递方面,这两者各有特点,都可以使用。比如两个类之间的数据流动,使用两者都可以,而且,都没有什么明显的优缺点。但当你的模块的规模足够大时,你单独使用协议或者匿名函数,其意义可以做到完全不同。

我们应该将统一的基础模块和业务模块的耦合行为用协议抽象。其优点有两个:1,业务模块耦合抽象的协议,这种做法是低耦合的表现,有益于整体架构的稳健。同时,对抽象层的扩展要远远优于对实体类的扩展。2,抽象的协议可以由不同的实现模块来对应,这样做有利于基础模块的扩展维护。对修改关闭,对扩展开放。

我们理解架构,其实就是在理解维护问题。架构需要解决的最基本问题之一就是整体项目维护的成本和风险。而维护无非两点:1,在原来模块的基础上新增代码来满足需求的变化。2,构建全新的模块来对应新的业务需求。那就相对应的存在两个问题:1,旧模块如何维护扩展?2,新模块如何做到与旧模块的业务对接和设计模式的统一?

所以,架构就包含两个非常重要的宏观任务:1,制定规则,2,维护推广并保证规则的施行。我们往往会把精力放在第一点,但其实第二点也同样重要。

说句题外话:一个良好的架构组织者,其社交压力往往比肩产品。尤其是在大团队中,每个小伙伴对技术的理解会有较大差异,如果让大家接受并且严格按照规则去构建相关代码(我是说相关代码,不是全部代码,架构不应该干扰具体的功能代码,架构只是解决如何组织和组装代码块或者组件)。我所在团队目前有20多人,如何让自己的小伙伴都能参与规则的制定修改和执行,是比较耗费精力和时间的。当然,如果你认为,你可以一个人就把整个项目的架构做出来,那向大佬致敬。我倾向于记录,沉淀,反馈和推广的模式,由于这些规则是大家一起总结出来的,那么宣导起来也就省时省力。而架构组织者负责什么呢?基本规则的制定和沉淀,规则的收集评审和落地,以及最终规则的宣导推广。所以,技术只是你工作的工具,并不是你工作的全部。

设计模式6大基本原则:

1、单一职责原则(Single Responsibility Principle,简称SRP )

  • 核心思想: 应该有且仅有一个原因引起类的变更

  • 问题描述: 假如有类Class1完成职责T1,T2,当职责T1或T2有变更需要修改时,有可能影响到该类的另外一个职责正常工作。

  • 好处: 类的复杂度降低、可读性提高、可维护性提高、扩展性提高、降低了变更引起的风险。

  • 需注意: 单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可以度量的,因项目和环境而异。

2、里氏替换原则(Liskov Substitution Principle,简称LSP)

  • 核心思想: 在使用基类的的地方可以任意使用其子类,能保证子类完美替换基类。

  • 通俗来讲: 只要父类能出现的地方子类就能出现。反之,父类则未必能胜任。

  • 好处: 增强程序的健壮性,即使增加了子类,原有的子类还可以继续运行。

  • 需注意: 如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系 采用依赖、聚合、组合等关系代替继承。

3、依赖倒置原则(Dependence Inversion Principle,简称DIP)

核心思想:高层模块不应该依赖底层模块,二者都该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象;

说明:高层模块就是调用端,低层模块就是具体实现类。抽象就是指接口或抽象类。细节就是实现类。

通俗来讲: 依赖倒置原则的本质就是通过抽象(接口或抽象类)使个各类或模块的实现彼此独立,互不影响,实现模块间的松耦合。

问题描述: 类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

解决方案: 将类A修改为依赖接口interface,类B和类C各自实现接口interface,类A通过接口interface间接与类B或者类C发生联系,则会大大降低修改类A的几率。

好处:依赖倒置的好处在小型项目中很难体现出来。但在大中型项目中可以减少需求变化引起的工作量。使并行开发更友好。

4、接口隔离原则(Interface Segregation Principle,简称ISP)

  • 核心思想:类间的依赖关系应该建立在最小的接口上

  • 通俗来讲: 建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

  • 问题描述: 类A通过接口interface依赖类B,类C通过接口interface依赖类D,如果接口interface对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。

  • 需注意:

  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度

  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情

  • 为依赖接口的类定制服务。只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。

5、迪米特法则(Law of Demeter,简称LoD)

  • 核心思想: 类间解耦。

  • 通俗来讲: 一个类对自己依赖的类知道的越少越好。自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的。

6、开放封闭原则(Open Close Principle,简称OCP)

  • 核心思想: 尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化

  • 通俗来讲: 一个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计的时候尽量适应这些变化,以提高项目的稳定性和灵活性。

一句话概括: 单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。