小程序中的大道理之二--抽象与封装
继续扒
接着 上一篇 的叙述, 健壮性也有了, 现在是时候处理点实际的东西了, 但我们依然不会一步到底, 让我们来看看.
一而再地抽象(Abstraction Again)
让我们继续无视那些空格以及星号等细节, 我们看到什么呢?

我们只看到一整行的内容, 当传入 3 时就有 3 行, 传入 4 时就有 4 行. 我们用一个方法 getLineContent 来表示这样一个抽象. 代码如下:
public String getPattern(int lineCount) {if (lineCount < 1) {throw new IllegalArgumentException("行数不能小于1!");}if (lineCount > 20) {throw new IllegalArgumentException("行数不能大于20!");}StringBuilder pattern = new StringBuilder();for (int lineNumber = 0; lineNumber < lineCount; lineNumber++) {pattern.append(getLineContent(lineNumber));}return pattern.toString();
}
黑盒子, 输入以及输出(Black Box, Input & Output)
先不急着让 IDE 生成代码, 现在集中精力思考一下, 我们仅仅在这一层面上去思考, 把 getLineContent 看作类似电路那样有一些输入端和输出端的黑盒子:
- 返回的值是我们想要的吗?
- 传入的参数是否足够让
getLineContent里面完成它的工作呢?
第一点是可以肯定的, 但传入的参数是否足够了呢?
如果按上述代码, 不管是 3 行的情况, 还是 5 行的情况, 获取第一行的内容时, 调用的都是
getLineContent(0), 按照输入决定输出的原则, 结果将一样.但我们很清楚, 5 行情况下的第一行前面的空格肯定要多于 3 行的情况, 如下图:
所以很显然, 只传入一个 lineNumber 是不够的, 还要把总的行数 lineCount 也传进去.
自顶向下(Top-down)
现在把代码改下, 多传入一个参数, 并让 IDE 为我们生成 getLineContent 的代码, 作些简单修改, 最终如下:
public String getPattern(int lineCount) {if (lineCount < 1) {throw new IllegalArgumentException("行数不能小于1!");}if (lineCount > 20) {throw new IllegalArgumentException("行数不能大于20!");}StringBuilder pattern = new StringBuilder();for (int lineNumber = 0; lineNumber < lineCount; lineNumber++) {pattern.append(getLineContent(lineCount, lineNumber));}return pattern.toString();
}private String getLineContent(int lineCount, int lineNumber) {// TODO Auto-generated method stubreturn null;
}
那么, 这样一种先从高层考虑起的做法, 就是所谓的自顶向下了, 接下来我们还会不断地以这种方式来完成这个小程序.
自顶向下是一种很重要的思考及处理问题的方式, 如果你还不习惯这样去考虑问题(包括写代码), 现在是时候尝试一下了.
项目进度(Project Progress)
另外, getPattern 方法里面的 TODO 标识可以去掉了, 这个方法已经算是完成了, 如果现在太阳就快下山了, 那么你也可以提交它了, 你的项目经理也很乐意看到"代码量天天在增长", 这给了他信心, 让他觉得"项目正在稳步推进", 当他给项目总监或者客户汇报时, 他就可以展示一些"进度"给他们看了.
当管理者看不到进度时, 他们就会感觉到压力, 这种压力会转移到你身上, 你甚至会"被志愿加班". 这种压力除了损害我们的健康外没有任何好处, 所以你要学聪明一点, 当管理者问起你的时候, 你就大声对他们说: "我今天又提交了 XXX 行代码. ", 然后你就拍拍屁股准时下班了.
再而三的抽象(Abstraction, again and again)
现在把目光投向 getLineContent 方法. 经过观察, 可以看出一行内容由三个部分组成, 我们再一次忽略具体的细节:

