设计模式学习笔记 - 设计原则 - 6.KISS原则和YAGNI原则

前言

今天,将两个设计原则:KISS 原则和 YANGI 原则。其中,KISS 原则比较经典,耳熟能详,但 YANGI 你可能没怎么听过,不过它理解起来也不难。

理解这个两个原则的时候,经常会有一个共同的问题,那就是看一眼就觉得懂了,但深究的话,又有很多细节不是很清楚。

  • 怎么理解 KISS 原则中的 “简单” ?
  • 什么代码才算 “简单”?怎样的代码才算 “复杂”?
  • 如何才能写出 “简单” 的代码?
  • YANGI 原则和 KISS 预原则说的是一回事吗?

理解 KISS 原则

KISS 原则的意思是尽量保持简单。它的英文描述有好几个版本:

  • keep it Simple and Stupid。
  • keep it Short and Simple。
  • keep it Simple and Straightforward。

我们知道,代码的可读性和可维护性是衡量代码质量非常重要的两个标准。而 KISS 原则就是保持代码可读性和可维护性的重要手段。代码足够简单,也意味着很容易读懂,bug 难以隐藏。即使出现 bug,修复起来也比较简单。

不过这条原则只是高速我们,要保持代码简单,但是并没有给出特别明确的方法论,来指导如何开发出 “简单” 的代码。所以看着简单,但是不能落地。

接下来,为了能让这条原则落地,来进一步进行讲解。

代码行数越少就越“简单”吗?

我们先来看一个例子。下面的三段代码实现同一个功能:检查输入字符串 ipAddress 是否是合法的 IP 地址。

合法的 IP 地址由四个数字组成,并通过 “.” 进行分割。每组数字的取值范围是 0~255。第一组数字比较特殊,不能为 0。

对比下面的代码,你觉得哪段代码最符合 KISS 原则呢?如果让你实现,你选用哪种实现方式?

// 第一种实现方式:使用正则表达式
public boolean isValidIpAddress(String ipAddress) {if (StringUtils.isBlank(ipAddress)) { return false; }String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";return ipAddress.matches(regex);
}// 第一种实现方式:使用现成的工具类
public boolean isValidIpAddress2(String ipAddress) {if (StringUtils.isBlank(ipAddress)) { return false; }String[] ipUnits = StringUtils.split(ipAddress, ".");if (ipUnits.length != 4) { return false; }for (int i = 0; i < 4; i++) {int ipUnitIntValue;try {ipUnitIntValue = Integer.parseInt(ipUnits[i]);} catch (NumberFormatException e) {return false;}if (ipUnitIntValue < 0 || ipUnitIntValue > 255) { return false; }if (i == 0 && ipUnitIntValue == 0) { return false; }}return true;
}// 第三种方式:不适用工具类
public boolean isValidIpAddress3(String ipAddress) {char[] ipChars = ipAddress.toCharArray();int length = ipChars.length;int ipUnitIntValue = -1;boolean isFirstUnit = true;int unitsCount = 0;for (int i = 0; i < length; i++) {char ch = ipChars[i];if (ch == '.') {if (ipUnitIntValue < 0 || ipUnitIntValue > 255) { return false; }if (isFirstUnit && ipUnitIntValue == 0) { return false; }if (isFirstUnit) { isFirstUnit = false; }ipUnitIntValue = -1;unitsCount++;continue;}if (ch < '0' || ch > '9') { return false; }if (ipUnitIntValue == -1) { ipUnitIntValue = 0; }ipUnitIntValue = ipUnitIntValue * 10 + ch - '0';}if (ipUnitIntValue < 0 || ipUnitIntValue > 255) { return false; }if (unitsCount != 3) { return false; }return true;
}

第一种实现方式,利用的是正则表达式,只用三行代码就把这个问题搞定了。它的代码行数最少,但是它不符合 KISS 原则。虽然代码行数少,看似简单,实际上去很复杂。

  • 一方面,正则表达式本身是比较复杂的,写出完全没有 bug 的正则表达式本身就比较有挑战性;
  • 另一方面,并不是每个程序员都精通正则表达式。对于不怎么懂正则表达式的同时来说,看懂并且维护这段正则表达式是比较困难的。

这种实现方式会导致代码的可读性和可维护性变差,所以,从 KISS 原则设计初衷上来讲,这种实现方式并不符合 KISS 原则。

