主页

索引

模块索引

搜索页面

许式伟的架构课

目录
  • 操作系统的 “管道化” 与 应用程序价值讨论: https://36kr.com/p/1638705659905

  • 架构师掌控全局的核心思想是打通经络,让自己的内力在全身自然流通,浑然一体。在不影响理解的情况下,你需要放弃很多实现细节的专研,但有一天你需要细节的时候,你能够知道存在这些细节并且快速钻研进去。

  • 架构就说分解系统,明确每个子系统(或模块)的规格。架构思维就是分解系统的思考方式(方法论)

  • 架构设计的角度去看最重要的是: 分而治之的能力

解剖架构的关键点:

1. 需求分析。
    从需求分析角度来说,关键要抓住需求的稳定点和变化点。
    需求的稳定点,往往是系统的核心价值点;
    而需求的变化点,则往往需要相应去做开放性设计
    例:
      对于“电脑”这个产品而言,
      需求的稳定点是电脑的“计算”能力。
      需求的变化点,一是用户“计算”需求的多样性,二是用户交互方式的多样性。

    需求分析的重要性怎么形容都不过分。
    准确的需求分析是做出良好架构设计的基础。
    很多优秀的架构师之所以换到一个新领域一上来并不一定能够设计出好的架构,
    往往需要经过几次迭代才趋于稳定,原因在于新领域的需求理解需要一个过程。
    除了心里对需求的反复推敲的严谨态度外,对客户反馈的尊重之心也至关重要。

2. 确定规格
    规格是零部件的连接需求的抽象。
    符合规格的零部件可以有非常多种可能的实现方案,
    但一旦规格中某个条件不能满足,它就无法正常完成与其他零件的连接,以达到预期的需求目标。

    规格的约束条件会非常多样化:
      可能是外观(比如形状和颜色)
      可能是交互方式(比如用键盘、鼠标,或者语音和触摸屏)
      可能是质量(比如硬度、耐热性等等)

    规格就是指接口: 接口与实现的分离

3. 概要设计
    目的: 做子系统的划分
      a) 子系统职责范围的定义
      b) 子系统的规格(接口),子系统与子系统之间的边界
      c) 需求分解与组合的过程,系统如何满足需求、需求适用性(变化点)的应对策略
    从架构角度来看,进程至少应该是子系统级别的边界。
      子系统和子系统应该尽可能是规格级别的协同,而不是某种实现框架级别的协同。
      规格强调的是自然体现需求,所以规格是稳定的,是子系统的契约。
      而实现框架是技巧,是不稳定的,也许下次重构的时候实现框架就改变了。

中央处理器支持的指令:

1. 计算类,也就是支持我们大家都熟知的各类数学运算,如加减乘除、sin/cos 等等;
2. I/O 类,从存储读写数据,从输入输出设备读数据、写数据;
3. 指令跳转类,在满足特定条件下跳转到新的当前程序执行位置、调用自定义的函数。
1. 存储让数据跨越时间,(今天存储明天读取)
2. 传输让数据跨越空间,
3. 计算让数据改变形式

描述需求需要有几个典型的要素:

1. 用户,面向什么人群
2. 他们有什么要解决的问题
3. 我解决这个问题的核心系统

架构师核心:

是把知识串起来,构建一个完整的认知,不留疑惑。
大部分知识是不需要深入细节的,只在你需要的时候深入,但深入的时候要很深

架构师在整个架构的过程中,至少应该花费三分之一的精力在需求分析上。

一个优秀的架构师除了需要“在心里对需求反复推敲”的严谨态度外,对客户反馈的尊重之心也至关重要。
只有心里装着客户,才能理解好需求,做好架构。

cpu直接支持的叫内置存储,包括:

寄存器、内存(RAM)、主板上的ROM
https://img.zhaoweiguo.com/knowledge/images/iots/punched_card.webp

IBM 制造的打孔卡

编程语言

