欧阳亮的博客

编程不止是一份工作,还是一种乐趣!!!

架构设计原则

如果一个技术已经存在2年,比如现在很火的前端技术react和vue等,那么我们能预估这个技术大致还有2年的生命期,再久就不确定了;如果一个架构或设计原则已经存在15年,例如单一职责和依赖倒置原则,我可以预期它还有15年甚至更久的生命期。原则比具体技术更抽象,更接近事物本质,也更经得起时间考验的东西。这些原则沉淀在架构师的脑海中,最终内化成他的思维模式,以潜意识方式影响和指导他的架构和设计工作。

SOLID 面向对象设计原则


一、单一职责原则(Single Responsibility Principle - SRP)


原文:There should never be more than one reason for a class to change.

译文:修改某个类的理由应该只有一个,如果超过一个,说明类承担不止一个职责,要视情况拆分

理解:对于一个类而言,应该仅有一个引起它变化的原因。说白了就是,不同的类具备不同的职责,各施其责。这就好比一个团队,大家分工协作,互不影响,各做各的事情。


二、开闭原则(Open Closed Principle - OCP)


原文:Software entities like classes, modules and functions should be open for extension but closed for modifications.

译文:软件实体应该对扩展开放,对修改封闭

理解:设计时要尽可能抽象出不变和易变的部分,当需求有改动,要修改代码了,此时您要做的是,尽量用继承或组合的方式来扩展类的功能,而不是直接修改类的代码。当系统的架构和设计满足当足的业务,我们应该尽可能采用开闭原则;如果业务发生本质变更,设计已经不再适用了,那别犹豫,直接修改吧。


三、里氏替代原则(Liskov Substitution Principle - LSP)


原文:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

译文:使用基类的指针或引用的函数,必须是在不知情的情况下,能够替换成派生类的对象

四层含议:

  • 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的入参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

疑问:里氏替换原则要求子类避免重写父类方法非抽象方法,而多态却是要求子类重写父类的方法。不明白里氏替换原则与多态之间的取舍。


四、最少知识原则(Least Knowledge Principle - LKP)

原文:Only talk to you immediate friends.

译文:只与你最直接的朋友交流

理解:这个很好理解,软件设计时应该尽可能的做到低偶合,系统间、模块间、类之间要追求最小化依赖。

最少知识原则(Least Knowledge Principle - LKP)也叫:迪米特法则(Law of Demeter)


五、接口隔离原则(Interface Segregation Principle - ISP)


原文:The dependency of one class to another one should depend on the smallest possible interface.

译文:不要强迫用户去依赖它们不使用的接口。换句话说,使用多个专门的接口比使用单一的大而全接口要好

理解:客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。


六、依赖倒置原则(Dependence Inversion Principle - DIP)


原文:High level modules should not depends upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.

译文:高层模块不应该依赖于低层模块,它们应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象

理解:应该面向接口编程,不应该面向实现类编程。面向实现类编程,相当于就是论事,那是正向依赖(正常人思维);面向接口编程,相当于通过事物表象来看本质,那是反向依赖,即依赖倒置(程序员思维)。

例如一个web应用内,Controller是上层模块,Service是接口抽象,ServiceImpl是低层实现。Controller依赖Service来完成业务逻辑时,而ServiceImpl需要实现Service,Controller和ServiceImpl都依赖Service,即高层和低层实现都依赖抽象;高层模块不应该依赖于低层实现,即我们不应该在Controller层直接通过ServiceImpl来处理业务逻辑。


其它设计原则


组合/聚合复用原则(Composition/Aggregation Reuse Principle - CARP)

当要扩展类的功能时,优先考虑使用组合,而不是继承。

这条原则在23种经典设计模式中频繁使用,如:代理模式、装饰模式、适配器模式、策略模式、状态模式等,可见江湖地位非常之高!


无环依赖原则(Acyclic Dependencies Principle - ADP)

当A模块依赖于B模块,B模块依赖于C模块,C依赖于A模块,此时将出现循环依赖。

在设计中应该避免这个问题,可以分析各个模式,区分出依赖与被依赖的部分。


好莱坞原则(Hollywood Principle - HP)

好莱坞明星的经纪人一般都很忙,他们不想被打扰,往往会说:Don’t call me, I’ll call you. 翻译为:不要联系我,我会联系你。

对应于软件设计而言,最著名的就是“控制反转”(或称为“依赖注入”),我们不需要在代码中主动的创建对象,而是由容器帮我们来创建并管理这些对象。


不要重复你自己(Don’t repeat yourself - DRY)

不要让重复的代码到处都是,要让它们足够的重用,所以要尽可能地封装。

DRY的核心思想不仅仅是指代码重复,更多的是指业务逻辑处理重复或类似。


保持它简单与傻瓜(Keep it simple and stupid - KISS)

不要让系统变得复杂,界面简洁,功能实用,操作方便,要让它足够的简单,足够的傻瓜。


高内聚与低耦合(High Cohesion and Low Coupling - HCLC)

模块内部需要做到内聚度高,模块之间需要做到耦合度低。


惯例优于配置(Convention over Configuration - COC)

尽量让惯例来减少配置,这样才能提高开发效率,尽量做到“零配置”。很多开发框架都是这样做的。

想想使用Maven、Springboot后,比起原来我们的幸福指数上升了多少!


命令查询分离(Command Query Separation - CQS)

在定义接口时,要做到哪些是命令,哪些是查询,要将它们分离,而不要揉到一起。

符合CQS的设计,系统的后期伸缩会比较好做。


关注点分离(Separation of Concerns - SOC)