第二种实现方式,利用了 StringUtilsInteger 类提供的一些现成的工具函数,来处理 IP 地址字符串。这三种实现方式。第三种实现方式,不使用任何工具函数,而是通过逐一处理 IP 地址中的字符,来判断是否合法。从代码行数上来说,这两种实现方式差不多。但是,第三种要比第二种更加有难度,更容易写出 BUG。从可读性上来说,第二种实现方式的代码逻辑更清晰、更好理解。所以,在这两种实现方式中,第二种实现方式更加 “简单”,更加符合 KISS 原则。

你可能会说,第三种实现方式虽然会有点复杂,但是性能比第二种实现方式高。从性能角度来说,选择第三章方式是不是更好些呢?

第三种方式性能高的原因
一般来说,工具类的功能都比较通用和全面,所以,在代码实现上,需要考虑和处理更多的细节,执行效率会有所影响。而第三种实现方式,完全是自己操作底层字符串,只针对 IP 地址这一种格式的数据输入来做处理,没有太多多余的函数调用和其他不必要的处理逻辑,所以,在执行效率上,这类定制化的处理代码方式肯定比通用的工具类要高些。

尽管第三章方式性能更高,但是我还是倾向于第二种实现方式。因为第三种实现方式实际上是一种过度优化。除非 isValidIpAddress() 函数是影响系统性能的瓶颈代码,否则,这样优化的投入产出比并不高,增加了代码的实现难度、牺牲代码的可读性,性能上的提升去并不明显。

代码逻辑复杂就违背 KISS 原则吗?

刚刚提到,并不是代码行数越少就越 “简单”,还要考虑逻辑复杂度、实现难度、代码的可读性等。那如果一段代码的逻辑太复杂、实现难度大、可读性也不好,是不是就一定违背 KISS 原则呢?我们先看看下面一段代码:

// KMP algorithm: a,b 分别是主串和格式串;n,m 分别是主串和模式串的长度。
public static int kmp(char[] a, int n, char[] b, int m) {int[] next = getNexts(b, m);int j = 0;for (int i = 0; i < n; ++i) {while (j > 0 && a[i] != b[j]) { // 一直找到a[i]和b[j]j = next[j - 1] + 1;}if (a[i] == b[j]) {++j;}if (j == m) { // 找到匹配模式串了return i - m + 1;}}return -1;
}// b表示模式串,
public static int[] getNexts(char[] b, int m) {int[] next = new int[m];next[0] = -1;int k = -1;for (int i = 0; i < m; i++) {while (k != -1 && b[k + 1] != b[i]) {k = next[k];}if (b[k + 1] == b[i]) {++k;}next[i] = k;}return next;
}

这段代码完全符合我们提到的逻辑复杂、实现难度大、可读性差的特点,但它不违反 KISS 原则。为什么这么说呢?

KMP 算法以快速高效著称。当我们需要处理长文本字符串匹配问题(几百 MB 大小文本内容的匹配),或者字符串是某个产品的核心功能,又或者字符串匹配算法是系统性能瓶颈的时候,我们就应该选择尽可能搞下的 KMP 算法。而 KMP 算法本身具有逻辑复杂、实现难度大、可读性差的特点。本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。

不过,平时的项目开发中涉及的字符串匹配问题,大部分都是针对比较小的文本。在这种情况下,直接调用编程语言提供的现成的字符串匹配函数就足够了。如果非用 KMP 算法、BM 算法来实现字符串匹配,那就真的违背 KISS 原则了。也就是说,同样的代码,在某个业务场景下符合 KISS 原则,换一个应用场景可能就不满足了。

如何写出满足 KISS 原则的代码?

实际上,前面已经讲到了一些方法了。这里总结下:

  • 不要使用同时可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。
  • 不要重复造轮子,要善于使用已有的工具类库。经验证明,自己去实现这些类库,出 BUG 的概率会高,维护的成本更高。
  • 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数)来优化代码,牺牲代码的可读性。

实际上,代码是否足够简单,是一个挺主观的评判。同样的代码,有的人觉得简单,有的人觉得不简单。所以,评判代码是否简单,还有一个很有效的间接方法,那就是 code review。如果在 code review 的时候,同事对你的代码有很多疑问,那就说明你的代码有可能不够 “简单”,需要优化啦。

我们在做开发的时候,一定不要过度设计,不要觉得简单的东西就没有技术含量。实际上,越是能用简单的方法解决复杂的问题,越能体现一个人的能力。

YAGNI 和 KISS 说的是一回事吗?

YAGNI 原则的英文全称是:You Ain’t Gonna Need It。翻译:你不需要它。

在软件开发中,它的意思是:

  • 不要去设计当前用不到的功能;
  • 不要去编写当前用不到的代码。

这条原则的核心思想是不要过度设计

比如,我们的系统暂时只用 Redis 存储配置信息,以后可能会用到 Zookeeper。根据 YAGNI 原则,在未用到 Zookeeper 之前,我们没必要提前编写这部分代码。当然,这并不是说我们就不需要考虑代码的扩展性。我们还是要预留好扩展点,等到需要的时候,再去实现 Zookeeper 存储配置信息这部分代码。

还有,我们不要在项目中提前引入不需要依赖的开发包。对于 Java 程序员来说,经常使用 Maven 或者 Gradle 来管理类库。有些同时为了避免开发中 Library 包缺失而频繁地修改 Maven 或者 Gradle 配置文件,提前往项目里引入大量常用的 library 包。实际上,这样的做法是违背 YAGNI 原则的。

从刚刚的分析可以看出,YAGNI 原则和 KISS 原则并非一回事。KISS 原则讲的是“如何做”的问题(尽量保持简单),而 YAGNI 原则说的是 “要不要做的问题”(当前不需要的就不要做)。

总结

KISS 原则是保持代码可读性和可维护性的重要手段。KISS 原则中的 “简单” 并不是以代码行数来考量的。代码行数越少并不代表代码越简单,还需要考虑代码的复杂度、实现难度、代码的可读性等。而且,本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。除此之外,同样的代码,在某个业务场景下满足 KISS 原则,换一个场景就可能不满足了。

对于 KISS 原则,还总结了下面几条原则:

  • 不要使用同事可能不懂的技术来实现代码
  • 不要重复造轮子,要善于使用已有的工具类库
  • 不要过度优化

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

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

相关文章

后台组件-语言包

<groupId>org.qlm</groupId><artifactId>qlm-language</artifactId><version>1.0-SNAPSHOT</version> 平台提供多语言支持&#xff0c;以上为语言包&#xff0c;提供后台多语言支持。首批实现&#xff1a; public class LanguageConstan…

Git快速上手二

对Git命令的深入理解快速上手Git&#xff08;包含提交至GitHub和Gitee&#xff09;-CSDN博客 1.5 分支操作 1.5.1 分支原理 系统上线后,又要修改bug,又要开发新的功能。 由于新功能没有开发完,所以需要建立分支,一边修改bug,一边开发新功能,最终合并. 1.5.2 分支实操 创建…

Java基于微信小程序的旅游出行必备小程序,附源码

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

第三百八十六回

文章目录 概念介绍使用方法示例代码 我们在上一章回中介绍了Snackbar Widget相关的内容,本章回中将介绍TimePickerDialog Widget.闲话休提&#xff0c;让我们一起Talk Flutter吧。 概念介绍 我们在这里说的TimePickerDialog是一种弹出窗口&#xff0c;只不过窗口的内容固定显示…

18.网络游戏逆向分析与漏洞攻防-网络通信数据包分析工具-数据分析工具数据与消息配置的实现

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 上一个内容&#xff1a;17.数据分析工具配置功能的实现 码云地址&#xff08;master 分支&#xff09;&#xff1a;https://gitee.com/dye_your_fingers/titan…

于建筑外窗遮阳系数测试的太阳光模拟器模拟太阳光照射房屋视频

太阳光模拟器是一种用于测试建筑外窗遮阳系数的高科技设备。它能够模拟太阳光照射房屋的情景&#xff0c;帮助建筑师和设计师更好地了解建筑外窗的遮阳性能&#xff0c;从而提高建筑的能源效率和舒适度。 这种模拟器的工作原理非常简单&#xff0c;它通过使用高亮度的光源和精密…

Positional Encoding 位置编码

Positional Encoding 位置编码 flyfish Transformer模型没有使用循环神经网络&#xff0c;无法从序列中学习到位置信息&#xff0c;并且它是并行结构&#xff0c;不是按位置来处理序列的&#xff0c;所以为输入序列加入了位置编码&#xff0c;将每个词的位置加入到了词向量中…

Netty之WebSocket协议开发

一、WebSocket产生背景 在传统的Web通信中&#xff0c;浏览器是基于请求--响应模式。这种方式的缺点是&#xff0c;浏览器必须始终主动发起请求才能获取更新的数据&#xff0c;而且每次请求都需要经过HTTP的握手和头部信息的传输&#xff0c;造成了较大的网络开销。如果客户端…

爆肝!Claude3与ChatGPT-4到底谁厉害,看完你就知道了!

前言&#xff1a; 相信大家在pyq都被这张图片刷屏了把~ 昨天&#xff0c;为大家介绍了一下什么是Claude&#xff0c;今天咱终于弄到号了&#xff08;再被ban了3个号之后终于是成功的登上去了&#xff0c;如果各位看官觉得咱文章写的不错&#xff0c;麻烦点个小小的关注~你们的…

【详识C语言】自定义类型之三:联合

本章重点 联合 联合类型的定义 联合的特点 联合大小的计算 联合&#xff08;共用体&#xff09; 联合类型的定义 联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员&#xff0c;特征是这些成员公用同一块空间&#xff08;所以联合也叫共用体&#xff09;…

mysql 数据库查询 查询字段用逗号隔开 关联另一个表并显示

文章目录 问题描述解决方案 问题描述 如下如所示&#xff1a; 表一&#xff1a;wechat_dynamically_config表&#xff0c;重点字段&#xff1a;wechat_object 表二&#xff1a;wechat_object表&#xff0c;重点字段&#xff1a;wxid 需求&#xff1a;根据wechat_dynamically_…

模仿Gitee实现站外链接跳转时进行确认

概述 如Gitee等网站&#xff0c;在有外部链接的时候如果不是同域则会出现一个确认页面。本文就带你看看这个功能应该如何实现。 效果 实现 1. 实现思路 将打开链接作为参数传递给一个中间页面&#xff0c;在页面加载的时候判断链接的域名和当前网站是否同域&#xff0c;同域…

Redis线程模型解析

引言 Redis是一个高性能的键值对&#xff08;key-value&#xff09;内存数据库&#xff0c;以其卓越的读写速度和灵活的数据类型而广受欢迎。在Redis 6.0之前的版本中&#xff0c;它采用的是一种独特的单线程模型来处理客户端的请求。尽管单线程在概念上似乎限制了其扩展性和并…

collection的遍历方式

增强for遍历 增强for的底层就是迭代器&#xff0c;为了简化迭代器的代码书写的。 他是jdk5之后出现的&#xff0c;其内部原理就是一个Iterator迭代器。 所有的单列集合和数组才能用增强for进行遍历。 package myCollection;import java.util.ArrayList; import java.util.C…

嵌入式学习day32 网络

htons()&#xff1b;//host to network short 将端口号转换为网络通信中的大端存储 eg:htons(50000); ntohs()&#xff1b;//host to network short 将大端存储转换为主机端口号 inet_addr();将IP地址转换为二进制 eg:inet_addr(192.168.1.170)&#xff1b; inet_ntoa()…

软考65-上午题-【面向对象技术】-面向对象分析、设计、测试

一、面向对象分析OOA 1-1、面向对象分析的定义 面向对象分析的目的&#xff1a;为了获得对应用问题的理解。理解的目的是确定系统的功能、性能要求。 面向对象分析包含5个活动&#xff1a;&#xff08;背&#xff01;&#xff09; 认定对象&#xff1b;&#xff08;重要一点…

QT和OPENGL安装和集成

1.QT安装 1.1官网下载&#xff1a; 网址&#xff1a;https://download.qt.io/archive/qt/ 1.2 开始安装 点击运行 首先注册sign up 然后Login in 选择安装目录 改为D盘&#xff1a; 选择安装项&#xff1a; 准备安装 开始安装&#xff1a; 安装完成&#xff1a; 1.3测试 …

SPI 接口

SPI 接口 SPI 简介寻址方式通信过程极性和相位IIC 和 SPI 的异同相同点不同点 SPI 简介 SPI&#xff08;Serial Peripheral Interface&#xff09;是串行外设接口的缩写&#xff0c;SPI是一种高速的、全双工、同步的串行通信总线&#xff1b;SPI采用主从方式工作&#xff0c;一…

题目 1604: 蓝桥杯-阶乘

题目描述: 一个整数n的阶乘可以写成n!&#xff0c;它表示从1到n这n个整数的乘积。阶乘的增长速度非常快&#xff0c;例如&#xff0c;13!就已经比较大了&#xff0c;已经无法存放在一个整型变量 中&#xff1b;而35!就更大了&#xff0c;它已经无法存放在一个浮点型变量中。因…

UART 接口

UART 接口 1. UART 协议原理与编程1.1 UART 简介1.2 UART 帧格式1.3 UART 缺点1.4 Verilog 代码 2. RS232、RS485 协议原理2.1 RS232 协议简介2.1.1 RS232 接口2.1.2 RS232 信号2.1.3 RS232 缺点 2.2 RS4852.2.1 RS485协议简介2.2.2 RS458 信号2.2.3 RS458 接口2.2.4 RS485 优点…