编程范式

  1. 过程式:

    最核心的两个概念是结构体(自定义的类型)和过程(也叫函数)
    通过结构体对数据进行组合,可以构建出任意复杂的自定义数据结构
    通过过程可以抽象出任意复杂的自定义指令,复用以前的成果,简化意图的表达
    
  2. 函数式:

    函数式本质上是过程式编程的一种约束,它最核心的主张就是变量不可变,函数尽可能没有副作用
    (对于通用语言来说,所有函数都没副作用是不可能的,内部有 IO 行为的函数就有副作用)。
    
    既然变量不可变,函数没有副作用,自然人们犯错的机会也就更少,代码质量就会更高。
    大部分语言会比较难以彻底实施函数式的编程思想,但在思想上会有所借鉴。
    
    函数式编程相对小众。因为这样写代码质量虽然高,但是学习门槛也高。
    举一个最简单的例子:
      在过程式编程中,数组是一个最常规的数据结构,
      但是在函数式中因为变量不可变,对某个下标的数组元素的修改,就需要复制整个数组
      (因为数组作为一个变量它不可变),非常低效。
    
      函数式编程,需要复杂的平衡二叉树来实现一个使用界面(接口)上和过程式语言数组一致的“数组”
      这个简单的例子表明,如果你想用函数式编程,你需要重修数据结构这门课程
    
    Purely Functional Data Structures - https://www.cs.cmu.edu/~rwh/theses/okasaki.pdf
    
  3. 面向对象:

    面向对象在过程式的基础上,引入了对象(类)和对象方法(类成员函数)
    它主张尽可能把方法(其实就是过程)归纳到合适的对象(类)上,不主张全局函数(过程)
    
    面向对象的核心思想是引入契约,基于对象这样一个概念对代码的使用界面进行抽象和封装
    
    两个显著的优点:
    a) 清晰的使用界面
      某种类型的对象有哪些方法一目了然,而不像过程式编程,数据结构和过程的关系是非常松散的
    b) 信息的封装
      面向对象不主张绕过对象的使用接口侵入到对象的内部实现细节
      因为这样做破坏了信息的封装,降低了类的可复用性
    
    面向对象还有一个至关重要的概念是接口:
      通过接口,我们可以优雅地实现过程式编程中很费劲才能做到的一个能力:多态
    
  4. 面向连接:

    Go 自己发明出来的范式名称
    
    所谓面向连接就是朴素的组合思想。
    研究连接,就是研究人与人如何组合,研究代码与代码之间怎么组合
    

编程框架和编程范式区别:

编程框架通常是领域性的,比如面向消息编程是多核背景下的网络服务器编程框架;
编程范式则是普适性的,不管解决什么领域的问题都可以适用

编程范式就好比是接口,编程框架就好比是实现了接口的抽象类,
使用编程框架编写的应用就好是继承了该抽象类的类。

契约

语言设计的方方面面都需要契约:

1. 面向对象创造性地把契约的重要性提高到了非常重要的高度,但这还远远不够。
    这是因为,并不是只有对象需要契约,语言设计的方方面面都需要契约。
2. 比如: 代码规范约束了人的行为,是人与人的连接契约。
    如果面对同一种语言,大家写代码的方式很不一样,语言就可能存在很多种方言,这对达成共识十分不利。
    所以 Go 语言直接从语言设计上就消灭掉那些最容易发生口水的地方,让大家专注于意图的表达。
3. 再比如: 消息传递约束了进程(goroutine)的行为,是进程与进程的连接契约。
    消息传递是多核背景下流行起来的一种编程思想
    其核心主张是:尽可能用消息传递来取代共享内存,从而尽可能避免显式的锁,降低编程负担。

    Go 语言不只是提供了语言内建的消息传递机制(channel),同时它的消息传递是类型安全的。
    这种类型安全的消息传递契约机制,大大降低了犯错的机会。

继承实现了代码复用和多态两个东西,揉在一起。在go里面,组合实现代码复用,接口实现多态,彼此完全独立,非常清晰

工程化能力

工程化能力主要体现在如下这些方面:

包(package),即代码的发布单元
版本(version),即包的依赖管理
文档生成(doc)
单元测试(test)

语言分类

从语言的执行器的行为看,出现了这样三种分类的语言:

1. 编译的目标文件为可执行程序
    典型代表是 Fortran、C/C++、Go 等。
2. 生成跨平台的虚拟机字节码,有独立的执行器(虚拟机)执行字节码
    典型代表为 Java、Erlang 等。
