当你在一个类中使用字段的时候,发现这个字段必须要和其他数据或者行为一起使用才有意义。你就应该考虑把这个数据项改成对象。在开发初期,我们对于新类中的字段往往会采取简单的基本类型形式来保存,但随着我们开发进度的增加,这些简单的数据项就不再那么简单了。比如一开始你会使用一个字符串来表示一串电话号码,但是随后你会发现,这个电话号码已经变的不再纯粹,它可能还需要“格式化”,“抽取取号”等特殊行为。一开始你可能会不以为意,觉得这个数据项就这么一两个,不会对你造成影响。但重复代码(Duplicate Code)和依恋情结(Feature Envy)这两个坏味道会很快就散发出来。什么样的行为就应该放在什么样的类中去,当这些坏味道出现的时候你就应该将数据值改为对象了。
- 做法:
- 为待替换数值新建一个类,并在这个新类中新建一个const字段(Java final)并保持其类型和源类中你需要替换的数值类型一样,然后在新类中加入一个这个字段的取值函数(get),并加上一个接受此字段为参数的构造函数。
- 编译。
- 将源类中待替换数值的类型改为你前面新建的类类型。
- 修改源类中关于这个字段的取值函数,令他调用新类的取值函数。
- 如果源类构造函数中用到这个待替换字段(多半是赋值动作)你就应该修改构造函数,让它变为用新类的构造函数来给这个字段赋值。
- 修改源类中待替换字段的设值函数(set)令他为新类创建一个实例。
- 编译,测试。
- 现在,你可能还会对新类使用Change Value to Reference。
例子:
class Order
{
public:
Order(const QString &customer) :
m_customer = customer; QString customer()
{
return m_customer;
} void setCustomer(const QString &customer)
{
m_customer = customer;
}
private:
QString m_customer;
};
我们可以看到Order这个类中,用一个字符串来代表订单客户,但随着开发进度,很可能到了后期你需要为这个客户增加客户地址,信用等级等,这个时候你就不应该用字符串来表示客户,取而代之你应该使用对象。一开始使用Order这个类的客户端代码可能是这样
static int numberOfOrdersFor(QList<Order> orders, QString customer)
{
int result = ; foreach (Order order, orders)
{
if (order.customer() == customer)
{
result++;
}
} return result;
}
首先我们新建Customer类用来表示我们需要替换的新类,然后在这个新类中增加一个const字段用来表示源类中替换字段,这里表示客户姓名name这个概念,然后为这个字段增加取值函数和构造函数。
class Customer
{
public:
Customer(const QString &name) :
m_name(name)
{
} QString name() const
{
return m_name;
}
private:
const QString m_name;
};
接下来我们就需要修改源类了,首先我们将替换字段的类型从QString替换为我们所定义的类型Customer。然后修改所有引用该字段的函数,让他们改而去引用Customer这个对象,其中取值函数和构造函数的修改比较简单,设值函数我们让他重新创建一个新的Customer对象来跟之前的语义(字符串)达成一致形成值对象而非引用对象的概念。
class Order
{
public:
Order(const QString &customer)
{
m_customer = new Customer(customer);
} QString customer() const
{
return m_customer.name();
} void setCustomer(const QString &value)
{
delete m_customer;
m_customer = NULL; m_customer = new Customer(value);
}
private:
Customer m_customer;
};
这也就意味着每个Order有属于自己的Customer,注意这样一条规则:值对象是不可以修改内容的。这便可以让你避免一些别名问题。如果你日后想让Customer成为引用对象(reference object)那就是另外一个重构手法了,现在我们进行编译并且测试。
同时需要观察Order类中m_customer字段的操作函数,并作出一些修改使它更好的反应修改后的形式。对于取值函数你可以使用Rename Method让它更清晰的表示自己,因为他返回的是消费者名称,因为此时的消费者已经是一个确确实实的类了。
QString customerName() const
{
return m_customer.name();
}
至于构造函数和设值函数,就不需要修改签名了,但你可以改变他们的参数名称。
Order(const QString &customerName)
{
m_customer = new Customer(customerName);
} void setCustomer(const QString &customerName)
{
delete m_customer;
m_customer = NULL; m_customer = new Customer(customerName);
}
当然,后续的重构也许会添加接受现有的Customer对象作为参数的构造函数和设值函数。
本次重构到此为止,但这个案例和其他案例一样,只是一个中间步骤,还需要后期处理。因为从上文我们可以看到目前我们对待Customer这个类是采用值的概念来对待。如果要给这些Customer增加地址和信用等级我们做不到,因为多个Order存储的Customer不是共同引用同一个对象,所以我们必须使用Change Value to Reference,这样一来,同一个客户的所有Order就可以共享一个Customer对象达到我们所要的效果了。