广义的编程思维

引言

PC互联网应用与移动互联网应用发展至今,有很多的软件开发技术问题得到了很好的解决,但有很多中小规模企业的软件,没有得到很好的指引和撑握到很好的方法。

中国目前有着大概99%的软件公司使用SQL进行着日常的数据存取、使用请求返回进行着日常的数据展示、使用类进行着日常的数据抽象处理。

在这些开发者们习以为常的重复事情中,是否会存在些问题?如何发现问题和改善?

软件工程是一门很好的软件理论指导的知识,有着很早的历史和有着深厚技术的先辈专家遗留的实践知识理论,由于知识中有很多很早期的概念用词与现在发展的新概念用词不同,以及有些深入的理论讲解不够简单具体,使得网上总结分析的文章较少和文章中对理论的举例理解的质量较低或有误,大部分中小规模软件企业的软件仍没有与软件工程理论相结合得很好,笔者一直专注研究软件开发技术,对软件工程理论也只是刚参透一部分,未来还需要仔细研究分享。

软件开发有其复杂的地方,需要对每一个相关的重要概念理解其含义,可以逐步的理解和发现软件开发存在的问题。未来的软件领域的企业发展,只靠一个CTO懂得软件重要问题还不够,全体开发工程师都理解和撑握软件的重要问题是有必要的,这样的组织方式会更好的保证软件质量和持续深化学习讨论。

广义

从狭义上我们都是人类,从广义上我们都是动物,因此我们常常会拿白老鼠做实验研究,再把研究成果应用到人类上。

广义编程,就是从广义的角度研究软件开发的方法理论。

HTML从广义上是xml标签,狭义上是html标签、head标签、body标签等,并且head标签和body标签必需在HTML里面。同样把这种广义的思想应用到软件开发的各个方面。

广义编程包括三个层面的内容:

  1. 从广义层面理解狭义,寻找更广阔的意义。
  2. 从广义层面保障狭义。狭义认为靠人力能解决问题,广义则认为仅靠人力不能完全有效的解决问题总会有些意外出现,并提出宏观上管理的办法解决。狭义认为现有很多技术已经问题不多,广义认为现在有很多技术还存在不确定因素,并提出些办法来保障。
  3. 学习各个层面的知识,开阔视野,对编程有一个全面的理解。

事物

事物指客观存在的一切事情(现象)和物体,我们人类理解这个世界一切元素归为万事与万物。

人类

软件开发如同是在创造生灵的过程,程序如同创造大脑的智慧,输入设备如同为大脑提供信息,控制输出设备如同皮肉骨骼与大脑协调,为软件考虑外在病毒功击的安全威胁,每一个程序里的方法属性如同为生灵创造沟通的语言,每一个程序里的类结构如同为生灵创造认知,每一个程序员如同是一位创造智慧的圣贤。

软件开发的过程中,软件是为了人类需求而开发的,软件的开发与生灵产生有着许多相似之处,回顾人类作为高智慧生灵被创造与文明发展的过程,对软件开发有着一些相似与区别的启发作用。

安全需要
安全是人类的根本需求,人类的本质是动物细胞进化而来,生命是细胞本质的需要,没食物进食就会饿死,没地耕作就没食物来源,人类是一种单独个体的存在,必然需要与不可知的外界相处,外界是凶险还是友好需要独自判断,安全成了人类作为单独个体的根本需求。

安全是人类重要事物发展的根源,因为生命安全人类需要寻找食物生存,因为生存安全人类需要更多自身价值与自尊,因为美好生活的安全人类有了各种的心理需求。

信息需要
人类通过感官收集周围环境的信息,眼睛收集图像信息、耳收集声音信息、鼻收集气体信息、舌收集固体与液体信息、身体收集温度、压感信息,动物在进化过程中更多依靠物理条件的变化,适应环境生存下去的其中重要进化是收集更多周围环境信息。

计算需要
人类收集到的信息需要计算处理,也就是大脑对信息的感受思考,看见一只猛兽然后思考如何避开危险,听到风吹草动思考声源方向与声源事物,人类开始有资产信息后就需要计算资产。

存储需要
人类对收集到的信息需要存储处理,也就是大脑对信息的记录,人类宝宝一出生本能只知道饥饿,对周围环境信息一无所知,看见老虎需要把老虎的特征信息记录下来,下次再看到老虎就知道老虎的习性了,人类之间相遇需要记录五官特征信息与行为特征信息,下次遇到就知道是朋友还是敌人了。

人类对更复杂的计算需要存储处理,也就是大脑对信息的复杂推理过程,需要将推理过程的中间结果临时存储。

人类对存储的信息需要更持久的存储需要,人类大脑对于信息的记忆并不稳定,有的信息可以记很久,有的信息很容易忘记,需要把大脑中存储的信息转变成符号文字放置在更稳定的存储介质上,比如:石板、龟壳、竹简、纸张等。

互联需要
人类之间需要互联信息,人类本质是一种群体繁衍的物种,注定需要互动并且相互传达信息,信息的传达不能通过心灵直接感应到,需要物理上的表达并经过空间传到人的感官上,人类可以通过发声、支体动作、表情直接传递信息,逐渐人类对互联本质的需要,发展出运用文字、信、书、电话、收音机、电视、短信、互联网、论坛、博客、聊天工具等。

模块需要
人类对信息需要模块化的认知,就是对所有事物的分类有利于理解和处理,对物品的分类摆放、对文字的创造、对事物认知的学说书籍,都是在事物信息认知过程中进行归纳总结的结果,人类几千年文明至今仍在认知事物与归纳事物,用于更好的理解和处理复杂事物。

自动需要
人类对事物需求需要自动化,要实现自动化的前提条件是能量和机械的配合,从奴隶社会、封建社会的阶层化开始,人类首先把自动化的目标投向人体劳动力,以人体作为自动化的智能计算机械和能量,其次古人运用最多的自动化是水力机械,接着就是工业革命的蒸汽时代与电气时代,时至今日人类仍然还需要更多自动化,人民群众日益增长的物质文化生活需要与落后的生产力之间的矛盾、人民群众日益增长的美好生活需要和不平衡不充分的发展之间的矛盾,这两个矛盾正是反映了人类对自动的需要。

信息

信息意思指有一定可信度的没有物理形态的,“信”指真实的、客观的、可靠的,“息”在商周时期是指事字,甲骨文字形上部像鼻子(写作“自”),下部用几笔短画表示呼出气的样子,意为没有物理形体。

信息存于自然世界的事物中,人认知信息后存于人的记忆中,可以以数据的形式记录在介质中,如:书画、硬盘数据、内存数据。

数据

数,数值,符号,有物理表达形式
据,依据,逻辑,有其存在的意义

性质:
(1)符号,有物理表达形式
(2)逻辑,有其存在的意义
(3)位数,数据占用存储器空间的位数,由0和1表示
(4)赋值,可将数据传递给变量
(5)多态,同一个数据可以有多种表达方式,如9、九、1001

数据定义
不同的数据类型会有不同的定义方式,同一种数据类型也有可能会有多种数据定义方式,一般用阿拉伯数字直接表示数字定义,用true或false表示布尔值定义,双引号表示字符串定义等。

对象是数据吗?
对象是由一组数据集合构成的非基本数据,对象有其自身属性(数据)和行为(方法),否则不能称之为对象,定义一个无属性、无行为的对象是没有意义的对象,仅有行为的对象称之为工具类而非对象。

位置类型 外置类型 内置类型

内置
顾名思义,指内部设置,如内置类型、内置api、内置类、内置方法……等

内置数据类型
顾名思义,指内部设置的数据类型,就是某种语言内部自己定义的一些东西的类型,如:基本数据类型、引用类型、变量类型……等

性质:
(1)内部设置,内部定义
(2)类型,分类

外置数据类型
指外部设置的数据类型,与内置类型相反的是外置类型,就是非语言官方设置的类型,如:第三方类库……等

基本数据类型

基本
指根本的,所有事物的前提根源。

数据
数,数值,符号,有物理表达形式
据,依据,逻辑,有其存在的意义

基本数据
指根本的数据,很常用的数据,是其它非基本数据的构成数据。

基本数据类型
顾名思义,指最基本数据的类型,可简称为基本类型,如int、char、boolean、byte……等

