计算机(手机)的构造是什么?
键盘鼠标(输入)+ 内存CPU(计算)= 屏幕显示声音输出(输出)
简单吧?
输入+计算=输出 的抽象,本文简称为三要素。它远不止用在计算机的体系构造上。
实际上,它抽象了计算机(硬件)和互联网(软件)世界里的一切。
越理解这个抽象,就越能:
- 抓住编写代码和系统架构的重点
- 掌握正确的学习方向
- 事半功倍,不会越学越迷茫
这就是本文的目标。
方法设计
首先,让我们从编程设计的最小粒度说起:方法(也叫函数)。
如下(接口中方法定义):
public Document queryById(String docId);
public boolean updateTitle(String docId, String title);
抛开 public 关键字不谈,不是重点,仅为语言的周边细节。
第一个方法(根据文章ID查询文章对象),拆解如下:
- Document 是方法的返回值,定义了输出
- queryById 是方法名,方法名称和方法体,就是计算
- String docId 是方法参数,定义了输入
所以这个方法的含义也可以这么定义:
输入(String docId)+ 计算(queryById)= 输出(Doc)
第二个方法(根据文章ID更新它的标题),拆解类似。
发现了规律没有?
事实上所有的方法定义,都只表述了同一种抽象,那就是输入、计算、输出。
这三个要素唯一重要,三个中的每一个的定义都应该力求简单直观,没有二义性,突出逻辑重点。
好,我们再看一个例子。大家先看方法定义:
public void update(Document doc, Map<String, String> segmentCategory, int minTitleLen, Map<String, Long> categoryExpire);
能看出来这个方法是做什么的吗?
这个方法的逻辑是:
- 对文章做分词
- 然后根据segmentCategory这个分词到分类的映射,取到文章的分类
- 然后根据categoryExpire分类的过期时间算出文章的过期时间
- 最终更新文章的过期时间
回过头来看下这个方法定义,你觉得这个方法的定义做的好还是不好?
判断方法定义好坏的一个策略就是拆解三要素:
- 输入:不清晰。参数众多,参数的目标不清晰,比如根据Document的什么属性来分词,minTitleLen这个参数是干嘛的。这么多参数到底是做什么的?
- 计算:update。这个方法名不知所云,完全不知道是要更新什么字段
- 返回值:void。也就是没有返回值,从返回值角度也看不出这个方法核心要做什么逻辑
所以,改造方案如下:
public String getCategory(String title, int minTitleLen, String content, Map<String, String> segmentCategory);
public Long getExpire(Date docPublishDate, String category, Map<String, Long> categoryExpire);
之前的方法被重构为两个方法:
输入(满足最小标题长度的标题、正文、分词到分类的映射)+ 计算(计算出文字的所属分类)= 输出(分类)
输入(文章发布时间、分类、分类到过期间隔的映射)+ 计算(计算出文章过期时间)= 输出(过期时间)
可以看到重构之后,三要素简单清晰,两个方法高内聚低耦合。
对于当前短期的业务逻辑开发,不容易出错,对于长期自己或别的维护的人,也容易理解。
其实,无论复杂或简单的业务逻辑,高并发还是低负载的程序系统,核心抽象完全一样。
小到一个函数,大到一个单机程序。
单机程序
有人会问:
- 方法确实是由输入、计算和输出三要素组成的,因为方法定义就那么简单一行。
- 可是对于一个独立运行的服务程序,有很多模块包、依赖包、各种类,怎么可能用简单的输入、计算和输出来衡量呢?复杂得多呢?
如下图所示:
一个程序(服务),归根结底是由方法组成的。
- 类是为了将相似操作的方法,归类到一起
- 包是为了将相似业务的类,归类到一起
- 单机程序是将一组业务(包),归类到一起
类与类之间,是由多组方法,也就是多组输入计算输出组成的;包与包之间,也一样。
只是越往上层走,输入、输出的维度越多,包含越丰富。
甚至广义上看,不同类、不同包,也是可以基于输入、计算和输出来作为划分原则的。
以一个标准的spring服务程序来举例:
- controller层:输入(用户请求参数)+ 计算(service)= 输出(用户响应)
- service层:输入(controller层部分参数)+ 计算(本地逻辑)= 输出(部分结果)
- dao层:输入(service层部分参数)+ 计算(存储交互)= 输出(存储交互结果)
小结:
狭义上看,单机程序是由很多组方法有机构成的,定义好每一个方法的输入、计算和输出,非常重要;
广义上看,类或包(模块)之间,也是基于数据流的输入、计算和输出的抽象,理清或划分好输入、计算和输出这三要素,就能做好一个单机程序的架构设计。
分布式服务
是的。
输入、计算和输出,还能抽象更加复杂的分布式服务集群的架构。
从这个角度理解,实际上,分布式服务抽象成输入、计算和输出,比单机程序更加显而易见。
如下图:
上面举例画的一个分布式服务的组成架构图,体现了由不同组服务集群以及不同组存储集群的组成和核心的交互。
之所以说分布式服务,更加容易看出是输入、计算和输出三要素的抽象,是因为图中的箭头刚好表示了计算的方向。
- 箭头的起点方向的服务是整体架构中的输入
- 箭头的终点方向的服务是整体架构中的计算
- 从计算服务出发的,箭头的终点方向的存储或服务,是输出
服务与服务之间的交互、服务与存储之间的交互、存储与存储之间的交互,都可以用输入、计算和输出三要素来解释抽象。
分布式服务架构的设计,本质上也就是理清和划分好这三要素。
一个架构图设计出来,如何分辨好坏,就是看每一个服务集群、每一组存储集群,他的输入、计算和输出是否定义的明确、唯一、简单。
总结
方法逻辑即方法体可以做到非常复杂,单机程序也可以实现众多复杂的业务逻辑,分布式服务除了实现复杂的业务逻辑外,本身的技术交互也很复杂。
但其本质都非常简单,且无论多么复杂、粒度粗细,都可以有一个统一的抽象:输入、计算和输出。
站在这个角度读懂或者自己设计,方法、单机程序和分布式服务,都将变得简洁明了,且可以看透本质。
主要是因为复杂的本质都是在计算里,而三要素中的计算部分,实际上忽略了它的内在明细。
因为只要定义好了输入和输出,怎么计算其实不重要。
本文完。
本文是编码人生和程序本质系列的第1篇。下一篇:编程让程序或集群运行起来。
