一次重构小记
- 起因
有很多毫无关系又或者有亲戚关系的对象,对这些对象有很多相同的处理(比如在界面上显示某些和修改属性)。你并不确定这些对象来自何方又将去往何处,所需要做的是对它们做某些共同的修改,而根据对象的不同又需要各自做一些不同处理。比如有人(person)和树(tree)两个对象,共同的操作是要对他们的名称和数量做修改(这里假设名称和数量对于人和树有不同意义,比如数量对人说是身高而对树来说是直径)。于是乎前辈定义了类似于下面的基类和子类(有新旧对象是为了做Undo和Redo用)。
/// <summary> /// 基类 /// </summary> public class ViewBase { /// <summary> /// 名称 /// </summary> public virtual string Name { get; set; } /// <summary> /// 数量 /// </summary> public virtual double Number { get; set; } /// <summary> /// 旧对象 /// </summary> public object ObjOld { get; set; } /// <summary> /// 新对象 /// </summary> public object ObjNew { get; set; } /// <summary> /// 改变标志 /// </summary> public virtual bool IsChanged { get { return !ObjOld.Equals(ObjNew); } } }
/// <summary> /// 人子类 /// </summary> public class PersonView : ViewBase { /// <summary> /// 构造函数 /// </summary> public PersonView(Person person) { ObjOld = CloneHelper.DeepClone(person); ObjNew = person; } /// <summary> /// 相应的对象 /// </summary> public new Person ObjNew { get { return objNew; } set { base.ObjNew = objNew = value; } } private Person objNew; /// <summary> /// 原始对象 /// </summary> public new Person ObjOld { get { return objOld; } set { base.ObjOld = objOld = value; } } private Person objOld; /// <summary> /// 名称 /// </summary> public override string Name { get { return ObjNew.Name; } set { ObjNew.Name = value; } } /// <summary> /// 身高 /// </summary> public override double Number { get { return ObjNew.Height; } set { ObjNew.Height = value; } } public override bool IsChanged { get { //检查是否变化的代码 return base.IsChanged; } } //其他处理 //.......... }
public class TreeView : ViewBase { public TreeView(Tree tree) { ObjOld = CloneHelper.DeepClone(tree); ObjNew = tree; } /// <summary> /// 相应的对象 /// </summary> public new Tree ObjNew { get { return objNew; } set { base.ObjNew = objNew = value; } } private Tree objNew; /// <summary> /// 原始对象 /// </summary> public new Tree ObjOld { get { return objOld; } set { base.ObjOld = objOld = value; } } private Tree objOld; /// <summary> /// 品种 /// </summary> public override string Name { get { return ObjNew.Type; } set { ObjNew.Type = value; } } /// <summary> /// 直径 /// </summary> public override double Number { get { return ObjNew.Diameter; } set { ObjNew.Diameter = value; } } public override bool IsChanged { get { //检查是否变化的代码 return base.IsChanged; } } //其他处理 //.......... }
看到使用new关键字我就有种蛋蛋的忧伤。对于new关键字的使用我一直持比较谨慎的态度,因为我感觉应该用到它的场景并不多。就像上面这种情况,基类和子类应该拥有相同的Obj实例,虽然子类在给ObjNew和ObjOld赋值的时候同样对基类的对象赋值,但肯定又有不同同一实例的风险,而且重复代码。在这里用new关键重写基类属性只是为了在使用特定子类的对象的时候不需要做类型转换,但却付出了代码重复和容易产生bug的代价。
- 题目话
在刚开始结接手项目的时候,在别的部分程序代码中,前辈在类似场景了也做了同样的设计。当时我就在想重写那部分代码,但是时间紧任务重,加之那部分代码太过庞大复杂(基类有个几十个属性和方法,并且还有几十个子类),有牵一发而扯着蛋的危险,实在是不敢动手。今天在修改另外一段程序的时候又看到了同样设计,实在是有点忍无可忍不想再忍,加上这段程序里面的类比较简单,于是决定重写。
- 修改
对于这种场景,很容易想到使用泛型基类。于是我先把基类和子类做了如下修改,去掉了子类的重复代码,而且保证基类的类型安全:
/// <summary> /// 基类 /// </summary> public class ViewBase<T> { /// <summary> /// 名称 /// </summary> public virtual string Name { get; set; } /// <summary> /// 数量 /// </summary> public virtual double Number { get; set; } /// <summary> /// 旧对象 /// </summary> public T ObjOld { get; set; } /// <summary> /// 新对象 /// </summary> public T ObjNew { get; set; } /// <summary> /// 改变标志 /// </summary> public virtual bool IsChanged { get { return !ObjOld.Equals(ObjNew); } } }
/// <summary> /// 人子类 /// </summary> public class PersonView : ViewBase<Person> { /// <summary> /// 构造函数 /// </summary> /// <param name="hasi"></param> /// <param name="cadType"></param> public PersonView(Person person) { ObjOld = CloneHelper.DeepClone(person); ObjNew = person; }
/// <summary> /// 名称 /// </summary> public override string Name { get { return ObjNew.Name; } set { ObjNew.Name = value; } } /// <summary> /// 身高 /// </summary> public override double Number { get { return ObjNew.Height; } set { ObjNew.Height = value; } } public override bool IsChanged { get { //检查是否变化的代码 return base.IsChanged; } } //其他处理 //.......... }
public class TreeView : ViewBase<Tree> { public TreeView(Tree tree) { ObjOld = CloneHelper.DeepClone(tree); ObjNew = tree; } ///// <summary> ///// 相应的对象 ///// </summary> //public new Tree ObjNew //{ // get { return objNew; } // set { base.ObjNew = objNew = value; } //} //private Tree objNew; ///// <summary> ///// 原始对象 ///// </summary> //public new Tree ObjOld //{ // get { return objOld; } // set { base.ObjOld = objOld = value; } //} //private Tree objOld; /// <summary> /// 品种 /// </summary> public override string Name { get { return ObjNew.Type; } set { ObjNew.Type = value; } } /// <summary> /// 直径 /// </summary> public override double Number { get { return ObjNew.Diameter; } set { ObjNew.Diameter = value; } } public override bool IsChanged { get { //检查是否变化的代码 return base.IsChanged; } } //其他处理 //.......... }
这貌似很美好,类型安全,代码减少,降低bug风险。 但是问题在于C#是强类型语言,必须在使用泛型类的时候为其指定特定的类型,因此失去了作为统一接口的能力。
如何保证在使用的泛型基类的情况下,又可以使子类有统一的处理接口?
答案自然是使用interface,于是我添加一个IView的interface,让ViewBase<T>实现IView,代码如下:
public interface IView { /// <summary> /// 名称 /// </summary> string Name { get; set; } /// <summary> /// 数量 /// </summary> double Number { get; set; } /// <summary> /// 旧对象 /// </summary> object ObjOld { get; set; } /// <summary> /// 新对象 /// </summary> object ObjNew { get; set; } /// <summary> /// 改变标志 /// </summary> bool IsChanged { get; } }
ViewBase<T>做相应修改
/// <summary> /// 基类 /// </summary> public class ViewBase<T>: IView { /// <summary> /// 名称 /// </summary> public virtual string Name { get; set; } /// <summary> /// 数量 /// </summary> public virtual double Number { get; set; } /// <summary> /// 旧对象 /// </summary> public T ObjOld { get; set; } /// <summary> /// 新对象 /// </summary> public T ObjNew { get; set; } /// <summary> /// 改变标志 /// </summary> public virtual bool IsChanged { get { return !ObjOld.Equals(ObjNew); } } object IView.ObjOld { get { return this.ObjOld; } set { this.ObjOld = (T)value; } } object IView.ObjNew { get { return this.ObjNew; } set { this.ObjNew = (T)value; } } }
这里需要注意的是接口要显示实现,关于显示接口实现和隐式接口实现的区别大家可以查看《CLR via C#》。
嗯,这下貌似完美了,类型安全,代码减少,降低bug风险,统一接口。
但但是,人生总是有那么多的但是。。。。。
这时候来了一个Student的类,继承自Preson,PersonView所有的处理它都有,而且还有一些自己的处理, 学生总是喜欢搅和。。。
很自然定义StudentView继承自PersonView,这时问题又出来了,StudentView里面Student对象又要如何来实现呢?
嗯,要不把PersonView在泛型化吧,于是乎,我又修改了PersonView的代码
/// <summary> /// 人子类 /// </summary> public class PersonView<T> : ViewBase<Person> where T : Person { /// <summary> /// 构造函数 /// </summary> /// <param name="hasi"></param> /// <param name="cadType"></param> public PersonView(Person person) { ObjOld = CloneHelper.DeepClone(person); ObjNew = person; } ///// <summary> ///// 相应的对象 ///// </summary> //public new Person ObjNew //{ // get { return objNew; } // set { base.ObjNew = objNew = value; } //} //private Person objNew; ///// <summary> ///// 原始对象 ///// </summary> //public new Person ObjOld //{ // get { return objOld; } // set { base.ObjOld = objOld = value; } //} //private Person objOld; /// <summary> /// 名称 /// </summary> public override string Name { get { return ObjNew.Name; } set { ObjNew.Name = value; } } /// <summary> /// 身高 /// </summary> public override double Number { get { return ObjNew.Height; } set { ObjNew.Height = value; } } public override bool IsChanged { get { //检查是否变化的代码 return base.IsChanged; } } //其他处理 //.......... }
public class StudentView : PersonView<Student> { public StudentView(Student student) :base(student) { } //其他处理 //...... }
MD,问题重现啊,Person有很多子类和子类的子类。。。。。。 PersonView<T>又是失去了接口性。嗯蛋疼,不知如何是好。。。。想来想去没想到好办法,我饶了一圈回到起点。
结果,PersonView还是采用非泛型,PersonView的子类依旧使用了最开始的写法。
不知道各位大侠有啥好办法,还望告知,不胜感激。