首页 > 代码库 > IOC演义 第一回: 重构类步步为营 新框架萌芽胎动

IOC演义 第一回: 重构类步步为营 新框架萌芽胎动

 问题探讨
目前的网站在编程文章的表现上,一般都会把代码折叠起来,而文字部分却始终显示,给人代码似乎可有可无的感觉。实际情况往往恰恰相反:代码和插图、文字应该是文章的三个表现手段,许多情况下插图和文字应该是围绕代码展开的,是对代码的说明和原理展示,或者说主角往往应该是代码,文字和插图则是点缀。这样我们为什么不能把文字作为代码的注释呢?一段代码想要表达什么意思,每个人看后的领悟应该不会完全相同,最权威的表达无疑是代码的作者。把代码和文字存放在一块,不就把作者的意图共同保存了吗?
本文就是在vs2015里面编写的,使用/*和 */注释对把文字部分注释掉,然后使用办公软件稍加排版;读者可以选择全部内容,粘贴到IDE里面,添加必要的引用就可以运行。文字则完全就是代码的注释。这样还可以保证不会因为多选、漏选等等意外而导致的错误。
此部分不是文章的一部分,编辑可以去掉。

开篇按语

 
洪荒少女傅园慧以其丰富的表情包和夸张的语言瞬间征服了全世界,赢得了比大多数冠军还高的关注度。这充分说明了仅仅有实力是不够的,还要有必要的营销手段。
每个人都有不同程度的武侠梦想和江湖情节,下面我们就以宏大的NET Framwork世界为背景,以c#为武器,看一个无名小辈如何炼成了名震江湖的编码大侠。一代IOC神器在大家的见证下即将呱呱坠地……

必备技能

 
想要在江湖扬名立万,自然要有两把刷子,这里要求你能够看懂c#(java也没问题,二者简直是同卵双胞胎)源代码,知道泛型、反射和弱引用的使用,当然还有单元测试。这是一个设计IOC框架的故事,所以什么是IOC和最佳拍档AOP你最好也了解一些,面向接口编程这样高端大气上档次的东西也需要知道。重构作为码农的基本功,是必须掌握的,你完全可以把这篇文章看做重构的教程,可以根据行文自己重构一下,然后对比看看二者的异同。
行走江湖,不免要展示自己的能力。如果只是欺负一些无名小辈和普通百姓,最多只能成为镇关西和南霸天这样的地痞,不可能成为名震江湖的大侠。而向有头脸的腕主们挑战或者征服、消灭前面的地痞无疑是展示武功的绝佳途径。下面开始我们的征服之旅:
  • 正式开始

 