将一个复杂的问题分离为多个简单的问题,然后逐个解决这些简单的问题,那么这个复杂的问题就解决了。难就难在如何进行分离。


契约式设计(Design by Contract - DBC)

模块或系统之间的交互,都是基于契约(接口或抽象)的,而不要依赖于具体实现。该原则建议我们要面向契约编程。


你不需要它(You aren’t gonna need it - YAGNI)

不要一开始就把系统设计得非常复杂,不要陷入“过度设计”的深渊。应该让系统足够的简单,而却又不失扩展性,这是其中的难点。

YAGNI感觉和KISS是同一个原则。


15 条架构原则


N + 1 设计

永远不要少于两个,通常为三个。比方说无状态的 Web API,一般部署至少 >= 2 个。


回滚设计

确保系统可以回滚到以前发布过的任何版本。可以通过发布系统保留历史版本,或者代码中引入动态开关切换机制 (Feature Switch)。


禁用设计

能够关闭任何发布的功能。新功能隐藏在动态开关机制 (Feature Switch) 后面,可以按需一键打开,如发现问题随时关闭禁用。


监控设计

在设计阶段就必须考虑监控,而不是在实施完毕之后补充。例如在需求阶段就要考虑关键指标监控项,这就是度量驱动开发 (Metrics Driven Development) 的理念。


设计多活数据中心

不要被一个数据中心的解决方案把自己限制住。当然也要考虑成本和公司规模发展阶段。


使用成熟的技术

只用确实好用的技术。商业组织毕竟不是研究机构,技术要落地实用,成熟的技术一般坑都被踩平了,新技术在完全成熟前一般需要踩坑躺坑。


异步设计

能异步尽量用异步,只有当绝对必要或者无法异步时,才使用同步调用。


无状态系统

尽可能无状态,只有当业务确实需要,才使用状态。无状态系统易于扩展,有状态系统不易扩展且状态复杂时更易出错。


水平扩展而非垂直升级

永远不要依赖更大、更快的系统。一般公司成长到一定阶段普遍经历过买更大、更快系统的阶段,即使淘宝当年也买小型机扛流量,后来扛不住才体会这样做不 scalable,所以才有后来的去 IOE 行动。


设计时至少要有两步前瞻性

在扩展性问题发生前考虑好下一步的行动计划。架构师的价值就体现在这里,架构设计对于流量的增长要有提前量。


非核心则购买

如果不是你最擅长,也提供不了差异化的竞争优势则直接购买。避免 Not Invented Here 症状,避免凡事都要重造轮子,毕竟达成业务目标才是重点。


使用商品化硬件

在大多数情况下,便宜的就是最好的。这点和第 9 点是一致的,通过商品化硬件水平扩展,而不是买更大、更快的系统。


小构建、小发布和快试错

全部研发要小构建,不断迭代,让系统不断成长。这个和微服务理念一致。


隔离故障

实现故障隔离设计,通过断路保护避免故障传播和交叉影响。通过舱壁泳道等机制隔离失败单元 (Failure Unit),一个单元的失败不至影响其它单元的正常工作。


自动化

设计和构建自动化的过程。如果机器可以做,就不要依赖于人。自动化是 DevOps 的基础。


微服务的4个设计原则


一、AKF扩展立方体(Scalability Cube)

《架构即未来》一书中提出的可扩展模型,这个立方体有三个轴线,每个轴线描述扩展性的一个维度:

X轴:指的是水平复制,比如采用集群的方式提升应用的负载能力、采用冗余的方式实现数据的读写分离,提升存诸的查询能力等等。

Y轴:指的是数据分片,主要用于数据存储。比如采取分库、分表的方式提升存储的最大访问能力、缩短访问时间等等。

Z轴:指的是功能拆分。当应用变得臃肿时,把功能拆分成一个个独立的小系统,更利于整体的把控。


二、前后端分离

前后端分离原则,简单来讲就是前端和后端的从代码到部署都分离开来。不要继续以前的服务端模板技术,比如JSP ,把Java、JS、HTML、CSS 都堆到一个页面里,稍复杂的页面就无法维护。这种分离模式的方式有几个好处:

  • 前后端技术分离,可以由各自的专家来对各自的领域进行优化,这样前端的用户体验优化效果会更好。
  • 分离模式下,前后端交互界面更加清晰,就剩下了接口和模型,后端的接口简洁明了,更容易维护。
  • 前端多渠道集成场景更容易实现,后端服务无需变更,采用统一的数据和模型,可以支撑前端的web UI、移动App等访问。


三、无状态服务

如果一个数据需要被多个服务共享,才能完成一笔交易,那么这个数据被称为状态。进而依赖这个“状态”数据的服务被称为有状态服务,反之称为无状态服务。

那么这个无状态服务原则并不是说在微服务架构里就不允许存在状态,表达的真实意思是要把有状态的业务服务改变为无状态的计算类服务,那么状态数据也就相应的迁移到对应的“有状态数据服务”中。例如我们以前在本地内存中建立的数据缓存、Session缓存,到现在的微服务架构中就应该把这些数据迁移到分布式缓存中存储,让业务服务变成一个无状态的计算节点。迁移后,就可以做到按需动态伸缩,微服务应用在运行时动态增删节点,就不再需要考虑缓存数据如何同步的问题。


四、Restful通讯风格

无状态协议HTTP,具备先天优势,扩展能力很强。例如需要安全加密是,有现成的成熟方案HTTPS可用。JSON报文序列化,轻量简单,人与机器均可读,学习成本低,搜索引擎友好。语言无关,各大热门语言都提供成熟的Restful API框架,相对其他的一些RPC框架生态更完善。