开发可伸缩系统必须遵守这11个软件设计原则

01简单

◆隐藏复杂与构建抽象

随着系统的发展,会发现越来越复杂,可能没法了解整个系统的全部,每个人的大脑处理能力有限,不可能了解系统的每个细节。


所以,保持软件简单可以帮助你更好的了解系统。随着系统的逐渐壮大,我们只能做到的是保持局部简单,无法保持整体简单。


开发系统服务时,要创建暴露更高层次的抽象,实现抽象允诺的功能,从而隐藏其复杂性。


◆避免过度设计

我们工程师一般更喜欢挑战高难度的问题,可能一个简单的问题会使其复杂化,使得可能开发出难以维护的代码。早期可以构建合理的抽象层次,可以给以后迭代增加新特性,这样比一开始就设计开发复杂的系统更有好处。


开发易于理解的简单系统并能证明其将来是可扩展的才是真正的难题。


◆尝试测试驱动开发TDD

TDD:Test Drive Develop。先写测试用例,再编程实现功能。


从用户角度看待问题,助于开发更清晰更简单接口。


◆从软件设计的简化范例中学习

从认识的系统或者软件中学习,比如:Grails(细节设计简单易扩展)、Hadoop(隐藏复杂实现,提供极简单的编程API)、Google Maps API(简单API,解决复杂问题时保证足够弹性)等。


从优秀的软件中学习设计思想及其模式。


02低耦合


我们一般追求的都是高内聚低耦合的系统。耦合是度量两个系统之间的关联和依赖程度。耦合度越高则表示依赖越强。
高耦合与低耦合对系统的影响

①高耦合,在你改动代码的时候需要检查系统其它部分可能存在的问题;

②低耦合,可以保证复杂性局部化,即复杂只体现在模块内部,不影响整个系统。

③将系统解耦成不同子系统,部分系统可能需要更多CPU或更多IO吞吐能力,或者更多内存的应用。针对不同的应用特点配置不同硬件达到更好的伸缩性。


◆促进低耦合

促进低耦合最重要的实践是小心管理你的依赖。比如:类之间、模块之间、应用之间的依赖。


◆避免不必要的耦合

避免不必要的耦合的例子:

①对于Java开发,public setter getter暴露private成员变量是比较常见的,成员变量有时没必要暴露(习惯使用IDE可能经常会把所有成员变量自动生成全部setter和getter暴露成员变量);

②模块或者客户端端代码必须按照特定的顺序进行方法调用。有时这种是比较合理,但是大部分时候是因为设计了糟糕的API,不仅需要依赖方法签名的影响,也要依赖方法调用的方式的影响,增加了耦合性。


暴露访问域或者要求调用方式时很可能就出现了不必要的耦合了。


◆低耦合范式

理解低耦合可以学习别人的优秀代码获取经验。
比如:UNIX命令行编程及管道的用法,暴露了简单的API;SLF4J日志框架;优秀的开源软件等等。
学习优秀的软件源码或设计思想。保持系统低耦合对于系统的健康和可伸缩至关重要。低耦合是开发弹性软件的最基本原则之一。



03DRY-不要重复自己


重复做一件事是非常枯燥无味、无价值的一件事。同样的事重复做了一次又一次,简直就是浪费生命。


在软件工程的生命周期里,经常会遇到重复的事,从重复的应用程序代码,到每次代码发布前重复的测试等等。


做重复的事的原因

①采用低效的过程

在软件开发的过程中,设计、交付、开会等,持续对这些进行改进可以获得很好的收益,获得反馈、持续改进、然后重复这个过程。团队意识到很多都是在浪费时间并无可奈何。当你经常听到“本来就是这么干的”或“一直是这么干的”,那么往往说明这里有低效的过程并且有改进的机会。


②缺乏自动化

我们在开发的项目的过程中经常要做的事:手工部署、编译、测试、打包、配置等等。有时候你会发现你一整天的时间都消耗在了这些过程,但是你的新任务却没有开始,然后只能加班加点的完成新功能的开发了。如果有一套自动化的工具帮你做这些事,那就可以很好的完成工作了,而无需浪费时间在这些重复过程之上。


③重复造轮子

