重复性管理--抽象的重要性(下)

news2024/12/22 0:12:03

接着 上一篇的谈论, 继续谈论抽象在重复性管理中的重要作用.

好的抽象与糟糕的抽象?

通过前面的一些例子, 你可能形成了一个印象: 所谓抽象很多时候就是把一些代码封装到一个方法中.

不过事实上并不是这么简单的. 抽象的结果确实很多时候产生了一个方法, 但不是说我把一堆代码整在一块就是一个抽象, 又或者说, 即便它是一个抽象, 但可能却不是一个好的抽象, 而是一个糟糕的抽象.

假如你看到一段代码很长, 然后你"啪"的一声把它从中间拦腰截断, 划分出两个方法来, 一个叫 firstPart(), 一个叫 secondPart(), 那么这算是怎样的一个抽象呢?

我们还是回到前面的例子, 假如把它分成两个部分,

代码的两部分

那么, 这两个抽象是存在问题的. 如果我们足够诚实的话, 就应该把前面的方法命名为 makeRandomArrayAndPrint, 因为这样才算较好的概括了它的行为. 但这样一来这个名字就显得很别扭了.

别人看到这个名字可能就不想调用它了, 虽然他可能需要一个随机数组, 但他也许并不需要把它打印出来. 如果看到你的方法不能满足需要, 自然他又只能是重新发明一个更为"纯粹的" makeRandomArray, 重复又产生了.

另一方面, 像代码中那样没有暗示存在打印, 那别人看到这个方法名觉得 OK, 然后使用它, 结果却出现了副作用, 输出中多了一些莫名其妙的东西, 而这是他不希望出现的.

如果生成的数组很大, 这些输出甚至会导致严重的性能问题, 最终可能还是导致别人放弃调用你的方法.

所以问题出在哪里呢? 其实就在于我们做了一个错误的, 糟糕的抽象. 生成随机数组的方法中是不应该包含打印的, 这实际是两件不同的事, 但我们却把它们整合在一起, 然后起了一个不适当的名字.

这样一来代码的灵活性就下降了. 以 sort 为例. 有些初学者可能在写完排序的代码后就顺手也写个打印在里边, 这样一来, sort 实际就变成了 sortAndPrint.

一个方法越大越长, 自然是离具体越近而离抽象越远.

是的, 其它地方会有很多对单独的 sort 的需求, 也会有很多对单纯的 print 的需求;但同时需要 sort 跟 print 的需求有多少呢? 恐怕就不多了.

越是具体, 能恰好匹配的需求就越少, 所以, 硬是把 sort 和 print 封装在一起, 这样的代码能被复用的几率就小很多了, 或者在勉强复用它的情况下, 不得不忍受它所带来的副作用, 而最终, 别人可能还是觉得重复发明一个"纯粹的"轮子用得舒服些.

所以, 有意无意地往 sort 里增加一个打印, 就属于画蛇添足, 破坏了抽象, 好心办了坏事.

所谓的内聚性

综上, 代码不是机械地把它们分开就是抽象. 被一个方法所封装在一起的一系列语句它们应该是紧密围绕一个主题的, 这就是所谓的"内聚性(Cohesion)".

只有当把彼此关系非常紧密的一系列语句封装在一起, 这样才能构成一个好的抽象.

如前面把打印的功能跟生成随机数组的代码整合在一块, 但它们彼此的关系却是疏远的, 不是非得要在一起的. 硬是把它们的绑在一块, 那就成了强扭的瓜, 成了拉郎配.

它们在一起擦出的不是火花而是火光, 也因此阻碍了复用, 因为很可能会带来各种副作用.

在前面 小程序中的大道理之四–单元测试 也曾经介绍过所谓的"单一职责原则(SRP: Single Responsibility Principle.)", 简单地讲, 那就是:

一个方法只做好一件事.

而很多时候, 命名是一件非常重要的事, 因为命名本身就是一个对事物进行抽象的过程. 如果你的一个方法做了太多的事, 你为它取名时就会面临很大的困难.

正如前面的 makeRandomArrayAndPrint 这个方法名会很长很别扭那样.

反之, 如果你面对一个别扭的名字, 你应该想到这可能是个糟糕的抽象. 很不幸, 我们可能会时不时看到诸如:

