Loading... > 一个重要的观念就是封装,将零散的代码封装成一个又一个可复用的模块。 造成的结果就是:写代码的人认为自己提供了封装,但实际上,我们还是看到许多的代码散落在那里。 这种坏味道违反了迪米特法则 # 一、火车残骸 > 火车残骸的代码就是连续的函数调用,它反映的问题就是把实现细节暴露了出去,缺乏应有的封装。**重构的手法是隐藏委托关系,实际就是做封装**。 软件行业有一个编程指导原则,叫`迪米特法则`,可以作为日常工作的指导,规避这种坏味道的出现。 ```cpp String name = book.getAuthor().getName(); ``` 这段代码表达的是“获得一部作品作者的名字”。作品里有作者信息,想要获得作者的名字,通过“作者”找到“作者姓名”,这就是很多人凭借直觉写出的代码,不过它是有问题的。 是不是必须得先了解 Book 和 Author 这两个类的实现细节?也就是说,我们必须得知道,作者的姓名是存储在作品的作者字段里的。 这段代码只是用来说明这种类型坏味道是什么样的,在实际工作中,这种在一行代码中有连续多个方法调用的情况屡见不鲜,数量上总会不断突破你的认知。 这种坏味道起的名字叫过长的消息链(Message Chains),而有人则给它起了一个更为夸张的名字:[`火车残骸(Train Wreck)`](https://wiki.c2.com/),形容这样的代码像火车残骸一般,断得一节一节的。 解决办法: 解决这种代码的重构手法叫**隐藏委托关系**(Hide Delegate),说得更直白一些就是,把这种调用封装起来: ```cpp class Book { ... public: String getAuthorName() { return author_.getName(); } ... } String name = book.getAuthorName(); ``` 火车残骸这种坏味道的产生是缺乏对于封装的理解,因为封装这件事并不是很多程序员编码习惯的一部分。 现在写出一个 getter 往往是 IDE 中一个快捷键的操作,甚至不需要自己手工敲代码。 \*\*要想摆脱初级程序员的水平,就要先从少暴露细节开始。\*\*声明完一个类的字段之后,**请停下生成 getter 的手**,转而让大脑开始工作,思考这个类应该提供的行为。 Note: **链式调用不一定都是火车残骸。比如builder模式,每次调用返回的都是自身,不牵涉到其他对象,不违反迪米特法则。** 在软件行业中,有一个编程的指导原则几乎就是针对这个坏味道的,叫做`迪米特法则(Law of Demeter)`,这个原则是这样说的: 每个单元对其它单元只拥有有限的知识,而且这些单元是与当前单元有紧密联系的; 每个单元只能与其朋友交谈,不与陌生人交谈; 只与自己最直接的朋友交谈。 # 二、基本类型偏执 > 基本类型偏执就是用各种基本类型作为模型到处传递,这种情况下通常是缺少了一个模型。解决它,常用的重构手法是以对象取代基本类型,也就是提供一个模型代替原来的基本类型。基本类型偏执不局限于程序设计语言提供的基本类型。 应该构建模型,封装散落的代码。 ```cpp public: double getEpubPrice(const boolean highQuality, const int chapterSequence) { ... } ``` 问题就出在返回值的类型上,也就是价格的类型上。 虽然价格本身是用浮点数在存储,但价格和浮点数本身并不是同一个概念,有着不同的行为需求。比如,一般情况下,我们要求商品价格是大于 0 的,但 double 类型本身是没有这种限制的。 解决办法: **这就是很多人使用基本类型(Primitive)作为变量类型思考的角度。但实际上,这种采用基本类型的设计缺少了一个模型。** 如果补齐这里缺失的模型,我们可以引入一个 Price 类型,这样的校验就可以放在初始化时进行: ```cpp class Price { public: Price(const double price) { if (price <= 0) { throw new IllegalArgumentException("Price should be positive"); } price_ = price; } private: long price_; } ``` 这种引入一个模型封装基本类型的重构手法,叫做以对象取代基本类型(Replace Primitive with Object)。一旦有了这个模型,我们还可以再进一步,比如,**如果我们想要让价格在对外呈现时只有两位,在没有 Price 类的时候,这样的逻辑就会散落代码的各处**,事实上,代码里很多重复的逻辑就是这样产生的。 ```cpp public: double getDisplayPrice() { BigDecimal decimal = new BigDecimal(price_); return decimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); } ``` 大部分程序员都学过这样一个设计原则:`组合优于继承`, ```cpp class Books : public List<Book> { ... } ``` Note: STL中的容器类是可以继承的,但由于STL中的容器类都是没有virtual析构的,所以其衍生类与基类并不符合IS-A关系,这种继承其实是为了重用代码,而从重用代码的角度来看,公有继承不如私有继承,继承不如组合。 而应该写成组合的样子,也就是: ```cpp class Books { private: List<Book> books; ... } ``` **Books 可能不需要提供 List 的所有方法。** **封装之所以有难度,主要在于它是一个构建模型的过程**,而很多程序员写程序,只是用着极其粗粒度的理解写着完成功能的代码,根本没有构建模型的意识;还有一些人以为划分了模块就叫封装,所以,我们才会看到这些坏味道的滋生。 最后修改:2025 年 07 月 01 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