【代码精进】| 总结/Edison Zhou


作为一个后端工程师,想必在职业生涯中都写过一些不好维护的代码。本文是我学习《代码之丑》的学习笔记,今天最后一天,一起品品滥用控制语句的味道,再看看策略模式的使用。

上一篇:一天一点代码坏味道(3)

1滥用控制语句

这是一个我们经常都在制造,却又毫无感知的坏味道。它可能是我们熟悉的if/else,又可能是for循环等等。

多层嵌套

网上有一张多层嵌套的代码示例图,你能很快看懂我就服你。很多时候,代码的复杂度都是来源于多层嵌套,在软件开发中有一个常用的复杂度的标准,称之为 圈复杂度。即圈复杂度越高,代码也就越复杂,理解和维护的成本也就随之越高。而在圈复杂度的判定规则中,循环语句和选择语句就是一个重要的指标。

图片来源:互联网

下面来一个实际中可能会出现的短小一点的坏味道代码:

public void DistributeEpubs(long bookId)
{List<Epub> epubs = GetEpubsByBookId(bookId);foreach (var epub in epubs){if (epub.IsValid()){bool registered = RegisterIsbn(epub);if (registered){SendEpub(epub);}}}
}

这是一个“平铺直叙写代码”的示范,代码没错,只是不容易理解和维护,特别是for循环遍历中的内容。

因此,不妨将其提取出来,让这个方法只处理一个元素:

public void DistributeEpubs(long bookId)
{List<Epub> epubs = GetEpubsByBookId(bookId);foreach (var epub in epubs){DistributeEpub(epub);}
}private void DistributeEpub(Epub epub)
{if (epub.IsValid()){bool registered = RegisterIsbn(epub);if (registered){SendEpub(epub);}}
}

提取之后,DistributeEpubs方法就只有一层缩进了。而私有方法DistributeEpub还有多层缩进,可以考虑适度继续处理一下。

if / else

在刚刚的DistributeEpub方法中,造成缩进的原因是if语句。那么,我们可以采用一种典型的重构手法:卫语句(guard clause),如下所示:

private void DistributeEpub(Epub epub)
{if (!epub.IsValid()){return;}bool registered = RegisterIsbn(epub);if (!registered){return;}SendEpub(epub);
}

使用卫语句重构后,没有了嵌套,也就没有了多层缩进。

当代码里只有一层缩进的时候,代码的可读性是最高的。

《ThoughtWorks文集》中曾经提出,“不要使用else关键字”,它认为 else 也是一种代码坏味道。那么,如何不写else呢?

先看一段代码,典型的if/else整体:

public double GetEpubPrice(bool highQuality, int chapterSequence)
{double price = 0.0;if (highQuality && chapterSequence > START_CHARGING_SEQUENCE){price = 4.99;}else if (SequenceNumber > START_CHARGING_SEQUENCE&& SequenceNumber <= FURTHER_CHARGING_SEQUENCE){price = 1.99;}else if (SequenceNumber > FURTHER_CHARGING_SEQUENCE){price = 2.99;}else{price = 0.99;}return price;
}

相信你跟我一样,第一眼看上去应该是比较烦躁的,那么如果我们想办法移除else呢?嗯,仿照卫语句的思路,来优化一下:

public double GetEpubPrice(bool highQuality, int chapterSequence)
{if (highQuality && chapterSequence > START_CHARGING_SEQUENCE){return 4.99;}if (SequenceNumber > START_CHARGING_SEQUENCE&& SequenceNumber <= FURTHER_CHARGING_SEQUENCE){return 1.99;}if (SequenceNumber > FURTHER_CHARGING_SEQUENCE){return 2.99;}return 0.99;
}

优化之后,是不是感觉看上去至少没有那么烦躁了。对于这种比较简单的逻辑,可以这样改造。但是,稍微复杂些的逻辑,可能就需要引入多态来改进了。

重复的switch

对,重复的switch也是一种坏味道,而之所以会重复出现,根据郑晔老师的话来说,都是缺少了一个模型。

坏味道代码:

public double GetBookPrice(User user, Book book)
{double price = book.Price;switch (user.Level){case UserLevel.SILVER:price = price * 0.95;break;case UserLevel.GOLD:price = price * 0.85;break;case UserLevel.PLATINUM:price = price * 0.8;break;default:break;}return price;
}public double GetEpubPrice(User user, Epub book)
{double price = book.Price;switch (user.Level){case UserLevel.SILVER:price = price * 0.95;break;case UserLevel.GOLD:price = price * 0.85;break;case UserLevel.PLATINUM:price = price * 0.8;break;default:break;}return price;
}

应对类似于这种重复的switch味道,可以看到,不管是Book还是Epub都是其实都是根据用户的等级来判断的,而其余的各种需要根据用户等级来区分的场景可能都会有相同的代码,那么这时候可能就是缺少了一个模型,一个针对用户等级的模型,我们需要引入多态来取代这个条件表达式。