doSomeThing, handle, process, execute, apply

等这样特别抽象的名字, 而这倒不是说这些方法中干的是多么抽象的事,

毕竟你并不是在写那些特别抽象的如解析器之类的,

最大可能其实是方法中做了太多的事, 以至于不知道要怎样给它取名了, 最后只好取一个特别抽象的名, 这样仅从方法名中就基本得不到什么有效的信息了.

当别人想复用你的代码时, 基本都会先从方法名入手, 而不会深入到里面去看.

太抽象的名字让人不知所云, 最终别人会放弃尝试你的方法, 哪怕你的方法确实能解决他的问题.

当然, 如果名字特别抽象, 更大概率是它没有被复用的可能性.

所以, 一个好的名字是特别重要的, 因为它是一种适当的, 良好的抽象的暗示, 而这样的抽象是正是可复用性的关键, 也只有这样才能更好的管理系统中的重复.

在编程中, 我们可能不自觉地就会在一个方法中塞入太多的语句, 或在一个类中塞入太多的方法, 或在一个模块或 jar 塞入太多的类, 这样它们的主题必然就是模糊的, 而没有聚焦到"尽量做好唯一的一件事情"上.

这样的东西即便你把打成一个 jar 包, 别人可能还是不愿意去使用, 因为他也许只想使用其中一小块功能, 却必须引入一个庞大的 jar 包(还可能潜在地传递性地依赖更多的其它 jar), 其中可能有一堆的东西他都是不需要的, 导致系统特别臃肿.

所以更可能的情况就是他说, 算了, 别引入了, 还是自己发明轮子吧.

如果你看现在的有些框架, 比如 spring, 你会发现它现在分得很细,

比如 IoC 成为一个模块, 打包成一个单独的 jar, AOP 可能成为另一模块, 另一个 jar;

而早期这些东西可能都是在一起的, 一个单独的巨大的 spring.jar

那么这些细分的抽象自然有它的好处, 比如我只要 IoC 的功能, 那我就只引入 IoC 的, AOP 我则已有其它的解决方案, 我不要它的.

这样一来我就能避免引入重复的解决方案, 系统也不用那么臃肿.

甚至说, 这还不单单是重复性的问题. 类似的东西太多还可能存在潜在的冲突, 给未来带来潜在的不确定性.

毕竟, 代码越多, 就越容易出错, 这是一条最基本的原理.

概念层次上的匹配

最后, 回到概念层次的匹配这个话题上来. 在前面的例子中谈到, 当没有抽象时, 是用"具体对抽象", 而只有建立了抽象后, 才能有"抽象对抽象":

抽象与具体在概念层次的匹配

而这点为什么特别重要呢? 有人可能会说: "你怎么知道有人可能要复用它呢? 你是不是过度抽象了? 这也没几行代码呀? "

是的, 在这里, 你没抽象出来一个 makeRandomArray 来, 也许可能并不是很大的问题, 但有时候事情不是这样的.

我们再举一个例子, 下面的一段程序, 计算二月份的天数和一年的天数:

概念与代码不匹配的情况

那么其中的 ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0), 我相信你也清楚, 就是判断一个年份是否是闰年(leap year).

那么这段程序存在什么问题呢? 我们经常说程序是对现实世界的一个映射, 如果把这段程序跟现实对比一下:

概念与代码不匹配的情况

会发现, 现实世界的一个"概念"(也就是"闰年")在程序中没有对应! 现实世界的一处"具体的细节"则在程序中出现了两次.

我们的程序中缺失了一个概念!

而所谓"概念"的"概", 也就是"概括的"的意思, 其实也就是"抽象的"的一个近义词, 也就是说我们的程序实际缺少了一个"抽象".

现在把这个抽象补上, 增加一个叫"isLeapYear"的判断:

闰年

现在再来看对应关系:

概念与代码匹配的情况

是不是合理了很多呢? 现实中你有一个概念"闰年", 在程序中我则有一个抽象"isLeapYear"跟你对应;

因为在程序中主要用于判断, 所以变成了 isXXX 这样的形式.

