【编程概念】类成员函数设计

今天看公司代码,发现有一块代码很别扭。我思考了一下,感觉跟类的接口设计模式有关。在这里把思考过程贴出来分享一下。先给出一个暴论:类的成员函数越少越好。整个分析过程涉及:模块间交互的方式总结 和《effective c++》一书的 条款 23:使用 non-member、non-friend 函数替换 member 函数。

原始代码大概是这样的。有一棵树,每个节点中存放了孩子和父亲。

class TreeNode {
public:
	void setParent(TreeNode *parent) { parent = parent}
	void changeParent(TreeNode *newParent) { // 有问题的接口
		setParent(newParent);
		newParent->removeChild(this);
	}
	void onRemove();
	void removeChild(TreeNode *child) {
		children.erase(child); // 简写
		child->onRemove();
	}
private:
	TreeNode *parent;
	vector<TreeNode> children;
};

这里有问题的接口是 changeParent。,以调用 changeParent 的那个对象的视角来看,changeParent 干了如下几件事

  1. 把自己的父亲重新设置一下
  2. 调用父亲的removeChild 接口。
    2.1 父亲的 removeChild 接口在自己的孩子列表中移除自己
    2.2 父亲调用了自己的 onRemove 接口,

我认为 操作2 把自己传递给 parent 的 removeChild 接口这种事很傻很绕。但我又说不出个所以然来。

我从以下两个角度思考了这个问题

设计模式中的访问者模式

访问者模式就是把 visitor 传递给 host 对象,然后 visitor 会接受 被访问对象,并且操作 被访问对象。也是一个比较绕的操作,跟这种把 parent 传递给自己(changeParent(parent)),然后自己又把自己传递给 parent (removeChild(this))这种方式类似。

但这里有一个区别:parent 和自己属于同一个抽象层次,都是 TreeNode。而访问者模式中 visitor 和被访问对象是不同的类。

关键是,我认为这种功能,完全可以通过静态函数来实现

class TreeNode {
	static void changeParent(TreeNode* child, TreeNode* parent) {
		parent->removeChild(child);
		child->setParent(parent);
	}
}

改成静态成员函数后更加直观。

模块交互的三种形式

想到访问这模式后,我又想到,模块之间进行数据交互有哪几种形式呢?我认为除了 IPC 外,在 client 视角下,只有如下三种形式:

  1. client 主动调用,获得返回数据。

完整的接口可以包括:返回值,入参、执行 flag、出参、异常值

Ret Foo_getReturn(Parms in, int flag, Parms& out, Error& err);
  1. client 主动调用,改变模块的整体状态
Ret Foo_setState(State s); // 设置某些运行行为
Ret Foo_init(); // 初始化,进入初始化完成的状态
  1. client 被动调用,通过注册回调函数、观察者、访问者,模块 Foo 需要 client 时会通过回调的方式来传递数据给 Client
Obj Foo_setObserver(Observer ob);
Obj Foo_setCallback(Callback cb);
Obj Foo_accept(Visitor vistor);// 设计模式中的很多行为模式都是这样

被动调用的使用要很谨慎,因为被动调用建立了反向依赖,使代码阅读更困难,更容易使人迷惑。被动调用就像双重否定表肯定一样,比较绕。有些时候能主动实现的话,尽量主动实现。就像你读:我不得不承认,他并不是不可靠我得承认,他很可靠。哪一句话更清楚是一目了然的。

但是如果有精巧的设计的话,也可以用。设计模式中的大部分行为模式都是这样相互依赖的,所以要熟悉设计模式。

《effective c++》: 条款 23:使用 non-member、non-friend 函数替换 member 函数。

看到这个条款后,我发现我的想法是正确的。确实应该把 changeParent 函数换成 静态方法。有以下两个原因

  1. 成员函数带来的封装性比非成员函数低。
  2. 非常成员函数不仅灵活性更高,而且最后的编译依赖度也更低。

书中举的例子如下

class WebBrowser{
public:
    void clearCache();
    void clearHistory();
    void removeCookies();
    
    // 如果想一整个执行所有这些动作
    // 方式一:
    // 在类中提供这样一个函数,调用上面三个函数
    void clearEveryThing();
};
 
// 方式二: 
// 也可以用一个non-member函数实现
void clearBrowser(WebBrowser& wb){
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}

有一个网页浏览器类,其中有三个函数用于:清除缓存,清除历史记录,清除Cookies。现在有一个需求要清除浏览器内的全部内容。应该使用成员函数还是非成员函数?

封装

封装的本质是让内部实现不再可见。越多的东西被封装,越少的人可以看到它。越少的人看到它,我们就有越大的弹性去改变它。因为我们改变的东西,只会影响到看到它的人。

对于一个对象来说,越少的代码可以访问和修改数据,那么封装的数据越多,我们改变对象内的数据就越自由。如何测量有多少代码可以看到某一块数据呢?能够访问 private 数据的函数,只有 class 的成员函数和 friend 而已。所以在能实现相同功能的情况下,非成员函数的增加不会增加能够访问 private 数据的个数,因此不会改变 class 的封装性。而成员函数的增加会降低class 的封装性。从这个角度上来说,类的成员函数越少越好

c++ 做法与优势

在c++中,可以把 clearBrowser 放在同 namespace 内。

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

clearBrowser 是一个提供便利的函数。即使不存在这个函数,用户也可以自己调用那三个清除函数,来实现相同的功能。换句话说,便利函数可以提供更高的包裹弹性。

一个像 WebBrowserStuff 这样的类,可能拥有大量的便利函数。某些与数千有关,某些与打印有关,还有一些与 cookie 管理有关。因为 namespace 可以跨文件。所以将这些相关的便利函数按照模块放在多个文件中,可以最大限度的减少编译依赖。

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值