性质:
(1)基本,根本,是其它非基本数据的构成数据
(2)数据,具有符号与意义
(3)位数,数据占用存储器空间的位数,由0和1表示
(4)取值范围,有些数据类型的取值范围数量等于其数据位数的值范围
(5)默认值,有些编程语言的基本数据类型具有默认值,有些编程语言的基本数据类型默认值为null
(6)赋值,可将数据传递给变量
(7)独享,具备值类型的性质,赋值时是对值进行复制给到变量,每个变量独享自己的数据的物理存储空间,对数据的变动是相互独立的,互不影响。
(8)类型,分类

引用数据类型

引用
在汉语词典中引用有两个动词意思:
(1)引出事例,用他人的事例或言词作为根据,如:引用诗句、格言、成语等,以表达自己思想感情的修辞方法。
(2)引荐任用,如:引用天下名士

在计算机编程中,引用则是名词作修饰,表示数据的兵符,有了兵符才具备对数据传达命令的权力。

数据
数,数值,符号,有物理表达形式
据,依据,逻辑,有其存在的意义

引用数据
表示那些需要通过兵符传达命令的数据,有了兵符才具备对数据传达命令的权力。

引用数据类型
表示那些需要通过兵符传达命令的数据的类型,如对象、实例
引用(兵符)可直接调用,如 new MyData().sayHello()
引用(兵符)可赋于变量中,通过对变量的调用(传达命令),就是对数据的调用。

性质:
(1)引用(兵符),通过引用(兵符)对数据传达命令
(2)数据,具有符号与意义
(3)位数,数据占用存储器空间的位数,由0和1表示
(4)赋值,可将引用(兵符)传递给变量,通过对变量的调用(传达命令),就是对数据的调用。
(5)共享性,可将引用(兵符)传递给多个变量,多个变量共享同一个数据的调用权,对数据的变动会影响多个变量的数据。
(6)类型,分类
(7)引用计数,即引用(兵符)发放了多少给变量或其它拥有者。

浏览器的数据

表现层

浏览器内存数据类型

浏览器内存的基本数据类型是String、Number、Boolean、Object、Array、Null、对象指针、Function、函数指针、变量、变量指针、Undefined。HTML数据模型是界面结构的数据类型,由基本数据类型构成。CSS数据模型是界面样式的数据类型,由基本数据类型构成。

数据类型 数据结构
String 双引号"任意字符"或单引号'任意字符',有长度限制
Number 0123456789.的组合,有最大值最小值限制
Boolean true、false
Array 内容组,引用计数
Null 空值
变量 变量内容,引用计数
Object 键值组,引用计数
对象指针 内存位置
Function 变量组、函数名、参数组、函数逻辑内容
函数指针 内存位置
Undefined 未定义值
HTML数据模型 标记名、标记属性组、标记内容组
CSS数据模型 样式名称组、样式内容组

浏览器内存数据定义

数据类型 数据定义
String 双引号"hello"或单引号'world'
Number 0123456789
Boolean true、false
Array [ ]
Null null
变量 var
Object { }
对象指针 赋值对象时
Function function 函数名(参数){ }
函数指针 赋值函数时
Undefined undefined
HTML数据模型
或 document.createElement("div")
CSS数据模型 #myDiv{ color:red } 或 style="color:red"

计算

计算指根据已知数通过数学方法求得未知数,即计算是对已知的信息处理得出结果信息的过程。

信息处理

从软件技术发展至今,会发现每一种信息数据类型,都会有对应的信息处理工具:

信息处理工具 信息数据类型 代码混合
javascript 基本数据类型
html dom api HTML数据模型
css api CSS数据模型
服务器动态页面
(ASP、JSP、PHP)
后端数据类型、HTML数据模型、CSS数据模型、前端逻辑代码
JQuery HTML数据模型
JSX HTML数据模型
MV模板引擎 HTML数据模型
MVVM模板引擎 HTML数据模型
less、scss CSS数据模型

其中服务器动态页面技术,已经不是主流的前端信息处理工具,因为业务逻辑容易跑到表现层中,甚至业务逻辑代码与前端代码聚集在一起,不利于可读和模块化的理解。

JQuery使用函数式编程处理HTML数据模型的集合,简单理解就是代码量更少的处理相同的功能,代码量更少代码就越容易观察可读,更容易理解。JQuery相对于“html dom api”处理信息的性能要慢比较多。

JSX是一种可以在javascript上写html标签来创建HTML数据模型,也就是在js中的嵌入式模块,简单理解就是代码量更少的创建HTML数据模型,代码量更少代码就越容易观察可读,更容易理解,同时保证了HTML数据模型创建的性能,但HTML信息的处理依然使用javascript。

MV模板引擎使HTML信息处理与javascript分离,使HTML信息更聚集在一块,更容易观察可读,更容易理解。

MVVM模板引擎是在MV模板引擎的基础上改进,具备MV模板引擎的特点的同时,使HTML数据模型的修改性能更快,因为数据的修改不需要HTML数据模型重建。

“less、scss”用于处理CSS信息数据,使得避免使用服务器动态页面技术来处理CSS信息数据,避免业务逻辑代码混入其中。并且“less、scss”是预处理,服务器动态页面技术属于动态处理,预先处理的每次请求CSS文件无需计算,动态处理的每次请求都需要重新计算。

可以发现,为了使某些常用的信息具备更高的可读性,会对信息采用代码量越少可读性更高的表达形式,不同的表达还需要独立的信息处理。

异步处理

异步指一个执行中的任务,拆开成多个子任务各自执行。异步与同步处理相对,同步指多个执行中的子任务,等待所有子任务执行完再继续执行。异步的作用是使有I/O操作的任务最大化利用处理器的计算,以达到缩短任务的完成时间。

异步可以用多进程、多线程或其它技术实现,常见实现技术有:

语言 异步技术 多异步依赖顺序表达 返回值函数表达 返回值直接表达 处理非阻塞
javascript setTimeout/setInterval
异步请求
Promise
generator
async/await
web worker




















java Thread
Executor




C# Thread
Task
async/await








多异步依赖顺序表达,指后一个异步处理需要前一个异步处理的返回值作为参数,这种多异步依赖有两种表达,一种是嵌套表达,一种是顺序表达。

嵌套表达:

myAsyncMethod1().then(function(result1){
    myAsyncMethod2(result1).then(function(result2){
        console.log(result2);
    })
})

嵌套表达会造成异步嵌套地狱的现象,即形容代码可读性很混乱。

顺序表达:

var result1 = await myAsyncMethod1();
var result2 = await myAsyncMethod2(result1);
console.log(result2);

返回值函数表达,指返回值通过函数的参数返回,如:

myAsyncMethod1().then(function(result1){
    console.log(result1);
})

返回值直接表达,指返回值通过赋值表达式直接表达,如:

var result1 = await myAsyncMethod1();
console.log(result1);

处理非阻塞,指处理过程中,是否会占用处理,而无法处理其它任务。阻塞表现在假如异步中有死循环或计算量需要很长时间,软件应用的显示画面卡住无法展示新的内容以及无法操作。

并行处理

并行处理指多个计算设备同时为一个任务的多个子任务同时计算。简单理解就是有两个具备计算能力的机器同时工作,因此并行的前提一定是有多个具备独力计算的机器。并行处理的作用是加大计算的数量以缩短任务的完成时间。

并发与并行的区别:
顺序:你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
并发:你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
并行:你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。此处注意理解:是同时吃,同时说,要真严格的说的话,需要2张嘴才是并行。

多核处理器(CPU)
即然并行处理需要多个独立计算,可以多台计算机,也可以多核处理器(CPU)。

要实现多核处理器处理同一个功能,只需将功能的处理拆分多个线程处理。

存储

存储器

顾名思义,指存储数据的器物,对于学习计算机编程和手机编程,需要理解好内存储器和外存储器。


计算机存储器类型

存储空间
数据会占用一定的物理空间,组合后的数据也一样会占用空间,可以存放数据的地方称为数据空间

内存储器

数据的运算,需要经过内存储器,才能给到中央处理器进行数据运算,故而叫内存储器,可简称为内存。
内存储器是一种随机存储器,存储多份不同地址的数据,存储的速度几乎相似,将同一份数据拆开多份数据存储,总速度不会太大影响,存储每一份数据的平均速度很快。
内存储器只能在通电情况下保持数据存储,断电后存储的数据会消失。

外存储器