如上, 三种颜色表示了三个部分, 我们再一次运用抽象, 先不考虑传什么参数, 有点像是写 伪代码(pseudo code) 那样快速把程序的 骨架(Skeleton) 写出来:
private String getLineContent(int lineCount, int lineNumber) {// TODO Auto-generated method stubStringBuilder content = new StringBuilder();// 1. 空格部分content.append(getFirstPart());// 2. 星号部分content.append(getSecondPart());// 3. 换行部分content.append(getThirdPart());return content.toString();
}
现在再来仔细考虑往里面传入参数的问题:
-
第一个方法
getFirstPart, 其实是有关于输出前置空格的, 前面已经分析过"5 行情况下的第一行前面的空格肯定要多于 3 行的情况", 所以它需要两个参数. -
第二个方法
getSecondPart, 是关于输出星号的, 可以看出, 无论是 3 行还是 5 行, 第一行都是 1 个星, 第二行都是 3 个星, 所以这个跟总行数lineCount无关, 只与行号lineNumber有关, 所以只要传入一个参数即可. -
第三个方法
getThirdPart, 其实就是一个换行, 所以不需要传任何参数.
有人可能有些疑问: 这样是不是分得太细了? 抽象与封装究竟要到什么样的程度呢?
过度工程(Overengineer)
特别地, 让我们看看第三个方法: getThirdPart. 我们知道, 这最后其实就是一个换行, 一条语句即可搞掂, 所以再封装就没有必要了.
过度的抽象与封装有时反而使得程序臃肿难读, 半天也找不到具体"干活"的语句在哪, 性能方面也会受到损害.
Java 语言中已经可以直接表达换行的语义, 最终结果如下:
private String getLineContent(int lineCount, int lineNumber) {StringBuilder content = new StringBuilder();// 1. 空格部分content.append(getFirstPart(lineCount, lineNumber));// 2. 星号部分content.append(getSecondPart(lineNumber));// 3. 换行部分content.append(System.lineSeparator());return content.toString();
}private String getFirstPart(int lineCount, int lineNumber) {// TODO Auto-generated method stubreturn null;
}private String getSecondPart(int lineNumber) {// TODO Auto-generated method stubreturn null;
}
抽象不足(Lack of Abstraction)
另一方面, 我们也要警惕缺少必要的封装层次的情况. 不幸的是, 很多情况, 我们都是缺少必要的抽象与封装.
做过维护的同学可能都见过那种超长超恐怖的方法, 里面的语句有的甚至高达几千行, 哪怕是在方法内找一个变量的定义, 也能让你想起周杰伦与费正清合唱的那首歌–<<千里之外>>, 去维护这样的方法自然不是什么愉快的经历.
这里之所以不厌其烦地对这个小程序不断的抽象下去, 是想告诉大家, 即使是如此之小的一个程序, 抽象到这一地步, 语义层面依然还没有过度的倾向.
通常, 如果程序语言已经可以直接表达出我们想要的语义, 封装就可以结束了. 我们来审视一下前两个方法, 显然, 还不能直接表达, 所以封装还可以继续.
一般地, 如果一条语句就能表达的时候, 抽象与封装也就基本到头了.
同时, 不必过于刻板地去遵循这些, 有时三两条语句可以表达时, 不封装也是很正常的;
而有时为了提供更清晰的语义, 哪怕只有一条语句, 你再封装一下也是可取的.
当然了, 对于目前这个小程序, 大家可能觉得已经有些过度封装了, 但在后面我们将看出, 其实还没到最抽象的阶段. 现在先不争论这一点, 说到后面我们就明白了.
分而治之(Divide and Conquer)
其实抽象与封装还能带来什么好处呢? 那就是这里要讨论的分而治之了.
我们可以回顾一下程序写到现在, 我们可曾遇到什么"阻碍"没有?
答案是没有. 你可以看看前面的代码, 都是简简单单的 for 循环,
append之类的.
有人可能不服气地说:
"困难的地方都被你这种一层又一层的抽象与封装延后了, 代码写了半天啥事也没干到. "
这种评价对不对呢? 的确, 前面通过抽象不断地压制那些细节的表达, 不断地推迟对其的处理.
想像有一个房间, 衣服, 物品堆放得乱七八糟, 这时有人拿来一个大箱子, 把这些东西通通塞了进去. 把这些东西"封装"起来后, 房间自然整洁了, 但我们也很清楚, 箱子里依旧是一团糟.
但这个比喻并不适合这里的情况, 我们的抽象并不是简单地把问题转移了, 通过一层层抽象的手段, 一个大问题在不断被分解成一个个小问题.
- 有些足够清晰的小问题, 我们已经在这一过程中把它解决掉了.
比如, 输出一个换行的问题.
- 而那些还不够清晰的小问题, 也已经通过抽象被我们所 孤立(isolate) 或者叫 隔离 出来了, 有的已经看到了解决的曙光.
比如, 在上一步, 我们还是有两个参数传了进来, 但通过在里面进一步划分成新的子问题, 可以看到, 有些子问题只要一个参数即可解决了.
所以, 抽象并不是什么事也没干, 相反, 它干了很重要的事情.
通过抽象, 问题正在被分解与简化;通过抽象, 我们构建出了程序的骨架.
在这一过程中, 大问题分解成小问题并被安排到了适当的位置, 与其它的小问题隔离开来, 有个词怎么说的, “众神归位”, 大概就是这样一个意思.
群魔乱舞, 你怎么去应付呢? 如果他们都呆在自己的位置上, 我们就可挨个收拾他们了.
抽象不存在"事不过三"(No Limits for Abstraction)
让我们继续, 我们还可以继续抽象吗? 答案是肯定的. 无论是参数更多的 getFirstPart, 还是参数更少的 getSecondPart, 它们都还可以分成两部分:
- 拿到一个数量 N(你甭管怎么算出来)
- 输出 N 个空格或星号
代码如下:
private String getFirstPart(int lineCount, int lineNumber) {int count = getElementCountOfFirstPart(lineCount, lineNumber);StringBuilder part = new StringBuilder();for (int i = 0; i < count; i++) {part.append(" ");}return part.toString();
}private String getSecondPart(int lineNumber) {int count = getElementCountOfSecondPart(lineNumber);StringBuilder part = new StringBuilder();for (int i = 0; i < count; i++) {part.append("*");}return part.toString();
}private int getElementCountOfFirstPart(int lineCount, int lineNumber) {// TODO Auto-generated method stubreturn 0;
}private int getElementCountOfSecondPart(int lineNumber) {// TODO Auto-generated method stubreturn 0;
}
现在再来看看如何实现最后的两个方法, 以一个四行的图案为例:

图中规律已经很明显, 最终结果如下:
/*** 获取每行第一部分的元素个数* @param lineCount 总行数* @param lineNumber 行号, 从0开始* @return*/
public int getElementCountOfFirstPart(int lineCount, int lineNumber) {return lineCount - lineNumber - 1;
}/*** 获取每行第二部分的元素个数* @param lineNumber 行号, 从0开始* @return*/
public int getElementCountOfSecondPart(int lineNumber) {return lineNumber * 2 + 1;
}
这里把最后的两个方法加了注释, 并把它们改成了 public, 为什么呢? 下面将作些解释.
抽象到数字
有人可能不太理解, 为什么要抽象到如此之深, 这里最后两个方法都只有一条语句, 直接在上一层就写了不就完了?
可以看到, 最后两个方法, 返回的都是 int 类型, 也即一个数字. 我们都知道, 数字是非常纯粹, 非常抽象的一种概念, 抽象到了这一层, 已经不能再抽象了. 比如, 单独拿一个"1"出来, 它是非常抽象的:
1 可以是一粒土豆, 1 也可以是一颗红薯;
1 可以是一匹元代马, 1 也可以是一头程序猿.
抽象(abstract)作为一个动词而言, 它的原始意义, 有"把…抽取出来"的意思, 即有把东西抽离, 剥离的意思.
我们说数字很纯粹, 为什么要追求这种纯粹呢? 这一过程中我们又把什么剥离了?
耦合, 解耦合, 得意而忘形(Coupling, Decoupling, $%#&…)
我们都听过一种说法, 叫"言不达意"或者又叫"词不达意", 表明我们用"言"来表达"意", 当然"达不达"就是另一回事了;另一方面:
"言者所以在意, 得意而忘言. "–<<庄子 外物>>
而<<晋书·阮籍传>>中有一段对阮籍的描述:
"嗜酒能啸, 善弹琴. 当其得意, 忽忘形骸. "
这就是所谓的"得意忘形"的最初意义:
指得其意, 即其思想精髓, 而不必计较形, 即表现形式.
而"形意交融"则表明形跟意常常是混在一起的, "意"需要通过"形"传递给我们.
要表达的意思与它的载体之间的这种紧密关系, 用我们软件领域的说法, 就叫"耦合".
这里可以算是耦合的一种, 耦合还可以有很多其它方面的理解.
这种形与意的交融有时并不是件什么好事, 陶渊明在他的<<归去来兮辞>>里说:
既自以**心为形役,**奚惆怅而独悲.
回到我们的问题, 前面一直在处理这么一个图案, 那么, 这个图案它的"形"是什么呢? 而它的"意"又是什么呢?
显然, 那些一个个的星号(以及前面的空格)就是所谓的"形"了, 而"意"呢?
其实就是前面说的"抽象到了极致的数字"了, 这就是图案的"意".
通过把"形"从图案中剥离, 或者说把"意"从图案中抽取出来, 我们就能"得意而忘形", 从而达到解耦合的目的.
把握住了"意", 我们就不必拘泥于空格或者星号, 我们可以使用各种各样的"形", 最终出来的图案依然可以看到"三角形"的影子.
如果你已经对所谓的 MVC(Model-View-Control, 模型-视图-控制)有些了解, 那你是否在这里看到了 Model 跟 View 的影子呢?
再一次的, 由于篇幅过长, 这次还是不能"扒到底", 美腿有点长, 再扒一半, 就此膝斩. Hold 住, 余下主题我们下回再见. 下一篇见
小程序中的大道理三
相关文章:
小程序中的大道理之二--抽象与封装
继续扒 接着 上一篇 的叙述, 健壮性也有了, 现在是时候处理点实际的东西了, 但我们依然不会一步到底, 让我们来看看. 一而再地抽象(Abstraction Again) 让我们继续无视那些空格以及星号等细节, 我们看到什么呢? 我们只看到一整行的内容, 当传入 3 时就有 3 行, 传入 4 时就…...
基于卷积神经网络CNN开发构建HAR人类行为识别Human Activity Recognition【完整代码实践】
行为识别相关的开发实践在我们之前的博文中也有过相关的实践了,感兴趣的话可以自行移步阅读即可:《python实现基于TNDADATASET的人体行为识别》 《UCI行为识别——Activity recognition with healthy older people using a batteryless wearable sensor Data Set》《人体行为…...
excel自己记录
1、清除换行符号 2、添加特殊符号&并清除换行符号 7日&15日&30日&60日 3、判断单元格最后一个字符是不是数字,不是就删掉 IF(ISNUMBER(--RIGHT(B2,1)),B2,SUBSTITUTE(B2,RIGHT(B2,1),"")) ISNUMBER(--RIGHT(B2,1))判断最右边的一个数是否…...
vcsa6.7 5480无法登录
停电维护硬件后,发现vcsa异常,https://ip:5480无法登录,https://ip/ui正常,ssh登录页正常 kb资料 通过端口 5480 登录到 VMware vCenter Server Appliance Web 控制台失败 (2120477) 操作过程 Connecting to 192.16.20.31:22..…...
CSS 属性列表
CSS属性列表 序号 属性类别 属性 描述 1 动画属性 keyframes 定义一个动画,keyframes定义的动画名称用来被animation-name所使用。 2 animation 复合属性。检索或设置对象所应用的动画特效。 3 animation-name 检索或设置对象所应用的动画名称 ,必须与规则keyfra…...
浅谈能源智能管理系统在大学高校中的应用
安科瑞 华楠 摘要:结合深圳南方科技大学能效系统工程设计实例,针对校园中电耗、热量消耗、冷量消耗及水资源消耗数据的采集、传输、分析管理系统,分析了系统中的水、电、气在高校中的能耗分布,并阐述了节能应用方案,可…...
脚本自动化定制开发:实现高效工作的魔法钥匙
在当今这个快节奏的工作环境中,自动化已成为提高工作效率的黄金标准。如果你是一名Windows用户,那么通过Windows脚本自动化,你可以将你的工作流程化繁为简,实现高效工作。而在众多Windows脚本自动化工具中,Python以其简…...
使用websocket获取thingsboard设备的实时数据
背景 有一个读者前来咨询,如何实时获取设备的遥测数据。 其实tb是有提供websocket接口来获取设备数据的。而且还支持js跨域调用。下面给大家演示一下。 websocket地址 完整代码 <!DOCTYPE HTML> <html><h...
使用Linux JumpServer堡垒机本地部署与远程访问
🌈个人主页:聆风吟 🔥系列专栏:网络奇遇记、Cpolar杂谈 🔖少年有梦不应止于心动,更要付诸行动。 文章目录 📋前言一. 安装Jump server二. 本地访问jump server三. 安装 cpolar内网穿透软件四. 配…...
js的防抖与节流
目录 一、防抖 实现方式 二、节流 实现方式 一、防抖 所谓防抖,单位时间内,某个动作只能执行矗后一次,可以用在搜索框业务中。 性能优化的手段 防抖 --- 在同一时间内 频繁触发事件,只处理最后一次 实现方式 1、用第三方库Lodash防抖的…...
中职组网络安全-Windows操作系统渗透测试 -20221219win(环境+解析)
B-4:Windows操作系统渗透测试 任务环境说明: 服务器场景:20221219win 服务器场景操作系统:Windows(版本不详)(封闭靶机) 1.通过本地PC中渗透测试平台Kali对服务器场景Server08进行系统服务及版本扫描渗透测试,并将该操作显示结果中1433端口对应的服务版本信息作为F…...
git本地账户如何从一台电脑迁移到另外一台
为了表述方便,我们此处用旧电脑、新电脑指代。 在新电脑上安装git 例如,我旧电脑上安装的git版本是2.33.1版本,新电脑安装git的版本是2.43.0,这不妨碍迁移。 将git的全局配置文件从旧电脑拷贝到新电脑 Git的全局配置文件&…...
HOOPS Web平台助力开发3D应用,实现超大规模3D web轻量化渲染与数据格式转换!
一、包含的软件开发工具包 HOOPS Web平台帮助开发人员构建基于Web的工程应用程序,提供高级3D Web可视化、准确快速的CAD数据访问和3D数据发布。 HOOPS Web平台包括三个集成软件开发工具包 (SDK): (1)Web端3D可视化引擎 HOOPSCom…...
GDB Debugging Notes
1 Debugging programs using gdb 1.1 gdb简介 gdb是一个功能强大的调试工具,可以用来调试C程序或C程序。在使用这个工具进行程序调试时,主要涉及下面几个方面的操作: 启动程序:在启动程序时,可以设置程序运行环境。设置断点:程序…...
Azure Machine Learning - 创建Azure AI搜索服务
目录 准备工作查找 Azure AI 搜索产品/服务选择订阅设置资源组为服务命名选择区域选择层创建服务配置身份验证扩展服务何时添加第二个服务将多个服务添加到订阅 Azure AI 搜索是用于将全文搜索体验添加到自定义应用的 Azure 资源,本文介绍如何创建Azure AI搜索服务 …...
鸿蒙(HarmonyOS)应用开发——安装DevEco Studio安装
前言 HarmonyOS华为开发的操作系统,旨在为多种设备提供统一的体验。它采用了分布式架构,可以在多个设备上同时运行,提供更加流畅的连接和互动。HarmonyOS的目标是提供更高的安全性、更高效、响应更快的用户体验,并通过跨设备功能…...
成都数字孪生技术推进制造业升级,工业物联网可视化应用加速
成都数字孪生技术推进制造业升级,工业物联网可视化应用加速。灯塔工厂转型的关键在于第四次工业革命新技术的应用。数字孪生灯塔工厂是工业4.0技术的应用典范,工业4.0的核心技术包括:数字孪生、大数据分析,工业物联网,…...
管理类联考——数学——汇总篇——知识点突破——代数——函数——记忆
文章目录 整体文字提炼图像绘画 考点记忆/考点汇总——按大纲 本篇思路:根据各方的资料,比如名师的资料,按大纲或者其他方式,收集/汇总考点,即需记忆点,在通过整体的记忆法,比如整体信息很多&am…...
Flash Attention:高效注意力机制的突破与应用
注意力机制彻底改变了自然语言处理和深度学习领域。它们允许模型在执行机器翻译、语言生成等任务时专注于输入数据的相关部分。 在这篇博客[1]中,我们将深入研究被称为“Flash Attention”的注意力机制的突破性进展。我们将探讨它是什么、它是如何工作的,…...
Flutter开发警告Constructors in ‘@immutable‘ classes should be declared as ‘const‘
文章目录 警告信息报错代码警告原因修改后的代码 警告信息 Flutter开发遇到如下警告 Constructors in ‘immutable’ classes should be declared as ‘const’. 报错代码 class TaskWidget extends StatefulWidget {final String title;final bool isChecked;final int ord…...
智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql
智慧工地管理云平台系统,智慧工地全套源码,java版智慧工地源码,支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求,提供“平台网络终端”的整体解决方案,提供劳务管理、视频管理、智能监测、绿色施工、安全管…...
基于服务器使用 apt 安装、配置 Nginx
🧾 一、查看可安装的 Nginx 版本 首先,你可以运行以下命令查看可用版本: apt-cache madison nginx-core输出示例: nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...
Python爬虫实战:研究feedparser库相关技术
1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...
关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...
电脑插入多块移动硬盘后经常出现卡顿和蓝屏
当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时,可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案: 1. 检查电源供电问题 问题原因:多块移动硬盘同时运行可能导致USB接口供电不足&#x…...
Cinnamon修改面板小工具图标
Cinnamon开始菜单-CSDN博客 设置模块都是做好的,比GNOME简单得多! 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...
智能分布式爬虫的数据处理流水线优化:基于深度强化学习的数据质量控制
在数字化浪潮席卷全球的今天,数据已成为企业和研究机构的核心资产。智能分布式爬虫作为高效的数据采集工具,在大规模数据获取中发挥着关键作用。然而,传统的数据处理流水线在面对复杂多变的网络环境和海量异构数据时,常出现数据质…...
CSS设置元素的宽度根据其内容自动调整
width: fit-content 是 CSS 中的一个属性值,用于设置元素的宽度根据其内容自动调整,确保宽度刚好容纳内容而不会超出。 效果对比 默认情况(width: auto): 块级元素(如 <div>)会占满父容器…...
6个月Python学习计划 Day 16 - 面向对象编程(OOP)基础
第三周 Day 3 🎯 今日目标 理解类(class)和对象(object)的关系学会定义类的属性、方法和构造函数(init)掌握对象的创建与使用初识封装、继承和多态的基本概念(预告) &a…...
第八部分:阶段项目 6:构建 React 前端应用
现在,是时候将你学到的 React 基础知识付诸实践,构建一个简单的前端应用来模拟与后端 API 的交互了。在这个阶段,你可以先使用模拟数据,或者如果你的后端 API(阶段项目 5)已经搭建好,可以直接连…...

