前言

在软件变得越来越大,越来越复杂的今天,像学生时代那样不使用任何设计模式、设计架构和设计工具来开发一款大型软件绝对是不可接受的。代码量一旦上来了,会出现很多代码量小的时候难以察觉到的设计思路缺陷和问题。这就要求我们一定要有工程化的思维。如果前期设计不好,代码量增加后后期修改和维护的成本将指数级上升。你会发现改任何一个点都会牵一发而动全身。

非常幸运的是,计算机与软件行业的前辈们已经将遇到的诸多问题一一克服,并迭代改进出了许多优秀的设计模式和设计方法。本文将简单介绍这些模式和方法当时所面临的问题以及它们克服问题所使用的方法蕴含的思想。一是理顺自己的思路,提醒笔者自己不断学习、不断进步;二是为了能给后来的学弟学妹门一些引导和启发,希望能对大家有所帮助。

设计模式

首先是最基础的OOP(Object Oriented Programming,面向对象编程)及其四大特性:封装、抽象、继承、多态。它能够将复杂事物建模为可重现的简单结构,实现良好的可重用。内容比较基础,笔者在这里不过多赘述。

其次就是大名鼎鼎的设计模式。这些设计模式的目的其实都是一样的,就是让代码保持高内聚、低耦合,使得程序更加灵活,容易修改。因为在实际的业务中,需求一定是经常变更的。有时候甚至一天变好几次(祝大家遇不到这种leader和产品经理)。如果你的代码全部耦合在一起,那你改的时候会非常痛苦。笔者在此列举一下,看到不熟悉的可以再去针对性复习:

  1. 简单工厂模式:用一个单独的类来创造实例。
  2. 策略模式:针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。
  3. 单一职责原则:一个类应该仅有一个引起它变化的原因。
  4. 开放-封闭原则:软件实体应该可以扩展,但不可修改。
  5. 依赖倒转原则:高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
  6. 里氏替换原则:子类型必须能够替换它们的父类型。
  7. 装饰模式:动态地给一个对象添加一些额外的职责。
  8. 代理模式:为其他对象提供一种代理以控制对这个对象的访问。
  9. 工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。
  10. 原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
  11. 模板方法模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。通过把不变的行为搬移到超类,去除子类中的重复代码。
  12. 迪米特法则(最少知识法则):如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。
  13. 外观模式:为子系统中一组接口提供一个一致的界面。在设计初期就应该有意识地将两个不同的层分离。
  14. 建造者模式:将一个复杂对象的构建和它的表示分离,使得同样的构建过程可以创建不用的表示。
  15. 观察者模式(发布-订阅模式):多个观察者对象同时监听某一个主题对象。这个主题对象发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
  16. 抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们相关的类。
  17. 状态模式:当一个对象的内在状态改变时允许其改变其行为。
  18. 适配器模式:将一个类的接口转换为客户希望的另外一个接口。
  19. 备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。(类似于游戏存档)
  20. 组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构。
  21. 迭代器模式:提供一个方法顺序访问一个聚合对象的各个元素,而又不暴露该对象的内部表示。(Python中直接就有迭代器、可迭代对象和特殊的迭代器——生成器)
  22. 单例模式:保证一个类仅有一个实例,并提供了一个访问它的全局访问点。(比如软件的页面,不能点一下就出来一个页面吧)分为饿汉式(线程安全)和懒汉式(多线程中线程不安全)。\
  23. 桥接模式:将抽象部分与它的实现分离,使它们都可以独立变化。
  24. 合成/聚合复用原则:尽量使用合成/聚合,尽量不用类继承。(进一步降低类之间的耦合)
  25. 命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化。
  26. 职责链模式:使多个对象都有机会处理请求,避免请求的发送者和接受者之间的耦合关系。
  27. 中介者模式:用一个中介对象来封装一系列的对象交互。
  28. 享元模式:运用共享技术有效地支持大量细粒度的对象。
  29. 解释器模式:如正则表达式。
  30. 访问者模式:表示一个作用于某对象结构中各元素的操作。使得可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

IOC与AOP

Spring中的两大护法:IOC(Inverse of Control,控制反转)和AOP(Aspect Oriented Programming,面向切面编程)。这两个模块作为Spring中最重要的两个设计思想,也反映了目前软件设计的主流技术思路。

IOC 控制反转

