什么是子程序
子程序是为实现一个特定的目的而编写的一个可以被调用的方法或过程。
为什么要创建子程序
- 降低复杂度
- 引入中间、易懂的抽象
比如说一个方法函数,比起复杂的实现过程,方法函数名更容易快速的让人理解要做什么事情。
public static void mainRunLog(Environment env) {
String applicationName = env.getProperty("spring.application.name");
String serverPort = env.getProperty("server.port");
String hostAddress;
try {
hostAddress = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
String contextPath = env.getProperty("server.servlet.context-path");
log.info("""
\t
----------------------------------------------------------
\tApplication '{}' is running! Access URLs:
\tLocal: \t\thttp://localhost:{}{}
\tExternal: \thttp://{}:{}{}
\tDoc: \thttp://{}:{}{}/doc.html
----------------------------------------------------------""",
applicationName,
serverPort, contextPath,
hostAddress, serverPort,
contextPath,
hostAddress,
serverPort, contextPath);
}
上面的java代码中的mainRunLog方法是一段在个人的spring boot项目中编写的静态方法,只是为了在spring boot程序的main函数运行时打印相关的程序信息而已,但比起方法中的方法体来说,准确的函数名更能快速了解方法体的实现效果。
- 避免代码重复
这个应该都能理解,不管是什么样的编程语言,创建函数本质上就是为了对某一个操作或者过程的封装,使其能够被重复调用。 - 支持子类化
《代码大全2》中原文是这样描述的:覆盖简短而规整的子程序所需新代码的数量,要比覆盖冗长而邋遢的子程序更少。如果你能让可覆盖的子程序保持简单,那你在实现派生类的时候也会减少犯错的几率。 这让我想到另外一句话: “一个函数应该只做一件事”。 - 隐藏顺序
简单来说就是子程序可以隐藏某一个操作的具体处理的顺序,其实还是隐藏实现。而对某一个处理事件创建成子程序,你也就不用在关心这个事件的处理顺序了。 - 隐藏指针操作
指针操作的可读性通常很差,容易出错,把这些操作隔离在子程序里面,就可以把精力集中在操作的意图本身,而不是指针的操作机制的细节上。像java就是将指针的操作彻底封装,Java程序员无需关心指针问题。 - 提高可移植性
可以用子程序来隔离程序中不可移植的部分,从而明确识别和隔离未来的移植工作。 - 简化复炸的布尔判断
举一个最简单的例子,判断一个数字是不是偶数、质数等这样的情况,你是不是都会写一个用来做判断的函数呢? - 改善性能
使用子程序,你可以只在一个地方优化代码,把代码集中在一处可以更方便的查出哪些代码的运行效率低,而对其进行优化,而子程序优化之后又会对所有调用了该子程序的地方同时得到优化。
书中还特意提到了一个点:并不需要确保所有的子程序都很小。有些事情是一个大的子程序来完成还会更好。对应着这句话 一个函数应该只做一件事其实并不冲突,这一件事并不是说一定只是一件小事,而是确保这一件事是完整的、独立的。
除了上诉的原因意外,创建类的理由很多也是创建子程序的理由:
- 隔离复杂度
- 隐藏实现细节
- 限制变化带来的影响
- 隐藏全局数据
- 形成中央控制点
- 促成可重用的代码
- 达到特定的重构目的
在子程序层上设计
子程序的设计要确保高内聚。
以下是不同层次的内聚性概念:
- 功能内聚性 (最好的内聚性,也是最理想的内聚性)
让一个子程序只执行一项操作。
下面是其它的种类的内聚性,通常认为不够理想
- 顺序上的内聚性
指子程序内包含有需要按特定顺序执行的操作,这些步骤需要共享数据,而且只有在全部执行完毕后才完成了一项完整的功能 - 通信上的内聚性
一个子程序中的不同操作使用了通向的数据,但不存在其他任何联系 - 临时的内聚性
含有一些因为需要同时执行才放在一起的子程序,典型例子:startup() - 过程上的内聚性
一个子程序中的操作是按照特定的顺序进行的 - 逻辑上的内聚性
若干个操作被放在一个子程序中,通过传入的控制标记选择执行其中的一项操作 - 巧合的内聚性
子程序中的各个操作之间没有任何可以看到的关联,也就是”无内聚性“或者”混乱的内聚性“
好的子程序的名字
-
描述子程序所作的所有事情
一个好的名称,可以让你快速知道这个子程序需要做什么,比如说我上面展示的mainRunLog函数,我自认为是一个不怎么好的名称,本意是想表达其是在程序启动后打印程序信息日志的函数,但从名称上直接翻译是 主运行日志,表达非常的模糊,所以这样的函数一定要添加函数注解才可以。
如果说遇到某个子程序名称又长又笨,也可能就是因为这个子程序提取不够简洁,可能是将好几件事情放在了一起,或许将其再一次的拆分会更好。 -
避免使用无意义的、模糊或表达不清的动词
-
不要仅通过数字来形成不同的子程序名称
-
根据需要确定子程序名字的长度
-
给函数命名时要对返回值有所描述
这个得看具体情况,因为如果时面向对象的语言中,比如说Java,函数本身是有返回值的,如果本身这个返回值的类型是可以体现其返回值含义的化,函数名对返回值不做描述,我觉得并不会妨碍理解函数。但是想JavaScript,虽然说它支持面向对象,但其函数的返回类型却很模糊,如果函数名没有对其描述,大多数情况下你是不知道它返回的是什么,甚至有没有返回都不知道。 -
给过程起名时使用语气强烈的动词加宾语的形式
比如说checkOrderInfo() -
准确使用对仗语
这个对仗语可以理解为反义词,比如说添加/删除(add/remove或者add/delete),加锁/解锁(lock/unlock),如果说FileOpen()对应DocClose()就不对称,容易使人迷惑,应该改成FileClose()。 -
为常用操作确立命名规则
在系统里,区分不同类别的操作非常重要。而命名规则往往时指示这种区别的最简单的也是最可靠的方法。比如说File**(),Http**(),很容易就能区分一个是针对文件一个是针对网络http的。
什么是函数?什么是过程?
函数指的是有返回值的子程序,而过程是指没有返回值的子程序。
如果你是高级面向对象语言的开发者,比如Java、C#、python,子程序就是函数方法,只是是否存在返回值而已。
最后总结:
- 创建子程序最主要的目的是提高程序的可管理性
- 子程序可以按照其内聚性分为很多种类,而我们在编写子程序时应该尽可能的让子程序具有功能上的内聚性,因为这是最佳的一种内聚性。也就是 让一个子程序只执行一项操作(一个函数应该只做一件事)
- 子程序的名字是它的质量的指示器,如果名字糟糕但恰如其分,那就说明这个子程序设计的很差劲,如果名字糟糕又不准确,那么它就反应不出程序是干什么的。不管怎么样,糟糕的名字就意味着程序需要修改。当然命名是不可能完美的,有时候添加注释描述也是对子程序命名的一种补充。
- 有时候,一些简单的操作写成独立的子程序也非常有价值,因为有些简单的操作随着时间的推移也可能会编程复杂操作。(想要深入了解可以阅读原文166页的似乎过于简单而没必要写成子程序的操作)