引入一个UserLevel模型(接口):

public interface IUserLevel
{double GetBookPrice(Book book);double GetEpubPrice(Epub epub);
}public class RegularUserLevel : IUserLevel
{public double GetBookPrice(Book book){return book.Price;}public double GetEpubPrice(Epub epub){return epub.Price;}
}public class GoldUserLevel : IUserLevel
{public double GetBookPrice(Book book){return book.Price * 0.8;}public double GetEpubPrice(Epub epub){return epub.Price * 0.85;}
}public class SilverUserLevel : IUserLevel
{public double GetBookPrice(Book book){return book.Price * 0.9;}public double GetEpubPrice(Epub epub){return epub.Price * 0.85;}
}public class PlatinumUserLevel : IUserLevel
{public double GetBookPrice(Book book){return book.Price * 0.75;}public double GetEpubPrice(Epub epub){return epub.Price * 0.8;}
}

修改入口方法代码:

public double GetBookPrice(User user, Book book)
{IUserLevel level = user.GetLevel();return level.GetBookPrice(book);
}public double GetEpubPrice(User user, Epub book)
{IUserLevel level = user.GetLevel();return level.GetEpubPrice(book);
}

这里的User类可以如下定义,通过依赖注入将具体的UserLevel类传递进去:

public class User
{private readonly IUserLevel _level;public User(IUserLevel level){_level = level;}public IUserLevel GetLevel(){return _level;}
}

综述,循环和选择语句都会增加圈复杂度,可能都是坏味道

2策略模式

在类似于计算价格的业务场景中,我们经常会使用到策略模式,它是一个典型的开放封闭原则的最佳实践,也避免了多重的if/else选择语句,有利于系统的维护。

策略模式定义

策略模式的主要目的主要是将算法的定义和使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类中,每一个策略类封装一个实现算法。而使用算法的环境中针对抽象策略编程,而不是针对实现编程,符合依赖倒置原则。

策略模式包含以下3个角色:

(1)Context(环境类):负责使用算法策略,其中维持了一个抽象策略类的引用实例。

(2)Strategy(抽象策略类):所有策略类的父类,为所支持的策略算法声明了抽象方法。=> 既可以是抽象类也可以是接口

(3)ConcreteStrategy(具体策略类):实现了在抽象策略类中声明的方法。

策略模式案例

X公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下:

(1)学生凭学生证可享受票价8折优惠;

(2)年龄在10周岁以及以下的儿童可以享受每张票减免10元的优惠;

(3)影院VIP用户除享受票价八折优惠外还可以进行积分,积分累计到一定额度可以换取电影院赠送的奖品;

该系统在将来还可能会根据需求引入更多的打折方案(潜在的可扩展性的需求)。

如果不假思索的设计,我们可能就会写一串重复的if/else或者switch代码出来,通过上面的分析,我们知道,这个代码的圈复杂度会很高,不利于维护和理解。

因此,我们在OCP原则(面向修改封闭面相扩展开放)下,参考策略模式来实现。

具体的类图设计如下:

具体的代码实现如下:

(1)Context 环境类:MovieTicket

public class MovieTicket
{private double _price;private IDiscount _discount;public double Price{get{return _discount.Calculate(_price);}set{_price = value;}}public IDiscount Discount{set{_discount = value;}}
}

(2)Strategy 抽象策略类:IDiscount

public interface IDiscount
{double Calculate(double price);
}

(3)ConcreteStrategy 具体策略类:StudentDiscount, VIPDiscount 和 ChildrenDiscount

public class StudentDiscount : IDiscount
{public double Calculate(double price){Console.WriteLine("学生票:");return price * 0.8;}
}public class VIPDiscount : IDiscount
{public double Calculate(double price){Console.WriteLine("VIP票:");Console.WriteLine("增加积分!");return price * 0.5;}
}public class ChildrenDiscount : IDiscount
{public double Calculate(double price){Console.WriteLine("儿童票:");return price - 10;}
}

最后在客户端调用时,通过给MovieTicket传递不同的打折策略即可实现正确计算票价。而以后有其他打折方案的需求时,只需要扩展一个新的打折策略即可,不用修改原有代码。

策略模式的本质就是OCP原则的一个具体应用,将变化的算法与不变的环境区分开来,可以在类似电商业务商品复杂的计算价格等场景中使用。

3小结

本文总结了滥用控制语句如循环和选择语句造成的高复杂度代码的应对方法,还介绍了策略模式的定义、类图以及案例,希望能对你的代码精进之路有用。

最后,感谢郑晔老师的这门《代码之丑》课程,让我受益匪浅!我也诚心把它推荐给关注EdisonTalk公众号的各位童鞋!

参考资料

郑晔,《代码之丑》(推荐订阅学习)