IOC的主要目的是把对象的创建和对象之间的调用过程,交给Spring管理,降低耦合度。传统设计(正转) : 由我们主动控制去直接获取依赖对象。IOC(反转) : 由容器来创建及注入依赖的对象。为什么要使用IOC?设想一下,我们现在想写一个网上商城,商城中有许多商品,商品可以分成许多类,这些类内部有一些跟其他类并不相同的属性:比如衣服类,它有尺码,面料等、比如食品类,有生产日期、保质期等属性。这些商品品类众多,我们不可能在设计系统的时候就一一创建好这些类。所以,更好的方法是创建一个抽象商品类,后面出现新的品类时直接创建好了新商品类后提供给这个总类就可以了。在实现IOC的方法中,有一种非常常用的方法,就是Spring的DI(Dependency Injection,依赖注入),在程序运行的时候由IOC容器动态的将所依赖的对象加入到当前类中的过程。

AOP 面向切面编程

而AOP解决的这样一个问题:假设我们现在有很多个不同的服务,但每个服务中都需要加入性能统计模块,日志记录模块等。相比于一个个服务这种竖向的操作,这些功能模块更像是横切在所有服务中的横切面。

software2.png

AOP将这些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。AOP把软件系统分为两个部分:核心关注点横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

数据库与缓存

在业务开发中,最核心的不是代码,而是数据。可以说,代码就是为数据服务的。所以,对数据的处理至关重要。目前市面上已经有许多数据库,主要分为关系型和非关系型数据库,以及最近才流行起来的向量数据库等。

对于一般的数据,我们将其存放在传统数据库中即可,如MySQL等。对于SQL类的数据库,重点掌握三大范式、事务的四种隔离级别以及它们引起的脏读、幻读等问题、索引等。数据库表的设计也需要注意高内聚、低耦合。查询语句方面注意避免笛卡尔积。索引方面注意几个让索引失效的场景。另外,当数据量或者用户量太大时,单个数据库往往不够用。这个时候我们需要使用多个数据库协同工作,这又会引起一系列的问题,如主从数据库、分区分片、一致性解决方案等

缓存

当我们数据量太大,或者某些数据请求频次过高时,我们如果继续使用MySQL等数据库响应时间就会过慢。这个时候我们需要将这些使用频率过高的数据放在读取更快的数据库里以提高系统的性能,最常用的就是Redis。Redis是键值数据库,使用起来非常方便。由于Redis使用内存作为存储空间,配合上高效的数据结构,使得它的读取速度非常快。但是,使用的时候需要注意缓存雪崩、击穿。穿透等问题,保证高频请求不直接请求到SQL数据库中。

ETL

ETL是将业务系统的数据经过抽取(Extract)清洗转换(Transform)之后加载(Load)到数据仓库的过程,目的是将企业中的分散、零乱、标准不统一的数据整合到一起,为企业的决策提供分析依据, ETL是BI(商业智能)项目重要的一个环节。

数据抽取

数据源是指存储数据的源头,包括结构化数据、半结构化数据、非结构化数据等。

  1. 结构化数据:可以采用直连数据库的方式进行抽取,一般采用JDBC(Java Database Connectivity)。这种方式的优点是数据抽取效率高,但会增加数据库负载,因此需要控制抽取时间,一般企业选择在凌晨进行结构化数据的抽取。另外,也可以通过数据库日志方式进行抽取,这种方式对数据库产生的影响极小,但需要解析日志。
  2. 半结构化数据和非结构化数据:一般进行抽取所采用的方式为监听文件变动。这种方式的优点是比较灵活,可以实时抽取变动的内容,但需要解决增量抽取和数据格式转换等问题。
    在抽取数据时,一般会采以下两种方式:
  • 全量同步:将全部数据抽取到目标系统中,一般用于数据初始化装载。
  • 增量同步:检测数据变动,只抽取发生变动的数据,一般用于数据更新。

数据转换

数据转换主要是将抽取的数据进行标准化处理,使其符合目标系统和业务需求。

  1. 对于结构化数据,转换的逻辑相对简单,主要是对表结构和字段进行标准化处理。

  2. 对于半结构化数据和非结构化数据,转换的逻辑更为复杂,需要进行文本解析、数据提取、数据关联和数据格式转换等操作。

在数据转换过程中,需要根据数据源的不同,针对性地选择合适的转换工具,例如数据仓库ETL(Extract-Transform-Load)工具、ELT(Extract-Load-Transform)工具、自定义脚本等。同时,还需要根据业务需求和目标系统的要求,对转换规则进行定义和调整,以保证转换后的数据符合目标系统的要求。