3. 直接解释执行
    典型代表是 JavaScript。
    大多数语言也只是看起来直接执行,内部还是会有基于字节码的虚拟机以提升性能。

规格描述语言描述模块的规格:

1. 选择某种语言无关的接口表示
2. 选择团指定语言来描述接口

对象范式的原始概念其实根本不包括类和继承,只有1,程序由对象组成,2,对象之间互相发送消息,协作完成任务。最初世界上第一个面向对象语言是 Simula-67,第二个面向对象语言是 Smalltalk-71。Smalltalk 受到了 Simula-67 的启发,基本出发点相同,但是最大的不同是Smalltalk是通过发消息来实现对象方法调用,而Simula是直接调用目标对象的方法。Bjarne Stroustrup 在博士期间深入研究过 Simula,非常欣赏其思想,C++的面向对象思路直接受其影响,因为调用目标对象的方法来“传递消息”需要事先知道这个对象有哪些方法,因此,定义对象本身有哪些方法的“类”和“继承”的概念,一下超越了对象本身,而对象只不过是类这个模子里造出来的东西,反而不重要。随着C++的大行其道,继承和封装变成了面向对象世界的核心概念,OOP 至此被扭曲为 COP ( Class Oriented Programming,面向类程序设计)。 但是COP这套概念本身是有缺陷的:每个程序员似乎都要先成为领域专家,然后成为领域分类学专家,然后构造一个完整的继承树,然后才能 new 出对象,让程序跑起来。 到了 1990 年代中期,问题已经十分明显。UML 中有一个对象活动图,其描述的就是运行时对象之间相互传递消息的模型。1994 年 Robert C. Martin 在《 Object-Oriented C++ Design Using Booch Method 》中,曾建议面向对象设计从对象活动图入手,而不是从类图入手。而 1995 年出版的经典作品《 Design Patterns 》中,建议优先考虑组合而不是继承,这也是尽人皆知的事情。这些迹象表明,在那个时候,面向对象社区里的思想领袖们,已经意识到“面向类的设计”并不好用。只可惜他们的革命精神还不够,delphi 之父在创建.Net Framework 的时候,曾经不想要继承,在微软内部引起了很大的争议,最后是向市场低头,加上了继承。 2000 年后,工程界明确的提出:“组合比继承重要,而且更灵活”,Go和Rust也许是第一批明确的对这种思路进行回应的语言,他们的对象根本不需要类本身来参与,也能完成对象范式的多态组合。 历史让 C++走上了舞台,历史也终将让 COP 重新回到 OOP 的本来面目

操作系统

  • 操作系统的核心目标是软件治理,只有在计算机需要管理很多的任务时,才需要有操作系统。

1. 从客户价值来说:

    a) 操作系统首先要解决的是软件治理的问题,大体可分为以下六个子系统:
      进程管理、存储管理、输入设备管理、输出设备管理、网络管理、安全管理等
    b) 操作系统其次解决的是基础编程接口问题
      这些编程接口一方面简化了软件开发,另一方面提供了多软件共同运行的环境,实现了软件治理

2. 从商业价值来说:

   a) 操作系统是基础的刚需软件
   b) 操作系统也是核心的流量入口
  • GPU的shared memory

  • TPU的local buffer

  • 实模式的操作系统,以微软的 DOS 系统为代表

  • 保护模式的操作系统

应用程序所依赖的基础架构:

1. 首先是冯·诺依曼计算机体系,它由 “中央处理器 + 存储 + 一系列的输入输出设备” 构成
2. 其次是编程语言
3. 最后就是操作系统

动态库设计,包括:

1. Windows 的 dll(Dynamic Link Library)
2. Linux/Android 的 so(shared object)
3. Mac/iOS 的 dylib(Mach-O Dynamic Library)

提供动态库不是操作系统的责任(因为其他语言完全可以通过系统调用来自己实现动态库的功能)
只是操作系统为了方便其他语言(减少不必要的冗余)而做的多余的事

cpu是如何检查是否有中断的。是怎么及时知道发生了中断:

硬件中断和软中断不一样。
硬件中断你可以理解为总是会定期检查。
软中断本身是一条指令,所以不存在检查这样的概念

