程序原本(二):应用开发技术

2020 年 2 月 11 日

程序原本(二):应用开发技术

编者按:本文节选自周爱民著《程序原本》一书中的部分章节。


模块化的精髓不在于外在形式的分离,而在于内在逻辑的延续


图 1 展示了在稍早一些的应用开发语言中,从“代码的粒度”出发的抽象概念。



图 1 从“代码的粒度”出发的抽象概念12


1 语句与行的不同,通常也被称为逻辑行与物理行概念上的不同。此外,有些语言是强制要求以物理行来表达“语句”这一概念的,即一行语句必须书写于一行代码中。


2 单元与模块除了称谓的不同,很多时候其抽象概念也并不完全相同或者互相覆盖,例如一个单元可以是(或不是)一个模块。我们这里只取在某些语言中、将模块特指为“一系列函数”的这一概念。


其中,单元或模块用于组织一系列函数,而一个应用3则是由单元或模块构成。在这样的体系中,“化整为零”的问题会变得相对简单,即如何有规则或有逻辑地将一堆函数组织成单元。这里的“规则”与“逻辑”阐述了组织法则的两个方向。


3 早期的应用开发语言也直接将应用称为“程序”(program)。


其一,我们可以设定一个简单的分类依据,使得位于同一个单元中的函数表现出一定的相似性。例如开发一个图形库,我们可以将与图形设备相关的功能放在 device 库中,将绘制功能放在 graph 库中,将渲染功能放在 render 库中,将与图形库无关但又与计算机基础环境相关的功能放在 base 库中,如此等等。最后,我们将一些杂乱无章的功能放在 misc 库中。请注意,这一切的分类依据是“功能的归属与使用者”。类似地,我们也可以依据数据的位置来建立分类依据。例如同样是这个图形库,我们可以将基础数据运算放在 bits 库中,并基于此建立关于图形运算的类型抽象库 types。接下来我们定义在不同设备上适用的数据结构,比如在存储设备中的种种文件格式 fileTypes、在内存中复制和运算的 dibs(设备无关位图,Device-Independent Bitmap)以及在某种具体显示设备中适用的 cudaTypes(CUDA,Compute Unified Device Architecture)等。这样依据数据(所处的)位置以及需要进行的计算进行分类,也便于将数据及其副本放在不同的环境下开发。


这一类的方案或试图交付一个可以被使用甚至被共用的功能集,或通过抽取不同层次(例如面向不同设备或不同场景)的代码,使之可以“或多或少”应用于不同的环境。与这个组织原则密不可分的一个问题是:如何使一个“单元/模块”向外公布它所具有的功能集。这形成了著名的“开放细节”与“公开功能但隐藏细节”之争4,如今后者已成为应用接口设计思想的主流,前者则部分地影响并推进了开放源代码这一思想。


4 参见《人月神话》中“关于信息隐藏,Parnas 是正确的,我是错误的”小节,以及 David Parnas 关于信息隐蔽理论的著名论文:《论将系统分解为模块的准则》、《设计易于扩展和收缩的软件》和《复杂系统的模块化架构》。


但总的来说,这个组织法则只解决了一个应用中能被静态规则化的部分。无论如何,它无法满足“让程序运行起来”之后可能带来的种种变化。


其二,我们可以使得一个单元或多个单元中的函数存有某种逻辑关系。著名的“自顶向下程序设计”的思想,就处于这一组织法则所代表的方向上。例如:


设有一个逻辑(X),功能是将 m 变换为 n,如图 2 所示;



图 2 基本功能:将 m 变换为 n


由于 X 的规模巨大,我们将它分成三个逻辑步骤(顺序逻辑 1→2→3)来实现,如图 3 所示。



图 3 分解:三个步骤


虽然向下一层的分解并不限定各步骤之间的关系,但我们注意到此前讨论过的一个事实,即所有逻辑都可以被理解为顺序逻辑中的一个步骤。也就是说,步骤 2 依赖于步骤 1,步骤 3 依赖于步骤 2。随后我们继续向下一层分解,如图 4 所示。



图 4 持续分解:更多的步骤、逻辑或子系统


在图 4 中:


  • 步骤1的子步骤1.1分成了1.2和1.3两个分支,最终以1.3为出口,传出数据n’;

  • 步骤2则被分解为三个子步骤的循环,并总是以步骤2.3为出口,传出数据n“;

  • 步骤3分解为两个顺序的子步骤,得到转换结果n。