现实中你有一个具体的含义"年份能被4整除且不能被100整除;或年份能被400整除", 在程序中我则有一段具体的代码 ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0) 跟你对应.

而且这段具体的代码只出现一次!

现在, 我们的程序也拥有了一套完整的词汇, 无论是抽象的还是具体的, 无论在哪一个层次上, 我都能跟现实对应上, 达到了层次与概念上的匹配.

这种匹配特别重要, 特别是抽象层次上的匹配, 它为程序中的其它部分构筑了一道抽象的屏障(abstract barrier):

抽象的屏障

现在, getDayCountOfFebgetDayCountOfYear 只依赖于抽象的isLeapYear, 细节被隔绝了, 被隐藏了, 被压制在了抽象之下.

它们不需要也不应该知道那些细节.

为什么这些细节的隐藏很重要呢? 首先, 正如前面引入"环比"和"同比"这些概念类似, 抽象的 isLeapYear 要比具体的 ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0) 的更为简短.

更为重要的则是, 细节是很可能会发生变化的. 你可能有所不知的是, 在早期, 闰年的具体含义仅仅是"年份能被4整除. "

但按照每四年一个闰年计算, 结合地球公转的实际情况, 平均每年就要多算出0.0078 天, 经过四百年就会多出大约 3 天来, 这个误差不断累积, 所以后来就把闰年的具体含义调整为现在这样.

可是你注意到一个事实没有, 那就是"闰年"这个概念本身是稳定的, 不管具体含义怎么变, 闰年还是叫闰年.

名字是我们取的, 变不变我们说了算, 况且本身就是抽象的, 也没什么可变的;

具体含义则不受我们控制, 而且可能经常发生变化.

说到二月份的天数, 我们还是这么说:

如果是闰年, 则是 29 天, 否则是 28 天.

说到一年的天数, 我们还是这么说:

如果是闰年, 则是 366 天, 否则是 365 天.

当我们引入一个概念时, 后续的很多叙述就会建立在这个抽象的概念之上, 具体含义的变化不会影响到这些叙述;

与此类似, 建立在抽象屏障之上的代码也不会受到这些具体细节调整变化的冲击:

代码中引入抽象屏障

不难看出, 代码中的叙述跟现实中的是匹配的, 都是构筑在抽象的基础上, 根本就不知道细节是怎样的, 自然也不会受到细节变化的冲击.

反之, 如果早期只是简单地用具体的 if (year % 4 == 0) 来做判断, 那么现在就要一一调整为 if ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0).

如果有两处, 就要做两处调整;如果有 N 处, 就要做 N 处调整. 显然, 这种重复性的劳动是不受欢迎的.

更为严重的是, 参数名可能有的地方叫 year, 而有的地方则叫 y;

而做具体判断时, 有的地方可能把 (year % 400 == 0) 写在前面, 种种情况下使得你很难一一找出那些需要更改的地方!

你可能要写点复杂的正则表达式而不是一个简单的查找才能把那些地方一一揪出来.

那么这种情况就属于细节没有被压制, 它逃逸了, 泄露了, 具体的形式上也可能出现了一些变形, 这些细节可能散落在系统的各个角落, 最终在面临调整变化时, 我们可能需要大量的重复性劳动, 甚至可能无法确保每一处都得到了调整. 真是糟糕!

而有了抽象屏障, 并把一切都建立在这个屏障之上后呢, 一切就轻松很多了:

在抽象内部的代码调整

因为抽象让细节集中到了一处, 现在只要简单改改具体定义即可, 一切就调整过来了.

而且能很清楚知道哪些地方会受到调整的冲击, 因为只需看看什么地方调用了它即可.

简单粗暴的方法就是暂时删掉方法定义, 看哪些地方报编译的错误就知道了;

也可以简单地查找方法名, 这比查找那些具体定义简单稳定多了, 也不需要什么高超的正则表达式技巧;

又或者, 有了现代 IDE 的帮助, 比如在 Eclipse 中, 你只需简单选中方法名, 然后选择"菜单–Navigate—Open Call Hierarchy", 也就是"打开调用层级"即可查看有哪些地方调用了它.

IDEA 中也有类似操作, 具体细节此处从略.