数据的运算,外存储器的数据需要先给到内存储器,才能给到中央处理器进行数据运算,故而叫外存储器,可简称为外存。
外存储器是一种顺序存储器,存储多份不同地址的数据,存储的速度相差甚远,将同一份数据拆开多份数据存储,总速度变化很大,存储每一份数据的平均速度相对很慢。
外存储器需要在通电情况下读写数据,断电后已存储的数据仍然可以保存着。

存储器 读写规则 拆开读写时间 读写速度 数据持久性 空间成本
内存储器 随机存储 变化不大 通电存储
外存储器 顺序存储 变化很大 永久存储

计算机的存储器为何要区分内外?
存储器发展到目前为止,永久存储的存储器的读写速度跟不上通电存储的存储器的读写速度,通电存储的存储器在断电后数据会消失,因此在数据储存上需要内存储器与外存储器共同配合完成计算机的存储需要。

数据库

指按照数据结构来组织、存储和管理数据的仓库。数据库可以是用于内存的数据库,一般是用于外存的数据库,用于内存的数据库不能持久存储,用于外存的数据库可以持久存储。

数据读取

基于外存储器的读写特点,批量读取会比单条拆分读取速度要快。
数据读取关键在于对索引的查找算法的理解:
无索引查询
如果一本汉语词典里的字没有按拼音排序,要找到想要找的字是很因难的。

多索引查询
索引过多更新慢,索引过少查询慢,这是一个矛盾关系。如果系统需要建立很多索引,应该怎么办?

或过滤排序
查询日考勤表的人事部和财务部的考勤数据并按年龄进行排列:

SELECT * FROM dayTable WHERE partName = '人事部' OR partName = '财务部' ORDER BY age ASC

这是一条很常见的或过滤排序的SQL查询语句,但在数据量很大的时候,就会出现查询缓慢的清况,这个情况跟百度的查询多个关键字并按权重排序显示有些相似。

这里可以理解为“查询人事部按年龄排序的数据集合”与“查询财务部按年龄排序的数据集合”的两个集合的按年龄排序并集,在有索引的情况下单独查询两个集合会很简单,再把两个集合再组合起来排序,如果数据量很大展示很多页后的数据,就会有问题。

因此一般这个情况的业务场景的解决办法可能会让它不显示太后面的数据,只并集处理前面的几千条数据体验速度不会受太大影响,当然百度的处理会更复杂些,可能会处理前第一页就马上给到浏览器展示效果,并异步预处理第2页到50页的查询内容,当用户点击其它页时就从已处理的数据中返回信息给用户。

并集查询
或过滤排序反应的是其中并集查询的一种,例如查询入职日期大于2020年1月1日且离职日期小于2020年12月31日的所有员工,在数据量很大的情形下,一样会出现缓慢的状况。

数据写入

基于外存储器的读写特点,批量写入会比单条拆分写入速度要快。
数据写入需要注意写入的性能理解:
未持久写入返回
数据只是写入到数据库的内存储器中,未写入到外存储器中,即数据已经给到数据库,但未持久保存就返回告诉程序已处理,此时程序马上读取数据可能并不能获得写入后的数据,并且此时如果系统断电或数据库进程停了,未持久的数据会丢失。

已持久写入返回
指数据库已把数据保存到外存储器中,并建立好索引后返回给程序,程序此时马上读取数据是安全的,但写入的速度会比未持久写入返回要慢很多,因为外存储器的读写速度要慢很多以及要处理索引。

浏览器外存

表现层

浏览器外存数据类型有文本、图片、代码文件。

浏览器外存数据空间类型有localStorage、cookie、sessionStorage、浏览器缓存,localStorage是本地存储,只存放文本数据类型,sessionStorage是会话存储,主要用于存放会话数据,只存放文本数据类型,cookie是浏览器早期仅有的用于长时间存放数据的外存空间,只存放文本数据类型,浏览器缓存用于存放请求返回的数据,一般用于存在代码文件、图片、文本。

sessionKey又名sessionId,用于会话密钥身份的数据,是一个文件数据,一般会存放在cookie或sessionStorage中。

浏览器外存数据空间大小很有限,cookie空间大小一般是4K,localStorage空间大小一般是5M,sessionStorage空间大小一般是5M,不同浏览器外存空间大小会有差异。

浏览器外存数据时间存在有效期,cookie的数据有效期可针对不同数据进行有效期设置,localStorage的数据有效期是永久性,sessionStorage的数据有效期是会话期,关闭页面或关闭浏览器时清空数据。

浏览器外存演变历史,cookie最早是网景公司的前雇员Lou Montulli在1993年3月的发明,localStorage和sessionStorage是2015年6月ECMAScript 6(ES6)发布的其中新增功能之一。

浏览器外存空间类型 可存放数据类型 数据空间大小 数据时间有效期 演变历史
cookie 文本(sessionKey) 4K 设置期限 1993年
localStorage 文本 5M 永久期限 2015年es6
sessionStorage 文本(sessionKey) 5M 会话期限 2015年es6
浏览器缓存 代码文件、图片、文本 - 设置期限 -

寻址空间

顾名思义,寻址指寻找地址,寻址空间指最大可寻找地址的空间大小。32位应用程序的最大寻址空间是4GB,62位应用程序的最大寻址空间是128GB。

位数 寻址空间
32位 4GB
64位 128GB

互联

传输层

传输层中最为常见的两个协议分别是传输控制协议TCP和用户数据报协议UDP。TCP传输可靠相对较慢,UDP传输不可靠相对较快,如:视频不需要保证每一份数据传输都可靠,一般会使用UDP传输数据。

消息推送

消息推送是指通过客户端与服务器端建立连接,客户端可以接收由服务器端不定时发送的消息。

| 推送技术 | 计算消耗 | 网络带宽消耗 | 连续推送及时性 | 浏览器兼容性 |
|--|--|--|--|--|--|
| 短轮询(短连接) | 大 | 大 | 低 | 高 |
| 长轮询(短连接) | 中 | 中 | 中 | 高 |
| 长连接| 低 | 低 | 高 | 一般 |

短轮询,指在特定的的时间间隔(如每1秒),由客户端对服务器发出网络请求,然后由服务器返回最新的信息给客户端,结束请求连接再在时间间隔后重新发送请求。

长轮询,指客户端向服务端发起请求后将该请求挂起(不返回响应),然后由服务器返回最新的信息给客户端,结束请求连接再马上重新发送请求。

长连接,指客户端向服务端发起请求后将该请求挂起,然后由服务器返回最新的数据给客户端,请求连接保持不断继续等待响应数据。

信息推广

robots.txt文件,用于授权搜索引擎,位于网站根目录下。
sitemap.txt文件,用于告诉搜索引擎有哪些页面信息,位于网站根目录下。

ajax不能信息推广
ajax的作用是使得页面可以局部刷新数据,可以更好的用户体验,但并不是所有的搜索引擎都可以收录到ajax的信息,到目前为止只有(Google 和 Bing)可以很好的搜录ajax的内容。

服务端渲染
指可以判断是否是搜索引擎,如果是搜索引擎,则在服务端执行页面并执行初始化的ajax脚本,完全初始化工作后把网页的全部内容返回给搜索引擎。

预渲染
指通过服务端渲染,预先处理好网页内容。再等客户端请求时,直接返回预先渲染好的页面。

架构

“架”指用做支承的东西,如:骨架、支架、书架、衣架,“构”指构造、结成、组合,架构意为用于支撑的构造。

模块

“模”指具有一定模型的物体,“块”指物体的其中一部分,模块意思是某个整体的一块。
模块化意思是对事物内部的成分进行分类归纳后,形成大小不一的模块,把复杂的问题切分成细小的问题来解决。

在事物观中的模块就是把相关的事物或事物关系与规律进行思考整理归类,人类一直以来对事物的理解,就是采用模块化的形式来理解和处理。

在软件开发的信息观中的模块就是把相关的信息(属性)分到一块,或相关的信息规则(方法)放到一块,或相关的信息(属性)以及信息规则(方法)放到一块,人类依然无法避免的沿用模块来理解和编程。