由此无论是针对顶层 X 这个逻辑,还是针对第二、三层各分解的逻辑,整体的逻辑关系都没有变化。所有的逻辑关系在函数与函数之间,以及在“一堆函数”与“另一堆函数”之间都可以被简单地抽象为“顺序依赖”。即使将这些单元/模块之间的关系映射到最终子系统的划分之上,这种逻辑关系也不会变化。例如从子系统划分上看:


  • 子系统1被设计为“预处理器”(preProcessor);

  • 子系统2被设计为“分析器”(analyzer)或“过滤器”(filter);

  • 子系统3被设计为(下一阶段的)“数据供应器”(dataProvider)。


这三个子系统以及它们的组织关系就可以构成某种数据处理系统的、整体的、面向运行期的逻辑架构。


进一步地说,通常单元/模块之间的逻辑关系只是简单的依赖关系。这一关系足以支撑由结构化程序设计带来的计算需求,包括支持数据流转与逻辑执行。5


5 DFD(DataFlow Diagram,数据流图)通常用于解释在上述执行过程中 m-n 之间的转换关系不变(即数据单一入口与单一出口)。但在本例中,它亦用于解释自顶向下过程中的逻辑关系不变,整体保持着顺序执行关系。这一过程是抽象概念——从程序语言中逻辑的结构化,到应用系统中组织的模块化——的延伸。


“没有坏味道”的诀窍:如何更好地组织代码


将数据或逻辑具有类似性质的代码放在一起,或者将逻辑之间存有关系的代码放在一起,这两种思路与面向对象用封装性来解决的问题是相类似的。一个对象,其属性是一系列(具有同类抽象性质的)相关数据,其方法是一系列与上述属性相关或相互间存有依赖的逻辑;对象的封装性决定了对象对外或对某个范围公布的接口。因此,一个对象或这个对象的类,其实有着与“单元”(unit)相同的抽象意义。


所以当面向对象出现之后,“一个单元中应该放多少个类”成了一个问题:如果一个单元可以放多个类,那么它与“库(library)6是用来容纳多个类的组织单元”这一抽象概念又重叠了。因此在早期面向对象语言的设计中,对这个问题的解释是含糊不清的,例如 Pascal/Delphi 允许在一个单元中放任何多个类,这导致“单元的组织原则”变得更加简单而含混:如果类之间相关或相似,就放在同一个单元吧。而晚一些的面向对象语言则较好地解释了这个问题:一个类即是一个单元/模块,或干脆进一步地取消了单元/模块概念。在具体表现上,例如 JAVA 或 C#就推荐在一个文件中存放一个(可以公开的)类,这个文件——或包含许多函数与数据的单元——将作为一个独立的组织单位存在。


6 这里指的是对象库(object library)或类库(class library),而普遍意义上的“库”是下一小节讨论的重点。


图 5 说明了在面向对象的设计观念中,“类”其实是用来替代“function/unit/ module…”等组织方式。不过在某些多范式语言中,例如 pascal 或 javascript,通常也允许这两类组织方式同时存在。但从本质上来说,这只是代码组织方式决定了一个“代码集”(source code package)在形式上有所不同,其内部的逻辑、数据以及更为底层的算法观念其实是大同小异的。



图 5 “类”的价值与局限:对传统组织方式的一种替代


随着系统规模的扩大,应用产品对“引入第三方代码”的需求也越来越明显。而“类”作为一个组织单位,其实是将逻辑和与其相关的数据、相关的抽象目标集中在一起发布,因此面向对象技术提供了相当高的可复用性。这一点正好迎合了上述需求(当然,换个角度也可以说,是需求推动了面向对象复用技术),因而如何在类的基础上进行更大规模的代码组织,成了一个重要的问题。


名字空间(命名空间)的出现与对象复用的思想有着密不可分的关系,但究其本质而言,名字空间下是否包含一个“类簇(class cluster)”却并不要紧。因为名字空间本身只是用于隔离不同的软件厂商、产品和子项目之间的代码,以及这些代码对外交付的接口。这种隔离需求原本是来自于交付物的名字冲突(例如 A 公司与 B 公司的代码库中都存在 TDynamicArray 类),而不是缘于这些交付物的类型或结构抽象冲突。所以无论面向对象是否出现,在“函数/单元/函数库”这样的组织单位持续进化之后,必然会由于跨公司、跨领域的复用而出现与“名字空间”相类似的代码组织方式。


常见的名字空间的命名规范为:


CompanyName.TechnologyName
复制代码


例如:


Microsoft.Office
复制代码


名字空间可以由更复杂的分类规则构成。例如:


Microsoft.Office.Tools.Word.Controls
复制代码


通常其具体规则是由不同的公司/产品/产品线来决定的。例如:


com.companyName.puc.biz.deduct.data.types---------------           公司               ---         产品/系统                  -----------  系统层次/工程/项目组织结构                           ----                     子系统/模块                                    ------                                    系统内定义或其他定义
复制代码


如同所有的代码组织形式一样,名字空间通常也与作用域相关。由此带来的效果,也就是它解决的需求是:A 公司与 B 公司代码库中的 TDynamicArray 类之所以存在“不同”,是因为它们所处的名字空间不同。这一点与用“单元内、单元外”来隔离标识符系统,以及用函数、语句甚至表达式的“作用域”来隔离标识符系统的性质是完全相同的。它们只是组织规模上的差异,而其抽象概念以及目的是一致的,只是自然地随着规模扩张而延伸罢了。


图书简介https://www.ituring.com.cn/book/2429



相关阅读


程序原本(一):应用开发基础


2020 年 2 月 11 日 14:00753

评论

发布
暂无评论
发现更多内容

Forrester 最新报告:阿里云稳居领导者地位,引领云原生开发浪潮

阿里巴巴云原生

阿里云 Serverless Kubernetes 容器 云原生

大四女学霸社招竟成功签约字节跳动,拿下30万年薪?

Java架构师迁哥

【云图说】第189期 初识数据仓库服务

华为云开发者社区

数据库 数据仓库 数据

力扣(Leetcode)练习--给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序

Wynne

京东开发4年,想要跳槽去拼多多,落泪四4面,这年头跳槽可真难啊(还好不是裸辞)

马士兵老师

架构 面试 编程语言 Java 面试 java架构师

云算力矿机源码价格,区块链挖矿平台开发

13530558032

胡继晔:中国应建区块链行业准入制度

CECBC区块链专委会

区块链 金融 数字经济

天啊!怎么会有人把Spring Cloud微服务架构讲得这么透彻?

Java架构之路

Java 程序员 架构 面试 编程语言

架构师Week5总结

lggl

总结

苹果首发ARM架构电脑芯片,将对PC格局带来哪些影响?

脑极体

【涂鸦物联网足迹】涂鸦云平台消息服务—顺带Pulsar简单介绍

IoT云工坊

人工智能 物联网 云服务 Apache Pulsar 云平台

区块链数字货币钱包源码价格,区块链多币种钱包

13530558032

KubeVela 正式开源:一个高可扩展的云原生应用平台与核心引擎

阿里巴巴云原生

阿里云 开源 Kubernetes 云原生 OAM

某美团程序员爆料:筛选简历时,用go语言的基本不看!网友:当韭菜还当出优越感了!

Java架构师迁哥

SQL数据库:GROUPING运算符

大规模数据处理学习者

GROUPING运算符

阿里作为内部参考的Redis文档现在开放下载,姐夫半夜不睡都在看

小Q

Java redis 学习 编程 面试

面试题总结--HashMap、Volatile相关

彭阿三

收藏!数据建模最全知识体系解读

华为云开发者社区

数据仓库 数据 数据建模

分布式事务太繁琐?官方推荐Atomikos,5分钟帮你搞定

互联网应用架构

分布式事务 springboot

太赞了!腾讯T3-3架构师整理了5000页的Java学习手册免费开放下载

Java架构之路

Java 程序员 架构 面试 编程语言

《垃圾回收的算法与实现》.pdf

田维常

垃圾回收

《Python程序员面试算法宝典》PDF 超清版免费领取

计算机与AI

Python 面试 算法

涛涌天际,水利万物:黄浦江畔读懂城市智能体

脑极体

开个交易所需要多少费用?数字货币交易所搭建

13530558032

基于SpringBoot、SpringCloud、Docker微服务架构实战,资源分享

Java架构之路

Java 程序员 架构 面试 编程语言

架构师Week5作业

lggl

作业

LAXCUS大数据集群操作系统挖矿

陈泽云

大数据 分布式计算 挖矿

区块链在债券市场如何应用

CECBC区块链专委会

区块链 债券

JVM入门,认识Class文件

Simon郎

JVM Java 分布式

《迅雷链精品课》第五课:账户与账本

迅雷链

区块链

区块链,音乐,流媒体和版税

CECBC区块链专委会

区块链 艺术

程序原本(二):应用开发技术-InfoQ