简介
串一串编程范式
架构是软件系统的一部分,所以要明白架构的价值,首先要明确软件系统的价值。软件系统的价值有两方面,行为价值和架构价值。
- 行为价值是软件的核心价值,包括需求的实现,以及可用性保障(功能性 bug 、性能、稳定性)。这几乎占据了我们90%的工作内容,支撑业务先赢是我们工程师的首要责任。如果业务是明确的、稳定的,架构的价值就可以忽略不计,但业务通常是不明确的、飞速发展的,这时架构就无比重要,因为架构的价值就是让我们的软件(Software)更软(Soft)。
-
架构价值
- 当需求变更时,所需的软件变更必须简单方便。
- 变更实施的难度应该和变更的范畴(scope)成等比,而与变更的具体形状(shape)无关。
实现行为价值的需求通常是 PD 提出的,都比较紧急,但并不总是特别重要;架构价值的工作内容,通常是开发同学提出的,都很重要但基本不是很紧急,短期内不做也死不了。我们开发同学,在低头敲代码之前,一定要把杂糅在一起的“重要且紧急”和“不重要但紧急”分开,把我们架构工作(“重要但不紧急”)插进去。
其实所谓架构就是限制,限制源码放在哪里、限制依赖、限制通信的方式,但这些限制比较上层。编程范式是最基础的限制,它限制我们的控制流和数据流:结构化编程限制了控制权的直接转移(限制了goto语句),面向对象编程限制了控制权的间接转移(限制了函数指针的使用),函数式编程限制了赋值
编程范式 | 描述 | ||
---|---|---|---|
非结构化编程 | 其实就是汇编随便写 | 用到哪块逻辑,就 goto 到哪里执行一段代码 | 很难预测代码的执行结果,代码规模稍微一大,就几乎难以维护了 |
结构化编程 | 限制控制权的直接转移 | 就是函数调用或者 goto 语句,代码在原来的流程里不继续执行了,转而去执行别的代码,并且你指明了执行什么代码。 | 限制goto语句 |
面向对象编程 | 限制控制权的间接转移 | 就是代码在原来的流程里不继续执行了,转而去执行别的代码,但具体执行了啥代码你也不知道,你只调了个函数指针或者接口。 | 限制函数指针 |
函数式编程 | 限制赋值 | 函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值 |
没有结构化编程,程序就无法从一块块可证伪的逻辑搭建,没有面向对象编程,跨越组件边界会是一个非常麻烦而危险的过程,而函数式编程,让组件更加高效而稳定。没有编程范式,架构设计将无从谈起。
架构工作的基本方针
- 尽可能长时间地保留尽可能多的可选项。
- 低层次解耦方式能解决的,不要用高层次解耦方式。
组件拆分需要在两个维度进行:按层次拆分、按变更原因拆分。
许式伟:编程范式的进化
- 过程式编程中最核心的两个概念是结构体(自定义的类型)和过程(也叫函数)。通过结构体对数据进行组合,可以构建出任意复杂的自定义数据结构。通过过程可以抽象出任意复杂的自定义指令,复用以前的成果,简化意图的表达。但数据结构和过程的关系是非常松散的。
- 函数式本质上是过程式编程的一种约束,它最核心的主张就是变量不可变,函数尽可能没有副作用(对于通用语言来说,所有函数都没副作用是不可能的,内部有 IO 行为的函数就有副作用)。
- 面向对象在过程式的基础上,引入了对象(类)和对象方法(类成员函数),它主张尽可能把方法(其实就是过程)归纳到合适的对象(类)上,不主张全局函数(过程)。
- 封装。面向对象不主张绕过对象的使用接口侵入到对象的内部实现细节。因为这样做破坏了信息的封装,降低了类的可复用性,有一天对象的内部实现方式改变了,依赖该对象的相关代码也需要跟着调整。PS:多想想一个对象的输入输出是什么
- 多态。由于对象和对象方法的强关联,我们可以引入接口来抽象不同对象相同的行为(比如鸟和猪是不同的对象,但是它们有相同的方法,比如移动和吃东西)。这样(对调用方来说)不同对象就可以用相同的代码来实现类似的复杂行为,这就是多态了。PS:是不是可以认为封装和多态都是为了对调用方屏蔽变化
- 大家对继承褒贬不一。这件事情最完美的答案是 Go 语言给出来的:放弃继承,全面强化组合能力
《C++ Primer Plus》计算机语言要处理两个概念——数据和算法。c最初是过程性语言,意味着它强调的是编程的算法方面。OOP强调的是数据,试图让语言来满足问题的要求,理念是设计与问题的本质特性相对应的数据格式。在C++中,类是一种规范,它描述了这种新型数据格式,对象是根据这种规范构造的特定数据结构。它不是将重点放在任务上,而是放在概念上。
多范式编程
多范式编程会越来越多,是因为我们的关注点是做出好的设计,写出更容易维护的代码,所以,我们会尝试着把不同编程风格中优秀的元素放在一起。比如,我们采用面向对象来组织程序,而在每个类具体的接口设计上,采用函数式编程的风格,在具体的实现中使用结构化编程提供的控制结构。
面向对象创造性地把契约的重要性提高到了非常重要的高度,但这还远远不够。这是因为,并不是只有对象需要契约,语言设计的方方面面都需要契约。
- 比如,代码规范约束了人的行为,是人与人的连接契约。如果面对同一种语言,大家写代码的方式很不一样,语言就可能存在很多种方言,这对达成共识十分不利。所以 Go 语言直接从语言设计上就消灭掉那些最容易发生口水的地方,让大家专注于意图的表达。PS:go 的一个设计理念就是 实现一个目的只能有一种实现方式
- 消息传递约束了进程(这里的进程是抽象意义上的,在 Go 语言中叫 goroutine)的行为,是进程与进程的连接契约。消息传递是多核背景下流行起来的一种编程思想,其核心主张是:尽可能用消息传递来取代共享内存,从而尽可能避免显式的锁,降低编程负担。PS:虽然本质都是共享内存,但并发处理的方式统一了。
符号式编程
命令式编程
def add(a,b):
return a + b
def fancy_func(a,b,c,d):
e = add(a,b)
f = add(c,d)
g = add(e,f)
return g
print(fancy_func(1,2,3,4))
对应的符号式编程
def add_str():
return '''
def add(a,b):
return a + b
'''
def fancy_func_str():
return '''
def fancy_func(a,b,c,d):
e = add(a,b)
f = add(c,d)
g = add(e,f)
return g
'''
def evoke_str():
return add_str() + fancy_func_str() + '''
print(fancy_func(1,2,3,4))
'''
prog = evoke_str()
y = compile(prog,'','exec')
exec(y)
定义的三个函数以字符串形式返回计算流程,最后通过compile 编译完整的计算流程并执行,由于编译器能够完整的获取整个程序,因此有更多空间优化计算。