cpu不需要检查是否发生了中断,它的原理类似于开关和灯泡的关系,
当开关合上,灯泡就会亮,灯泡不需要定期检查开关是否合上了

同一进程内的隔离是通过:

1. 分内核态很用户态
2. 内存页的保护属性(不可读、不可写、不可执行

外置存储

外置存储设备依据其功能特性不同,简单可以分为如下三类:

1. 顺序读写型
  如:磁带。
2. 随机只读型
  更准确说是单次完整写入多次读取型,也就是每次写数据都是整个存储介质一次性完整写入数据。
  如:光盘(含可擦写光盘)。
3. 随机读写型
  如:软盘、硬盘、U 盘、SSD 等等。
https://img.zhaoweiguo.com/knowledge/images/architectures/historys/file_format1.webp

随机只读型的外置存储(如光盘),常见的文件系统

https://img.zhaoweiguo.com/knowledge/images/architectures/historys/file_format2.webp

随机读写型的存储(如硬盘),常见的文件系统

“CPU只能操作内存” VS “大量的磁盘 IO 操作,非常占用 CPU 时间”:

所有外设cpu都统一基于数据交换(io)的方式操作。
cpu并不知道数据的含义,但是设备的使用方和设备知道。
这种情况下你可以简单理解cpu只是一根网线,但是很重要的一点是它让设备使用方和设备可以交互。
cpu并不负责磁盘io,但是它要等它结束以接收数据。
这方面当然也有一些新技术出现改善这一点

数据结构:

1. 外存的数据结构的特征是io是很费时的操作,所以外存数据结构+算法的优化方向是减少io次数,这个和内存很不一样。
2. 平常数据结构大部分是内存;但一般数据结构书最后有几章会谈到外存数据结构+算法。

裸io应该是指direct io。有两种绕过文件系统的方法:

1. 禁止掉文件系统的io缓存。
    这种做法实际上不是绕过文件系统,而是不采纳文件系统的io缓存算法。
    因为数据库可能自己有自己的缓存算法,如果文件系统也有缓存,就比较累赘,浪费了宝贵的内存空间,
    同样的内存空间给数据库扩大缓存空间更好。
2. 直接裸写磁盘分区。
    这时不存在文件系统,也就是说磁盘分区不需要格式化。
    这种做法在对象存储系统中用得更多一点,在数据库中用得不多。

执行体:

1. 进程
2. 线程
3. 协程

同步互斥相关的内容有:

1. 锁(Mutex)
2. 读写锁(RWMutex)
3. 信号量(Semaphore)
4. 等待组(WaitGroup)
5. 条件变量(Cond)

信号量(Semaphore):

S 代表着某种共享资源的数量
P 操作意味着请求或等待资源
V 操作意味着释放资源并唤醒执行体

需求分析

备注

从需求分析角度来说,关键要抓住需求的稳定点和变化点。需求的稳定点,往往是系统的核心价值点;而需求的变化点,则往往需要相应去做开放性设计。

需求分析的意义:

1. 满足用户需求
    用户需求到底为何,我们需要清楚定义。
2. 需求边界定义的需要
    用户需求理清楚了,不代表产品理清楚了。
    用户需求的满足一定会有行业分工,我们做什么,合作伙伴做什么,需要厘清大家的边界。
3. 架构设计的需要
    架构需要切分子系统,需要我们梳理并对用户需求进行归纳与抽象。
    架构还需要防止过度设计,把简单的事情复杂化。

从几个维度来看需求分析:

1. 我们要面向的核心用户人群是谁?
2. 用户原始需求是什么?最核心问题是哪几个?
3. 已经有哪些玩家在里面?
    上下游有哪些类型的公司,在我们之前,用户是怎么解决他们的问题的?
    我们的替换方案又是怎样的?
4. 我们的产品创造的价值点是什么?用户最关注的核心指标是什么?
5. 用户需求潜在的变化在哪些地方?区分出需求的变化点和稳定点。

接口继承最大的问题是搞错了接口的主体。其实接口是组件的使用方定义的,而不是组件的实现方定义的。这是根源。

详细设计

  1. 现状与需求:

    现在在哪里,遇到了什么问题,要做何改进
    
    『现状』不要长篇累牍。
    更多的是陈述与我们要做的改变相关的重要事实,侧重点在于强调这些事实的存在性和重要性。
    
    比如,假设我们要对某个模块重构。
    那么,现状就是要谈清楚现在的业务架构是怎样的?它到底有什么样的问题。
    
    『需求陈述』是对痛点和改进方向的一次共识确认。
    痛点只要够痛,大家都知道,所以同样不需要长篇累牍。
    
    每个子系统或模块,都有自己的角色分工与用户故事。
    我们不用重新做一遍需求分析,但对需求分析的核心结论,在详细设计开始之前需要明确。
    这很重要。它是我们详细设计所要满足的业务目标。
    
  2. 需求满足方式:

    要做成啥样?交付物的规格,或者说使用界面(接口)
    怎么做到?交付物的实现原理
    
    分两个方面:
        一方面是交付物的规格,或者说使用界面(接口)
            体现的是别人要怎么使用我。
        另一方面是背后的实现原理,我们怎么做到的
    

概要设计和详细设计的区分:

概要设计的核心目标是串联整个系统,消除系统的重大风险。

在这个过程中,对一些关键模块的实现细节有所考虑是非常正常的。
从这一个角度,分解粒度也不能过粗,不应该把特别庞大的子系统直接分出去,这样项目执行的风险就太高了。

两者的分工不同,考虑的问题重心不同。
比如,从使用界面(接口)来说,概要设计不一定会把子系统或模块的完整接口都列出来,
    实际上它只关注最核心的部分。
但是从详细设计角度来说,接口描述的完备性是必需的。
在架构过程中,需求分析阶段,我们关注用户需求的精确表述,
  我们会引入角色,也就是系统的各类参与方,以及角色间的交互方式,也就是用户故事。

到了详细设计阶段,角色和用户故事就变成了子系统、模块、类或者函数的使用界面(接口)。
  我们前面一直在强调,使用界面(接口)应该自然体现业务需求,就是强调程序是为用户需求服务的。
  而我们的架构设计,在需求分析与后续的概要设计、详细设计等过程之间也有自然的延续性。

所以算法,最直白的含义,指的是用户故事背后的实现机制。

数据结构 + 算法,是为了满足最初的角色与用户故事定义,这是架构的详细设计阶段核心关注点。

导致故障的因素

1. 软硬件升级与各类配置变更。
  变更是故障的第一大问题源头。
  保证系统不出问题的最简单的方法当然是不去升级。
2. 软硬件环境的故障也可能引发我们的服务异常。
    软硬件环境的故障包括:单机故障如硬盘坏、内存坏、网卡坏、系统死机失去响应或重启等。
    机房或机架故障如断网、断电等。
    区域性故障如运营商网络故障、DNS 服务商故障、自然灾害比如地震等。
3. 终端用户的请求也可能引发故障。
    比较典型的场景是秒杀类,短时间内大量的用户涌入,
      导致系统的承载能力超过规划,产生服务的过载。
    还有些场景如有针对性的恶意攻击、特定类型的用户请求导致的服务端资源大量消耗等,
      都可能引发服务故障。

及时故障恢复的手段:

1. 首先要看故障原因的初判。

   a) 如果是在软硬件环境升级中发现的故障,通常的做法是版本回滚。
   b) 如果是因为用户请求导致负载过高,则考虑扩容。
   c) 如果扩容不能让问题消失,则考虑服务降级,关闭一些不重要的功能以降低负载,或者主动抛弃一定比例的请求。
   d) 如果是软硬件环境本身故障导致的业务故障,则通常是进行流量切换,把用户流量导向其他没有问题的任务实例。

