小程序中的大道理之二--抽象与封装

news2024/12/23 18:16:10

继续扒

接着 上一篇 的叙述, 健壮性也有了, 现在是时候处理点实际的东西了, 但我们依然不会一步到底, 让我们来看看.

一而再地抽象(Abstraction Again)

让我们继续无视那些空格以及星号等细节, 我们看到什么呢?

pattern as a whole line

我们只看到一整行的内容, 当传入 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 看作类似电路那样有一些输入端和输出端的黑盒子:

  1. 返回的值是我们想要的吗?
  2. 传入的参数是否足够让 getLineContent 里面完成它的工作呢?

第一点是可以肯定的, 但传入的参数是否足够了呢?

如果按上述代码, 不管是 3 行的情况, 还是 5 行的情况, 获取第一行的内容时, 调用的都是 getLineContent(0), 按照输入决定输出的原则, 结果将一样.

但我们很清楚, 5 行情况下的第一行前面的空格肯定要多于 3 行的情况, 如下图:

same line but diff space

所以很显然, 只传入一个 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 stub
    return null;
}

那么, 这样一种先从高层考虑起的做法, 就是所谓的自顶向下了, 接下来我们还会不断地以这种方式来完成这个小程序.

自顶向下是一种很重要的思考及处理问题的方式, 如果你还不习惯这样去考虑问题(包括写代码), 现在是时候尝试一下了.

项目进度(Project Progress)

另外, getPattern 方法里面的 TODO 标识可以去掉了, 这个方法已经算是完成了, 如果现在太阳就快下山了, 那么你也可以提交它了, 你的项目经理也很乐意看到"代码量天天在增长", 这给了他信心, 让他觉得"项目正在稳步推进", 当他给项目总监或者客户汇报时, 他就可以展示一些"进度"给他们看了.

当管理者看不到进度时, 他们就会感觉到压力, 这种压力会转移到你身上, 你甚至会"被志愿加班". 这种压力除了损害我们的健康外没有任何好处, 所以你要学聪明一点, 当管理者问起你的时候, 你就大声对他们说: "我今天又提交了 XXX 行代码. ", 然后你就拍拍屁股准时下班了.

再而三的抽象(Abstraction, again and again)

现在把目光投向 getLineContent 方法. 经过观察, 可以看出一行内容由三个部分组成, 我们再一次忽略具体的细节:

line as three part

如上, 三种颜色表示了三个部分, 我们再一次运用抽象, 先不考虑传什么参数, 有点像是写 伪代码(pseudo code) 那样快速把程序的 骨架(Skeleton) 写出来:

private String getLineContent(int lineCount, int lineNumber) {
    // TODO Auto-generated method stub
    StringBuilder 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 stub
    return null;
}

private String getSecondPart(int lineNumber) {
    // TODO Auto-generated method stub
    return null;
}

抽象不足(Lack of Abstraction)

另一方面, 我们也要警惕缺少必要的封装层次的情况. 不幸的是, 很多情况, 我们都是缺少必要的抽象与封装.

做过维护的同学可能都见过那种超长超恐怖的方法, 里面的语句有的甚至高达几千行, 哪怕是在方法内找一个变量的定义, 也能让你想起周杰伦与费正清合唱的那首歌–<<千里之外>>, 去维护这样的方法自然不是什么愉快的经历.

这里之所以不厌其烦地对这个小程序不断的抽象下去, 是想告诉大家, 即使是如此之小的一个程序, 抽象到这一地步, 语义层面依然还没有过度的倾向.

通常, 如果程序语言已经可以直接表达出我们想要的语义, 封装就可以结束了. 我们来审视一下前两个方法, 显然, 还不能直接表达, 所以封装还可以继续.

一般地, 如果一条语句就能表达的时候, 抽象与封装也就基本到头了.

同时, 不必过于刻板地去遵循这些, 有时三两条语句可以表达时, 不封装也是很正常的;

而有时为了提供更清晰的语义, 哪怕只有一条语句, 你再封装一下也是可取的.

当然了, 对于目前这个小程序, 大家可能觉得已经有些过度封装了, 但在后面我们将看出, 其实还没到最抽象的阶段. 现在先不争论这一点, 说到后面我们就明白了.

分而治之(Divide and Conquer)

其实抽象与封装还能带来什么好处呢? 那就是这里要讨论的分而治之了.

我们可以回顾一下程序写到现在, 我们可曾遇到什么"阻碍"没有?

答案是没有. 你可以看看前面的代码, 都是简简单单的 for 循环, append 之类的.

有人可能不服气地说:

"困难的地方都被你这种一层又一层的抽象与封装延后了, 代码写了半天啥事也没干到. "

这种评价对不对呢? 的确, 前面通过抽象不断地压制那些细节的表达, 不断地推迟对其的处理.

想像有一个房间, 衣服, 物品堆放得乱七八糟, 这时有人拿来一个大箱子, 把这些东西通通塞了进去. 把这些东西"封装"起来后, 房间自然整洁了, 但我们也很清楚, 箱子里依旧是一团糟.