数据清洗是数据转换的一个子集,主要是对原始数据进行清理、过滤、去重、处理异常数据等操作,以消除数据中的问题,如数据重复、二义性、不完整、违反业务或逻辑规则等,保证数据的准确性和稳定性。

数据加载

数据加载主要是将清洗、转换后的数据导入到目标数据源中,为企业业务提供数据支持。

数据加载的方式有两种:全量加载和增量加载。

  1. 全量加载是将所有数据都导入目标数据源中,适用于首次加载或者数据量较小的情况。

  2. 增量加载是只将新增或修改的数据导入目标数据源中,以节省加载时间和系统资源,适用于数据量较大的情况。

数据加载可以采用多种工具和方式,如数据仓库ETL工具、手动编写的SQL脚本、程序编写等。其中数据仓库ETL工具是最常用的工具之一,能够提供可视化的操作界面和强大的处理能力,可大幅减少开发和维护工作量。

数据加载时,需要注意数据类型、长度、格式等问题,保证数据的完整性和准确性。同时,也要根据业务需求和目标系统的要求,对数据进行拆分、合并、计算等操作,使之符合业务需求和目标系统的要求。

中间件

中间件是系统软件和用户应用软件之间连接的软件,以便于软件各部件之间的沟通。引入中间件的目的一是为了屏蔽了底层操作系统的复杂性,二是屏蔽技术架构的复杂性。使程序开发人员面对一个简单而统一的开发环境,减少程序设计的复杂性,将注意力集中在自己的业务上,不必再为程序在不同系统软件上的适配而重复工作,从而大大减少了技术上的负担。中间件带给系统的,不只是开发的便捷,开发周期的缩短,也减少了系统的维护、运行和管理的工作量,还减少了总体费用的投入。

常用的中间件有以下几类:

  • 路由与web服务器:处理和转发其他服务器通信数据的服务器。

  • RPC框架:微服务时代的远程服务调用框架。

  • 消息中间件:支持在分布式系统之间发送和接收消息的软件。 如 Apache kafka, Apache RabbitMQ。

  • 缓存服务: 分布式的高速数据存储层,一般是内存存储。如Redis。

  • 配置中心:用来统一管理各个项目中所有配置的系统。

  • 分布式事务:事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

容器技术

容器技术是一种高效的应用程序部署方法,允许开发人员在隔离环境中打包和运行应用程序,这一过程被称为容器化。容器技术的出现解决了传统部署方法中存在的一系列问题,使得软件开发和部署更加一致、高效。

相信大家之前都被各种环境不兼容等问题折磨过。明明在本地跑得好好的,一到服务器那边就跑不通了。这就是因为程序运行时依赖的组件不同。容器化是一种将应用程序及其依赖项打包到一个独立、可移植的容器中的技术。这个容器包含了应用程序的所有运行时所需的组件,例如代码、运行时环境、库和系统工具。与传统的部署方法不同,容器的关键思想是在不同的环境中实现一致性,无论是在开发人员的本地工作站还是在生产服务器上部署,都无需担心操作系统配置和底层基础设施的差异。

有些同学可能会问,那我用虚拟机不也一样吗?容器是轻量级的,它需要的资源更少,更为高效。另外,它的启动时间、管理与维护难度等也要低不少。另外,容器具有很高的可移植性,正如下图中最流行的容器Docker的图标一样,容器这个“集装箱”可以被很方便地运往各地。

software3.png

容器调度

在大型系统中,不可能只有一个容器。一个系统可能是由几百上千甚至更多的容器组成的。它们之间彼此协同彼此通信共同构成一个完整的系统。那么这些容器的编排调度也就成了一个亟待解决的问题。k8s就是一个开源的业内广为使用的解决方案。k8s全称Kubernetes,名字缩写的由来是K和s之间有8个字母(同类的缩写还有Internationalization,i18n)。它是一个开源的容器编排引擎,用于自动部署、扩展和管理容器化应用程序的开源系统。它将组成应用程序的容器组合成逻辑单元,以便于管理和服务发现。

除了管理调度各容器之外,k8s还使得容器可以进行动态缩放。比如,公司现在有2个不同的业务。周一到周五A业务访问量很大,但周末几乎无人访问。而B业务周末访问量很大,但工作日无人问津。在这种场景下,两个业务都配备访问量最大时的服务器就非常不划算。如果使用k8s,那么就可以动态地增减两个业务的容器数量,减少服务器成本。

分布式与集群

分布式

