冒号课堂笔记
编程语言与编程范式
范式译自英文的paradigm,也有译作典范、范型、范例的。所谓编程范式(programming paradigm),指的是计算机编程的基本风格或典范模式,是编程者在其所创造的虚拟世界中不自觉采用的世界观和方法论。每种范式都引导人们带着特有的倾向和思路去分析和解决问题。OOP就是一种范式。
一、程序
库(lib)与框架(framework)
-
为保证软件开发快速有效,通常采取:
在宏观管理上选取框架以控制整体的结构与流程;在微观实现上利用库和工具包来解决细节问题。
-
框架的意义在于使设计者在特定领域的整体设计上不必重新发明轮子;库和工具包的意义在于使开发者摆脱底层编码,专注于特定问题和业务逻辑。
-
库和工具包是为给程序员带来自由的,框架是为程序员带来约束的。
-
库和工具包侧重代码重用,框架侧重设计重用。
-
框架是通过控制反转(IoC)机制控制全局,而库和工具包用callback知识局部的控制反转。程序员牺牲了对应用程序流程的主导权,换来的是更简洁的代码和更高的生产效率。
设计模式
设计模式是软件的战术思想,架构师软件的战略决策。与框架、库和工具包不同,他们不是软件产品,是软件思想。
设计模式与惯用法都是针对常发问题的解决方法,但前者重设计,后者偏实现。
控制反转
控制反转(Inversion of Control)是一种软件设计原则,与朝哪个用的用户代码调用可重用库(library)代码不同,IoC倒转控制流方向:由库代码调用用户代码。有人将此比作好莱坞原则:”不要打电话给我们,我们会打给你的”。
二、重要范式
编程是寻求一种机制,将指定的输入转化为指定的输出。
-
命令式:自动机机制,通过设计指令完成从初始态到最终态的转变。(命令式-过程式-结构化过程式)
命令式编程的变量代表抽象化的内存,所存内容可能改变;声明式编程的变量代表抽象化的符号,所指对象一般不会改变。
命令式语言擅长业务逻辑的,尤其是交互式或事件驱动型应用。
-
声明式编程专注问题的分析与表达,而不是算法实现,不用指明执行顺序,一般没有或极少有副作用,也不存在内存管理问题。大大降低了编程的复杂度,也适合并发计算。
-
函数式:数学变换机制,通过设计函数完成从自变量到因变量的计算。
-
逻辑式:逻辑证明机制,通过逻辑推理完成从题设到结论的证明。
函数式语言和逻辑式语言擅长数理逻辑的应用。
-
三大范式比较:
范式 | 程序 | 输入 | 输出 | 程序设计 | 程序运行 |
---|---|---|---|---|---|
命令式 | 自动机 | 初始状态 | 最终状态 | 设计指令 | 命令执行 |
函数式 | 数学函数 | 自变量 | 因变量 | 设计函数 | 表达式转换 |
逻辑式 | 逻辑证明 | 题设 | 结论 | 设计命题 | 逻辑推理 |
OOP(Object-Oriented programming)是一种计算机编程模式,它将对象作为问题空间的基本元素,利用对象和对象间的相互作用来设计程序。
OOP大多是命令式,也有函数式和逻辑式的OO语言。
OOP的核心思想可以归纳为:以数据为中心组织逻辑,将系统视为相互作用的对象集合,并利用继承与多态来增强可维护性、可扩展性与可重用性。
过程式编程以过程为中心,自顶向下,逐步求精。
对象式编程以数据为中心,自底向上,逐步合并。
过程式程序的世界是君主制,OO程序的世界是民主制。
封装使得对象拥有个体身份,继承使对象拥有家庭身份,多态使得对象拥有社会身份。
并发式编程以进程为导向,以任务为中心,以资源共享与竞争为主线。
并发式编程有助于提高运行效率、充分利用资源、提高软件的响应能力、改善用户体验、保证公平竞争,同时以进程为单位将系统模块化,更加真实地模拟世界。
合理的并发式设计应该做到:软件易于重用、维护和测试;有效地利用资源、优化程序性能;保障进程安全和活性;减少性能损失和复杂度。
五大范式比较:
范式 | 体系 | 模块 | 模块关系 |
---|---|---|---|
过程式 | 君主体系 | 过程 | 授命与听命 |
函数式 | 数学体系 | 函数 | 替换与合成 |
逻辑式 | 逻辑体系 | 断言 | 归纳与演绎 |
对象式 | 民主体系 | 对象 | 交流与服务 |
并发式 | 生产体系 | 进程 | 竞争与合作 |
三、常用范式
STL
STL有3要素:算法(algorithms)、容器(container)和迭代器(iterator)
-
算法是一系列切实有效的步骤;
-
容器是数据的集合,可以理解为抽象的数组;
-
迭代器是算法与容器之间的接口,可以理解为抽象的指针或游标。
-
算法串联数据,如脊贯肉;数据实体算法,如肉附脊。
元编程 Meta-programming
元编程是编写、操作程序的程序。在传统的编程中,运算是动态的,程序本身是静态的;但在元编程中,二者都是动态的。
元编程能减少手工编程,突破原语言的语法限制,提升语言的抽象级别与灵活性,从而提高程序员的生产效率。
元编程应用:
-
许多开发工具、框架引擎之类的基础软件都有自动生成代码的功能,如许多IDE如Visual Studio、Delphi、Eclipse均能通过向导、拖放控件等方式自动生成源码;
-
创造DSL以便更高效地处理专门领域的业务;
-
自动生成重复代码;
-
动态改变程序的语句、函数、类等。
产生式编程与静态元编程都能够自动生成源代码。产生式编程强调代码的生成,元编程强调代码的可执行性。此外,动态元编程并不生成源代码,但能够在运行期间修改程序。
元程序将程序作为数据来看待,有着其他程序不具备的自觉性、自适应性和智能性,可以说是一种最高级的程序。
领域特定语言 DSL
语言导向式编程(LOP)通过创建一套专用语言DSL来编写程序,相比通用语言,DSL更简单、更加抽象、更加专业、更加接近自然语言和声明式语言、开发效率更高。
语言导向式编程一般通过元编程将专用语言转化为通用语言。
DSL一般不会一步到位地编译成机器语言或汇编语言,而是通过现成的编译器生成器(compiler-compiler 或 compiler generator)首先转化为高级语言。这样不仅大大降低了难度,也方便程序的调试。
切面范式
SoC 就是 Separation of Concerns,即关注点分离;DRY就是Don’t repeat yourself,即尽量减少重复代码。
-
不良代码通病:
-
结构混乱或代码紊乱、松散;
-
代码重复。解决此问题就是要做到——抽象与分离原则。
-
-
抽象与分解的原则:单一化、正交化。
-
每个模块职责明确专一,模块之间独立,即高内聚低耦合(high cohesion & low coupling)。
AOP
切面 Aspect 描述的是横切关注点(cross-cutting concerns),是与程序纵向主流执行方向横向正交的关注焦点。
接入点是附加行为——建议(advice)的执行点,切入点(pointcut)是指切入点(join point)结合。这些接入点共享一段插入代码。切入点与建议组成切面(aspect),是模块化的横切关注点。
AOP的实现原理:
AOP的实现关键是将advice的代码嵌入到主题程序中,术语称之为编织(weaving)。
编织可以分为两种:
一种是静态编织,通过修改源码或字节码(bytecode)在编译器(compile-time)、后编译器(post-compile)或加载器(load-time)嵌入代码(元编程、产生式编程实现);
另一种是动态编织,通过代理(proxy)等技术在运行时(run-time)实现嵌入。
AOP实施的3步:切面分解、切面实现和切面合成。
事件驱动
事件驱动:采用警觉式者主动去轮询(polling),行为取决于自身的观察判断,是流程驱动的,符合常规的流程驱动式编程(Flow-Driven Programming)的模式。
采用托付式者被动等通知(notification),行为取决于外来的突发事件,是事件驱动的,符合事件驱动式编程(Event-Driven Programming, aka EDP)的模式。
什么是事件?
事件是已经发生的某种令人关注的事情。在软件中,它一般表现为一个程序的某些信息状态上的变化。
事件分类
内建事件(built-in event)
-
底层事件(low-level event)或原生事件(native event)
在用户图形界面(GUI)系统中,这类事件由鼠标、键盘等硬件设备出发;
-
语义事件(semantic event)
一般代表用户的行为逻辑,是若干个底层事件的组合。比如鼠标的拖放(drag-and-drop)多表示移动被拖放的对象,由鼠标按下、移动和释放三个底层事件组成。
-
用户自定义事件(user-defined event)
-
虚拟事件(virtual event)
-
原有内建事件基础上的包装
-
此外,事件还有自然事件(natural event)和合成事件(synthetic event)。
事件驱动步骤
实现事件处理器;注册事件处理器;实现事件循环。
事件驱动式的特征
被动性与异步性。控制反转导致了事件驱动式编程的****被动性passivity**。此外,事件驱动式编程还具有异步性(asynchrony) 的特征,这是由于事件的不可预测性和随机性决定的。
回调函数(callback)
Callback是指能作为参数传递的函数或代码,它允许底层模块调用高层模块,使调用者与被调者从代码上解耦。异步Callback在传入后并不会立即调用,使调用者与被调者从时间上解耦。
在C、CPP中函数指针可以实现callback。此外,抽象类(abstract class)、接口(interface)、CPP中的泛型函子(generic functor)和C#中的委托(delegate)都可以实现callback。
控制反转
控制反转一般通过callback实现,其目的是降低模块之间的依赖性,从而降低模块的耦合度和复杂度。
依赖反转、控制反转和依赖注射是近义词,它们的主题是控制与依赖,目的是解耦,方法是反转,实现一切的关键是抽象接口。
依赖反转原则(Dependency-Inversion Principle,aka DIP)与控制反转相比更加具体——高层模块不依赖于低层模块,它们都应依赖抽象;抽象不应依赖于细节,细节应该依赖抽象。
依赖注射(Dependency Injection,aka DI)——动态地为一个软件组件提供外部依赖。
四、范式的使用
软件的可伸缩想(scalability)一般指从容应对工作量增长的能力,常与性能(performance)等指标一起考量。而控制反转的主要作用是降低模块之间的依赖性,从而降低模块的耦合度和复杂度,提高软件的可重用性、柔韧性和可扩展性。
独立是异步的前提,耗时是异步的理由。
观察者模式又称为发行/订阅模式,既是事件驱动式的简化,也是事件驱动式的核心思想。MVC架构是观察者模式在架构设计上的一个应用。
函数式编程
函数式编程中,函数是程序的核心,是头等公民,一般没有货很少有副作用,同时没有显示的内存管理。
函数式编程没有副作用(side affect)的好处:
-
没有副作用的函数易于重构、调试和单元测试。
-
代码有效性与函数顺序无关,方便并发处理和优化处理。
没有副作用的函数式是引用透明的(referential transparency),即一个表达式随时可以用它的值来替换,如数学中的函数一样,保证了数学思维的贯彻与运用。
惰性求值是需求驱动的,可以避免不必要的等待和计算。
相比于过程式和OOP,函数式思想过于数学化和抽象化,语言表现力和运行效率也不足。
逻辑范式
代码的长度不是衡量软件复杂度的唯一标准。其中,逻辑结构越复杂、越微妙、受需求变化影响越大,软件越难控制和维护。
算法=逻辑+控制 。逻辑式编程将算法中的控制部分大都移交给编程语言,开发人员主要关注算法的核心逻辑。这样大大减轻了开发人员的负担,编码也更加简洁,更具有可维护性和可扩展性。
-
区别于过程式和函数式,逻辑式没有明显的输入和输出。
-
逻辑式编程不仅适用于人工智能方面的学术领域,还广泛适用于各种设计知识管理、决策分析等方面的应用领域。
设计模式
相比于设计模式,编程范式针对问题领域更广泛,提出的思想和方法更为普遍适用、更抽象、更系统。此外, 设计模式重在设计,对语言和工具要求不高,而编程范式要求建立一套抽象机制和方法体系,离不开语言或工具的支持。
- 编程范式的核心价值在于:突破原有编程方式的某些限制,带来新思维和新方法,从而进一步解放程序员的劳动力。
闭包
闭包是一种能保存当初创建时环境变量的函数。它通常以匿名方式存在,多用于函数式编程中,能够让代码结构更加清晰简洁。Java中的匿名函数可以看做是OO化的闭包形式。
五、编程语言
所谓迭代学习法,是指在具体知识和抽象理论之间进行增量式的循环学习。
Duck类型
Duck类型的哲学:名义不重要,重要的是能力。
鸭子类型是动态类型的一种风格,允许非继承性多态,即一个对象的类型可以由其接口集合来去定,不需要通过显示继承,有利于代码重用。由于Duck类型的接口组合是隐性的,其使用者须要比普通的interface更小心,以免误用;其维护者也要小心,以免破坏客户端代码;另外,它也可能造成滥用。
数据类型
数据类型包含两个要素:
-
允许取值的集合
-
允许参与的运算
如int
类型在Java中既定义了介于和之间的整数集合,也定义了该集合上的整数所能进行的运算。
限定一个变量的数据类型,就意味着限制该变量的取值范围和所参与的运算,这从一定程度上保证了代码的安全性。
数据类型意义
数据类型既有针对机器的物理意义,又有针对人的逻辑意义。前者进行底层的内存分配和数值运算等,后者用于表达高层的逻辑概念。既然类型如此重要,类型检查就必不可少。
动态类型和静态类型
所谓动态类型语言(dynamic typing language),正是指类型检查发生在运行期间(run-time)的语言。
- 优点:代码灵活简明、易于重用,适合泛型编程和快速原型开发。
静态类型语言(static typing language)是类型检查发生在运行之前(包括编译期间,compile-time)的语言。
- 优点:运行之前的类型检查增强了代码的可靠性,使编译器有可能进行优化处理而提高运行效率,节省了运行期的类型检查所占用的时间和空间,同时类型声明有辅助文档的功效。
动态类型的变量不需要显示声明,静态类型的变量需要通过显示声明或类型推断。
静态类型检查实行“疑罪从有”的有罪推定制,动态类型检查实行“疑罪从无”的无罪推定制。取舍原则是:Static Typing Where Possible, Dynamic Typing When Needed。即尽可能守规则,必要时变通。
动态类型
动态语言秉承的理念:优化人的时间而不是机器的时间。为提高人的生产效率,宁肯牺牲部分程序性能或者购买更高配置的硬件。
动态语言在程序运行期间改变数据结构、函数定义、对象行为或指令流程等,相比静态语言在结构和功能上的更就有动态性。
优点:
-
代码量少,从一定程度上减轻了维护难度;
-
提供字节码编译或JIT编译,弥补了运行效率上的不足;
-
一些模块的结构和功能上的变化不会导致相关模块的重新编译和链接;
-
具有灵活、适应力强和开发周期短的特点,能够快速响应客户端的需求变化,并且适合快速原型开发。
强类型、弱类型
类型按安全性来划分,可分为类型安全(type-safe language)和类型不安全语言(type-unsafe language)。
类型检查的目的就是为了避免类型错误(type error),即杜绝因类型问题而产生的错误或不良代码。
弱类型语言允许类型的隐性转化,被认为是类型不安全的;而强类型的语言则不允许这种转化,被认为是类型安全的。
类型的动静与强弱完全是正交的两个概念。静态类型语言中,有强类型的Java,也有弱类型的C;动态类型语言中,有强类型的Smalltalk,也有弱类型的JavaScript。前者通过类型的绑定时间来划分,后者以类型的约束强度来划分。 通常弱类型语言(weakly-typed language)允许一种类型的值隐性转化为另一种类型。
语言简评
系统语言
C++提倡使用RAII原则解决包括内存在内的资源管理问题。RRIF(Resource Release Is Finalization),即“资源释即终结化”,其思想是:将资源的取放与某一对象的生命周期绑定,初始化对象是获取资源,终结化对象时释放资源。用户代码不再直接管理资源,只需控制相应对象即可。这样代码得以简化,资源的有效性也得以保障,并且还是异常安全的(exception-safe)。
平台语言
相比C++,Java和C#更加安全(限用指针、数组边界检查、类型安全、资源管理等)、简单(自动垃圾回收、废除多继承和头文件等)、中性(编译成机器无关的字节码,运行于虚拟机)、OO(无全局变量和函数等)、丰富(支持反射和并发编程,更完备的api)、标准(注释行文档、更好的Unicode支持)。
后台脚本
脚本语言一般是解释型语言,不需要通过“编译 - 链接 - 运行”的循环圈,便利快捷,加之简单宽松的语法、面向字符的特性,以及较强的文本处理能力,尤其适合作为年和语言,多用于系统管理和集成。
LAMP是指Linux、Apache、Mysql和PHP(Python、Perl、Ruby)组成的网络开发平台,是一种轻量级解决方案。
抽象机制与对象范式
六、抽象封装
-
抽象是去粗取精以化繁为简;由表及里以异中求同;凑想就是做减法和除法。
-
软件开发过程分为:
-
分析阶段的主要任务是在问题领域和业务需求的基础上置顶功能规范;
-
设计阶段的主要任务是在分析的基础上制定实现规范;
-
实现阶段则在设计的基础上完成软件编码。
如果采用对象导向式(OO)的方法,则分别对应OOA(Object-Oriented Analysis)、OOD(Object-Oriented Design)和OOP(Object-Oriented Programming)。
-
-
抽象贯穿于整个软件开发的核心过程:
-
分析阶段采用性质导向式抽象(property-oriented abstraction);
-
设计阶段采用模型导向式抽象(model-oriented abstraction);
-
实现阶段采用参数抽象(abstraction by parameterization)和规范抽象(abstraction by specification)。
-
-
抽象本身并不能够解决问题,但却是解决问题的必经之路。通过抽象能简化和分解问题,是指更容易理解和控制,相应的解法也更具有稳定性、普适性、重用性和可维护性。
编程中常用的基本抽象
抽象类型 | 抽象引入 | 抽象结果 | 抽象目的 |
---|---|---|---|
过程抽象 | 运算 | 函数 | 将行为的逻辑属性与实现细节分离 |
数据抽象 | 类型 | 抽象数据类型 | 将数据的逻辑属性与表示细节分离 |
迭代抽象 | 循环 | 迭代器 | 将集合遍历与元素的获取细节分离 |
类型层级 | 类族 | 类型层级结构 | 将类型家族的公共行为与具体类型分离 |
多态抽象 | 多态类型 |
数据抽象
-
数据结构与抽象数据类型同为数据与运算的有机集合体,常可以看作同一事物的两个方面。前者强调具体实现,多从实现者和维护者的角度来考虑;后者强调抽象接口,多从设计者和使用者的角度考虑。
-
具体数据类型一般只是被动地作为数据存储或打包的工具,很少行为特征,与实现细节紧密相关。
-
数据抽象依赖于规范,包括所有的接口规范和数据类型的整体规范。
-
Programming to an Interface, not an Implementation.
-
抽象是OOP的核心和起源,是封装、继承和多态的基础。
-
数据抽象的好处:
-
开发者:接口与实现的分离,有利于开发时间的分离及开发人员的分离。
-
使用者:通过高层接口来操纵对象,保证了客户代码的可读性和稳定性。
-
封装
封装性是将数据与相关行为包装在一起以实现信息隐藏。信息隐藏是一种原则,封装是实现这种原则的一种方式。
-
广义上的封装是把一些数据和方法捆绑在一起,从而引入了一种被称为类的模块,并使函数(方法)的调用更简洁、更符合认知模式。
-
狭义的封装是在广义的基础上增加访问控制,以实现信息隐藏。
-
狭义的封装从语法上强化了抽象,即用户不仅可以从高层接口来对待一类对象,而且不用关心也能无权访问底层实现。
访问控制:
-
访问控制不仅是一种语法限制,也是一种语义规范,明确地将接口与实现分离开来。
-
访问控制并不是牢不可破的,C++可以通过指针、Java和C#可以通过反射绕过访问控制。
信息隐藏的关键是掩盖实现细节,包括内部数据结构和算法逻辑。常见的错误有:
-
将可变的内部对象引用作为返回值;
-
将可变的对象引用直接复制给内部对象;
-
没有进行必要的深拷贝;
-
接口方法的参数或返回值类型不够抽象;
-
在方法命名上透露实现策略。
七、抽象接口
编程的难点有两个方面:逻辑的复杂性和需求的变化性。
软件的变化主要来自两个方面:一个以改善软件指令为目的的内在结构变化;一个是以满足客户需求为目的的外在功能性变化。
信息隐藏屏蔽了一个模块中非本质、容易变化的部分,从而保证内部修改不会波及客户。它的目的是提高软件的应变性和灵活性,而不是安全性。信息隐藏能够将接口与实现从逻辑上分离,桥梁模式则能够进一步将二者从物理上分离,从而使代码更具有应变能力。在C++中,桥梁模式(Pimpl惯用法)还能够减少编译期间的依赖性,从而节省编译链接时间。
开闭原则(OCP)指软件实体(类、模块、函数等)应对扩展开放、对修改封闭。开闭原则是编程设计(包括OOD)的一个重要原则,对提高软件应变能力由具有指导意义。遵循这一原则的关键在于引入抽象,特别是抽象接口。
访问控制
访问控制划分了抽象的边界,从语义上将抽象层次话,从语法上保护主客双方的代码不受彼此影响。访问控制是对静态关联代码的控制,划分代码的修改边界。访问控制也使得类的接口层次化、职责层次化、服务层次化、客户层次化。
C++的友元
C++中的一个类与其有类或者友函数是联合关系非主次关系。合理使用friend
修饰符,不仅不会破坏封装,相反能够加强封装。
接口服务
使用类时应该以规范文档为依据,不能够以源代码为依据。前者代表抽象接口,后者代表具体实现。如果文档不够明确,应尽可能地与作者通过不断改进的提交文档来交流。