但这个比喻并不适合这里的情况, 我们的抽象并不是简单地把问题转移了, 通过一层层抽象的手段, 一个大问题在不断被分解成一个个小问题.

  1. 有些足够清晰的小问题, 我们已经在这一过程中把它解决掉了.

比如, 输出一个换行的问题.

  1. 而那些还不够清晰的小问题, 也已经通过抽象被我们所 孤立(isolate) 或者叫 隔离 出来了, 有的已经看到了解决的曙光.

比如, 在上一步, 我们还是有两个参数传了进来, 但通过在里面进一步划分成新的子问题, 可以看到, 有些子问题只要一个参数即可解决了.

所以, 抽象并不是什么事也没干, 相反, 它干了很重要的事情.

通过抽象, 问题正在被分解与简化;通过抽象, 我们构建出了程序的骨架.

在这一过程中, 大问题分解成小问题并被安排到了适当的位置, 与其它的小问题隔离开来, 有个词怎么说的, “众神归位”, 大概就是这样一个意思.

群魔乱舞, 你怎么去应付呢? 如果他们都呆在自己的位置上, 我们就可挨个收拾他们了.

抽象不存在"事不过三"(No Limits for Abstraction)

让我们继续, 我们还可以继续抽象吗? 答案是肯定的. 无论是参数更多的 getFirstPart, 还是参数更少的 getSecondPart, 它们都还可以分成两部分:

  1. 拿到一个数量 N(你甭管怎么算出来)
  2. 输出 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 stub
    return 0;
}

private int getElementCountOfSecondPart(int lineNumber) {
    // TODO Auto-generated method stub
    return 0;
}

现在再来看看如何实现最后的两个方法, 以一个四行的图案为例:

line num space count and star count relation

图中规律已经很明显, 最终结果如下:

/**
 * 获取每行第一部分的元素个数
 * @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 住, 余下主题我们下回再见. 下一篇见

小程序中的大道理三

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1249614.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

postgresql从入门到精通 - 第35讲:中间件PgBouncer部署|PostgreSQL教程

PostgreSQL从小白到专家&#xff0c;是从入门逐渐能力提升的一个系列教程&#xff0c;内容包括对PG基础的认知、包括安装使用、包括角色权限、包括维护管理、、等内容&#xff0c;希望对热爱PG、学习PG的同学们有帮助&#xff0c;欢迎持续关注CUUG PG技术大讲堂。 第35讲&#…

Linux加强篇002-部署Linux系统

目录 前言 1. shell语言 2. 执行命令的必备知识 3. 常用系统工作命令 4. 系统状态检测命令 5. 查找定位文件命令 6. 文本文件编辑命令 7. 文件目录管理命令 前言 悟已往之不谏&#xff0c;知来者之可追。实迷途其未远&#xff0c;觉今是而昨非。舟遥遥以轻飏&#xff…

SpringBoot : ch05 整合Mybatis

前言 随着Java Web应用程序的快速发展&#xff0c;开发人员需要越来越多地关注如何高效地构建可靠的应用程序。Spring Boot作为一种快速开发框架&#xff0c;旨在简化基于Spring的应用程序的初始搭建和开发过程。而MyBatis作为一种优秀的持久层框架&#xff0c;提供了对数据库…

【微服务专题】SpringBoot自动配置源码解析

目录 前言阅读对象阅读导航前置知识笔记正文0、什么是自动配置0.1 基本概念0.2 SpringBoot中的【约定大于配置】0.3 从SpringMVC看【约定大于配置】0.4 从Redis看【约定大于配置】 一、EnableAutoConfiguration源码解析二、SpringBoot常用条件注解源码解析2.1 自定义条件注解2.…

web前端开发基础----标准流布局和非标准流布局

1&#xff0c;标准流布局 标准流&#xff0c;也称文档流或普通流&#xff0c;是所有元素默认的布局方式。 在标准流中&#xff0c;元素按照其在 HTML 中出现的顺序&#xff0c;自上而下依次排列&#xff0c;并占据其父容器内的可用空间。 标准流中的元素按照其自然尺寸和位置进…

Oracle研学-介绍及安装

一 ORACLE数据库特点: 支持多用户&#xff0c;大事务量的事务处理数据安全性和完整性控制支持分布式数据处理可移植性(跨平台&#xff0c;linux转Windows) 二 ORACLE体系结构 数据库&#xff1a;oracle是一个全局数据库&#xff0c;一个数据库可以有多个实例&#xff0c;每个…

【Rust日报】2023-11-22 Floneum -- 基于 Rust 的一款用于 AI 工作流程的图形编辑器

Floneum -- 基于 Rust 的一款用于 AI 工作流程的图形编辑器 Floneum 是一款用于 AI 工作流程的图形编辑器&#xff0c;专注于社区制作的插件、本地 AI 和安全性。 Floneum 有哪些特性&#xff1a; 可视化界面&#xff1a;您无需任何编程知识即可使用Floneum。可视化图形编辑器可…

【数据库篇】关系模式的表示——(1)问题的提出

1、关系模式的表示 R&#xff1a;表示关系的名字比如&#xff1a;sc选课表&#xff0c;student学生表。 U&#xff1a;表示一个关系模式的所有属性&#xff0c;比如student表&#xff1a;U&#xff08;sno&#xff0c;sname&#xff0c;sage&#xff0c;ssex&#xff09;。 …

代码随想录算法训练营第五十四天|392.判断子序列 115.不同的子序列

文档讲解&#xff1a;代码随想录 视频讲解&#xff1a;代码随想录B站账号 状态&#xff1a;看了视频题解和文章解析后做出来了 392.判断子序列 class Solution:def isSubsequence(self, s: str, t: str) -> bool:dp [[0] * (len(t)1) for _ in range(len(s)1)]for i in ra…

mysql 变量和配置详解

MySQL 中还有一些特殊的全局变量&#xff0c;如 log_bin、tmpdir、version、datadir&#xff0c;在 MySQL 服务实例运行期间它们的值不能动态修改&#xff0c;也就是不能使用 SET 命令进行重新设置&#xff0c;这种变量称为静态变量。数据库管理员可以使用前面提到的修改源代码…

吴恩达《机器学习》10-1-10-3:决定下一步做什么、评估一个假设、模型选择和交叉验证集

一、决定下一步做什么 在机器学习的学习过程中&#xff0c;我们已经接触了许多不同的学习算法&#xff0c;逐渐深入了解了先进的机器学习技术。然而&#xff0c;即使在了解了这些算法的情况下&#xff0c;仍然存在一些差距&#xff0c;有些人能够高效而有力地运用这些算法&…

为什么要隐藏id地址?使用IP代理技术可以实现吗?

随着网络技术的不断发展&#xff0c;越来越多的人开始意识到保护个人隐私的重要性。其中&#xff0c;隐藏自己的IP地址已经成为了一种常见的保护措施。那么&#xff0c;为什么要隐藏IP地址&#xff1f;使用IP代理技术可以实现吗&#xff1f;下面就一起来探讨这些问题。 首先&am…

【Qt之QTextDocument】使用及表格显示富文本解决方案

【Qt之QTextDocument】使用 描述常用方法及示例使用QTextList使用QTextBlock使用QTextTable表格显示富文本结论 描述 QTextDocument类保存格式化的文本。 QTextDocument是结构化富文本文档的容器&#xff0c;支持样式文本和各种文档元素&#xff0c;如列表、表格、框架和图像。…

oled的使用 动态的变量 51

源码均在IIC手写程序中 外部中断实现变量加一 #include "reg52.h" #include "main.h" #include <intrins.h> #include "OLED.h" #include "bmp.h" #include "Delay.h" sbit LED1 P1^0; sbit LED2 P1^1; sbit LED3…

项目实战详细讲解带有条件响应的 SQL 盲注、MFA绕过技术、MFA绕过技术、2FA绕过和技巧、CSRF绕过、如何寻找NFT市场中的XSS漏洞

项目实战详细讲解带有条件响应的 SQL 盲注、MFA绕过技术、MFA绕过技术、2FA绕过和技巧、CSRF绕过、如何寻找NFT市场中的XSS漏洞。 带有条件响应的 SQL 盲注 这篇文章的核心要点如下: 漏洞发现:作者在Portswigger提供的实验室中发现了一个盲SQL注入漏洞。这个漏洞存在于一个应…

【前端】数据行点击选择

前言 【前篇文章】说了,我们公司的核心价值就是让人越来越懒,能怎么便捷就怎么便捷,主打一个简单实用又快捷,为了实现这个目标,我看成这个列表陷入了深思在想,要不要子表的数据加载在点击这个行时,就可以展示数据,这样就不用每次都要点那个小圆圈啦。 查资料 这显然…

2、git进阶操作

2、git进阶操作 2.1.1 分支的创建 命令参数含义git branch (git checkout -b)<new_branch> <old_branch>表示创建分支-d <-D>删除分支 –d如果分支没有合并&#xff0c;git会提醒&#xff0c;-D强制删除-a -v查看分支-m重新命名分支commit id从指定的commi…

centos 7.9 下利用miniconda里的pyinstaller打包python程序为二进制文件操作方法

centos 7.9 下利用miniconda里的pyinstaller打包python程序为二进制文件操作方法 一.centos 7.9 操作系统安装 参考&#xff1a;https://blog.csdn.net/qq_46015509/article/details/134572030?utm_sourceminiapp_weixin 安装完成后用后台连接工具连上虚拟机 二.安装python3 …

「Verilog学习笔记」数据串转并电路

专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点&#xff0c;刷题网站用的是牛客网 关于什么是Valid/Ready握手机制&#xff1a; 深入 AXI4 总线&#xff08;一&#xff09;握手机制 - 知乎 时序图含有的信息较多&#xff0c;观察时序图需要注意&#xff1a…

【自主探索】基于 rrt_exploration 的单个机器人自主探索建图

文章目录 一、rrt_exploration 介绍1、原理2、主要思想3、拟解决的问题4、优缺点 二、安装环境三、安装与运行1、安装2、运行 四、配置自己的机器人1、Robots Network2、Robots frame names in tf3、Robots node and topic names4、Setting up the navigation stack on the rob…