有了这些在概念层次上的良好匹配, 现实中的一处改动, 系统中也只需要一处改动.

在软件的开发活动中, 你可能会经常碰到这样的情景: 需求方说, 某某地方我想做个小调整, 这时程序员则连连摆手, “不行不行, 改动太大了”, 那么需求方可能会想"这只是一个小调整呀, 为什么会改动很大呢? "他想不明白.

自然, 有些小调整确实可能会造成大改动, 这时你可以甩锅给需求方: "怎么不早说呢? "但是有时, 正如现在举的这个例子这样, 这个锅也许需要我们程序员自己来背.

我们需要经常反思所写的代码, 概念层次跟现实是否形成了良好的匹配? 是否做到了足够的抽象? 重复的细节被管理起来了吗?

很多时候, 变化根本不受我们的控制, 我们唯一能做的, 就是使所写的代码保持足够的弹性, 能够面对各种变化的冲击.

在未来, 当误差不断累积, 闰年具体含义还可能会再次变化, 但有了抽象屏障的保驾护航, 我们不需要担心太多.

从某种意义上说, 当建立好适当的抽象后, 我们不但消除了现在的重复, 甚至也消除了未来的重复. 我们的代码能够抵御未来未知变化的冲击, 使得在未来变化来临时, 无需那些重复性的折腾.

所以, 这就是抽象在重复性管理中的巨大作用, 关于这个主题就暂时讨论到这里.

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

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

相关文章

05 网络和防火墙等其他

网络和其他 ifconfig : 主机ip地址查询

从过去到未来:回顾DDR技术的演进和未来趋势

DDR技术作为内存的核心,其性能的飞速发展直接推动着计算机的计算能力。 本文将简明扼要地介绍DDR技术的发展历程。 一、SDR时代 20世纪90年代,Intel公司推出了首款同步动态随机存储器SDR SDRAM。其采用时钟同步接口,时钟频率典型为66MHz或8…

【洛谷算法题】P2433-小学数学 N 合一【入门2分支结构】

👨‍💻博客主页:花无缺 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P2433-小学数学 N 合一【入门2分支结构】🌏题目描述🌙问题…

MySQL数据库基本操作1

文章目录 主要内容一.DDL1.创建表代码如下(示例): 2.创建表的类型3.其他操作4.修改表结构格式代码如下(示例): 二.DML1.数据插入代码如下(示例): 2.数据修改代码如下(示例): 3.数据删…

力扣第406题 根据身高重建队列 c++ 贪心思维

题目 406. 根据身高重建队列 中等 相关标签 贪心 树状数组 线段树 数组 排序 假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] [hi, ki] 表示第 i 个人的身高为 hi &am…

Qt在Android上设置连接到指定的WIFI