故障排查过程:

定义为反复采用:
  假设 - 验证排除手段的过程:

针对某系统的一些观察结果和对该系统运行机制的理论认知,
我们不断提出一个造成系统问题的假设,进而针对这些假设进行测试和排除。

测试假设是否成立:

1. 第一种方式,可以将我们的假设与观察到的系统状态进行对比,从中找出支持假设或者不支持假设的证据
2. 另一种方式,可以主动尝试 “治疗” 该系统,也就是对系统进行可控的调整,然后再观察操作的结果。

第二种方式务必要谨慎,以避免带来更大的故障。
但它的确可以让我们更好地理解系统目前的状态,排查造成系统问题的可能原因。

无论用上述两种方式中的哪一种,都可以重复测试我们的假设,直到找到根本原因。

真正意义上的 “线上调试” 是很少发生的:

毕竟我们遇到故障的时候,首先不是排查故障而是去恢复它,这有可能会破坏掉部分的现场。
所以,服务端软件的 “线上调试” 往往在事后发生,我们主要依赖的就是日志。
这里的日志是广义的,它包括监控系统背后的各类观测指标的时序数据,以及应用程序的程序日志。

软件架构

光靠把控软件工程师的水平,依赖他们自觉保障工程质量,是远远不够的。软件工程是一项非常复杂的系统工程,它需要依赖一个能够掌控整个工程全局的团队,来规划和引导整个系统的演变过程。这个团队就是架构师团队。

