Loading... > 这一类的坏味道:可变的数据。 # 一、满天飞的 Setter > setter 几乎是排名第一的坏味道。 相比于读数据,修改是一个更危险的操作 比可变的数据更可怕的是,不可控的变化,而暴露 setter 就是这种不可控的变化。 ## 1、常见错误一:使用set来改变成员变量 ```cpp public: void approve(const long bookId) { ... book.setReviewStatus(ReviewStatus.APPROVED); ... } ``` 就是因为这里用了 setter。setter 往往是缺乏封装的一种做法。对于缺乏封装的坏味道。实际情况往往是,生成 getter 的同时,setter 也生成了出来。setter 同 getter 一样,反映的都是对细节的暴露。 解决办法: `移除设值函数(Remove Setting Method)`,**将变化限制在一定的范围之内。** `用一个函数替代了 setter,也就是把它用行为封装了起来`: ```cpp public: void approve(const long bookId) { ... book.approve(); ... } ``` 通过在 Book 类里引入了一个 approve 函数,**我们将审核状态封装了起来**。 ```cpp class Book { public: void approve() { reviewStatus_ = ReviewStatus.APPROVED; } } ``` **虽然审核状态这个字段还是会修改,但你所有的修改都要通过几个函数作为入口**。有任何业务上的调整,都会发生在类的内部,只要**保证接口行为不变**,就不会影响到其它的代码。 ## 2、常见错误2:使用连续的set来初始化对象 ```cpp Book book = new Book(); book.setBookId(bookId); book.setTitle(title); book.setIntroduction(introduction); ``` 我这个 setter 只是用在初始化过程中,而并不需要在使用的过程去调用,就像这样,实际上,对于这种只在初始化中使用的代码,压根没有必要以 setter 的形式存在,真正需要的是一个有参数的构造函数。 解决办法: **消除 setter ,有一种专门的重构手法,叫做移除设值函数(Remove Setting Method)。** ```cpp @Getter @Setter class Book { public: Book(BookId bookId, String title, String introduction) : bookId_(bookId), title_(title), introduction_(introduction) { } private: BookId bookId_; String title_; String introduction_; } BookId getBookId(){return bookId_;} BookId setBookId(BookId bookId){bookId_ = bookId;} string getTitle(){return title_;} ``` 不写 setter 的代码并不代表没有 setter。 为了清晰的表达这个设计意图,其实就没必要再加上setTitle。可以直接看出来,初始化之后就不会再有set的操作。 # 二、可变的数据 > 使用 setter,一个重要的原因就是它暴露了数据。 把 setter 封装成一个个的函数,实际上是把不可控的修改限制在一个有限的范围内。**在这种思路下,可变数据(Mutable Data)就成了一种坏味道。** `解决可变数据,还有一个解决方案是编写不变类` 我们怎么设计不变类呢?要做到以下三点: 所有的字段只在构造函数中初始化; 所有的方法都是纯函数; 如果需要有改变,返回一个新的对象,而不是修改已有字段。 解决办法: ```cpp class Book { public: Book approve() { return new Book(..., ReviewStatus.APPROVED, ...); } } ``` 回过头来看我们之前改动的“用构造函数消除 setter”的代码,其实就是朝着这个方向在迈进。如果按照这个思路改造我们前面提到的 approve 函数。 我们创建出了一个“其它参数和原有 book 对象一模一样,只是审核状态变成了 APPROVED ”的对象。 比如,用来表示时间的类。原来的 Date 类里面还有各种 setter,而新增的 LocalDateTime 则一旦初始化就不会再修改了。如果要操作这个对象,则会产生一个新的对象: ```cpp LocalDateTime twoDaysLater = now.plusDays(2); ``` **想要完全消除可变数据是很难做到的,但我们可以尽可能地编写一些不变类**。 `区分类的性质:对象分成两种,实体和值对象。实体对象要限制数据变化,而值对象就要设计成不变类。` ## 三、可变数据的解决办法 case1: 可以使用`封装变量`来确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进。 case2: 使用`移动语义`和`提炼函数` 尽量把逻辑从处理更新操作的代码中搬移出来,将没有副作用的代码与执行数据更新操作的代码分开。在设计API时,可以使用将`查询函数和修改函数分离`确保调用者不会调用到有副作用的代码。 ```cpp string alterForMiscreant(vector<Person> peoples) { for(auto people : peoples) { if (people == "Don") { setOffAlarms(); return "Don"; } if (people == "John") { setOffAlarms(); return "John"; } return ""; } } ``` 解决办法: 将修改和查询相分离 ```cpp string findForMiscreant(vector<Person> peoples) { for(auto people : peoples) { if (people == "Don") { return "Don"; } if (people == "John") { setOffAlarms(); return "John"; } return ""; } } string alterForMiscreant(vector<Person> peoples) { if(findForMiscreant(peoples) == "") { setOffAlarms(); } } ``` # 三、全局数据(Global Data) > 如果你能够理解可变数据是一种坏味道,全局数据也就很容易理解了,它们处理手法基本上是类似的。 全局数据的问题在于,从代码库的任何地方都可以修改它,而且没有任何机制可以探测出到底哪段代码做出了修改。 如果想搬移一处被广泛使用的额数据,最好的办法往往是先以函数形式封装所有对该数据的访问。 封装能提供一个清晰的观点,可以由此监控数据的变化和使用情况; 对于所有可变的数据,只要它的作用域超过单个函数,**就要将其封装其他,只允许通过函数访问**。 封装手段可以使用getter和setter来封装。 Note: **虽然对数据结构做了封装,但是不能控制对数据内部数据项的修改,有两种方式可以做到对数据不能做修改**: case1: 在取值函数中返回数据的一份副本,调用端可以随便修改它,但不会影响到共享的这份数据。 ```cpp 例子参考 P135 重构这本书 ``` case2: 阻止对数据的修改,比如通过封装记录就能很好的实现这一效果。 解决办法: 把全局函数使用一个函数包装起来,至少能看见修改它的地方,并开始控制它的访问。随后,最好将这个函数搬移到一个类或者模块中,只允许模块内的代码使用它。 **取值和设值时都可以返回副本,这取决于数据从哪儿来,以及是否需要保留对源数据的连接**。 最后修改:2025 年 07 月 01 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