已有现有的功能代码,却非要自己重新来写一个,并且写的还没有别人好,bug贼多。比如:排序、b树、MVC框架或数据库抽象层。这完全就是在浪费时间,已有的类库和工具就可以方便和快速的帮助我们了。

④复制黏贴Ctrl C、Ctrl V

我们在开发过程中,或者看到别人写的代码也容易发现,在同一个项目里(或项目组系统内)会存在大量拷贝其它地方的代码,这种重复性的代码想必你一定不陌生,并且有的IDE工具也有这些提示重复代码的功能。虽然有时拷贝是最方便快捷的事,但是一旦出现需要变更、维护等,其不足就表现出来了:维护多份代码、遗漏修改,造成bug再次出现等等。

⑤持有凑合态度——代码不会再用第二次了

有时候我们开发时,容易出现认为这份代码目前只是拿来简单用用,然后就随便写写。并没有文档、单元测试、很好的设计、很难用。这样的代码可能会存在很多漏洞,也可能给其它人复制粘贴,同样会给别人造成巨大的“灾难”。


04基于约定的编程

基于约定编程,也就是基于接口编程。将客户端代码和功能提供者代码进行解耦合。
在设计代码的时候,尽可能创建明确的约定,也尽可能的依赖约定(而不是实现)进行开发。


现实技术领域中,基于约定编程的比较常见的便是HTTP协议,客户端Web浏览器:Firefox、Chrome、HTTP服务提供者:Apache、Tomcat等,都按照约定进行编程设计与开发,这样就能很好发挥HTTP的实现弹性和提供者透明的可替代性。


05画架构图

一图胜千言!
通过架构图可以将系统设计文档化、跟他人分享信息、帮助自己更好的了解自己的设计。


很多时候,我们接到需求就直接拿起编辑器进行功能开发,并没有做前期的分析和设计,特别是目前很多工程师在追求敏捷开发更加容易造成这样的处境。


有时我们开发好功能了,到最后可能发现自己写的代码是有问题,比如跟需求要求不一致、逻辑漏洞很多,因为糟糕的设计(基本没有设计),你最后可能也得重头再来,这个是很常见的开发风险问题,我们应该重视它。


常见的架构图:用例图、类图、模块图。

用例图:定义系统的用户是谁,可以执行哪些操作。

类图:类的信息、类与类之间的关系。

模块图:描述结构和依赖关系。抽象层次更高。

架构图是你对于功能系统的设计表现,可以从多个角度审视自己的设计,可以降低开发风险。


06单一职责

单一职责原则,你的类应该只有一个职责而不是更多。

作用:降低代码复杂度,降低耦合、增加简单性、易于重构、代码复用、单元测试。


改善单一职责的一般最佳实践:

①一个类的代码少于2-4屏;

②确保一个类依赖不超过5个接口或类;

③确保类有明确的目标;

④用一句话总结一个类的职责。


进一步掌握单一职责办法可以研究这些设计模式:策略、迭代器、代理、适配器等模式。


07开闭原则


开闭原则,当需求变更或者增加新功能时,无需修改现有代码。


向扩展开放,向修改关闭。


开闭原则常见优秀例子:排序算法-Comparator接口(只要继承此接口,实现具体的算法逻辑),MVC框架(扩展各种组件,无需修改原有代码)。


08依赖注入

依赖注入是一种降低耦合改善开闭特性的简单技术。依赖注入对类需要依赖的对象提供一个引用实例,无须类自己创建依赖对象。


依赖注入的思想:知道的越少越好。尽可能让类不需要知道如何去创建需要的依赖,以及依赖来自哪里,如何实现。


依赖注入一般不需要new实例,而是由依赖的对象实例提供。
使用得当的话依赖注入可以有效降低局部复杂度,使其更简单。不用知道实例从何而来,不用知道谁来提供依赖的实例,这样就可以更好的聚焦自己的职责。


推荐学习依赖注入的优秀框架:Spring、Grails等框架。


09控制反转

控制反转IOC是一种从类中移除职责的方法,从而使类更简单,与系统其它部分更少耦合,其核心就是不必知道是谁、怎样、何时创建和使用对象,尽可能简单易于理解,对于软件设计而言,各部分互相知道得越少越好。
学过Spring的都知道,依赖注入、控制反转是其主要核心思想。


好的IOC框架应该包含

①可以为框架开发插件。

②插件的独立的,可以随时插入移除。