1. 产品经理关注的维度不太一样,关键词是:用户需求、技术赋能、商业成功。
2. 架构师的关键词是:用户需求、技术实现、业务迭代。
3. 项目经理在很多公司并不存在,它是交付类工作比较多的情况下的产物。

产品经理与架构师是一体两面,对人的能力要求的确会比较像,但是分工不同。

评价一个团队的架构能力好坏,看的关键是是否能够“坚持最小化的核心系统不变”,如果增加一个微服务团队不需要改变核心系统,那只能说你们的架构太牛了。

备注

设计强调规格,架构强调实现。规格设计是架构过程的最高共识。所以,规格高于实现。我们用架构的全局性和系统性思维去做设计。

架构设计文档

内容大体如下:

1. 现状:我们在哪里,现状是什么样的?
2. 需求:我们的问题或诉求是什么,要做何改进?
3. 需求满足方式:
  要做成什么样,交付物规格,或者说使用界面(接口)是什么?
  怎么做到?交付物的实现原理。

设计文档要素的关键在于以下几点:

1. 现状:不要长篇累牍。
    现状更多的是陈述与我们要做的改变相关的重要事实,侧重于强调这些事实的存在性和重要性。
2. 需求:同样不需要长篇累牍。
    痛点只要够痛,大家都知道,所以需求陈述是对痛点和改进方向的一次共识确认。
3. 需求满足方式:要详写,把我们的设计方案谈清楚。
    具体来说,它包括 “交付物规格” 和 “实现原理” 两个方面。

概要设计:

1. 第一关心的是模块关系
2. 第二关心的才是各个模块的核心接口

模块关系图

备注

表达模块关系在某种程度来说的确非常重要,这可能是许多人喜欢画架构图的原因。描述模块间的关系的确是一件比较复杂的事情

https://img.zhaoweiguo.com/knowledge/images/charts/model_relation2.webp

从模块关系表达上并不是好的选择,因为根本并没有对模块关系进行抽象。这类图更多被用在面向客户介绍 API SDK 的背后的实现原理时采用,而非出现在设计文档。

1. 对模块的调用接口进行分类

一个模块对外提供的访问接口无非是:

1. 常规 DOM API,即正常的模块功能调用
2. 事件(Event)的发送与监听
3. 插件(Plugin)的注册
https://img.zhaoweiguo.com/knowledge/images/charts/model_relation3.webp

[方法一]『接口分类』不同类型的访问接口,分别代表了模块间不同的依赖关系。

2. 表达模块关系的视角

https://img.zhaoweiguo.com/knowledge/images/charts/model_relation1.webp

[方法二]『表达模块关系的视角』: 是从架构分解看,我们把系统看作 “一个最小化的核心系统 + 多个彼此正交分解的周边系统”

备注

模块关系图的表达是非常粗糙的,虽然它有助于我们理解系统分解的逻辑。为了共识的精确,我们仍然需要将各个模块核心的使用界面(接口)表达出来。

实现原理

备注

对于模块的详细设计来说,需要先交代清楚 “数据结构” 是什么样的,然后再将一个个 UserStory 的业务流程讲清楚。

备注

