【复读EffectiveC++23】条款23:宁以 non-member、non-friend替换member函数

条款23:宁以 non-member、non-friend替换member函数

这是C++设计的一个基本原则,主要目的是减少面向对象设计中的耦合,提高软件的内聚性和可复用性。non-member、non-friend函数可以不受类内部实现的影响,因此更加灵活和可复用。

想象有个 class 用来表示web浏览器。这样的 class 可能提供的众多函数中,有一些用来清除下载的类,这样一个类提供了清除下载缓存,清除URL访问历史,从系统中移除所有cookies接口:

class WebBrowser {
public:...void clearCache();void clearHistory();void removeCookies();...
};

许多用户想将这些动作一块执行,所以web浏览器为此可以提供一个函数:

class WebBrowser {
public:...void clearEverything(); // calls clearCache, clearHistory,// and removeCookies...
};

当然,这个功能也可以通过非成员函数来提供,让它调用合适的成员函数就可以了:

void clearBrowser(WebBrowser& wb)
{wb.clearCache();wb.clearHistory();wb.removeCookies();
}

哪种方法才是更好的呢?是成员函数clearEverying还是非成员函数clearBrower?

一、非成员、非友元函数更好的原因

面向对象准则指出数据以及操作数据的函数应该被捆绑到一起,这就表明它建议成员函数是更好的选择。不幸的是,这个建议是不正确的。它曲解了面向对象的含义。面向对象准则指出数据应该尽可能的被封装。违反直觉的是,成员函数clearEverything实际上并没有比非成员函数clearBrower有更好的封装性。并且提供非成员函数能够为web浏览器的相关功能提供更大的包装(packaging)灵活性,相应的,就可以产生更少的编译依赖和更好的可扩展性。因此非成员函数比成员函数在许多方面都要好。重要的是,我们需要知道原因。

1、非成员非友元封装性更好

以封装开始。如果一些东西被封装了,它就不可见了,被隐藏了。封装的东西越多,就有更少的客户能看到它们。更少的客户能看到它们就意味着我们有更大的灵活性来进行对它们进行修改,因为我们的修改直接影响的是能看到这些修改的客户。因此封装性越好,就赋予我们更大的能力来对其进行修改。这也是我们将封装摆在第一位的原因:它以一种只影响有限数量的客户的方式为我们修改东西提供了灵活性。

考虑同一个对象相关联的数据。看到这些数据的代码越少(也就是可访问它),数据就被封装的越好,我们就有更加自由的修改这个对象的数据的一些特征,例如改变成员变量的数量,类型等等。通过确认有多少代码能够看到数据来判断数据的封装性是粗粒度的方法,我们可以计算出能够访问数据的函数的数量,能访问的函数越多,封装性越拉。

(1)减少暴露内部实现

成员函数(特别是public和protected成员函数)是类接口的一部分,它们直接暴露了类内部的一部分实现细节。当你通过成员函数提供对类内部数据的访问或操作时,你实际上是在告诉类的使用者:“这是你可以做的事情,这是类内部的一部分”。相比之下,非成员函数可以通过接收类的实例作为参数来操作数据,而无需成为类接口的一部分。这样,类的内部实现细节就可以更好地被隐藏和保护起来。

(2)限制访问权限

虽然成员函数可以通过访问控制符(public、protected、private)来限制对类内部数据的访问,但非成员函数本身就不具备直接访问类私有成员的能力(除非它们是友元)。这意味着你可以通过非成员函数来提供对类数据的有限访问,而无需将这些数据暴露给类的所有使用者。这种限制访问的方式有助于减少误用和潜在的错误。

(3)降低耦合度

当类的成员函数依赖于其他类的内部实现时,这些类之间就会形成紧密的耦合关系。如果将来需要修改这些内部实现,就可能会影响到依赖于它们的成员函数。相比之下,非成员函数可以更容易地与类的实现细节解耦,因为它们不依赖于类的内部状态。这样,当类的实现发生变化时,使用这些非成员函数的代码通常不需要进行修改。

(4)促进模块化

2、用非成员非友元可以减少编译依赖

在c++中,一个更加自然的方法是使clearBrower成为同WebBrowser有相同namespace(命名空间)的非成员函数:

namespace WebBrowserStuff {class WebBrowser { ... };void clearBrowser(WebBrowser& wb);...
}

然而这不仅仅是看起来更加自然,因为命名空间不像类,它是可以跨文件的。这是很重要的,因为像clearBrower这样的函数是很便利的函数。既不是成员也不是友元,对WebBrower类没有特殊访问权,因此它不能提供WebBrowser客户没有获取到的其他任何功能。举个例子,如果clearBrower这个函数不存在,客户只好自己调用clearCache,clearHistory,和removeCookies。

一个像webBrower这样的类可以有大量的便利函数,一些和标签相关,另一些和打印相关还有一些和cookie管理相关等等。通常大多数客户只对其中的一部分有兴趣。没有理由让只对标签便利函数感兴趣的客户编译依赖于cookie相关的便利函数。将它们分开的直接的方法是将它们声明在不同的头文件中。

