一、设计与架构定义
-
什么是系统?
系统是由一组相互关联的组件(代码、模块、服务等)组成的整体,旨在实现特定的功能或业务目标。系统的核心在于其行为价值(即它能做什么)和架构价值(即它是否易于维护和扩展)。- 行为价值:指系统能否正确执行用户需求,如计算、存储、交互等功能。
- 架构价值:指系统是否具备长期可维护性,即能否以较低成本适应变化。
-
什么是架构?
架构是系统的高层次组织结构,决定了组件如何划分、交互以及依赖关系如何管理。其核心目标是:- 最小化构建和维护成本。
- 控制依赖方向:遵循“依赖规则”(Dependency Rule),确保高层策略(如业务逻辑)不依赖低层细节(如数据库、UI框架)。
- 关注点分离:通过分层(如实体层、用例层、适配器层等)隔离不同职责,使业务逻辑独立于技术实现。
-
架构与设计的区别
架构和设计本质上是同一事物的不同抽象层次:- 架构关注系统的宏观结构(如分层、数据流);
- 设计关注微观实现(如类、接口的具体编码)。
-
糟糕架构的特征
- 混乱的依赖:外部框架或工具直接影响核心业务逻辑。
- 难以测试:业务规则与UI、数据库强耦合,无法独立验证。
- 修改成本递增:每次功能变更都导致后续开发效率下降
二、 编程范式
三种编程范式如下:
1. 结构化编程( structuredprogramming)
- 核心思想:通过限制程序控制流的直接转移(如禁止无限制的goto语句),强制使用顺序、选择和循环(if/then/else、do/while等)来组织代码。结构化编程对程序控制权的直接转移进行了限制和规范。
- Bohm和Jocopini证明了可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序。
- 结构化编程范式中最有价值的地方就是,它赋予了创造可证伪程序单元的能力。这就是为什么现代编程语言一般不支持无限制的 goto 语句。更重要的是, 这也是为什么在架构设计领域,功能性降解拆分仍然是最佳实践之一。
2. 面向对象编程( object”oriented programming)
- 核心思想:通过封装、继承和多态对程序控制流的间接转移(如函数指针)施加纪律,将数据和操作绑定为对象。面向对象编程对程序控制权的间接转移进行了限制和规范。
- 多态性是其关键,允许高层策略通过抽象接口调用底层实现,实现依赖反转。
- 也有一些缺点:有过度设计风险(如滥用继承导致“菱形问题”)
-
菱形问题
“菱形问题”是多重继承(Multiple Inheritance)中的一个经典问题,主要出现在支持多继承的语言(如C++)中。当某个类继承自两个父类,而这两个父类又继承自同一个基类时(类继承关系图呈如下菱形结构),会导致方法调用的歧义。A (基类) / \ B C (中间父类) \ / D (子类)问题表现:
如果B和C都重写了A的方法foo(),那么D调用foo()时,应该选择B的还是C的版本?
如果A有成员变量x,B和C是否各自持有一份x?
-
3. 函数式编程( functional programming)
- 核心思想:基于λ演算,通过限制赋值操作(如不可变数据)来避免副作用,强调纯函数和声明式风格。函数式编程对程序中的赋值进行了限制和规范。
范式与架构的关系
面向对象编程是跨越架构边界的手段,函数式编程是规范和限制数据存放位置与访问权限的手段,结构化编程则是各模块的算法实现基础。
三、软件设计原则SOLID
如果将系统比喻为房子,则
SOLID 原则是用于指导我们如何将砖块砌成墙与房间。
具体而言:SOLID 原则的主要作用就是告诉我们如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序。
1. SRP(单一职责原则,Single Responsibility Principle)
- 定义:“任何一个软件模块(类、函数等)都应该有且仅有一个被修改的原因”。
- 核心思想:每个模块应仅对一类行为者负责,避免因多个需求方修改同一模块导致冲突。
- 价值:解决代码耦合问题,提升模块的内聚性。例如,一个Employee类同时处理薪资计算(CFO需求)和工时报表(COO需求),修改一方可能影响另一方。
2. OCP(开闭原则,Open-Closed Principle)
- 定义:“设计良好的计算机软件应该易于扩展,同时抗拒修改“。
- 核心思想:通过抽象层(如接口)和多态扩展功能,而非直接修改原有代码。
- 价值:应对需求变更时减少对现有系统的破坏性修改。例如,新增报表格式(如PDF、Excel)时,不应修改核心逻辑代码,而是通过新增类实现。
3. LSP(里氏替换原则,Liskov Substitution Principle)
- 定义:“子类型必须能替换其基类而不影响程序的正确性。”
- 核心思想:继承关系应基于行为一致性,而非单纯“is-a”关系。
- 价值:解决继承滥用问题,确保子类不会破坏父类契约。例如,正方形继承长方形时,修改边长会同时影响长和宽,违反行为一致性(Square无法替换Rectangle)。
4. ISP(接口隔离原则,Interface Segregation Principle)
- 定义:“客户端不应被迫依赖它不使用的接口。” 任何层次的软件设计如果依赖了它并不需要的东西 ,就会带来意料之外的麻烦。
- 核心思想:将“胖接口”拆分为更小、更具体的接口。
- 价值:解决不必要的依赖问题。例如:
User1只会使用op1接口,如果按照图1所示的设计,当op2或者op3修改时,user1必须同时重新部署。这时候,应该按图2进行设计,为user1加一层接口。
5. DIP(依赖反转原则,Dependency Inversion Principle)
- 定义:“如果想要设计一个灵活的系统,在 源代码层次的依赖、关系中就应该多引用抽象类型,而非具体实现"。该设计原则归结为以下几条具体的编码 守则:
- 应在代码中多使用抽 象接口,尽量避免使用那些多变的具体实现类 。
- 不要在具体实现类上创建衍生类 。
- 不要覆盖( override) 包含具体实现的函数 。
- 应避免在代码中写入与任何具体实现相关的名字 ,或者是其他容易变动的事物的名字。
- 核心思想:通过依赖注入和接口抽象反转传统依赖方向(如业务逻辑不直接依赖数据库,而是通过接口调用)。
- 价值:确保系统核心逻辑与技术细节解耦,适应变化。例如抽象工厂模式。
中间的那条曲线代表了软件架构中的抽象层与具体实现层的边界,所有跨越这条边界源代码级别的依赖关系都应该是单向的,即具体实现层依赖抽象层。