③框架可以自动检测到插件,或者通过配置文件加载。

④框架为每种插件类型定义接口,而不会与其具体插件耦合。


10为伸缩而设计


每种伸缩和对应的方案实践之间都要仔细权衡,不要为了那些永远用不着的伸缩性需求进行过度设计,要认真评估系统最可能的实际伸缩性需求进行相应的设计。


在创业公司中,很多都没有做到任何系统可伸缩就倒闭了,所以我们应该在追求可伸缩性系统的同时也要实事求是根据业务发展情况进行调整,不能一味的追求完美的伸缩系统,而是要有大局观进行技术的规划和各项权衡等等。


讨论各种设计原则都是为了解决系统的耦合性和复杂性,这些原则大部分有助于我们改善系统的伸缩性。


伸缩系统设计方案主要是下面这三个基本的设计方法


◆增加副本

同一组件或者系统服务部署到多台机器上(集群)。


在Web层而言,增加副本是最容易、成本最低的伸缩方案,主要的挑战是对于有状态的服务难以使用这种方式,需要同步状态来实现副本任意可交换(正是如此,我们设计系统时一定要设计实现无状态的系统)。

◆功能分割

根据功能将系统拆分成粒度更细的子系统(特别是当前的微服务架构)。基础设施架构,服务器服务器进行分离部署:对象缓存服务器、消息队列服务器、Web服务器、数据存储服务器、负载均衡器等等。

功能分割更先进的方式:将系统分割成自给自足的应用。主要适用于Web服务层,是面向服务架构SOA的一种主要实践。

功能分割会带来很多好处也有缺点,分割后,最开始就会带来很多管理方面的问题还有相应的成本也会很高,我们也不可能无限制的进行分割,达到“适可而止”即可。这一方面可以了解下当前微服务架构技术。


◆数据分片

每台机器存储一部分数据(数据库分库分表等)。

数据分片+增加备份(副本),可以实现几乎是无限的伸缩性,只要能正确的切分,就能增加更多的用户,处理更多的并发连接,收集更多的数据,将系统部署在更多的机器上。

但是,数据分片是最复杂、代价最昂贵的技术。数据分片最大的调整是,数据在访问前,就得找到需要存储数据的所在分区的服务器,如果一个查询涉及到多个数据分区,实现起来就变得异常低效和困难了。

举个例子,比如现在的分布式数据库,或者分布式中间件(ShardingSphere),有很多框架或者系统在这方面都会存在问题或者实现起来麻烦,关键也会对性能上有很大的影响,所以有时就得权衡或者妥协各种问题。

为伸缩而设计,主要是要在这三个基本的设计方法:增加副本、功能分割、数据分片。
虽然有各种技术方法帮助或指导我们,但是实际上还会存在各种各样的问题需要我们去处理或者去权衡。


11自愈设计

一个系统高可用,是以用户的角度来看能够达到预期的运行,即使系统出现部分异常或者部分设备宕机,只要不影响用户使用。


我们都认为整个系统是可用的。


我们的系统越大,相应的失效概率就会更高。进行伸扩容时,失效也会变得更加频繁。设计一个伸缩性架构必须把各种失效状态当作常态,而不是特殊情况对待。


想要设计一个高可用系统,必须做最坏的准备才能得到最好的期待,要不停的假想哪里会报错出异常,然后是要怎么处理它。


我们应该如何识别失效单点问题?


一个简单的方法就是画出系统架构图,每个设备(路由器、服务器、交换机等网络基础设施)等要画上去,然后询问自己, 如果某个设备宕机时会发生什么,某个子系统挂了怎么办等。


当识别出实效单点后,就要和业务团队和基础技术团队开讨论如何做才能达到高可用,是否需要做哪些冗余(冗余在成本和是否复杂等方面需要仔细权衡)来做备份等等。


确保系统高可用的主要手段是消除失效单点的风险和优雅地进行失效转移。


推荐可以学习或了解相关技术:混沌技术(比如Netflix开发的Chaos Monkey、阿里开源混沌工程工具 ChaosBlade)、开源数据库Cassandra(很好的自愈处理能力),这些都极其优秀,值得一学。


设计软件一定要考虑高可用和系统自愈能力。

原文地址:https://www.cnblogs.com/yaoyinglong/p/11857854.html