模块性质:

  1. 接口(可用),模块通过接口提供信息处理服务,模块可以有多个接口对应多个信息处理服务。
  2. 调用(使用),模块可以使用其它模块的接口进行调用。
  3. 成分,模块由成分构成,成分可以是信息或信息规则。
  4. 信息,模块可以有直接管理的信息,即模块可以具有一定的信息特征,信息的数据可以是在外存储器或内存储器中,信息可以是基本数据类型,也可以是数据集合,也可以是另一个模块信息组。
  5. 信息规则。每一个信息可以有一定的信息规则,例如年龄一定是数字。信息与信息之间可以有信息规则,例如出生日期与年龄是有关联的。模块与模块的信息可以有信息规则,则包含在它们的父模块里,子模块作为父模块的成分。
  6. 信息隐蔽,模块管理的信息可以对外部隐蔽、模块的信息规则处理细节是对外隐蔽,外部只能通过接口去访问,无法通过正常方式直接获得隐蔽的信息以及处理细节中的临时数据等,简单的讲就是外部无法访问其实现细节。
  7. 嵌套,模块的成分可以是模块,即模块可以包含模块。嵌套规则会表现在访问顺序,例如从外面走进自己的睡房,也要先进大门,再进睡房门。有访问顺序即为有嵌套规则的模块,无访问顺序即为无嵌套规则的模块。
  8. 内聚,模块内部组成成分的相关联程度。
  9. 耦合,模块间的关联程度,更具体的意思是模块间在交换信息过程中可能存在破坏信息规则的程度。

程序模块
指通过调用接口可提供一定意义的数据处理能力,由汇编程序、编译程序、装入程序或翻译程序作为一个整体来处理的一级独立的、可识别的程序指令,不可修改但可被使用的模块,如:exe、dll、jar。
代码模块
指通过调用接口可提供一定意义的数据处理能力,由人类语言文字描述的程序代码,即可修改和可被使用的模块。
软件模块
指通过与人的交互操作可提供实际适用功能的数据处理能力,并按照某种共同意义特征划分的模块。
功能模块
指按照功能特征划分出来的程序模块、代码模块或软件模块。

软件工程中的模块

软件工程中谈到的模块是指整个系统中一些相对独立的程序单元,每个程序单元完成和实现一个相对独立的软件功能。
模块设计也叫详细设计,是系统设计阶段后续的一个软件开发阶段。在系统设计阶段要把整个应用问题分解成一个个独立的功能部分,叫做程序模块。
每个程序模块要有自己的名称、标识符、接口等外部特征。 模块设计的结果是提交技术文档《模块设计说明书》。

不过模块的概念,在现代软件工程已经不多使用了。模块概念后来发展成类和对象的概念,现在又发展到组件的概念,近来又发展成微服务的概念。换句话说,模块已经演变成其它不同的形态和概念代替了。不同形态类型的模块,其性质属于模块性质,但模块的嵌套规则、成分类型、信息规则等不同差异规则用不同的概念代替,例如命名空间下包含类和命名空间,类包含属性和方法,规则不同采用不同的用词代替。

从模块本质去解析其它模块概念会更有利于更好的理解它和使用它。

模块具体是什么?
(1)模块可以是一段程序代码
(2)模块可以是一个方法
(3)模块可以是一个类
(4)模块可以是一个命名空间下的类集合
(5)模块可以是一个程序或类库
(6)模块可以是一个微服务

内聚与耦合的拔河关系
内聚程度越高,耦合程度越低。
内聚程度越低,不代表耦合程度就越高。
耦合程度越高,内聚程度越低。
耦合程度越低,不代表内聚程度就越高。
内聚程度与耦合程度可以同时很低,但不能同时很高。

解耦,全意应该是指提高内聚降低耦合的缩语,如果只是表示降低耦合并不代表提高内聚。

内聚

顾名思义,指模块内部的聚集,具体指模块内部组成成分的相关联程度。

内聚程度越高,软件各模块和成分越容易理解。例如家里的衣服放衣柜、蔬菜水果放冰箱、书籍放书架,摆放有序就很容易知道物品放在哪里。例如与行政相关的事情设立行政部门、与财务相关的设立财务部门,事情安排有序就很容易知道找谁处理事情,有利于加快人员之间的协调合作。

内聚性质表
| 内聚 | 成分输出相关 | 成分处理相关 | 成分顺序相关 | 成分特征相关 |
|--|--|--|--|--|--|--|
| 偶然内聚 | - | 否 | 否 | 否 |
| 逻辑内聚 | - | - | - | 是 |
| 时间内聚 | - | - | - | 是 |
| 过程内聚 | - | - | 是 | 是 |
| 信息内聚 | - | 是 | - | 是 |
| 顺序内聚 | - | 是 | 是 | 是 |
| 功能内聚 | 是 | 是 | 是 | 是 |

-”表示是或否。

成分输出相关,指各成分处理的结果输出与模块目标结果输出的相关性。例如某模块根据打卡数据计算员工的工资数据,在计算过程中需要对员工的打卡数据计算成日考勤数据并保存在日考勤的数据表中,再根据日考勤数据计算成月考勤数据保存在月考勤数据中,再根据月考勤数据计算出工资结果,这个模块的各成分结果有日考勤数据结果、月考勤数据结果、工资数据结果,与模块的目标结果只有工资数据结果相关,其它数据结果与模块目标相关性不大。如果中间中间成分处理的数据结果是一个临时表只是为了当前模块使用,那中间成分的输出数据结果忽略不参与模块输出的相关比较。

成分处理相关,指各成分的处理结果会影响其它成分的处理结果。

成分顺序相关,指各成分的处理顺序或代码顺序是否有一定的规则,不能随意更改位置。

成分特征相关,指各成分是否有一种或多种相似的特征,例如信息内聚的成分都处理同一个信息相关,时间内聚的成分都需要在某一时间执行的相似特征等。

偶然内聚

如果一个模块的各成分之间毫无关系,则称为偶然内聚。模块完成一组任务,这些任务之间的关系松散,实际上没有什么联系。

例子一:
有一个工厂(模块)加工生产衣服,又加工生产矿泉水,这个工厂(模块)加工生产的两件事没有任何关联,即为偶然内聚。

例子二:

var getMyName = function(){
    var result = 1 + 2; //计算1+2
    console.log("hello"); //打印say hello
}

这个方法需要获取我的名字,但方法里面做了两件事情,与方法的需要没有关联,它们之间也没有关联,与方法主题不符,即为偶然内聚。

例子三:

var theApple = {
    sex:"boy",
    font:"宋体"
}

这个苹果对象有两个属性,这两个属性与苹果毫无关系,两个属性之间也毫无关系,即为偶然内聚。

逻辑内聚

几个逻辑上相关的功能被放在同一模块中,则称为逻辑内聚,如:图书馆日常、读取各种不同类型外设的输入。

例子一,图书馆:

  1. 打扫卫生
  2. 借书
  3. 还书
  4. 整理书籍
  5. 卖书

这5个行为只因为与图书馆相关,被放在一起,但是涉及许多数据集的操作,涉及多个角色的数据变更,如:书籍数据、财务数据、借还记录。

例子二,读取各种不同类型外设的输入:

var device = {
    //获取鼠标数据
    getMouseData(){  },
    //获取键盘数据
    getKeyboardData(){  },
    //获取屏幕数据
    getScreenData(){ }
}

时间内聚

如果一个模块完成的功能必须在同一时间内执行,但这些功能只是因为时间因素关联在一起,则称为时间内聚,如:早上上班、系统初始化

例子一,早上上班前:

  1. 刷牙
  2. 洗脸
  3. 换衣服

因为时间因素,事情放在一起处理,刷牙、洗脸、换衣服并没有先后顺序。

例子二,系统初始化:

var system = {
    init(){
        this.logStartTime(); //记录系统启动时间日志
        this.initCache(); //初始化数据缓存
        this.checkData(); //检测启动数据
        this.startMessageService(); //启动消息服务
        //……
    },
    //记录系统启动时间日志
    logStartTime(){},
    //初始化数据缓存
    initCache(){},
    //检测启动数据
    checkData(){},
    //启动消息服务
    startMessageService(){}
    //……
}

过程内聚

允许在调用前面的操作,马上调用后面的操作,即使两者之间没有数据进行传递。意思是两个操作有先后顺序,但不需要因为数据处理的前后因果关系造成的先后顺序,也可因数据处理的前后因果关系造成的先后顺序。如:起床刷牙吃早餐、开电视找沙发

例子一,起床刷牙吃早餐:

  1. 起床
  2. 刷牙
  3. 吃早餐

这三件事情有先后关系,要先起床才能做其它事情、要刷牙清理口腔细菌再来吃早餐,但每一件事情的信息数据又是相互独立的没有因果关系,起床更新起床的数据信息、刷牙更新刷牙的数据信息、吃早餐更新吃早餐的数据信息,只是因为人类做事过程的原因而有先后顺序的聚集。