SOLID原则的共性与目标
降低耦合:通过职责分离(SRP)、接口隔离(ISP)和依赖抽象(DIP)减少模块间的相互影响。
提升扩展性:OCP和LSP确保系统能通过新增代码(而非修改)适应需求变化。
增强可维护性:明确的职责划分和抽象层设计使代码更易理解和修改.
四、组件构建原则
组件构建原则就是用来指导我们如何将这些房间组合成房子
1. 组件是什么
组件是软件的部署单元,是整个软件系统在部署过程中可以独立完成部署的最小实体。例如,对于 Java 来说 , 它的组件是 jar 文件。而在 Ruby 中 , 它们是 gem 文件。在.Net 中,它们则是 DLL 文件。总而言之, 在编译运行语言中,组件是一组二进制文件的集合。而在解释运行语言中 , 组件则是一组源代码文件的集合 。无论采用什么编程语言来开发软件,组件都是该软件在部署过程中的最小单元 。
最开始,程序员需要将库函数和自己的源代码一起编译,手动指定程序加载到内存中的位置;这种形式编译十分缓慢。
接着,为了缩短编译时间,将库函数单独编译并存储在指定的内存地址。如果程序代码和库函数增加,需要延展各自的内存空间,碎片化严重。
重定位技术解决了这一问题。即程序员修改编译器输出文件的二进制格式,使其可以由 一个智能加载器加载到任意内存位置;并将外部引用打上标志,统一链接。由此可以将程序切分成多个可被分别编译、加载的程序段。
程序规模较大时,链接速度很慢。后来就将链接的过程抽出来,用链接器实现。
2. 组件依赖关系管理的指标
-
内部聚合原则
如何决定将哪些部分放在一起形成组件?有以下3个原则可以参考:- REP: 复用/发布等同原则
软件复用的最 小粒度应等同于其发布的最 小粒度。 - CCP: 共同闭包原则
我们应该将那些会同时修改,并且为相同目的而修改的类放到同 一 个 组件中,而将不会同时修改,并且不会为了相同目的而修改的那些类放到不同的组件中。 - CRP: 共同复用原则
不要强迫一个组件的用户依赖他们不需要的东西,不是紧密相连的类不应该被放在同 一个组件里。
- REP: 复用/发布等同原则
-
外部耦合原则
组件和组件之间的关系也需遵循以下原则:- ADP 无依赖环原则:纽件依赖关系图中不应该出现环。
去除环方法:
a. 使用接口进行依赖倒置。
b. 新增组件。 - SDP 稳定依赖原则:
依赖关系必须要指向更稳定的方向,这样可以确保自己设计中那些容易变更的模块不会被那些难于修改的组件所依赖。- 稳定性I公式如下,in和out代表组件依赖图中入度和出度的个数,依赖的单位是类。组件的入度越大,依赖它的组件越多,更改组件越难;组件的入度越小,其更改影响的范围越小,越容易修改,则越不稳定。
I = ( i n − o u t ) / ( i n + o u t ) . I = (in - out) / (in + out). I=(in−out)/(in+out).
- 稳定性I公式如下,in和out代表组件依赖图中入度和出度的个数,依赖的单位是类。组件的入度越大,依赖它的组件越多,更改组件越难;组件的入度越小,其更改影响的范围越小,越容易修改,则越不稳定。
- SAP 抽象依赖原则
稳定的组件同时应该是抽象的,这样它的稳定性就不会影响到扩 展性。 另一方面, 该原则也要求一个不稳定的组件应该包含具体的实现代码,这样 它的不稳定性就可以通过具体的代码被轻易修改。
抽象度A:$ N{c} $ 表示组件中类的数量, $ N{a} $ 为组件中抽象类和接口的数量。
A = N c / N a . A = N{c} /N {a}. A=Nc/Na.
- ADP 无依赖环原则:纽件依赖关系图中不应该出现环。
-
由以上稳定性和抽象度的定义,可绘制如下关系图:
- 痛苦区:靠近(0,0)这一位置点的组件,通常是具体并且稳定;如工具类string、数据库表。
- 无用区:靠近(1,1)这一位置点的组件,通常是无限抽象的,但是没有被其他组件依赖,这样的组件往往无法使用。
- 主序列线:这些组件不会为了追求稳定性而被设计得“太过抽象”,也不 会为了避免抽象化而被设计得“太过不稳定” 。 这样的组件既不会特别难以被修改, 又可以实现足够的功能。
组件离主序列线越近越好。
3. 整洁架构