Martin Flower著,熊杰译,《重构:改善既有代码的设计》(推荐至少学习第三章)


.NET代码质量 | 一天一点代码坏味道(4)相关推荐

  1. .NET代码质量 | 一天一点代码坏味道(3)

    [代码精进]| 总结/Edison Zhou 作为一个后端工程师,想必在职业生涯中都写过一些不好维护的代码.本文是我学习<代码之丑>的学习笔记,今天第三天,品品大类和长参数列表的味道. 上 ...

  2. .NET代码质量 | 一天一点代码坏味道(2)

    [代码精进]| 总结/Edison Zhou 作为一个后端工程师,想必在职业生涯中都写过一些不好维护的代码.本文是我学习<代码之丑>的学习笔记,今天第二天,品品重复代码和长函数方法的味道. ...

  3. java代码质量 圈复杂度,代码圈复杂度(Cyclomatic Complexity,CC)和Oclint工具 介绍...

    什么是代码圈复杂度 圈复杂度是一种度量程序复杂度的方法,由 Thomas McCabe 于 1976年定义,用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径条数,即合理的预防错误所需测试的最少 ...

  4. 快速改善代码质量的20条代码规范

    目录 1.关于命名 2.关于注释 3.关于代码风格 4.关于编码技巧 5.统一编码规范 1.关于命名 命名的关键是能准确达意.对于不同作用域的命名,我们可以适当地选择不同的长度. 我们可以借助类的信息 ...

  5. 消除代码中的坏味道,编写高质量代码

    消除代码中的坏味道,编写高质量代码 Intro 想要写出较好的代码,保证代码的高质量需要时刻警惕代码中的坏味道,今天分享一下,我觉得平时写的代码中可能会出现的坏味道代码的一些示例 常见的坏味道代码 B ...

  6. 从哪些维度评判代码质量的好坏?如何具备写出高质量代码的能力?

    文章目录 如何评价代码质量的高低? 最常用的评价标准有哪几个? 1. 可维护性(maintainability) 2. 可读性(readability) 3. 可扩展性(extensibility) ...

  7. 重构笔记——代码的坏味道(上)

    本文是在学习中的总结,欢迎转载但请注明出处:http://blog.csdn.net/pistolove/article/details/42041757 在重构入门篇中,简单地介绍了重构的定义.为何 ...

  8. 代码质量与规范,那些年你欠下的技术债

    提到"质量"二字时,我们的第一反应往往是"有多少BUG?""性能好不好?"这样的问题.我们对软件产品或服务的质量定义看其能不能满足用户的需求 ...

  9. 代码质量在「内卷时代」的重要性

    这里是Z哥的个人公众号 每周五11:45 按时送达 当然了,也会时不时加个餐- 我的第「173」篇原创敬上 大家好,我是Z哥. 提到代码质量,不知道你的脑海中浮现出的第一个词是什么?规范?可读性?优雅 ...

最新文章

  1. 图解数据库辅助软件教程
  2. 内存溢出,频繁full gc 处理思路
  3. ES6中的迭代器(Iterator)和生成器(Generator)(一)
  4. (138)FPGA面试题-Verilog HDL中function和task的区别(二)
  5. maven的标准pom.xml详解
  6. U盘量产及在虚拟机中测试
  7. Java中equals()和“==”区别
  8. 安卓手机上跑_直接在电脑上浏览操作安卓手机 #效率App #scrcpy
  9. L1-020 帅到没朋友 (20 分) — 团体程序设计天梯赛
  10. 计算机专业怎么转行做建筑,【土木转行·续集】土建类同学转行计算机,什么时间转最合适?...
  11. Ansible 书写我的playbook
  12. flash计算机硬件,实测Flash在硬件加速下的对比
  13. android输入法剪贴板,QQ输入法手安卓V5.4剪贴板 任性粘贴
  14. 店宝宝:电视剧“拼刀刀”梗引热议 拼多多紧急注册商标为哪版?
  15. 为win7系统盘减肥
  16. 湖南师范大学2018年大学生程序设计竞赛新生赛 F-小名的回答
  17. 【滤波器】2. 有源滤波器概述
  18. Java生成和解析二维码
  19. java FX 制作3D魔方
  20. C语言 结构体 来淄博旅游

热门文章

  1. 消灭误区:男人护肤保养的五宗罪
  2. python 实现tif图片转jpg图片 windows下
  3. 腾讯云大数据:神盾首创非对称联邦学习,深度保障数据隐私
  4. 分数计算器(C++课设)
  5. day21-java
  6. 计算机行业深度报告:精读新股系列(七),国内主要AI公司对比研究20210103.PDF
  7. golang解决go get下载失败解决办法
  8. Y470_机械硬盘(日立)4KB对齐_提升硬盘性能
  9. 看“降龙”绝技——浅析知识图谱技术生态链
  10. win10下 VS2019 C#创建web项目并发布部署在iis