『重构--改善既有代码的设计』读书笔记----Change Value to Reference

时间:2022-04-08 16:57:57

有时候你会认为某个对象应该是去全局唯一的,这就是引用(Reference)的概念。它代表当你在某个地点对他进行修改之后,那么所有共享他的对象都应该在再次访问他的时候得到相应的修改。而不会像值对象(Value)一样,不可修改。举个例子,你认识小明,我也认识小明,小明忽然把头发都踢了,这个时候你认识的小明和我认识的小明都是同一个人,都是光头,这个小明就是世界的唯一实例,然而,你有100块钱,我有50块钱,我把50块钱花到只剩20,你手里的100块钱并不会因为我的50块钱改变而改变,不会相应的修改,这就是值对象。放到C++代码中,对于引用的概念,我们可以参照上一篇的例子

void setCustomer(const QString &value)
{
delete m_customer;
m_customer = NULL; m_customer = new Customer(value);
}

为了让我的m_customer产生“值对象”的概念,我在每次进行set的时候都进行重新创建,这样可以保证我的对象是不可以进行修改的,也就确保了值对象的特性。总结来说,引用对象就像"客户",“账户”这样的东西,每个对象代表真实世界中的一个实物。你可以直接用“==”来比较两个对象是否相等,而值对象更像“日期”,“钱”这样的东西,你不需要去担心它副本的存在,也许系统中存在成百上千个100元钱这种对象,但这并不关系到你,当然如果你要做比较,你也可以重载“==”来做比较。

这也让我们在引用对象和值对象之间产生了两难。有时候,你会从一个简单的值对象开始,在其中保存少量不可修改的数据,而后,你可能希望给这个对象加上一些可修改的数据,并确保对任何一个对象的修改都能影响到所有引用到此对象的地方。这时候你就应该考虑将这个对象变成一个引用对象。

做法:

  • 使用Replace Constructor with Factory Method.
  • 编译,测试。
  • 决定由什么对象负责提供访问新对象的途径。可能是一个静态字典或者一个注册表对象,你也可以使用多个对象作为新对象的访问点。
  • 决定这些引用对象应该事先创建好,还是应该动态创建。如果这些引用对象是预先创建好的,而你必须从内存中将他们读取出来,那么就得确保他们在需要的时候被及时加载。
  • 修改工厂函数,令它返回引用对象。如果对象是预先创建好的,你就需要考虑万一有人索求一个其实并不存在的对象,要如何处理错误。你可能希望对工厂函数使用Rename Method,使其传达这样的信息,它返回的是一个既存对象。
  • 编译,测试。

例子:

class Customer
{
public:
Customer(const QString &name) :
m_name(name)
{
} QString name() const
{
return m_name;
}
private:
const QString m_name;
}; class Order
{
public:
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);
} QString customerName() const
{
return m_customer->name();
}
private:
Customer *m_customer;
};

我们引用了Replace Data Value with Object的例子,上一节我们也说过重构并没有因此结束,接下来就是要进行这一步骤。我们有Customer类,可以看到他正在被Order类所使用,并且对待的原则是『值对象』,我们可以看到客户端代码如下

static int numberOfOrderFor(const QList<Order> &orders, const QString &customerName)
{
int result = ; foreach (Order order, orders)
{
if (order.customerName() == customerName)
{
result++;
}
} return result;
}

到目前位置,Customer还是属于值对象,这就造成了就算多个订单属于一个客户,但这个一个客户对于每个Order来说都是自己的。我们希望改变这种现状,希望让客户Customer变成世界唯一。也就是说要让这些订单共享同一个Customer对象,也就意味着每一个客户只该对应一个Customer.

首先我们使用Replace Constructor with Factory Method,以此来控制Customer对象的创建过程,这在以后也是非常重要的,接下来我们在Customer类中定义这个工厂函数。

class Customer
{
public:
static Customer *create(const QString &name)
{
return new Customer(name);
}
};

然后把原本调用构造函数的地方都改为调用工厂函数。

class Order
{
public:
Order(const QString &customerName)
{
m_customer = Customer::create(customerName);
}
};

然后我们再把原来Customer的构造函数声明为private.

class Customer
{
private:
Customer(const QString &customerName)
{
m_name = name;
}
};

现在我们需要决定如何来访问这些唯一的Customer对象,作者比较喜欢通过Order中的一个字段来访问它,但本例没有这样一个明显的字段可以用来访问Customer对象,在这种情况下,通常我们需要创建一个注册表对象来保存所有的Customer对象,以此作为访问点。为了简化我们的例子,我们把这个注册表保存在Customer类的static中,让Customer来作为访问点。

static QHash<QString, Customer *> hashTables;

做完这个决定之后我们需要做下一个重要决定----是应该在请求时才创建还是应该预先创建好。这里我们选择后者,在应用程序的启动代码中,我们先把需要使用的Customer对象加载妥当,这些对象可能来自数据库,也可能来自文件。为求简单起见,我们在代码中明确生成这些对象,反正之后我们总是可以使用Subsititute Algorithm来改变他们的创建方式。

class Customer
{
public:
static void loadCustomers()
{
new Customer("Leo")->store();
new Customer("Tom")->store();
new Customer("Marry")->store();
}
private:
void store()
{
hashTables.insert(this.name(), this);
}
};

现在我们需要修改工厂函数,让它返回预先创建好的Customer对象。

static Customer *create(const QString &name)
{
return hashTables.value(name);
}

由于create总是返回既有的Customer对象,所以我们应该使用Rename Method来修改这个工厂函数的名称,以便强调这一点。

class Customer
{
public:
static Customer *getNamed(const QString &name)
{
return hashTables.value(name);
}
};