Loading... > 函数之间还是要传递信息的,既然不能用全局变量,参数就成了最好的选择,于是乎,只要你想到有什么信息要传给一个函数,就自然而然地把它加到参数列表中,参数列表也就越来越长了。 形参中有bool类型的标志位违背了单一职责原则和接口隔离原则 # 一、聚沙成塔 ```cpp public: void createBook(const String title, const String introduction, const URL coverUrl, const BookType type, const BookChannel channel, const String protagonists, const String tags, const boolean completed) { ... Book book = Book.builder .title(title) .introduction(introduction) .coverUrl(coverUrl) .type(type) .channel(channel) .protagonists(protagonists) .tags(tags) .completed(completed) .build(); repository_.save(book); } ``` 这是一个创建作品的函数,我们可以看到,这个函数的参数列表里,包含了一部作品所要拥有的各种信息,比如:作品标题、作品简介、封面 URL、作品类型、作品归属的频道、主角姓名、作品标签、作品是否已经完结等等。 但是如果继续拓展呢? 我们很自然地就会想到给这个函数增加一个参数。但正如我在讲“长函数”那节课里说到的,很多问题都是这样,每次只增加一点点,累积起来,便不忍直视了。 怎么解决这个问题呢? 这里所有的参数其实都是和作品相关的,也就是说,所有的参数都是创建作品所必需的。所以,我们可以做的就是将这些参数封装成一个类,一个创建作品的参数类: ```cpp class NewBookParamters { private: String title; String introduction; URL coverUrl; BookType type; BookChannel channel; String protagonists; String tags; boolean completed; ... } ``` 这里你看到了一个典型的消除长参数列表的重构手法:**将参数列表封装成对象**。 这个函数参数列表就只剩下一个参数了,一个长参数列表就消除了。 ```cpp public: void createBook(const NewBookParamters parameters) { ... } ``` 还有个疑问,只是把一个参数列表封装成一个类,然后,用到这些参数的时候,还需要把它们一个个取出来,这会不会是多此一举呢?就像这样: ```cpp public: void createBook(constNewBookParamters parameters) { ... Book book = Book.builder .title(parameters.getTitle()) .introduction(parameters.getIntroduction()) .coverUrl(parameters.getCoverUrl()) .type(parameters.getType()) .channel(parameters.getChannel()) .protagonists(parameters.getProtagonists()) .tags(parameters.getTags()) .completed(parameters.isCompleted()) .build(); repository_.save(book); } ``` 解决办法: **一个模型的封装应该是以行为为基础的**。 之前没有这个模型,所以,我们想不到它应该有什么行为,现在模型产生了,它就应该有自己配套的行为,那这个模型的行为是什么呢?从上面的代码我们不难看出,它的行为应该是构建一个作品对象出来 ```cpp class NewBookParamters { private: String title; String introduction; URL coverUrl; BookType type; BookChannel channel; String protagonists; String tags; boolean completed; public: Book newBook() { return Book.builder .title(title) .introduction(introduction) .coverUrl(coverUrl) .type(type) .channel(channel) .protagonists(protagonists) .tags(tags) .completed(completed) .build(); } } ``` 创建作品的函数就得到了极大的简化: ```cpp public: void createBook(const NewBookParamters parameters) { ... Book book = parameters.newBook(); repository_.save(book); } ``` ## 1、将数据组织成新的数据结构 > 将函数的参数封装成对象,一旦识别出新的数据结构。就可以重组程序的行为来使用这些结构,例如添加这个类的成员函数。 demo1: 下面的函数负责找到超出指定范围的温度度数 ```cpp int findOutRangeTemperature(vector<int>temperatures, int min, int max) { for(auto temperature : temperatures) { if(temperature < min || temperature > max) return temperature; } } 调用方: findOutRangeTemperature(temperatures, operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling) ``` operatingPlan对象用了另外的名字temperatureFloor和temperatureCeiling来表示温度的上下限,与findOutRangeTemperature所使用的名字不同。 像这种两项不相干的数据表示一个范围,最好是将其组合成一个对象。 ```cpp class NumberRange { public: NumberRange(int min, int max) : min_(min), max_(max) { } int min_; int max_; } int findOutRangeTemperature(vector<int>temperatures, NumberRange numberRange) { for(auto temperature : temperatures) { if(temperature < numberRange.min || temperature > numberRange.max) return temperature; } } 调用方: findOutRangeTemperature(temperatures, numberRange) ``` demo2: ```cpp public: void postBlog(String title, String summary, String keywords, String content, String category, long authorId); // 将参数封装成对象 class Blog { public: String title; String summary; String keywords; Strint content; String category; long authorId; } public: void postBlog(Blog blog); ``` 如果函数是对外暴露的远程接口,将参数封装成对象,`还可以提高接口的兼容性`。在往接口中添加新的参数的时候,老的远程接口调用者有可能就不需要修改代码来兼容新的接口了。 # 二、动静分离 > 把长参数列表封装成一个类,这能解决大部分的长参数列表,但并不等于所有的长参数列表都应该用这种方式解决,因为不是所有情况下,参数都属于一个类。 这种情况可以把静态不变的数据变为**类的成员变量** ```cpp public: void getChapters(const int bookId, const HttpClient httpClient, const ChapterProcessor processor) { HttpUriRequest request = createChapterRequest(bookId); HttpResponse response = httpClient.execute(request); List<Chapter> chapters = toChapters(response); processor.process(chapters); } ``` 这个函数的作用是根据作品 ID 获取其对应的章节信息。如果,单纯以参数个数论,这个函数的参数数量并不算多。 不过,**绝对的数量并不是关键点,参数列表也应该是越少越好。** 解决办法: 每次传进来的 bookId 都是不一样的,是随着请求的不同而改变的。但 httpClient 和 processor 两个参数都是一样的,因为它们都有相同的逻辑,没有什么变化。 换言之,`bookId 的变化频率同 httpClient 和 processor 这两个参数的变化频率是不同的。一边是每次都变,另一边是不变的` 这里表现出来的就是典型的`动数据(bookId)和 静数据(httpClient 和 processor)`,它们是不同的关注点,应该分离开来。 解决办法: **静态不变的数据完全可以成为这个函数所在类的一个字段[成为类的成员变量]**,而只将每次变动的东西作为参数传递就可以了。 ```cpp public: void getChapters(const int bookId) { HttpUriRequest request = createChapterRequest(bookId); HttpResponse response = httpClient_.execute(request); List<Chapter> chapters = toChapters(response); processor_.process(chapters); } ``` 这个坏味道其实是一个软件设计问题,代码缺乏应有的结构,所以,原本应该属于静态结构的部分却以动态参数的方式传来传去,无形之中拉长了参数列表。 长参数列表固然可以用一个类进行封装,但能够封装出这个类的前提条件是:**这些参数属于一个类,有相同的变化原因。** 应对长参数列表主要的方式就是减少参数的数量,一种最直接的方式就是将参数列表封装成一个类。但并不是说所有的情况都能封装成类来解决,我们还要分析是否所有的参数都有相同的变动频率。 变化频率相同,**则封装成一个类**。 变化频率不同的话: **静态不变的,可以成为软件结构的一部分;** **多个变化频率的,可以封装成几个类。** # 三、告别标记 > 不要在函数中使用布尔类型的标识参数来控制内部逻辑,true 的时候走这块逻辑,false 的时候走另一块逻辑。`这明显违背了单一职责原则和接口隔离原则`。 ```cpp public: void editChapter(const int chapterId, const String title, const String content, const boolean apporved) { ... } ``` 使用标记参数,是程序员初学编程时常用的一种手法,不过,正是因为这种手法实在是太好用了,造成的结果就是代码里面彩旗(flag)飘飘,各种标记满天飞。不仅变量里有标记,参数里也有。 **解决标记参数,一种简单的方式就是,将标记参数代表的不同路径拆分出来。** ```cpp // 普通的编辑,需要审核 public: void editChapter(const long chapterId, const String title, cosnt String content) { ... } // 直接审核通过的编辑 public: void editChapterWithApproval(const long chapterId, const String title, const String content) { ... } ``` 有的是布尔值的形式,有的是以枚举值的形式,还有的就是直接的字符串或者整数。无论哪种形式,我们都可以通过拆分函数的方式将它们拆开。在重构中,这种手法叫做**移除标记参数**(Remove Flag Argument)。 **Note**: 以下这种情况可以考虑不要移除标志位, 不过,**如果函数是 private 私有函数**,影响范围有限,或者拆分之后的两个函数经常同时被调用,我们可以酌情考虑保留标识参数。 ```cpp // 拆分成两个函数的调用方式 boolean isVip = false; //...省略其他逻辑... if (isVip) { buyCourseForVip(userId, courseId); } else { buyCourse(userId, courseId); } // 保留标识参数的调用方式更加简洁 boolean isVip = false; //...省略其他逻辑... buyCourse(userId, courseId, isVip); ``` # 四、考虑函数是否职责单一 > 虑函数是否职责单一,是否能通过拆分成多个函数的方式来减少参数 ```cpp public: User getUser(String username, String telephone, String email); // 拆分成多个函数 User getUserByUsername(String username); User getUserByTelephone(String telephone); User getUserByEmail(String email); ``` 最后修改:2025 年 07 月 01 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