// 头文件“webbrowser.h” 
namespace WebBrowserStuff {
class WebBrowser { ... };... // 核心机能,如所有用户都要用到
}// 头文件“webbrowserbookmarks.h”
namespace WebBrowserStuff {... // 书签相关的便利函数
} 
// 头文件“webbrowsercookies.h”
namespace WebBrowserStuff {
... // cookie相关的便利函数
} 

注意标准C++库就是这么组织的。它并没有在std命名空间中将所有东西包含在一个单一的<C++ Stand Library>头文件中,而是有许多头文件(,,等等),每个头文件声明了std命名空间中的一部分功能。只使用vector相关功能客户不需要#include ;不需要使用list的客户不必#include 。这就允许客户只编译依赖于它们实际用到的部分。(条款31中讨论了减少编译依赖的其他方法)。以此种方式切割机能并不适用于 class 成员函数,因为一个 class 必须整体定义不能被分割为片片段。

通过将功能分散到非成员函数中,你可以更容易地将代码组织成更小的、更可管理的模块。这些模块可以独立地进行编译和测试,从而提高了代码的可维护性和可扩展性。此外,非成员函数还可以为相关的功能提供更大的包装灵活性,使得你可以更容易地为特定的用例定制解决方案。

3、非成员非友元有更好的扩充性

将所有的便利函数放在不同的头文件内,但隶属同一个命名空间,同样意味着客户可以很容易的对便利函数进行扩展。他们需要做的是向命名空间中添加更多的非成员非友元函数。举个例子,如果一个WebBrower客户决定实现图片下载相关的便利函数,他只需要创建一个头文件,在命名空间WebBrowserStuff中将这些函数进行声明。新函数能像旧的函数一样同它们整合在一起。这也是类不能提供的另外一个性质,因为客户是不能对类定义进行扩展的。当然,客户可以派生出新类,但派生类没有权限访问基类的封装成员(像private成员),这样的“扩展功能”就是二等身份。此外,正如条款7中解释的,并不是所有类都被设计成基类。

二、方法论

  1. 如果某个函数与一个类密切相关,但不需要访问类的任何private或protected成员,那么考虑将其作为非成员函数或者非友元函数实现。
  2. 如果需要修改类的行为,但不应该修改类对象的状态,考虑使用const成员函数或者non-const的non-member、non-friend函数。
  3. 如果需要访问类的私有成员,考虑将函数声明为该类的友元。
  4. 如果需要在类内部实现一些简单的操作,考虑将其实现为inline成员函数。

三、总结

宁可拿非成员 非友元函数替换成员函数。这样做可以增加封装性、包裹弹性和机能扩充性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/50544.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Power Tower

Problem - D - Codeforces 牛客和codeforce都有 递归处理l,r&#xff0c;终点是lr && mod1 用扩展欧拉定理 // Problem: D. Power Tower // Contest: Codeforces - Codeforces Round 454 (Div. 1, based on Technocup 2018 Elimination Round 4) // URL: https://c…

学习HTML、CSS和JavaScript的完整路线指南

第一步&#xff1a;理解基础概念 HTML基础 HTML简介和结构 什么是HTML&#xff1f;它在Web开发中的角色。HTML文档的基本结构&#xff1a;<!DOCTYPE>, <html>, <head>, <body>等标签的作用和使用方法。 常用HTML元素 文本相关&#xff1a;段落 <p&g…

【Socket 编程】应用层自定义协议与序列化

文章目录 再谈协议序列化和反序列化理解 read、write、recv、send 和 tcp 为什么支持全双工自定义协议网络计算器序列化和反序列化 再谈协议 协议就是约定&#xff0c;协议的内容就是约定好的某种结构化数据。比如&#xff0c;我们要实现一个网络版的计算器&#xff0c;客户端…

【logstash】logstash使用多个子配置文件