在Android上使用Qt设置连接到指定的Wi-Fi网络需要使用Java代码来完成,涉及到Android平台特定的API和权限。接下来下面将会演示如何在Qt中调用Java代码来实现这一功能。 【1】在Qt项目中创建一个名为"AndroidWifiConnector"的Java类。 (新建文件,选择JAVA类型,名字…

解决 viteprees 中 vp-doc 内置样式影响组件预

解决 viteprees 中 vp-doc 样式影响组件预览 问题 当使用"vitepress": "1.0.0-rc.22"作为组件库文档时,会自动引入vitepress的默认主题, 其中vp-doc中有大量的html标签样式 ... .vp-doc table {display: block;border-collapse: …

前端开发技术栈(工具篇):2023深入了解webpack的安装和使用以及核心概念和启动流程(详细) 63.3k stars

目录 Webpack简介 Entry Module Chunk Loader Plugin Output Webpack的启动流程 Webpack的优缺点 Webpack的使用 1. 安装Webpack 2. 创建Webpack配置文件 3. 编写代码 4. 运行Webpack 5. 在HTML中引入打包后的文件 6. 执行编译命令 Webpack其他功能介绍 1. 使…

map与set

目录 set的key搜索树模拟 set内置的find与count函数 ​编辑 set的lower_bound与upper_bound内置函数、 map的key-value搜索树模拟 通过不同的方式向map中插入键值对。 map的遍历 ​编辑 map的operator的 [ ] 用法 关于map与set用法的几个例题 例题1: 随机…

V3Det大规模词汇视觉检测数据集与LaRS海上全景障碍物检测数据集

V3Det与LaRS是ICCV2023上发表的数据集工作,规模都比较大,后续有可能会用到,因此记录下来。 V3Det: Vast Vocabulary Visual Detection Dataset Paper: https://arxiv.org/abs/2304.03752 URL: https://v3det.openxlab.org.cn/ 在现实世界中…

Bootstrap中CSS媒体查询分辨率 @media(min-width)例子

Bootstrap中CSS媒体查询分辨率 media(min-width)例子 css media min-width max-width 解释: min-width 表示最小即大于等于max-width 表示最大即小于等于 media screen and (min-width : 320px) {html {font-size : 10px !important;} } medi…

geoserve 发布 Styles 样式时,设置边框、填充以及填充透明度

文章目录 需求分析需求 设置 geoserve 发布的 Styles 样式中的边框、填充以及填充透明度 分析 具体详细使用可参考这篇文文章:在 GeoServer 上发布 Shapefile 文件作为 WMS 数据 <?xml version="1.0" encoding=

Git Gui使用技巧

资料 https://www.runoob.com/w3cnote/git-gui-window.html 操作过程 创建仓库→添加远程仓库→扫描目录→文件移动→提交→上传 注意填注释 文件忽略 创建文件.gitignore→编写内容 *.log #文件 config.ini #文件 temp/ #目录

App分发的策略和注意事项2

当今的数字化时代中&#xff0c;移动应用程序已经成为了人们生活中不可或缺的一部分。随着智能手机的普及和移动互联网的快速发展&#xff0c;应用程序的分发方式也变得越来越多样化。 App分发是指将移动应用程序通过特定的渠道传递给终端用户的过程。在应用程序开发完成后&am…

MySQL 多表查询 事务 索引

目录 多表查询简介内连接查询 join on外连接查询 left join、right join子连接查询标量子查询列子查询 (in、not in)行子查询表子查询 多表查询案例 事务事务介绍操作 start transaction、commit、rollback事务四大特性(面试题) 索引索引介绍索引原理索引语法 index 上次学习了…

一款适用于勒索病毒应急演练加解密工具

decryption-encryption 介绍 #encryption.exe为加密脚本 #decryption.exe为解密脚本 1、运行加密脚本&#xff0c;点击运行程序输入密码&#xff1a;TaSt12.2 输入需要加密的文件路径或文件夹路径&#xff0c;确定后即可对路径下面的所有格式文件进行加密&#xff1b; 2、…

安卓核心板_天玑700、天玑720、天玑900_5G模块规格参数

5G安卓核心板是采用新一代蜂窝移动通信技术的重要设备。它支持万物互联、生活云端化和智能交互的特性。5G技术使得各类智能硬件始终处于联网状态&#xff0c;而物联网则成为5G发展的主要动力。物联网通过传感器、无线网络和射频识别等技术&#xff0c;实现了物体之间的互联。而…

淘宝API接口获取商品信息,订单管理,库存管理,数据分析

在淘宝开放平台中&#xff0c;每个API接口都有相应的文档说明和授权机制&#xff0c;以确保数据的安全性和可靠性。开发者可以根据自己的需求选择相应的API接口&#xff0c;并根据文档说明进行调用和使用。 淘宝开放平台API接口是一套REST方式的开放应用程序编程接口&…

Java注解及自定义注解

注解/元数据&#xff08;Annotation&#xff09;&#xff0c;是对代码级别的说明&#xff1b;在JDK1.5及以后版本引入的一个特性&#xff0c;与类、接口、枚举是在同一个层次。可以声明在包、类、字段、方法、局部变量、方法参数等的前面&#xff0c;用来对这些元素进行说明、注…

三、虚拟机的迁移和删除

虚拟机的本质就是文件(放在文件夹的)。因此虚拟机的迁移很方便&#xff0c;可以把安装好的虚拟系统这个文件夹整体拷贝或者剪切到另外的位置使用。删除也很简单&#xff0c;使用vmware进行移除&#xff0c;再点菜单->从磁盘删除即可&#xff0c;或者手动删除虚拟系统对应的文…