作为练手升级的第一站,自然要挑软柿子捏,下面就是我们需要征服的目标,非常简单,表示人相关信息的类:
普通类的属性一般是这样定义的:*/
  public interface IPerson {
    string Name { get; set; }
    int Age { get; set; }
  }
 
  internal class Person : IPerson {
    public string Name { get; set; }
    public int Age { get; set; }
  }
 
  internal class Person1 : IPerson {
    private string name;
    private int age;
 
    public string Name
    {
      get { return name; }
      set { name = value; }
    }
 
    public int Age
    {
      get { return age; }
      set { age = value; }
    }
  }/*
 
  上面两个版本的本质完全一样,前者为后者的“语法糖”;而第二个实现更接近本质,我们就以后者为蓝本开始本次的发现之旅。
  • 开发需求

 
(需要说明,这里仅仅出于演示目的而没有考虑是否合理,另外在平时如果不需要我们后面添加的功能,而仅仅储存相关数据,使用上面定义已经足够快捷和好用,不需要画蛇添足)。我们看看这个类的实例都有什么能力:*/
  [TestClass]
  public partial class PersonTest {
    [TestMethod]
    public void TestSample1() {
      IPerson p = new Person1();
      Assert.AreEqual(p.Age, default(int));
      TestPersion(p);
    }
 
    private void TestPersion(IPerson p) {
      Assert.AreEqual(p.Name, default(string));
      p.Name = "aa";
      p.Age = 1000;
      Assert.AreEqual(p.Name, "aa");
      Assert.AreEqual(p.Age, 1000);
    }
  }/*
 
  很简单,没有为属性赋值的时候返回缺省值;赋值以后就储存在字段里面,需要则返回储存的新值。这里出头露面的属性Name实际仅仅是个马仔和提线木偶,它是什么值不由自己决定,而是由它后面的大哥name字段决定,因此征服了大哥name(这个大哥也太寒酸了,仅仅控制了一个小弟)也就征服了属性Name;其他属性同理。

一统小孤山

 
经过血雨腥风、不眠不休、遇神杀神、遇佛杀佛、大战三百合后(……此处省略300字,没办法,功力浅,碰到蚂蚁都要踩三脚才能干掉),我们征服了所有的大哥,把这些字段收降在一个宝袋(集合)里面,这样就可以用这个集合取代这一群字段,因为这些字段的类型和名称五花八门,这个集合的类型自然是它们的共同祖先:object类型了;而原来不同的字段名称也统一在集合的名下,也就是集合成为了这群属性的老大(众属性高呼:老大威武,老大万岁)。当然了,对字段的操作也需要相应转换为对集合的操作。想想秦始皇统一全中国干的不就是类似的事情吗?*/
  internal class Person2 : IPerson {
    private string a;
    private int b;
    private object[] backFields = { null, 0 };
 
    public string Name
    {
      //get { return name; }
      //set { name = value; }
      get { return (string)backFields[0]; }
      set { backFields[0] = value; }
    }
 
    public int Age
    {
      //get { return salay; }
      //set { salay = value; }
      get { return (int)backFields[1]; }
      set { backFields[1] = value; }
    }
  }
  public partial class PersonTest {
    [TestMethod]
    public void TestSample2() {
      TestPersion(new Person2());
    }
  }/*
  需要说明,因为泯灭了原来字段的个性(字段类型统一为object),现在的属性出现了磨洋工的情形(值类型对象转换为object后在读写时需要装箱和拆箱操作,这会导致其性能降低)。这是没有办法的事情,统一也是有代价的,这是我们必须承受的代价。另外和后面的收获相比,这点代价也是微不足道的。
此正是:
昔日秦皇灭六国
是非功过后人说
今天寄出大杀器
只用欧不接可特(object)

 初步优化,使用字典

 
我们比较两个版本中不同属性定义代码的差异,由需要修改两处变为了3处:分别是返回的强制转换类型和两处数组索引。前者的类型和属性的类型一致或兼容,后者由变量名改为了数字。也就是修改的内容都是符合逻辑的,还去掉了一堆的字段定义,就算扯平了或者小输吧。这个集合目前只是暂时寄居在这个类里面,将来一定要成家立业的。在集合里面仅仅保存了属性的值,而没有其他信息,导致我们需要与属性对应的时候必须根据属性里面的索引来推测;另外如果添加属性还需要添加对应的默认值,这太不方便了。怎么办?嗯,属性名称和对应值保存在一个字典里面是个不错的主意。下面我们小小修改一下:*/
  internal class Person2A : IPerson {
    //private object[] backFields = { null, 0 };
    private Dictionary<string, object> backFields = new Dictionary<string, object>();
 
    public string Name
    {
      get { return (string)backFields[nameof(Name)]; }
      set { backFields[nameof(Name)] = value; }
    }
 
    public int Age
    {
      get { return (int)backFields[nameof(Age)]; }
      set { backFields[nameof(Age)] = value; }
    }
  }
  public partial class PersonTest {
    [TestMethod]
    [ExpectedException(typeof(KeyNotFoundException))]
    public void TestSample2A() {
      TestPersion(new Person2A());
    }
  }/*
 
用属性名称代替数字无疑直观了许多,也弥补了收编大哥的部分自尊心:有名字的教师爷或武师,毕竟还是比仅有编号9527的奴才地位高一些的。在以后查询的时候直接查看集合就知道所有属性和对应的值了。

 回头审视,修改错误

 
针对上面存在的问题,继续我们修改和优化旅程:我们把集合修改为字典,初始状态为空,不像上面数组版本里面保存了各个属性对应的缺省值。这样就引入了bug:字典里面没有保存值的时候返回和修改属性值都会引发异常。我们先封装操作字典的方法,然后在这些方法里面修改:*/
  internal class Person3 : IPerson {
    public string Name
    {
      get { return (string)Get(nameof(Name)); }
      set { Set(nameof(Name), value); }
    }
 
 
    public int Age
    {
      get { return (int)Get(nameof(Age)); }
      set { Set(nameof(Age), value); }
    }
 
    private Dictionary<string, object> backFields = new Dictionary<string, object>();
 
    private void Set(string name, object value) {
      if (backFields.ContainsKey(name))
        backFields[name] = value;
      else
        backFields.Add(name, value);
    }
 
    private object Get(string name) {
      if (backFields.ContainsKey(name))
        return backFields[name];
      return null;
    }
  }
  public partial class PersonTest {
    [TestMethod]
    public void TestSample3() {
      TestPersion(new Person3());
    }
  }/*
 
这个版本比上面的多了两个方法,分别用于对集合的读写操作。管理这些属性的老大也仅仅在这里露下面,这样当我们需要添加其他功能(比如通知)或者出问题的时候在这里修改一下就可以了。(注意get的最后一句,又引入了bug,你知道吗?)
每个属性保存两个信息:属性名称和属性值,比起数组版本似乎还浪费了空间。其实不一定,这个字典集合在没有修改属性前是空的,其内容随着修改属性的个数而增加。这在有很多属性(比如常用的控件最少都有几十个属性)的情形下,反而是节省了不少的空间。
 
修改到这里其实已经与第一个版本有了质的不同。不过这个字典老大只能管理一个实例对象,如果生成了100个对象,就有100个同样的老大,又出现了前面类似管理大哥的问题。怎么办?为了更方便管理,我们请一位总管来管理这些老大,而我们只需要控制这个总管就可以了。按照上面的思路,我们的总管需要管理实例和对应的全部属性,字典那是必须的。

继续优化,初露端倪

 
提示:到了这里,我们其实有必要把集合与对应的操作方法移到公共类里面,作为所有对象的公共容器。不过还需要一些修改进化,现在总管集合还是暂时寄居在这里,但是修改为了static(静态成员表示此成员不是某个实例所有,而是所有实例公有),也就是有了公共容器的性质:*/
  internal class Person4 : IPerson {
    public string Name
    {
      get { return (string)Get(this, nameof(Name)); }
      set { Set(this, nameof(Name), value); }
    }
 
    public int Age
    {
      get { return (int)Get(this, nameof(Age)); }
      set { Set(this, nameof(Age), value); }
    }
 
    public static Dictionary<object, Dictionary<string, object>> Containor =
      new Dictionary<object, Dictionary<string, object>>();
 
    public static void Set(object instance, string name, object value) {
      var backFields = new Dictionary<string, object>();
      if (Containor.ContainsKey(instance))
        backFields = Containor[instance];
      else
        Containor.Add(instance, backFields);
      if (backFields.ContainsKey(name))
        backFields[name] = value;
      else
        backFields.Add(name, value);
 
    }
 
    public static object Get(object instance, string propertyName) {
      var backFields = new Dictionary<string, object>();
      if (Containor.ContainsKey(instance))
        backFields = Containor[instance];
      if (backFields.ContainsKey(propertyName))
        return backFields[propertyName];
      return null;
    }
  }
  public partial class PersonTest {
    [TestMethod]
    public void TestSample4() {
      TestPersion(new Person4());
    }
  }/*
 
进行到这里,我们获得了一个相对稳定的容器类,而实际的Sample类里面属性的方法体使用传统的强类型方法已经无法修改,也就是到达极限了。这样不同属性代码之间的差异有三处。也就是如果添加新属性,除了名称外,需要修改3个地方。这太繁琐了。为了减少修改量,我们使用反射试试:

反射参与,柳暗花明

 
(提示:反射是威力巨大的大杀器,与战呼局张局并列4大神器。小伙子,一般人我不告诉他,看你根骨神奇,是编程的奇才,将来编程世界的统一就靠你了。这本反射秘籍10块给你,怎么样?)*/
  internal class Person5 : IPerson {
    public string Name
    {
      get
      {
        var mi = MethodBase.GetCurrentMethod() as MethodInfo;
        return (string)Get(this, mi);
      }
      set
      {
        var mi = MethodBase.GetCurrentMethod() as MethodInfo;
        Set(this, mi, value);
      }
    }
 
 
    public int Age
    {
      get
      {
        var mi = MethodBase.GetCurrentMethod() as MethodInfo;
        return (int)Get(this, mi);
      }
      set
      {
        var mi = MethodBase.GetCurrentMethod() as MethodInfo;
        Set(this, mi, value);
      }
 
    }
 
    public static Dictionary<object, Dictionary<string, object>> Containor =
      new Dictionary<object, Dictionary<string, object>>();
 
    public static void Set(object instance, MethodInfo methodInfo, object value) {
      var name = GetPropertyName(methodInfo);
      var backFields = new Dictionary<string, object>();
      if (Containor.ContainsKey(instance))
        backFields = Containor[instance];
      else
        Containor.Add(instance, backFields);
      if (backFields.ContainsKey(name))
        backFields[name] = value;
      else
        backFields.Add(name, value);
 
    }
 
    private static string GetPropertyName(MethodInfo methodInfo) {
      var name = methodInfo.Name;
      name = name.Substring(4, name.Length - 4);
      return name;
    }
 
    public static object Get(object instance, MethodInfo methodInfo) {
      string propertyName = GetPropertyName(methodInfo);
      var backFields = new Dictionary<string, object>();
      if (Containor.ContainsKey(instance))
        backFields = Containor[instance];
      if (backFields.ContainsKey(propertyName))
        return backFields[propertyName];
      return null;
      if (methodInfo.ReturnType.IsValueType)
        return Activator.CreateInstance(methodInfo.ReturnType);
      return null;
    }
  }
  public partial class PersonTest {
    [TestMethod]
    public void TestSample5() {
      TestPersion(new Person5());
    }
  }/*
这样修改以后类的代码变多了,但是在属性里面代码需要修改的地方却减少了;或者说,不同属性代码的相似度提高了,添加新属性的时候需要修改的地方就少了。我们看看不同属性的代码,除了get代码里面的return语句的强制转换类型不一样,其他完全相同。据我夜观天象,这一处修改是胎里带的缺陷,不管使用什么方法都去不掉的,大罗神仙来了也没用。
另外传入的名称参数改为了传入方法信息,因为它不仅仅携带了方法的名称信息,还携带了定义类型的信息,这样就方便我们顺手消除了一个bug。
修改到这里,基本到达我们的要求,而一个新的类也应该诞生了;同时考虑操作对象的方法:Get和Set,前者的参数要比后者少一个,另外方法信息里面的方法名称也有get和set字符串,因此就没有必要利用名称区分了;再考虑到属性信息不但携带了属性名称,还带有属性类型等等其他信息,使用它做键比属性名称好多了;最后,存放在这个容器里的对象为强引用,当其他地方都不再引用某个对象而应该被回收时,却因为这里的引用而不能被回收显然是不合理的,顺手修改一下上面存在的问题。

小结:

 
本文使用不同的思路,以实用为导向,用幽默风趣的语言,从一个普通的数据类作为起始点,利用统一、反射等手段,像化学的萃取实验一样,不断把相同元素集中起来;经过一步一步修改,从量变到发生质变,最后形成一个框架的雏形。为后面的演绎拉开了序幕……
此正是:
秦皇汉武功德高 
一统天下胆气豪 
依赖注入掀巨浪 
烦人藕合一扫光  
预知今天最终版本如何,请听下回分解:
Spring Net 相关文件下载:

 http://files.cnblogs.com/files/mrzhuZone/SpringAOPCORE.zip

IOC演义 第一回: 重构类步步为营 新框架萌芽胎动