- 特点:
- 独立于框架:这些系统的架构并不依赖某个功能丰富的框架之中的某个函数。
- 可被测试: 这些系统的业务逻辑可以脱离UI、 数据库、 Web服务以及其他 的外部元素来进行测试 。
- 独立于 UI:这些系统的 UI 变更起来很容易,不需要修改其他的系统部分。
- 独立于数据库。
- 独立于任何外部机构:这些系统的业务逻辑并不需要知道任何其他外部接口的存在。
- 依赖关系规则
- 外层圆代表的是机制,内层圆代表的是策略,源码中的依赖关系必须只指向同心圆的内层, 即由低层机制指向高层策略。
换句话说 , 就是任何属于内层圆中的代码都不应该牵涉外层圆中的代码,尤其 是内层圆中的代码不应该引用外层圆中代码所声明的名字,包括函数、类、变量以 及 一切其他有命名的软件 实体。 - 距离系统的输入/输出越远,它所属的层次就越高。源码中的依赖关系与其数据流向脱钩,而与组件所在的层次挂钩。
- 外层圆代表的是机制,内层圆代表的是策略,源码中的依赖关系必须只指向同心圆的内层, 即由低层机制指向高层策略。
总结
- 软件架构的实质:规划如何将系统切分成组件,并安排好组件之间的排列关系,以及组件之间互相通信的方式。
- 软件架构设计的主要目标:让系统便于理解、易于修改、方便维护,并且能轻松部署。
- 软件架构的终极目标:最大化程序员的生产力,同时最小化系统的总运营成本。
案例:线上收费视频网站
- 需求
向一些个人或者企业提供一批收费的线上教学视频。个人用户既可以选择在线支付之后直接在线观看视频,也可以选择付一笔更高的费用将视频下载到本地,永久地拥有它们 。而企业用户就只能在线播放, 但他们可以选择批量购买,以此来获得一定折扣 。 - 用例分析
根据单一职责原则,该系统包含如下四种角色。这四个角色将成为系统变更的主要驱动力,每当添加新功能,或者修改现有功能时,应该根据角色进行分区处理,避免其中一个角色的变更需求影响其他角色。
- 组件架构设计

- 双实线代表了系统架构边界。可 以看到这里将系统划分成视图、展示器、交互器以及控制器这几个组件,同时也按照对应的系统角色进行了分组。
- 架构实现的是两个维度上的隔离。 第一个是根据单一职责原则对所使用的系统的各个角色进行了隔离,第二个则是对依赖关系原 则 的应用 。 这两个维 度的隔离都是为了将不同变更原因和不同变更速率的组件分隔开来。譬如变更的原 因不同是因为组件使用的角色不同,而变更速率则取决于组件所在的层级。


845

被折叠的 条评论
为什么被折叠?



