设计模式之美
王争
前Google工程师,《数据结构与算法之美》专栏作者
立即订阅
20015 人已学习
课程目录
已更新 46 讲 / 共 100 讲
0/6登录后,你可以任选6讲全文学习。
开篇词 (1讲)
开篇词 | 一对一的设计与编码集训,让你告别没有成长的烂代码!
免费
设计模式学习导读 (3讲)
01 | 为什么说每个程序员都要尽早地学习并掌握设计模式相关知识?
02 | 从哪些维度评判代码质量的好坏?如何具备写出高质量代码的能力?
03 | 面向对象、设计原则、设计模式、编程规范、重构,这五者有何关系?
设计原则与思想:面向对象 (11讲)
04 | 理论一:当谈论面向对象的时候,我们到底在谈论什么?
05 | 理论二:封装、抽象、继承、多态分别可以解决哪些编程问题?
06 | 理论三:面向对象相比面向过程有哪些优势?面向过程真的过时了吗?
07 | 理论四:哪些代码设计看似是面向对象,实际是面向过程的?
08 | 理论五:接口vs抽象类的区别?如何用普通的类模拟抽象类和接口?
09 | 理论六:为什么基于接口而非实现编程?有必要为每个类都定义接口吗?
10 | 理论七:为何说要多用组合少用继承?如何决定该用组合还是继承?
11 | 实战一(上):业务开发常用的基于贫血模型的MVC架构违背OOP吗?
12 | 实战一(下):如何利用基于充血模型的DDD开发一个虚拟钱包系统?
13 | 实战二(上):如何对接口鉴权这样一个功能开发做面向对象分析?
14 | 实战二(下):如何利用面向对象设计和编程开发接口鉴权功能?
设计原则与思想:设计原则 (12讲)
15 | 理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?
16 | 理论二:如何做到“对扩展开放、修改关闭”?扩展和修改各指什么?
17 | 理论三:里式替换(LSP)跟多态有何区别?哪些代码违背了LSP?
18 | 理论四:接口隔离原则有哪三种应用?原则中的“接口”该如何理解?
19 | 理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?
20 | 理论六:我为何说KISS、YAGNI原则看似简单,却经常被用错?
21 | 理论七:重复的代码就一定违背DRY吗?如何提高代码的复用性?
22 | 理论八:如何用迪米特法则(LOD)实现“高内聚、松耦合”?
23 | 实战一(上):针对业务系统的开发,如何做需求分析和设计?
24 | 实战一(下):如何实现一个遵从设计原则的积分兑换系统?
25 | 实战二(上):针对非业务的通用框架开发,如何做需求分析和设计?
26 | 实战二(下):如何实现一个支持各种统计规则的性能计数器?
设计原则与思想:规范与重构 (11讲)
27 | 理论一:什么情况下要重构?到底重构什么?又该如何重构?
28 | 理论二:为了保证重构不出错,有哪些非常能落地的技术手段?
29 | 理论三:什么是代码的可测试性?如何写出可测试性好的代码?
30 | 理论四:如何通过封装、抽象、模块化、中间层等解耦代码?
31 | 理论五:让你最快速地改善代码质量的20条编程规范(上)
32 | 理论五:让你最快速地改善代码质量的20条编程规范(中)
33 | 理论五:让你最快速地改善代码质量的20条编程规范(下)
34 | 实战一(上):通过一段ID生成器代码,学习如何发现代码质量问题
35 | 实战一(下):手把手带你将ID生成器代码从“能用”重构为“好用”
36 | 实战二(上):程序出错该返回啥?NULL、异常、错误码、空对象?
37 | 实战二(下):重构ID生成器项目中各函数的异常处理代码
设计原则与思想:总结课 (3讲)
38 | 总结回顾面向对象、设计原则、编程规范、重构技巧等知识点
39 | 运用学过的设计原则和思想完善之前讲的性能计数器项目(上)
40 | 运用学过的设计原则和思想完善之前讲的性能计数器项目(下)
设计模式与范式:创建型 (2讲)
41 | 单例模式(上):为什么说支持懒加载的双重检测不比饿汉式更优?
42 | 单例模式(中):我为什么不推荐使用单例模式?又有何替代方案?
不定期加餐 (3讲)
加餐一 | 用一篇文章带你了解专栏中用到的所有Java语法
加餐二 | 设计模式、重构、编程规范等相关书籍推荐
春节特别加餐 | 王争:如何学习《设计模式之美》专栏?
免费
设计模式之美
登录|注册