例子二,开电视找沙发:

  1. 开电源电源
  2. 找沙发坐着看

同样,这两件事情有先后关系,先坐沙发就没有办法触碰电视的总电源开关,但这两件事情的信息数据又是相互独立的没有因果关系,打开电视电源开关更新电视的电源操作的数据信息、坐沙发更新坐沙发的数据信息,只是因为人类做事过程的原因而有先后顺序的聚集。

通信内聚(信息内聚)

如果一个模块的所有成分都操作同一数据集或生成同一数据集,则称为通信内聚,如:图书馆书籍管理、数据操作对象(DAO)

例子一,图书馆书籍管理:

  1. 新书入库
  2. 旧书出库
  3. 查找书籍

图书馆主要管理是目标是书籍数据,只对书数据的各种操作的聚集就是信息内聚。

例子二,数据操作对像(DAO):

var userDAO = {
    getUserById(id){ …… },
    deleteById(id){ …… },
    insertOne(name, age, sex){ …… }
    updateNameById(id){ …… }
}

顺序内聚

一个模块的各个成分和同一个功能密切相关,而且一个成分的输出作为另一个成分的输入。这里需要注意一点是,每个成分会有处理结果更新于数据库中,如:统计月考勤数据

例子一,统计月考勤数据:

  1. 取得某员工的当月的所有打卡数据
  2. 根据打卡数据统计日考勤数据存于日考勤表中
  3. 获取该员工该月的日考勤数据
  4. 根据日考勤数据统计月考勤数据存于月考勤表中

顺序内聚可能会有多个结果产生,对于中间成分产生的结果,并不是该模块的目标,是附加的结果,该模块只需要最终结果而产生多余的中间结果,这些中间结果并不是临时数据,会与其它模块有一定的联系,顺序内聚只要求各成分处理之间的相互联系并得到功能的最终输出结果,对于中间的输出结果对其它模块的影响不是顺序内聚的要求规则。

功能内聚

模块的所有成分对于完成单一的功能都是必须的。意思是模块的成分为一个功能而聚集,如:根据需要的数据计算工资

例子一,根据需要的数据计算工资:

//定义一个模块,对外接口只有计算工资
var wageAccounting = {
    //根据员工id和统计月份开始计算工资
    /*public*/ run(userId, month){
        //获取员工某月份的所有打卡数据
        var cardData = this.getCardData(userId, month);
        //获取员工某月份的所有请假数据
        var askOffData = this.getAskOffData(userId, month);
        //获取员工某月份的加班数据
        var overtimeData = this.getOvertimeData(userId, month);
        //根据打卡数据计算正常上班时数
        var workHours = this.countWorkHours(cardData);
        //获取员工每小时工资
        var hourlyWages = this.getHourlyWages(userId);
        //各种复杂计算
        ……
        ……
        ……
        //返回计算的工资结果
        return result;
    },
    
    //获取员工某月份的所有打卡数据
    /*private*/ getCardData(userId, month){ …… },
    //获取员工某月份的所有请假数据
    /*private*/ getAskOffData(userId, month){ …… },
    //获取员工某月份的加班数据
    /*private*/ getOvertimeData(userId, month){ …… },
    //根据打卡数据计算正常上班时数
    /*private*/ countWorkHours(cardData){ …… },
    //获取员工每小时工资
    /*private*/ getHourlyWages(userId){ …… }
}

这里的模块成分的聚集只为了计算工资一个功能而聚集。

耦合

“耦“古代指两人并肩而耕,“合”指相合,意思是某种相互行为下的结合。人们常常讨论的耦合,一般指模块的耦合,意思是模块间的关联程度,更具体的意思是模块间在交换信息过程中可能存在破坏信息规则的程度。

耦合程度越高,内聚程度就会越低,软件各模块和成分就会越复杂,就会越难于理解和修改。

耦合的强弱程度决于各个模块之间的接口的复杂程度、调用模块的方式以及哪些信息通过接口。

模块间相互调用对耦合程度有影响吗?
耦合的强弱取决于模块之间的接口的复杂程度、调用模块的方式以及哪些信息通过接口,是否双向相互调用对耦合程度没什么影响。

耦合性质表
| 耦合 | 外部数据用于只读 | 外部数据用于功能 | 外部数据已预处理 | 外部数据隶属模块 |
|--|--|--|--|--|--|--|
| 非直接耦合 | 是 | - | 是 | 是 |
| 数据耦合 | 是 | 是 | 是 | 是 |
| 标记耦合 | - | - | 是 | 是 |
| 控制耦合 | 是 | 否 | 是 | 是 |
| 外部耦合 | - | - | 是(简单) | 是(简单) |
| 公共耦合 | - | - | 否 | 否 |
| 内容耦合 | - | - | 否 | 是 |

-”表示是或否。

外部数据,指非隶属于本模块的数据,例如外部模块的数据集、外部模块的私有数据、外部模块调用本模块时传递进来的参数。

外部数据用于只读,指所使用的外部模块的数据只用于只读,不会写入数据进行修改。

外部数据用于功能,指所使用的外部模块的数据用于功能的计算相关,而不是用于在多功能选择某一功能的控制信号。

外部数据已预处理,指所使用的外部模块的数据,在使用前符合该外部数据模块的规则安全下使用。

外部数据隶属模块,指所使用的外部数据没有对应负责的隶属模块。

非直接耦合

两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的。即两模块没有任何联系,也没有相同的需要写入的公共参数数据,可能会有只读的预处理过的公共参数数据。

数据耦合

模块之间通过参数来传递数据,那么被称为数据耦合。数据耦合是最低的一种耦合形式,系统中一般都存在这种类型的耦合,因为为了完成一些有意义的功能,往往需要将某些模块的输出数据作为另一些模块的输入数据。

印记耦合

若一个模块A通过接口向两个模块B和C传递一个公共参数,那么称模块B和C之间存在一个标记耦合。
公共参数是经过模块A预处理过,再给到模块B和模块C,即模块B和摸块C拥有相同引用的并预处理过的数据。
如果公共参数是具有一定规则要求的数据结构,并且模块B或模块C会对公共参数进行修改,这个情况使耦合程度提高,否则是耦合程度降低。

控制耦合

一个模块通过接口向另一个模块传递一个控制信号,接受信号的模块根据信号值而进行适当的动作,这种耦合被称为控制耦合。
控制信号下,会使得几个动作成分只可能是信息内聚、逻辑内聚、偶然内聚,只要不是偶然内聚的情况,可以选择使用控制信号。

外部耦合

一组模块都访问同一全局简单变量而不是同一全局数据结构,而且不是通过参数表传递该全局变量的信息,则称之为外部耦合。
简单变量并不是指一定是基本数据类型,简单变量意思是指外部数据的没有使用规则限制,没有预处理的情形下等同于预处理过。而全局数据结构并不是一定要引用数据类型,而是指有一定使用规则的数据。
如果数据没有使用规则的情况下,数据的隶属模块可以看成数据自身,隶属模块的接口就是数据自身的读写。

公共耦合

两个或两个以上的模块共同引用一个全局数据项,这种耦合被称为公共耦合。在具有大量公共耦合的结构中,确定究竟是哪个模块给全局变量赋了一个特定的值是十分困难的。
全局数据项指的是具有一定使用规则的数据,直接使用具有一定使用规则的数据,会容易出现潜在问题,再寻找问题原因就会困难很多。
公共耦合与外部耦合的区别在于数据是否具有一定使用规则。

内容耦合

当一个模块直接修改或操作另一个模块的数据时,或一个模块不通过正常入口而转入另一个模块时,这样的耦合被称为内容耦合。内容耦合是最高程度的耦合,应该避免使用之。
最早用汇编语言编写软件的工程师,模块之间无法信息隐蔽,可以相互调用私有数据。高级编程语言则可以使用反射直接访问其它模块的私有属数或成员。这种访问其它模块的私有数据,可以理解为直接访问有使用规则的数据,故而对耦合程度影响很大。

分层

顾名思义,指按照层次规则的拆分。分层是模块化的一种方法,是一种具有模块间层级规则的模块,层级规则指相邻层级的分层(模块)可以相互直接调用,非相邻的层级不能直接调用。

