/*<<重构>>8.11节 Elcapsulate Collection(封装群集)想到的
概要:
有一个方法返回一个群集,让这个函数返回该群集的一个只读映射,并在这个类中提供添加/移除群集元素的函数。
动机:
类常常使用群集(array,list,set或vector)来保存一组实体。这样的Class通常也会提供一个针对该群集的[取值/设值函数](getter/setter)
但是,群集的处理方式应该和其他种类的数据有所不同。取值函数(getter)不应该返回群集本身,因为这将让用户可以修改该群集的内容但是群集的拥有者却一无所悉。
这也会对用户暴露过多[对象内部数据结构]的信息,而对于一个设计良好的类来说,用户对类信息需要知道的越少越好。如果用户确实需要得到该群集,应该避免将该群
集直接交给用户,一旦这样做,用户可以对该群集作任何事情,而拥有者并不知晓,这会导致数据的严重不一致,这也是稍大点的系统经常存在的硬伤。
另外,尽量不为这个群集提供一个Set函数,但群集的拥有者应该为群集提供添加/删除元素的函数。这样群集的拥有者可以知道群集的所有变化,保证数据的一致性。
至于这个群集的取值函数,又该如何设计呢?
节里讲到了Java 2里可以使用Collection.unmodifiableXxx()得到这个群集的只读映件,我不清楚Java 2里怎么实现这个方法的细节,但是能估计到几种可能,也不想详细分析了。
那么在没有提供该机制的语言环境里,我们该怎么做呢?
本文目的:
实现一个类的群集对象的取值函数(getter),要求用户不能使用获取到的该群集进行添加删除,只能通过类的AddXXX/RemoveXXX对该群集进行添加删除操作。
实现方式1:
返回这个群集的一个拷贝,也就是重新生成一个群集,复制所有的元素。
这种方式实现简单,但是有致命弱点:
1,用户可以对该群集的副本进行添加删除,这种操作却反映不到拥有者的群集里,但是用户很可能会认为这样的操作会直接反映到拥有者里面
2,复制一个群集的代价太大,特别是群集的规模很大的情况下。
实现方式2:
不提供群集的取值函数,取而代之的提供遍历该群集的方法,比如枚举(enumeration)
这种方式可行,但是也有严重的问题
1,客户如果已经大量饮用了该群集,那么重构所有引用点的工作量大,而且未必所有的客户程序都能给你提供重构的机会。
2,有的情况下,直接提供一个群集会让客户更加一目了然,这种方式会迷惑客户。
实现方式3:
实现Java 2里可以使用Collection.unmodifiableXxx()的机制,返回一个只读的群集。
怎么限制返回的群集只读呢?
这让我想到了修饰模式(重构和模式本来就是一对难兄难弟),修饰模式的功能就是在不改变对象行为的同时增加功能,比如[限制增加删除]也可以认为是一种新的功能。
我一直认为,如果一个对象经常会被描述为{[形容词]的XXX}的形式,就应该使用修饰。
在本例中,XXX就是“群集”,形容词就是“只读”,也就是“只读的群集”。
下面就以C#为例子,用《重构》8.11节的原汁原味的例子说明本人的思想,如有不到之处,多提宝贵意见。
节里重构前的代码类似如下:
//描述学生的需要学习课程
class Person
{
//需要学习的课程列表
private IList<string> m_Courses=new List<string>();
public IList<string> Courses
{
get { return m_Courses; }
}
}
现实中,很可能改变一个学生的课程后需要通知到学生本人(邮件,短信等等都是例子),但是客户程序使用该类的时候,通过课程列表的取值函数获取该列表后直接进行了添加删除了一门课程的操作,
而客户并不知道修改了这个列表后需要通知学生本人,这样就会造成学生到期末的时候缺上了一门客或者多上了一门客(却没有学分),悲剧就这么诞生了。
进行下面的重构后能不改变Person的接口,但是又保证了悲剧不在出现。
*/
using System;
using System.Collections.Generic;
using System.Text;
namespace WrapCluster
{
//描述学生的需要学习课程
class Person
{
//需要学习的课程列表
private IList<string> m_Courses = new List<string>();
//在学生的层面上添加一个[增加]的操作
public void AddCourse(string item)
{
m_Courses.Add(item);
}
//在学生的层面上添加一个[增加]的操作
public void RemoveCourses(string item)
{
m_Courses.Remove(item);
}
public void RemoveAt(int index)
{
m_Courses.RemoveAt(index);
}
public IList<string> Courses
{
//返回一个[只读的课程列表]
get { return new CoursesList(m_Courses ); }
}
}
//把一个IList<string>修饰为只读,在所有会修改列表的方法里抛出异常,强制客户使用学生类本身的对应操作
//当然也可以根据需要修饰为可写,但是每次改动都自动通知学生,构造的时候将该学生作为一个参数保留起来也未必不可,目的就是保证数据的一致性
class CoursesList : IList<string>
{
internal CoursesList(IList<string> courses)
{
m_Courses = courses;
}
private IList<string> m_Courses;
#region IList<string> 成员
//大部分成员可以直接转调用内部的m_Courses,我们只关心会修改列表的方法
public int IndexOf(string item){return m_Courses.IndexOf(item) ;}
public void Insert(int index, string item){m_Courses.Insert(index, item);}
public void RemoveAt(int index){throw new Exception("无法操作,因为该列表为只读!");}
public string this[int index]
{
get{return m_Courses[index];}
set { throw new Exception("无法操作,因为该列表为只读!"); }
}
#endregion
#region ICollection<string> 成员
public void Add(string item) { throw new Exception("无法操作,因为该列表为只读!"); }
public void Clear() { throw new Exception("无法操作,因为该列表为只读!"); }
public bool Contains(string item){return m_Courses.Contains(item);}
public void CopyTo(string[] array, int arrayIndex){m_Courses.CopyTo(array, arrayIndex); }
public int Count { get { return m_Courses.Count; } }
public bool IsReadOnly { get { return m_Courses.IsReadOnly; } }
public bool Remove(string item) { throw new Exception("无法操作,因为该列表为只读!"); }
#endregion
#region IEnumerable<string> 成员
public IEnumerator<string> GetEnumerator(){return m_Courses.GetEnumerator(); }
#endregion
#region IEnumerable 成员
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator(){return ((System.Collections.IEnumerable)m_Courses).GetEnumerator();}
#endregion
}
}