在用户量比较小的时候,我们只用一台服务器就能满足需求。但是,像阿里字节腾讯这种互联网大厂,它们的产品用户量级都是亿级。一台服务器是远远不够用的。因此,它们的业务需要部署在几万几十万台服务器上。这就需要构建集群,使用分布式的架构。

分布式架构是一种将应用程序分解成多个独立组件并将其分布在不同的计算机或服务器上的设计模式。这种架构可以提高系统的可伸缩性、可靠性和性能,并允许多个组件同时运行,提高系统的吞吐量。比如,抽出一个服务器只处理登录鉴权功能,一个服务器只处理MySQL数据查询,一个服务器只处理业务逻辑……

集群

但上述例子中的分布式有个问题。如果其中一个服务器突然宕机,对应的服务将彻底无法使用,从而带动整个系统瘫痪。为了避免这种现象,我们需要将多个服务器组合起来,形成一个集群。一群服务器处理一种请求,某一个服务器宕机了,请求可以被其他服务器接管处理,保证用户体验。

微服务

随着访问量的逐渐增大,单一应用只能依靠增加节点来应对,但是这时候会发现并不是所有的模块都会有比较大的访问量。以电商为例, 用户访问量的增加可能影响的只是用户和订单模块, 但是对消息模块的影响就比较小。那么此时我们希望只多增加几个订单模块, 而不增加消息模块。再比如,大公司内可能不同的组有不同的技术栈,支付模块用JAVA编写,消息管理模块的组却是用Go语言。这就需要我们将系统中的服务尽量小地拆分出来,每个模块只负责某一个很小的服务。服务原子化拆分,独立打包、部署和升级,保证每个微服务清晰的任务划分,利于扩展。微服务之间采用RESTful等轻量级Http协议相互调用。服务各自有自己单独的职责,服务之间松耦合,避免因一个模块的问题导致服务崩溃。

微服务架构中也需要注意许多问题:

  • 服务治理

    进行服务的自动化管理,其核心是服务的注册与发现。

  • 服务调用

    在微服务架构中,通常存在多个服务之间的远程调用的需求,目前主流的远程调用的技术有基于HTTP请求的RESTFul接口及基于TCP的RPC协议。

  • 服务网关

    随着微服务的不断增多,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信可能出现:客户端需要调用不同的url地址,增加难度。在一定的场景下,存在跨域请求的问题。每个微服务都需要进行单独的身份认证。为了解决这些问题,API网关顺势而生。

  • 服务容错

    在微服务当中,一个请求经常会涉及到调用几个服务,如果其中某个服务不可用,没有做服务容错的话,极有可能会造成一连串的服务不可用,这就是雪崩效应。

    我们没法预防雪崩效应的发生,只能尽可能去做好容错。服务容错的三个核心思想是:不被外界环境影响。不被上游请求压垮。不被下游响应拖垮。

  • 链路追踪

    随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要对一次请求涉及的多个服务链路进行日志记录,性能监控即链路追踪。

云计算

近些年随着云平台的不断完善,许多业务都不需要自己再购买服务器或者构建软件,而是使用云计算提供的服务完成。这些服务大致可以分为如下几类:

  • SaaS(Software as a Service,软件即服务):它提供给用户的是已经构建好的应用程序。用户通过互联网访问和使用这些应用程序,无需关心底层的基础设施和平台。用户可以根据需要订阅和使用这些应用服务,并按照订阅模式支付费用。例如,阿里巴巴旗下的企业级 SaaS 平台提供了多个企业级 SaaS 应用,如企业邮箱、云办公套件、智能客服等。
  • PaaS(Platform as a Service,平台即服务):它提供给用户的是一个开发环境和工具集合。用户可以在这个平台上构建、测试、部署和管理自己的应用程序。PaaS 为开发者提供了更高层次的抽象,使他们可以专注于应用程序的开发而不必关心底层的基础设施。PaaS 通常包括运行时环境、数据存储、开发工具和部署管理等功能。例如,阿里云云原生应用平台提供容器服务、云原生数据库、云原生网络等 PaaS 服务。
  • IaaS(Infrastructure as a Service,基础设施即服务):IaaS 是云计算的一种服务模式,提供给用户的是基础设施层的资源,包括虚拟化的计算资源(如虚拟机、服务器)、存储资源和网络资源。用户可以根据需要按需租用这些资源,灵活地扩展和管理自己的应用程序。IaaS 提供了更底层的灵活性和控制权,用户可以自行配置和管理操作系统、中间件和应用程序等。