分层包含模块的性质:

  1. 接口(可用),分层(模块)通过接口提供信息处理服务,分层(模块)可以有多个接口对应多个信息处理服务。
  2. 调用(使用),分层(模块)可以使用其它分层(模块)的接口进行调用。
  3. 成分,分层(模块)由多个信息处理的成分构成。
  4. 信息,分层(模块)可以有直接管理的信息,即分层(模块)可以具有一定的信息特征,信息的数据可以是在外存储器或内存储器中,信息可以是基本数据类型,也可以是数据集合,也可以是另一个分层(模块)信息组。
  5. 信息规则。每一个信息可以有一定的信息规则,例如年龄一定是数字。信息与信息之间可以有信息规则,例如出生日期与年龄是有关联的。
  6. 信息隐蔽,分层(模块)管理的信息可以对外部隐蔽,分层(模块)管理的信息可以对外部隐蔽、分层(模块)的信息规则处理细节是对外隐蔽,外部只能通过接口去访问,无法通过正常方式直接获得隐蔽的信息以及处理细节中的临时数据等,简单的讲就是外部无法访问其实现细节。
  7. 内聚,分层(模块)内部组成成分的相关联程度。
  8. 耦合,分层(模块)间的关联程度,更具体的意思是分层(模块)间在交换信息过程中可能存在破坏信息规则的程度
  9. 嵌套,分层(模块)的成分可以是分层模块或任意类型模块,即分层(模块)可以包含分层模块或任意类型模块。
  10. 层级,相邻层级的分层(模块)可以相互直接调用,非相邻的层级不能直接调用。

在分解复杂的软件系统时,软件设计者用得最多的技术之一就是分层。分层一般是软件模块化中属于最大的模块,然后各自的分层中再进行细分小的模块。

分层的目的

分层的目的是解耦,即提高内聚和降低耦合。分层是一种具有模块间层级规则的模块,与模块一样,也是将代码拆分归类,让不同类型的代码聚集到各自的分层中。拆分模块是内聚的前提,让不同类型的代码聚集到各自的分层中,让模块成分往分层中分类摆放,使得代码更容易理解。分层即模块化,模块化的目的是提高内聚和降低耦合。

分了层后就需要将代码准确放到不同的层中,如果代码没有准确的放入不同的层中,等同物品胡乱摆放,会使得内聚降低,也就是软件的代码模块的理解性降低。

软件三层架构

指用三个以层次规则的模块作为软件的主体架构,三层分别是“数据访问层”、“业务逻辑层”、“表现层”,也简称为三层架构,是分层的一种应用。


软件三层架构

数据访问层

数据访问层内的每一个成分模块代表一个数据概念表的处理模块,处理着数据概念表的信息规则、表内的信息与信息的规则。

业务逻辑层

业务逻辑层用于处理表与表间的信息规则,处理不同子模块间的信息规则。从模块性质角度会发现,数据访问层与业务逻辑层的区别在于数据访问层只处理简单模块内的信息规则,业务逻辑层需要处理多个模块内的信息规则。

面向过程

面向过程是一种以过程为中心的编程思想,何为过程为中心,从字面意思还不好理解其含意。

用事物观来解析的话,就是对事物不进行分类摆放或归纳,例如把苹果、如何使用扫把、扫把、如何吃苹果很多信息和信息规则堆放在一起,当要吃苹果的时候,没有人会告诉你苹果有多少种吃法(即开发工具不会提示),还要到处找如何吃苹果才能吃。

用软件开发的信息观来解析的话,就是信息和对应的信息规则没有绑定,例如找到了一个苹果,但不知道苹果怎么吃,还要上网查怎么吃才能吃。

即使把苹果和如何吃苹果摆在一起,开发工具也不会代码提示苹果如何吃,还是要自己寻找到那个方法。

结构体

C语言中的结构体是模块化的一种方法,是一种把相关的信息组织为一个整体来看待。
结构体只有信息,没有单个信息的规则、没有结构体内信息与信息的规则。

数据表

数据表是模块化的一种方法,是一种把相关的信息组织为一个整体来看待。
数据表有信息,单个信息有简单规则(如:最大长度),没有数据表信息与信息之间的规则,数据表信息与数据表信息间有简单规则(如:表连接)。
数据表不具备嵌套规则,即任意数据可以单独直接访问,不受访问顺序限制,即使是多对多的关系表也可以直接访问。

面向对象

面向对象是模块化的一种方法,是一种把相关的信息和信息规则组织为一个整体来看待。
对象可以有单个信息规则、信息与信息间的规则、对象信息与对象信息间的规则。
对象具备嵌套规则,即对象可以包含对象,具备访问顺序规则。
面向对象编程方法是目前模块化最常用的方法。

封装、继承、多态,是面向对象的三大特性,封装即模块的信息隐藏,继承使得相同部分内聚在父类,多态使得功能相似的方法可以取同名。

面向切面

面向切面的常用应用场景有:日志记录、事务、权限验证、数据验证、监控性能。

功能 功能
日志记录 辅助功能
事务 主功能
权限验证 主功能
数据验证 主功能
监控性能 辅助功能

面向切面的应用场景并非全用于主功能,还需要加以区分,像日志记录、监控代码执行性能,也可以是用户提出的业务需求,如果用户不作要求这些辅助功能不是必需要的,有时因为公司或开发需要,即软件开发商自己的需要而需要这些辅助功能,有利于更好的调试软件产品或保证软件产品质量。

事务、权限验证、数据验证即使用户不提出需求,如果不做好的话,用户使用起来会出现各种问题,因此这些功能也是主功能的组成成分,这些功能的代码应该与功能性的代码聚集在一起才符合功能的完整聚集,如果使它们分离就不符合模块化内聚性,使得系统更复杂,可读性可理解性降低。

辅助功能(日志记录、监控性能)与主功能属于两个功能目标,分别产生的两个数据结果是相互独立的,成分处理的结果互不依赖,但辅助功能又和主功能有相似的特征,即辅助功能是服务于主功能的,为主功能做日志记录、为主功能做监控性能。

因此辅助功能与主功能是一种若即若离的状态,应该聚集在一起,又不应该明显的聚集在一起,例如现在要找一个功能方法对应的性能监控的代码或日志记录的代码,例如现在要找整个系统所有相关的日志记录代码以便观察日志代码状况。

面向切面是模块化的一种方法,是一种可将辅助功能的代码从主功能的代码分离的技术,需要通过预编译或支持动态代理特性的语言实现,但最好只用于切分辅助功能,并且最好做到半分离半聚集状态。

面向接口

如果调用一个方法,需要传一个对象的两个属性和这两个属性的规则方法,这时会为两属性和规则方法定义一个接口,通过接口传递过去。
如果有一个功能,需要返回一个对象的两属性和两属性的一个规则方法,这时会为两属性和规则方法定义一个接口,通过接口返回出去。

面向接口是模块化的一种方法,用于聚集模块的部分,即部分聚集。为了使功能的所有成分是必要的而不使用类改用使用接口,类中不必要的成分用接口方式隐藏掉,而不需要重新编写一个类属性和方法,使得系统内聚度更高,耦合性更低,即==接口的作用是解耦==。

面向信息

在实际的应用软件的业务逻辑方法中,为了使得所有成分是有必要的,就需要定义大量的接口,为大量的接口而命名是费脑的事情。

任意一个信息变成两份放在同一个模块中,必然会有一个值相同的规则,例如在注册页面中,密码信息和确认密码信息,实际上都属于用户的密码信息。定义一个匿名接口,里面可以扩充一个属性变成两个,并自动添加判断两密码是否相同的方法。

假如人作为模块,耳和口就是人的其中两个成分,耳的行为(信息规则)是听声音,口的行为(信息规则)是发声音,耳和口的组合使得人能学会语言并讲话,两者缺一不可。如果现在有一个方法,只传“口”进去,则不能调用讲话行为,如果只传“耳”进去,则不能调用讲话行为,如果同时传“口”和“耳”进去,则可以调用讲话行为。

面向接口是模块化的一种方法,是一种匿名的接口的方式,设定好信息后自动组合信息规则到匿名接口中,当然目前还没有一种编程语言具备这种的特性,是笔者理论推理的一种实际应用中的需要。

反模块现状案例

具备相似特征的成分聚集在一起,是非常有利的,有利于培养新人、有利于代码质理、有利于代理更易于理解等,思考目前软件开发中,还有哪些概念应该聚集在一起却没有聚集的反模块现状案例:

更新

如果现在要寻找系统里,对某个表的某个字段的所有更新操作,以便对这个字段全局的理解和修改,避免难以理解和修改容易出现bug。
  1)如果用SQL字符串拼接更新语句,会发现没太多办法寻找。
  2)如果用ORM操作数据,用专业开发工具跟踪实体的字段,会找到字段用在计算、更新、读取、赋值、查询表达式等各种代码块中。

