代码之丑
郑晔
开源项目 Moco 作者
19833 人已学习
新⼈⾸单¥59
登录后,你可以任选2讲全文学习
课程目录
已完结/共 24 讲
代码之丑
15
15
1.0x
00:00/00:00
登录|注册

07 | 滥用控制语句:出现控制结构,多半是错误的提示

你好,我是郑晔。
在前面几讲,我们已经讲了不少的坏味道,比如长函数、大类等。对于有一定从业经验的程序员来说,即便不能对这些坏味道有一个很清楚的个人认知,但至少一说出来,通常都知道是怎么回事。
但这节课我要讲的坏味道对于很多人来说,可能就有点挑战了。这并不是说内容有多难,相反,大部分人对这些内容简直太熟悉了。所以,当我把它们以坏味道的方式呈现出来时,这会极大地挑战很多人的认知。
这个坏味道就是滥用控制语句,也就是你熟悉的 if、for 等等,这个坏味道非常典型,但很多人每天都用它们,却对问题毫无感知。今天我们就先从一个你容易接受的坏味道开始,说一说使用控制语句时,问题到底出在哪。

嵌套的代码

我给你看一张让我印象极其深刻的图,看了之后你就知道我要讲的这个坏味道是什么了。
图片来源于网络
相信不少同学在网上见过这张图,是的,我们接下来就来讨论嵌套的代码
考虑到篇幅,我就不用这么震撼的代码做案例了,我们还是从规模小一点的代码开始讨论:
public void distributeEpubs(final long bookId) {
List<Epub> epubs = this.getEpubsByBookId(bookId);
for (Epub epub : epubs) {
if (epub.isValid()) {
boolean registered = this.registerIsbn(epub);
if (registered) {
this.sendEpub(epub);
}
}
}
}
这是一段做 EPUB 分发的代码,EPUB 是一种电子书格式。在这里,我们根据作品 ID 找到要分发的 EPUB,然后检查 EPUB 的有效性。对于有效的 EPUB,我们要为它注册 ISBN 信息,注册成功之后,将这个 EPUB 发送出去。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文讨论了在软件开发中滥用控制语句的问题,特别是过多使用if、for等控制结构所带来的坏味道。作者通过分析嵌套的代码和if/else语句的使用,指出这种滥用控制语句会导致代码复杂度增加,理解和维护成本提高的问题。文章通过实际代码案例,展示了如何通过重构和改进代码结构,消除嵌套和else语句,从而降低代码的圈复杂度,提高代码的可读性和可维护性。重点讲解了重复的switch坏味道,提出了以多态取代条件表达式的重构手法。总的来说,本文强调了简化代码结构、减少控制语句使用对于提高代码质量和可维护性的重要性。文章内容颠覆了传统编码方式,挑战了程序员对控制语句的认知,呼吁读者重视代码复杂度和圈复杂度的影响。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《代码之丑》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(34)

  • 最新
  • 精选
  • 呆呆狗的兽
    你好,我看您建立了很多与枚举项一一对应的内部类,这些内部类的创建在那个位置与枚举对象本身绑定?还是说枚举UserLevel(枚举) implements UserLevel(接口)? 我一般都是习惯将这些逻辑封装在枚举的实现里具体代码我一般会实现为: public enum UserLevel { SILVER() { @Override public double getPrice(Book book) { return book.getPrice() * 0.9; } @Override public double getPrice(Epub epub) { return epub.getPrice() * 0.85; } }, GOLD() { @Override public double getPrice(Book book) { return book.getPrice() * 0.8; } @Override public double getPrice(Epub epub) { return epub.getPrice() * 0.85; } }, PLATINUMP() { @Override public double getPrice(Book book) { return book.getPrice() * 0.75; } @Override public double getPrice(Epub epub) { return epub.getPrice() * 0.8; } }; public abstract double getPrice(Book book); public abstract double getPrice(Epub epub); } 然后调用的地方: public double getBookPrice(final User user, final Book book) { UserLevel level = user.getUserLevel(); return level.getPrice(book); } public double getBookPrice(final User user, final Epub epub) { UserLevel level = user.getUserLevel(); return level.getPrice(epub); } 请问这里我用此种方式实现,是否有什么不妥? 我个人在项目中几乎很偏向于用枚举,来封装很多业务的不同性(业务针对不同枚举的实现与判断,都放在了枚举的方法实现中) 是否有问题?希望得到郑老师解答,感谢

    作者回复: 没什么不妥,实际上,我也经常这么做。在这个例子里,我选择了大家更容易理解的方式,适用面更广一些而已。

    2021-02-07
    10
    27
  • 于途
    以卫语句取代嵌套的条件表达式。我在第一家公司转正后,组内code review ,我们组长就推荐了这种做法,一直沿用到现在😁,只是不知道“卫语句”这个正式的概念!

    作者回复: 学习一些行业通用的语言还是需要的。

    2021-01-14
    3
    25
  • Demon.Lee
    郑大,请教两个问题,谢谢。 “对象健身操”中有这样几句话: 1)规则2:拒绝else关键字 “需要小心的是,如果过度使用“提前返回”,代码的清晰度很快会降低。”, “面向对象编程语言给我们提供了一种更为强大的工具——多态。它能够处理更为复杂的条件 判断。对于简单的条件判断,我们可以使用“卫语句”和“提前返回”替换它。” 这里的“卫语句” 和 “提前返回” 是一个意思么,我理解他们是一样的,都是提前check并return。 2)规则4:一行代码只有一个“.”运算符 “迪米特法则(The Law of Demeter,“只和身边的朋友交流”)是一个很好的起点。还可以这 样思考它:你可以玩自己的玩具,可以玩你制造的玩具,还有别人送给你的玩具。但是永远 不要碰你的玩具。” 什么叫 “不要碰你的玩具”?是不是 “不要碰别人的玩具”?

    作者回复: 卫语句是前提条件,结果是提前返回。 至于你的第二个问题,看不懂中文时,就去看英文。 The Law of Demeter (“talk only to your friends”) is a good place to start, but think about it this way: you can play with your toys, with toys that you make, and with toys that someone gives you. You don’t ever, ever play with your toy’s toys. 人家的原文是 You don’t ever, ever play with your toy’s toys. 别动玩具的玩具,和迪米特法则说的是一回事,所以,你不理解的原因是翻译的不好。

    2021-01-16
    4
    10
  • Geek_3b1096
    一直以来认为if-else成对出现

    作者回复: 就是要打破这种认知。

    2021-01-18
    8
  • Hobo
    真的棒,我现在写代码也是尽量只用if避免else,可读性比原来ifelse好太多

    作者回复: 肉眼可见的进步。

    2021-01-14
    8
  • adang
    对于if...else这种情况,印象很深刻,刚刚入行写代码的时候,TeamLeader就讲,不要写很长的if.....else,这样的代码,看了半天到最后发现还有一个else情况要处理,代码读进来太费劲,要把异常情况先处理掉先返回,这样代码看起来比较清爽。后面写代码的时候,也会按照这种思路来处理。在其他团队看到的代码,绝大多数情况下都是if...else if..else 这样平铺着写,常常怀疑自己的写法是不是错的:(。对于重复switch这种情况,真不知道是有这样的优化方案的,好好收藏。

    作者回复: 进一步有一步的欢喜。

    2021-01-14
    8
  • Nutopia.
    这节课最后的switch感觉很多人没理解到位,文章的意思应该是“丑”在重复,并不是“丑”在switch。

    作者回复: 你这个理解很到位

    2022-04-05
    7
  • 阿布黑皮诺
    郑老师, 以UserLevel为例,假设我需要提供一个api, 用户端提供request body, 我(service端)需要把request body serialize成一个UserLevel (body会额外提供一个usertype是enum(regular, gold, silver),之后需要getBookPrice()。 问题是,serialize之后没办法把UserLevel cast成具体的RegularUserLevel/GoldUserlevel, 至少c#不允许把父类cast成子类。我现在的解决方案是写了一个parser, 根据usertype写了一个switch语句,每个子类的构造函数接受一个父类 GoldUserLevel(UserLevel ul),然后把成员完全copy。感觉两个坏味道正在产生, 想请教一下,在这种情况下您会怎么处理呢? 1. 是否不同的 userlevel 需要提供不同的api? 2. 假设需求就是提供一个api解决所有的userlevel,这种情况下的best practice是怎样呢? 谢谢!

    作者回复: 这是一个好问题。 我不建议针对不同的用户级别提供不同的 API,因为这是和具体业务演进强相关的,每添加一个用户级别,API 都要修改,稳定性非常差。 这里一定要有防腐层的概念在心里,API 接口是外部的,它里面传输的内容不一定和业务是一一映射的。所以,把传输中的用户级别通过一个工厂(factory)转换成一个业务对象是非常正常的。在第 11 讲会有讲到一个类似的问题。 这里的重点就是提供 API,就要思考 API 应该怎么设计,然后是,API 和业务对象之间如何映射。

    2021-01-14
    7
  • 业余爱好者
    工作两年,从未用过switch

    作者回复: 好样的!

    2021-01-14
    7
  • qinsi
    smalltalk里没有控制结构;lisp里没有循环

    作者回复: 同样的事,可以有不同的做法。

    2021-01-15
    5
收起评论
显示
设置
留言
34
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部