35 | 实战一(下):手把手带你将ID生成器代码从“能用”重构为“好用”

王争 2020-01-22
上一节课中,我们结合 ID 生成器代码讲解了如何发现代码质量问题。虽然 ID 生成器的需求非常简单,代码行数也不多,但看似非常简单的代码,实际上还是有很多优化的空间。综合评价一下的话,小王的代码也只能算是“能用”、勉强及格。我们大部分人写出来的代码都能达到这个程度。如果想要在团队中脱颖而出,我们就不能只满足于这个 60 分及格,大家都能做的事情,我们要做得更好才行。
上一节课我们讲了,为什么这份代码只能得 60 分,这一节课我们再讲一下,如何将 60 分的代码重构为 80 分、90 分,让它从“能用”变得“好用”。话不多说,让我们正式开始今天的学习吧!

回顾代码和制定重构计划

为了方便你查看和对比,我把上一节课中的代码拷贝到这里。
public class IdGenerator {
private static final Logger logger = LoggerFactory.getLogger(IdGenerator.class);
public static String generate() {
String id = "";
try {
String hostName = InetAddress.getLocalHost().getHostName();
String[] tokens = hostName.split("\\.");
if (tokens.length > 0) {
hostName = tokens[tokens.length - 1];
}
char[] randomChars = new char[8];
int count = 0;
Random random = new Random();
while (count < 8) {
int randomAscii = random.nextInt(122);
if (randomAscii >= 48 && randomAscii <= 57) {
randomChars[count] = (char)('0' + (randomAscii - 48));
count++;
} else if (randomAscii >= 65 && randomAscii <= 90) {
randomChars[count] = (char)('A' + (randomAscii - 65));
count++;
} else if (randomAscii >= 97 && randomAscii <= 122) {
randomChars[count] = (char)('a' + (randomAscii - 97));
count++;
}
}
id = String.format("%s-%d-%s", hostName,
System.currentTimeMillis(), new String(randomChars));
} catch (UnknownHostException e) {
logger.warn("Failed to get the host name.", e);
}
return id;
}
}
前面讲到系统设计和实现的时候,我们多次讲到要循序渐进、小步快跑。重构代码的过程也应该遵循这样的思路。每次改动一点点,改好之后,再进行下一轮的优化,保证每次对代码的改动不会过大,能在很短的时间内完成。所以,我们将上一节课中发现的代码质量问题,分成四次重构来完成,具体如下所示。
第一轮重构:提高代码的可读性
第二轮重构:提高代码的可测试性
第三轮重构:编写完善的单元测试
第四轮重构:所有重构完成之后添加注释
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《设计模式之美》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(39)

  • chanllenge

    public class RandomIdGenerator implements LogTraceIdGenerator,应该是这么写吧?

    作者回复: 代码有点问题,我更新一下,抱歉

    2020-01-22
    1
    13
  • Yang
    1.应该需要继续抛出,因为在实际的业务开发中,会有对应的异常处理器,抛出可以让调用者明白哪出错了,而不是只是简单的打印日志。
    2.命名getLastSubstrSplittedByDot替换成getLastSubstrByDelimiter,具体要看需求会不会经常变化,如果经常变化,替换没有任何问题,因为有可能后面根据别的符号来分割,这种情况下我个人认为getLastFiledOfHostName()函数命名应该替换成getLastFiled(),命名不应该暴露太多细节,要是以后不是根据HostName获取最后一个字段呢,之前的所有用到该命名的地方都需要替换,不然可读性不是很好。
    如果需求不经常变化,那文中的命名就足够了。
    2020-01-22
    1
    13
  • 小晏子
    在获取主机名失败的时候,generate函数应该能正常返回,因为是随机id,所以只要有个满足要求的id就行了,用户并不关心能不能拿到主机名字,所以在获取主机名失败的时候,可以返回一个默认的主机名,之后在拼接上时间戳和随机数也是满足需求的id,所以我认为generate函数在主机名失败的时候应该使用默认主机名正常返回。另外对于小王的异常处理我认为是可以捕获处理的,只是不能该让整个函数都返回一个空id,而是应该捕获异常时使用一个默认主机名继续后面的逻辑。
    第二个问题:为了隐藏代码实现细节,我们把 getLastSubstrSplittedByDot(String hostName) 函数命名替换成 getLastSubstrByDelimiter(String hostName),这样是否更加合理?为什么?
    我认为是合理的,命名和代码的逻辑不绑定,避免了以后修改代码逻辑还要修改函数名的麻烦,比如将来可能不用点去分割hostname了,用空格分割,这时byDot函数名就不合适了,如果修改,那么所有使用到这个函数的地方都要改,大大增加了出错的概率。
    2020-01-22
    3
    7
  • 辣么大
    这两期争哥讲重构,我把Uncle Bob的《重构2》的第一章看了,大呼过瘾。自己也要操刀试一下!

    他和Kent Beck强调重构时要用baby step(小步骤),什么是baby step呢?就是一次改一小点,例如改一个变量名字都需要进行 modify-build-test的步骤。

    对于争哥的例子,我参考Uncle Bob书中的方法:
    第一步、先写好测试
    第二步、开始逐步重构(baby step)
    第三步、修改-> 测试

    经过重构之后代码总计50行。重构之后代码易读,且结构清晰。
    https://github.com/gdhucoder/Algorithms4/blob/master/designpattern/u35/RandomLogTraceIDGenerator.java
    2020-01-22
    6
  • Wings
    争哥,我是看了你的算法之美后立刻看到你出设计模式之美就立刻买。可是专栏更新到现在快一半,老实说,我觉得内容真的很基础甚至脱离实际开发,很多都是浅尝辄止。专栏一开始渲染了好多说会有很多可落地的代码,可目前为止看到的都是很虚无聊会或者是大家早就知道的东西。如果可以的话,能否在后续课程多分享一些真正的企业级的代码设计和重构呢?

    作者回复: 抱歉没有呢,让你失望了,不过,我还会出新课的,以后你就别买我的课程了。因为新课估计也会让你失望的~

    说实话,我觉得的我写的很好,而且很结合实际开发,很多人留言说我写的好,当然也有人根本不识货!如果你觉得哪一个不能落地,能具体指出来吗?或者你觉得哪篇写的不好,网上或者哪本书籍讲的比我讲的好,你指出来。不然你随口一说,无凭无据,那不就瞎喷吗?说实话啥都不懂瞎喷的人太多了,我也不可能一个一个的喷回去,没意思,如果你觉得有写的不好地方,你大可就事论事列举出来,我倒是会很认真的思考改进,不然,我就只能当你是喷子了啊 哥们😂。而且,你能说下什么是真正企业级的吗?我工作10多年,搞不清楚什么才是真正企业级的呢。。。

    你可以看下我写的这篇文章:公众号”小争哥“ 看看下面的留言:
    https://mp.weixin.qq.com/s/Od95pFonyLo7IlB3THa8Tw

    2020-01-23
    4
    5
  • 辣么大
    对于在ID generator中方法里写到
    void foo(){
        Random random = new Random();
    }
    有个疑问:

    1、为什么不声明成静态变量?
    2、能用成员变量么?而不是写成局部变量

    作者回复: 也可以,不过尽量的缩小变量的作用域,代码可读性也好,毕竟random只会用在某个函数中,而不是用在多个函数中,放到局部函数中,也符合封装的特性,不暴露太多细节。

    2020-01-22
    3
  • Ken张云忠
    读小争哥的注释就是种欣赏,小争哥的英文表达是怎么一步步积累的?
    我认为动词和介词是英文的精髓,还有英文的语法

    作者回复: 我英语也不好,多花点心思优化一下,实在不行,写中文注释也是可以的

    2020-01-22
    2
  • evolution
    代码的演变过程,真的是干货满满。不知道争哥有没有架构方面的演变课程?

    作者回复: 感谢认可,暂时没有呢

    2020-01-22
    1
    2
  • 牛顿的烈焰激光剑
    老师,对于获取 hostname(getLastfieldOfHostName()),我的想法是用 static 代码块,只在类加载的时候执行一次。请问这样处理的话会不会有什么坏处?

    作者回复: 有可能hostname会改变,你的代码就获取不到最新的hostname

    2020-01-25
    1
    1
  • 全时N多只
    34行代码是不是写错了?
    Assert.assertTrue(('0' < c && c > '9') || ('a' < c && c > 'z') || ('A' < c && c < 'Z'));

    作者回复: 好像没有吧

    2020-01-22
    3
    1
  • DullBird
    1. hostname不能影响业务逻辑,设置默认值,并且还有随机值可以区分。
    2. 通用的比较合适,但是看有没有复用这个代码的必要,否则暂时不需要改动
    2020-02-09
  • 胡子高兴了
    Assert.assertTrue(('0' < c && c > '9') || ('a' < c && c > 'z') || ('A' < c && c < 'Z'));
    应该改成Assert.assertTrue(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'));
    2020-02-08
  • 李小四
    设计模式_35
    # 作业
      1. 首先逻辑要自洽:
        (1) 如果在函数内吞掉异常,那么函数内要处理因为异常而导致的逻辑问题,而不是只返回一个空字符串。
        (2) 如果不准备处理异常带来的逻辑问题,那么需要把异常抛到上一层。
        具体的平衡的尺度,估下一下节会讲吧。
      2. 我认为合理,因为前一种方法预设了一些隐藏联系(host的delimiter一定是dot),当然这个改变的可能性不大,但是不预设多余得了联系是更好的办法。

    # 感想
    看到 ```RandomIdGenerator implements IdGenerator```, 我还以为发现了我不知道的新大陆,找IDE试了一下,才发现写错了。
    2020-02-07
  • 沈康
    1、不抛出异常吧,理由是抛出的异常需要调用出处理,这种id生成器明显需要自己处理干净否则异常处理将会暴露在各业务代码,明显增大了业务复杂度。
    2、dot就是".",改成delimiter有什么区别,已经封装了,改不改区别不大吧。。不懂
    2020-02-05
  • 斐波那契
    对于课堂讨论第一个问题:个人比较认同的是应该不往外抛,但是相应的逻辑要处理好。为什么这么说,首先,如果说hostname获取异常往外抛的话让使用者知道虽然这么做能很快定位到问题,但是从使用者角度说使用者并不会关心这个问题(这个底下也有人讲到) 其次 从逻辑上,往上抛异常跟打日志效果是一样的 既然打了日志那我们就应该可以知道有hostname获取异常的情况(不然日志一文不值) 只是异常抛出更简单粗暴一点。其实 这个问题主要是小争哥给我们限定了范围,如果小争哥用的不是id生成器这个案例 可能抛出异常更好一点。在id 生成器中 获取hostname只是获取随机id的一种手段,并不是唯一。如果某一天把获取hostname换成别的实现逻辑(例如用一个常量来代替) 那原本写好的往外抛出异常就变得毫无意义。
    2020-02-03
  • 传说中的成大大
    关于第一问 我觉得只要注视写清楚了 返回啥都都行
    关于第二问 我觉得在你上面已经讲过了 函数名太过具体和细节如果修改了函数内容就可能需要改函数名
    最后总结一点 高手之间的竞争在于细节 这句话真的很不错
    2020-02-03
    1
  • 弹簧人
    LogTraceIdGenerator logTraceIdGenerator = new RandomIdGenerator(); 没懂为什么有了IdGenerator还要新建一个LogTraceIdGenerator 。 直接LogTraceIdGenerator logTraceIdGenerator = new RandomIdGenerator()不行吗? 有人能说说为啥吗
    2020-02-01
  • Eclipse
    //代码使用举例LogTraceIdGenerator logTraceIdGenerator = new RandomIdGenerator();

    争哥,两个子类之间可以进行类型转换吗?
    2020-01-31
  • 早起不吃虫
    干货满满
    2020-01-29
  • 雪中亮
    争哥好,我看到这么一句:将 generateRandomAlphameric() 和 getLastSubstrSplittedByDot() 这两个函数的访问权限设置为 protected。这样做的目的是,可以直接在单元测试中通过对象来调用两个函数进行测试。

    我觉得将方法标记为默认访问级别就可以了,这样可以被同一包中的所有类访问。不需要标记为protected,即不需要提供子类的访问权限。

    如果我理解的不对,还请指教。
    2020-01-29
收起评论
39
返回
顶部