『重构--改善既有代码的设计』读书笔记----Extract Class
在面向对象中,对于类这个概念我们应该有一个清晰的责任认识,就是每个类应该只有一个变化点,每个类的变化应该只受到单一的因素,即每个类应该只有一个明确的责任。当然了,说时容易做时难,很多人可能都会和我一样,一开始建立类的时候信心满满,牢记SRP原则,但随着开发进度的不断进行,很有可能你会给你原本设计好的类增加新字段或者增加新函数,对于少量的增加你可能会因为麻烦,考虑不去单独做一个新类来分解。久而久之,你这个类会变得越来越臃肿,所掌管的责任也会越来越多。这样的类往往还有大量的数据和函数,往往太大而不易理解。这个时候你就应该考虑哪些东西可以单独分离出去作为一个单独的类。如果你发现某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依,这就表示你应该把它们都分离出去。当然,作者也提到了一个比较好的技巧,就是你自己问自己,如果你单独搬移了某些函数和字段,会发生什么事情,其他函数和字段是否会变得没有意义。
另外一种情况就是往往就是在开发后期出现的给类做子类化的问题。如果你发现子类化只影响类的部分特性,或如果你发现某些特性需要用一种子类化方式,某些特性需要另外一种子类化方式,这就意味着你需要分解原来的类了。
做法:
- 决定如何分解类所负的责任。
- 建立一个新类,用来表现从旧类分离出来的责任。如果旧类剩下的责任与旧类名称不符,为旧类更名。
- 建立从旧类访问新类的连接关系。如果你需要从新类去访问旧类,可能会出现一条“双向连接”,除非你真正需要它,否则不要建立从新类到旧类的连接。
- 对于你想搬移的每个字段,运用Move Field进行搬移。
- 每次搬移后,进行编译,测试。
- 使用Move Method将必要函数从旧类搬移到新类。这里有个技巧:先搬移较低层函数(被其他函数所调用的次数多余调用其他函数的函数)再搬移较高层函数。
- 每次搬移后,进行编译,测试。
- 检查,精简每个类的接口。如果你建立起双向连接,考虑下能否将他们改为单向连接。
- 决定是否公开新类,如果不公开,你可以让旧类完全变成新类的委托类。如果你的确要公开他,你还要考虑别名问题,即你公开给别的用户的时候,是引用还是普通的值对象。
例子:
class Person { public: QString name() { return m_name; } QString telephoneNumber() { return m_officeAreaCode + m_officeNumber; } QString officeAreaCode() { return m_officeAreaCode; } void setOfficeAreaCode(const QString &value) { m_officeAreaCode = value; } QString officeNumber() { return m_officeNumber; } void setOfficeNumber(const QString &value) { m_officeNumber = value; } private: QString m_name; QString m_officeAreaCode; QString m_officeNumber; };
看完这个例子我们可以发现我们完全可以把跟电话号码相关的特性放到一个单独的“电话”类中去。首先我们定义一个电话类来表示电话号码这个概念。
class TelephoneNumber { };
接下来我们要做的就是建立从Person到TelephoneNumber的连接,即给Person增加一个字段来保存TelephoneNumber对象
class Person { private: TelephoneNumber m_telephoneNumber; };
接下来我们就可以运用Move Field来搬移跟电话相关的字段,在这里是m_officeAreaCode和m_officeNumber,首先我们来移动m_officeAreaCode我们可以得到
class TelephoneNumber { QString officeAreaCode() { return m_officeAreaCode; } void setOfficeAreaCode(const QString &value) { m_officeAreaCode = value; } private: QString m_officeAreaCode; };
这是目标类经过Move Field之后的样子
class Person { public: QString telephoneNumber() { return officeAreaCode() + m_officeNumber; } QString officeAreaCode() { return m_telephoneNumber.officeAreaCode(); } void setOfficeAreaCode(const QString &value) { m_telephoneNumber.setOfficeAreaCode(value); } private: TelephoneNumber m_telephoneNumber; };
这是旧类经过Move Field之后关于officeAreaCode先关引用的改变。然后我可以移动其他字段,并运用Move Method把相关函数移动到TelephoneNumber类中
class Person { public: QString name() { return m_name; } QString telephoneNumber() { return m_telephoneNumber.telephoneNumber(); } TelephoneNumber telephoneClass() { return m_telephoneNumber; } private: TelephoneNumber m_telephoneNumber; QString m_name; }; class TelephoneNumber { QString telephoneNumber() { return m_officeAreaCode + m_officeNumber; } QString officeAreaCode() { return m_officeAreaCode; } void setOfficeAreaCode(const QString &value) { m_officeAreaCode = value; } QString officeNumber() { return m_officeNumber; } void setOfficeNumber(const QString &value) { m_officeNumber = value; } private: QString m_officeAreaCode; QString m_officeNumber; };
完成这些之后我们需要考虑需要不需要向用户公开这个新类,我可以让旧类做新类的委托类来完全隐藏新类,也可以直接向用户公开。如果考虑公开,你就需要去考虑别名问题,考虑返回值对象和引用对象对自己所带来的后果。
面对这种问题,有如下几种选择:
- 允许任何对象修改TelephoneNumber对象的任何部分,即你把这个类变成了一个引用对象,你可以使用Change Value to Reference来将值对象改成引用对象。这种情况下Person是TelephoneNumber的访问点。
- 不允许任何人不通过Person对象来修改TelephoneNumber对象,即TelephoneNumber是不可修改的,在这里你可以利用C++的特性const来加以修饰。
- 另一个办法就是复制一个和TelephoneNumber完全一样的新对象作为返回,这可能会给别的用户造成一定困惑,因为他可能觉得他已经修改了,为什么原来的对象没有改变。此外如果同一个TelephoneNumber对象传递给多个用户,也可能造成用户间的别名问题。
Extract Class是改善并发程序的一种常用技术,如何理解?简单来说他可以让你对分解后的两个类进行分别加锁,如果你有这方面的需求你可以这么做。当然也存在危险性,如果你要确保两个对象同时锁定,这里面又牵扯到了事务问题,需要使用其他类型的共享锁,这是一个复杂的领域,比起一般情况需要更繁重的机制,虽然事务很有实用性,但在这里如果你手动编写事务管理程序则会超出大多数程序员的职责范围。