对于系统的概要设计来说,核心是交代清楚不同模块的配合关系,所以无需交代数据结构,只需要把一个个 UserStory 的业务流程讲清楚。

软件质量管理

质量管理的抓手:

1. 自动化运行单元测试案例(unit test)
2. 单元测试覆盖率检查(code coverage)
3. 静态代码质量检查(lint)
4. 人工的代码互审(code review)
5. 灰度发布(gray release)
6. A/B 测试(A/B testing)

软件工程

备注

软件工程极大成熟的标志,是一体化的软件工程支撑系统,和高效的人才培养体系。到那个时候,软件工程就成为了一门真正成熟的科学。

备注

[工程篇最重要的一句话]严谨并非创新的对立面,而是创新的重要基础。每个人都有灵光乍现的时刻,但是唯有那些拥有严谨的科学态度的人才能抓住它,把它变成现实。

架构设计的优劣

架构设计会有它的一些基本准则。比如:

1. KISS:简单比复杂好
2. Modularity:着眼于模块而不是框架
3. Testable:保证可测试性
4. Orthogonal Decomposition:正交分解

架构图来表示架构的缺点:

1. 架构过程是团队共识形成与确认的过程。
    共识是需要精确的、无歧义的。而架构图显然并不精确。
2. 更精确描述架构的方法是定义每个模块的接口。
    接口可以用代码表达,这种表达是精确的、无歧义的。
    架构图则是辅助模块接口,用于说明模块接口之间的关联。
3. 为了证明接口的有效性,架构师还应该过一遍所有的用户故事
    以伪代码或流程图的方式,把所有用户故事过一遍,确认模块之间的接口串起来是可以正常工作的
4. 实际上更有效的方法是在概要设计(也叫系统设计)阶段就把框架代码写出来
    真真正正用代码,而不是伪代码,把用户故事串一遍。

备注

比框架(架构图)更重要的是数据结构,比数据结构更重要的是接口。

架构分解中有两大难题:

1. 其一,需求的交织。不同需求混杂在一起,也就是存在所谓的全局性功能。
2. 其二,需求的易变。不同客户,不同场景下需求看起来很不一样,场景呈发散趋势。

核心要掌握的架构设计的工具其实就只有两个:

1. 组合。用小业务组装出大业务,组装出越来越复杂的系统
2. 如何应对变化(开闭原则)

以下原因会导致架构老化难以避免:

1. 软件工程师的技术能力不行,以功能完成为先,不考虑项目的长期维护成本
2. 公司缺乏架构评审环节,系统的代码质量缺乏持续有效的关注
3. 需求理解不深刻,最初架构设计无法满足迭代发展的需要
4. 架构迭代不及时,大量因为赶时间而诞生的补丁式代码

要不要依赖公司内部的基础库:

完全不依赖意味着放弃生产力。这里基本的判断标准是:
  成熟度越高的基础库越值得依赖。

成熟度的评估依赖于个人经验,首先应该评估的是模块规格的成熟度
因为实现上的问题让时间来解决就行。
模块规格是否符合你的预期,以及经过了多少用户使用的打磨,这些是评估成熟度的依据。

依赖优化:

关注的重心不是某项功能本身的实现,而是它与系统之间的关系

小技巧:
  将散落在系统中的代码集中起来。我们把对系统的每处修改变成一个函数,比如叫 doXXX_yyyy。
  这里 XXX 是功能代号,yyyy 则依据这段搬走的代码语义命个名。
  某种程度来说故意起这太丑了的名字。它可以作为团队的约定俗成,代表此处待重新考虑边界。
  如果某个地方有好几个功能都加了 doXXX_yyyy 这样的调用,这就意味着
    这里需要重新考虑核心系统的边界
    这里需要提供一个事件机制,以便这些功能能够进行监听。

重构的挑战:

集架构设计(未来架构应该是什么样的)、
资源规划与调度(与新功能开发的优先级怎么排)、
阶段规划(如何把大任务变小,降低内部的抵触情绪和项目风险)
以及持久战的韧性与毅力的庞大工程

备注

高阶的技术可以按需学,按精力学,更根本的还是要打好基础,这也更有助于你判断是否应该深入学习某些技术。

参考

主页

索引

模块索引

搜索页面