查询排序

如果现在要寻找系统里,对某个表的所有查询排序操作,以便确认索引建立是否有多、有漏、字段顺序有误或潜在的低效索引。
  1)如果用SQL字符串拼接更新语句,会发现没太多办法寻找。
  2)如果用ORM操作数据,用专业开发工具跟踪实体类,会找到实体类用在计算、更新、读取、赋值、查询表达式、表连接条件等各种代码块中,即使找到了所有的查询代码块,索引还会根据不同的逻辑条件会有不同的查询,还需要根据逻辑推理索引种类。

部分信息

如果有一个界面需要显示用户性名和年龄,使用ORM的会直接查询一整个用户信息返回给界面,然后只显示性名和年龄。
这是一个功能内聚的案例,看似所有成分都符合功能必需的,但是查询出来的用户其它信息却不是必需的,这就违反了功能内聚的要求。

前端业务逻辑处理

如果有一个界面需要根据用户的角色权限查询不同的信息显示,常见做法是先发送请求查询当前用户的角色权限,再根据用户的角色权限发送对应的请求查询信息显示。从三层架构中来讲,这里的判断已经不符合表现层的内容,这个判断的代码应该属于业务逻辑层。

一对一特征

系统会把用户信息放在用户表,员工信息放在员工表,学生信息放在学生表,教师信息放在教育表,分开四个表,这四个表似乎没有相似特征,但这四个表符合一对一特征,符合一个用户的多重身份的特征,应该内聚成一个用户概念表,但可以是四个物理表组成,对写业务逻辑的人来讲,就只察觉到一个表。

子集特征

如果有一个表存放每个部门绩效分数最高的员工信息,这个表的员工记录是所有员工记录的子集,按照内聚规则,这时子表与主表应该看成一个概念表,但可以是两个物理表组成,对写业务逻辑的人来讲,就只察觉到一个表。

前端请求

一般来讲,表现层只负责如何表现功能,其它事情由业务逻辑层处理,笔者认为这是表现层最极致的内聚性,请求只管往哪个页面通知,至于需要发送什么参数则由业务逻辑层自行从表现层的数据模型获取,需要选择哪个业务逻辑方法开始处理怎么处理也是业务逻辑层的事情。

数据缓存

数据缓存用于提高数据的读写速度,常常会发现数据缓存被当成业务逻辑的缓存功能,从三层架构来讲,数据缓存更应该摆放在数据访问层中,对于业务逻辑层不需要知道数据是从缓存过来、还是从内存过来、还是从外存过来。

Session

Session是会话信息,包括会话开始时间、会话持续时间、会话登录用户等信息,如果现在要获取持续时间超过2小时以上的会话信息,又或者现在要获取某部门的女性会话信息,计算出来就会有些工作量了。

那如果Session是一个表,并且是数据访问层的其中一个子模块,像查询条件一样去查询,就是直接的事情了。

这说明Session本身的特征具备数据表特征,以往没有将Session与数据表联系起来,而是两个独立的概念,没有内聚性。

以往Session是保存在内存中,Session也应该具备可以保存在数据库中,这样不会因为系统的调试重启导致用户需要重新登录,根据项目大小项目情况随时更换不同的数据存储策略。

不只是Session,请求域信息也可以看成数据访问层的一个表模块,例如获取当前并发请求数,或获取某个部门当前并发请求数,用表查询的思维就很直接的获取了。

不必要功能参数

例如有一个具有5个参数的方法,这个方法的代码逻辑只用了其中三个参数,其它两个参数并没有使用,这是不符合功能内聚性,功能内聚性要求所有成分是必要,当然现实编程中也很少见这样的多余参数,除了刚入门的初级程序员。
如果现在不是5个参数,而是只传一个参数,这个参数是一个领域实体类,实体类中有10个属性,把这个实体类传入方法中,方法的代码逻辑只使用了其中3个属性,这也是不符合功能内聚性要求,这样的方式特别是采用大量实体处理的系统中,是很常见的现象。

==这个现象很特别,为什么会出现这样的现象?因为强类型语言中类型都需要提前定义和命名类名,没有用于参数的匿名类型,各种各样的参数都定义成类和需要命名是很头痛的事情。==

在C#中可以采用元组,可用于参数的匿名类型。JAVA中笔者还没发现可用于参数的匿名类型。

遇到上例中,其实只需要把无用的参数去掉,只需要留下3个参数即可。但实际应用系统的情况会有需要多个有结构的匿名类作为参数,使得可读性更好。

不必要返回数据

例如一个业务功能,需要根据用户的出生日期计算年龄,在ORM中,会先取出整个用户记录,然后只使用出生日期的数据计算出年龄,然后赋值到实体执行保存。这里取出了多余的用户其它信息,不符合功能内聚性,产生了不必要的成分,这在使用实体处理的系统中,是很常见的现象。

==这个现象很特别,为什么会出现这样的现象?因为强类型语言中类型都需要提前定义和命名类名,没有用于方法返回的匿名类型,各种各样的返回数据都定义成不同的类和需要命名是很头痛的事情。==

在C#中可以采用元组,可用于方法返回的匿名类型。JAVA中笔者还没发现可用于方法返回的匿名类型。

多特征

例如,用SQL进行多表连接查询在软件开发中很常见的事情,那这个只是查询多表连接的信息应该放到哪个模块?
例如,有一个功能方法,需要对某员工清楚当月月考勤数据和当月日考勤数据,那这个方法应该定义在员工业务逻辑类中,还是日考勤的业务逻辑类中,还是月考勤的业务逻辑类中,还是放在业务逻辑的清理类中?

常常会出现一些具有多特征的方法,在模块化分类时,容易放到其中一个特征相似的类中,但是方法却具备多个特征。

取名

程序员在开发过程中,需要经常给模块或成分取名,内聚程度越高,取名也会具有内聚相似,取名字的简单很多,效果提高,并且软件内使用的模块名称概念越少,代码结构的理解会越清晰。

安全

垃圾回收处理

软件开发中的“垃圾”指已经不再使用的数据,垃圾回收处理指对不再使用的数据进行内存回收处理。

垃圾数据
在一个方法里创建一个局部变量存放一个整数,等方法执行完返回后,这个整数就不再使用变成了垃圾数据,程序会自动清理这个整数并回收所占内存供其它数据使用,这就是其中一种方式产生垃圾数据情形,其本上所有方式原理都差不多。

变量回收
即使空值的变量也会占用内存,变量是跟所在域绑定的,如函数参数变量、函数内定义的变量在函数执行完后,直接回收变量内存。

引用计数器
指每一个引用数据类型,会有一个用于存放引用计数的内存,用于表示引用数据类型的引用被多少个变量或属性拥有。
当变量回收时,变量上的基本数据类型的数据会马上被回收,变量上的引用数据类型的引用计数器将减1。当引用数据类型的引用计数变成0时,表示数据已经没有任何用到的地方,此时引用数据类型将被马上回收。

相互引用的垃圾数据
在一个方法里创建一个“孩子”对象和一个“父亲”对象分别赋值到两个变量上,此时“孩子”对象和“父亲”对象的引用计数都为1。
将“父亲”对象赋值给孩子的“父亲”属性,将“孩子”对象添加到“父亲”的孩子集合里,此时“父亲”对象和“孩子”对象的引用计数都为2。
等方法执行完返回后,方法内的所有变量被回收,此时“孩子”对象和“父亲”对象的引用计数都为1。
“孩子”对象和“父亲”对象就成了相互引用的垃圾数据,但引用计数却是1,不是0程序要如何判断回收呢?
数据不能回收,内存就不能被回收,内存就会一直被占用,随着方法重复调用,内存就会占用越来越大,直到内存消耗为止。

相互引用数据

主动回收(析构函数)
在早期的编程语言中,使用析构函数处理相互引用的垃圾数据,例如有一个主对象内有一些属性具有相互引用的对象,当主对象不再使用计数为0时,程序会调用主对象的析构函数,对内部的相关引用数据类型的属性进行拆解,即将原来赋值的相互关系的属,重新将null赋值进行,主动断掉数据的关系即可。

如果是上例中方法内创建的相互引用,则需要在方法执行最后,编写一段分别对“父亲”对象和“孩子”对象的属性赋空值,使其没有相互引用关系,便可在方法执行完后回收数据。