这里有个误区在pipelines.yml中写conf.d/*&#xff0c;实测会有问题&#xff0c;不同的filter处理逻辑会复用。 现在有两个从kafka采集日志的配置文件&#xff1a;from_kafka1.conf&#xff0c;from_kafka2.conf 修改pipelines.yml配置文件 config/pipelines.yml- pipeline.i…

关于P2P(点对点)

P2P 是一种客户端与客户端之间&#xff0c;点对点连接的技术&#xff0c;在早前的客户端都是公网IP&#xff0c;没有NAT的情况下&#xff0c;P2P是较为容易实现的。 但现在的P2P&#xff0c;实现上面会略微有一些复杂&#xff1a;需要采取UDP打洞的技术&#xff0c;但UDP打出来…

asp.net mvc 三层架构开发商城系统需要前台页面代完善

一般会后端开发&#xff0c;都不太想写前台界面&#xff0c;这套系统做完本来想开源&#xff0c;需要前台界面&#xff0c;后台已开发&#xff0c;有需求的朋友&#xff0c;可以开发个前端界面完善一下&#xff0c;有的话可以私聊发给我啊

python_使用多进程来处理数据写入Excel文件_multiprocessing.Process

python_使用多进程来处理数据写入Excel文件 优势&#xff1a;与多线程相比&#xff0c;多进程写入速度要更快&#xff0c;12万多行数据处理用时3.52秒&#xff0c;比多进程快了1秒左右。 import pandas as pd from io import BytesIO import multiprocessing import time impor…

Spring源码-AOP

1、spring aop和aspectJ什么关系&#xff1f; aop是编程思想&#xff0c;spring aop被aspectJ都是aop思想的具体实现。spring aop为了不重复造轮子&#xff0c;通过一定的取舍选取了aspectJ中适合自己的注解。spring初期版本的aop只支持通过实现aop接口的方式来实现切面增强&a…

Nginx 最常用的命令

目录 一、Nginx 安装与配置 1.1 下载与安装 1.2 配置文件 二、Nginx 基本操作 2.1 启动与停止 2.2 重启与重新加载 三、日志管理 3.1 访问日志 3.2 错误日志 四、虚拟主机管理 4.1 配置虚拟主机 4.2 管理虚拟主机 五、性能优化 5.1 缓存配置 5.2 连接优化 Nginx…

Redis(三)

1. java连接redis java提高连接redis的方式jedis. 我们需要遵循jedis协议。 引入依赖 <!--引入java连接redis的驱动--><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>4.3.1</version&g…

Android Framework 之AMS

它管理了系统的四大组件:Activity、Service、ContentProvider、Broadcast。 它除了管理四大组件外&#xff0c;同时也负责管理和调度所有的进程 AMS相关目录结构 AMS代码主要在下面几个目录(AndroidQ上AMS相关部分功能移到了wm下)&#xff1a; frameworks/base/core/java/andro…

记录|LabVIEW从0开始

目录 前言一、表达式节点和公式节点二、脚本与公式2.1 公式 三、Excel表格3.1 位置3.2 案例&#xff1a;波形值存入Excel表中3.3 案例&#xff1a;行写入&#xff0c;列写入 四、时间格式化4.1 获取当前时间4.2 对当前时间进行格式化 更新时间 前言 参考视频&#xff1a; LabVI…

【STL】之 vector 使用方法及模拟实现

前言&#xff1a; 本文主要讲在C STL库中vector容器的使用方法和底层的模拟实现~ 成员变量的定义&#xff1a; 对于vector容器&#xff0c;我们首先采用三个成员变量去进行定义&#xff0c;分别是&#xff1a; private:iterator _start; // 指向数据块的开始iterator _finish…

React类组件生命周期与this关键字

类组件生命周期 参考链接 一图胜千言&#xff08;不常用的生命周期函数已隐藏&#xff09; 代码&#xff1a; //CC1.js import { Component } from "react";export default class CC1 extends Component {constructor(props) {super(props);console.log("con…

【Vue3】watchEffect

【Vue3】watchEffect 背景简介开发环境开发步骤及源码 背景 随着年龄的增长&#xff0c;很多曾经烂熟于心的技术原理已被岁月摩擦得愈发模糊起来&#xff0c;技术出身的人总是很难放下一些执念&#xff0c;遂将这些知识整理成文&#xff0c;以纪念曾经努力学习奋斗的日子。本文…

【代码随想录第37天| 完全背包,518. 零钱兑换 II ,377. 组合总和 Ⅳ,70. 爬楼梯 (进阶)】

完全背包 完全背包中&#xff0c;每件物品都有无数件&#xff1b;这主要影响了遍历背包容量时的遍历顺序&#xff0c;应该从小到大去遍历&#xff0c;这样才能包括有多件相同物品的情况。 思路 先遍历物品&#xff0c;再遍历背包 for(int i 0; i < weight.size(); i) {…

pnpm 设置国内源

pnpm config set registry https://registry.npmmirror.com/

16、DDD系列-向微服务迈进

在本章中&#xff0c;我们将探讨如何从传统的单体架构向微服务架构演进。这个过程需要考虑许多因素&#xff0c;包括微服务的驱动力、所需条件、服务粒度的确定以及系统复杂性的治理。 1、目的&#xff1a;微服务的驱动力 微服务的主要驱动力是业务需求的快速变化和系统的可扩…

[Mysql-数据库基本知识了解]

为什么学习数据库&#xff1f; 数据的保存&#xff1a; 大量程序产生的数据在程序 运行时和程序结束运行后 数据应该怎么保存&#xff1f; 数据的完整性 &#xff1a;数据和数据之间的结构关系&#xff0c; 数据和程序之间的依赖关系&#xff0c; 如何能让这些关系持久维系…

C++初学(7)

7.1、字符串 字符串是存储在内存的连续字节中的一系列字符。C处理字符串的方式有两种&#xff0c;第一种是来自C语言&#xff0c;常被称为C风格字符串&#xff0c;另一种则是基于string类库的方法。 存储在连续字节中的一系列字符意味着可以将字符存储在char数组中&#xff0…