主动回收(弱引用)
弱引用是使一个变量或属性,在数据赋值给这个变量或属性时,不对数据的引用计数器做递增或递减。
假如“孩子”对象的父亲属性是一个弱引用属性:
在一个方法里创建一个“孩子”对象和一个“父亲”对象分别赋值到两个变量上,此时“孩子”对象和“父亲”对象的引用计数都为1。
将“父亲”对象赋值给孩子的“父亲”属性,将“孩子”对象添加到“父亲”的孩子集合里,此时“父亲”对象的引用计数为1,“孩子”对象的引用计数为2。
等方法执行完返回后,方法内的所有变量被回收,此时“孩子”对象的引用计数为1,“父亲”对象的引用计数则为0。
“父亲”对象引用计数则为0被回收后,此时“孩子”对象的引用计数变成0,紧接着“孩子”对象将被回收。

弱引用也是一种主动回收的方法,因为需要对相关的属性编写一个标记代码,标记这个属性是一个弱引用属性,即仍然需要手动处理。相对析构函数的主动回收方式,编写的代码就少很多,因为只需要编写一个标记代码即可。

算法回收(垃圾回收机制)
由于需要编一段主动回收的程序代码,相互引用的数据才能被释放,但不能保障所有的相互引用的数据都进行了主动回收处理,有可能各种原因被遗漏,此时软件占用内存越来越大很难在代码中找出原因。

在后面的编程语言中,加入了自动回收机制,即通过一些算法,可以计算出,哪些未被主动回收的相互引用的数据是属于垃圾数据,这些算法的其中一种大概原理是从程序的根开始遍历一遍数据,如果数据不是垃圾数据总会能在树节点中遍历寻找得到,找不到的就是垃圾数据,当然这只是其中一种方法的原理,垃圾回收机制是多种算法并用。

算法回收性能
不管垃圾回收机制的算法有多先进,但是从广义上思考,仍有质疑的点,垃圾回收机制确实能保障遗漏的垃圾数据被回收处理,但无法保证回收的算法性能足够快。

解决办法:

  1. 通过持续的长时间监控垃圾回收机制,在回收相互引用的数据的回收量,如有发现让开发人员在代码中继续寻找未主动回收的相互引用的数据,如有发现使用弱引用处理。
  2. 通过宏观管理的方式,将系统切分成许多个足够小的独立软件,相互协调,如果其中某个软件有问题,则想办法解决,如果不能解决则重新编写。(微服务架构)

请求方法(GET、POST)

GET请求和POST请求都可以响应返回数据,获取数据并不是GET请求独有的。

GET请求和POST请求都不是决定是否安全的,决定安全由是否具有ssl加密通讯决定。通常情况下GET在提交有密码表单时,地址栏会显示密码,在显示上存在不安全,事实上GET也可以通过body发送数据。

GET请求和POST请求的URL长度在http协议中是没有规定限制长度的,只是某些URL和web服务器会对GET请求的url有长度限制。

GET请求的出发点是安全的可重复调用执行的,POST请求的出发点是不安全的不可重复调用执行的。例如有时候浏览器在刚在提交完表单后,点后退时会提醒用户,重复上次操作可能会有风险。

而有些操修改操作是安全可重复调用执行的,例如安全的可重复调用的添加或修改使用PUT请求,安全的可重复调用的删除使用DELETE请求。

应该如何理解使用呢?

请求方法 理解使用 参数明文 URL长度限制
GET 安全获取、安全修改
POST 非安全获取、非安全修改
PUT 安全获取、安全修改
DELETE 不需要

线程安全

如下代码,开启3个线程,每个线程循环10次。

public class Test implements Runnable {

    private static Integer count = 0;

    public void getCount() {
        count ++;
        System.out.println(count);
    }

    @Override
    public void run() {
        for(var i = 0; i < 10; i++) {
            this.getCount();
        }
    }
    
    public static void main(String[] args) {
        Test test1 = new Test();
        Test test2 = new Test();
        Test test3 = new Test();
        new Thread(test1).start();
        new Thread(test2).start();
        new Thread(test3).start();
    }
}

执行后,得到以下结果:


线程安全例子

可以发现打印了两个22,getCount里面是有两步的,分别是count++和打印,现在一步一步分解讲解,假如线程A执行getCount的第一步(count++),即现在count值为1,然后线程B执行getCount的第一步(count++),即现在count值为2,然后线程A执行第二步打印,打印出2,然后线程B执行getCount的第二步,也打印出2。

线程安全是由于多个线程分别执行多个步骤的程序,并且多个线程程序访问了一个相同的数据时,多个步骤可能存在同时执行到中间的步骤,导致会有意外的结果。

例如,现在不是访问一个简单变量,而是100个线程同时往List集合里加数据,分别循环100次:

public class Test implements Runnable {

    private static List list = 0;

    @Override
    public void run() {
        for(var i = 0; i < 100; i++) {
            list.add(0);
        }
    }
    
    public static void main(String[] args) {
        for(var i = 0; i < 100; i++){
            new Test().start();
        }
    }
}

很可能会出现程序执行崩溃,原因是“list.add(0)”方法内部是很多步骤的,而List又是内置类型,方法内部会是一些原始程序,不能有半点差错,否则会导致程序执行崩溃。

线程安全导致的最严重后果是程序崩溃,并且程序崩溃还很难找出崩溃的问题源头,因为程序崩溃是程序马上中断不能记录错误日志。

单线程模型与多线程模型
由于线程安全具有崩溃的风险,JS执行在客户端的浏览器中,浏览器不允许有这样的情况出现,因此JS采用单线程模型,即没有多线程访问同一个数据的可能,就不会有线程安全的出现。

多线程模型如何解决线程安全
避免多个线程使用静态数据或相同数据的可能性,如果有的话需要设置线程锁即可,线程锁可以使线程锁范围内的多个步骤的程序必需执行完才能下一个线程执行,即同时最多只能有一个线程在线程锁范围内执行。

事务安全

事务安全是指多个程序同时操作同一份持久数据时,可能导致数据出现意外的结果,使得程序运行跟着出现意外的情形。或指一个程序在操作多个持久的数据时,操作到一半由于停电或其它因素导致程序中止,多个持久数据处理到一半没有完整处理好。

js注入安全(xss)

如果用户可以在评论或文章内容中添加JS代码,使得进来观看内容的用户执行这段JS代码,这段JS代码可以偷取观看用户的sessionKey或数据,对观看用户造成安全隐患。

sql注入安全

指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。

通信安全(ssl)

指信息在网络传输中被截取,通过ssl对传输中的数据进行加密,避免信息泄漏。

线程异常安全

如下代码中,在线程中抛出异常,会导致程序崩溃推出:

public class Test {
    public static void main(string[] args) {
        new Thread(()=>{
            throw new Exception();
        }).start();
    }
}

需要对每个线程做try catch处理:

public class Test {
    public static void main(string[] args) {
        new Thread(()=>{
            try{
                ……
            } catch(Exception){ }
        }).start();
    }
}

微服务架构

微服务架构指将系统切分成许多个足够小的微服务软件,多个微服务软件间相互调用协调,并且每一个独立软件使用各自的数据库。

微服务架构的主要作用与出发点:

  1. 保障每一个微服务在宏观上有足够的内聚性,因为微服务间数据库各自独立,被迫每一个微服务的每一行代码都属于各自的信息或信息规则,大大降低软件系统的总复杂度。但是宏观上能保证内聚性,微观上不一定能保证内聚性,如果其中一个微服务随着时间的推移代码变得很臃肿,导致很难修改,则可以直接放弃这个微服务重新写一个新的微服务替换即可。
  2. 保障每一个微服务崩溃时互不影响,在单体应用架构中如果其中一个功能有线程安全或死循环等各种原因,会导致所有功能崩溃不能使用,在微服务架构中如果其中一个微服务崩溃,其它不相关的功能依然可以继续运作。

微服务架构是从宏观角度思考问题,认为程序员的水平各有不同,有高有低,因此需要寻找办法,保障程序运行和代码质量的风险。

宏观上的功能切分并不代表代码的内聚性足够好,功能切分让信息彼此分离,使得耦合性降低,根据内聚性与耦合性的拔河原理,耦合性降低并不意味着内聚性就提高,总的来讲按功能切分会对内聚性起到一定作用,但仍需要监督与调优。

自动

(未完待续,下期再见)

你可能感兴趣的:(广义的编程思维)