首页 > 代码库 > IOC演义1

IOC演义1

IOC演义1

开篇按语

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

必备技能

想要在江湖扬名立万,自然要有两把刷子,这里要求你能够看懂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字符串,因此就没有必要利用名称区分了;再考虑到属性信息不但携带了属性名称,还带有属性类型等等其他信息,使用它做键比属性名称好多了;最后,存放在这个容器里的对象为强引用,当其他地方都不再引用某个对象而应该被回收时,却因为这里的引用而不能被回收显然是不合理的,顺手修改一下上面存在的问题。

小结:

本文使用不同的思路,以适用为导向,用幽默风趣的语言,从一个普通的数据类作为起始点,一步一步修改,最后形成一个框架的雏形。为后面的演绎拉开了序幕……

结语

你看我嘚啵了大半天,肚子也咕咕叫了,你们谁请我吃顿饭……哎哎……别走呀,各位,怎么一到关键时候就掉链子?说你呢,还笑……不请吃饭就算了,点个赞,回个支持,这点小小的要求不过分吧?(此处应该有掌声)
此正是:秦皇汉武功德高 一统天下胆气豪 依赖注入掀巨浪 斩断一切紧藕合  
预知今天最终版本如何,请听下回分解:
互动问题:
1:大哥、老大、总管各有什么功能?
2:你觉得这几个版本哪两个之间的跨度最大?最有意义?哪个版本最糟糕?
3:总管是字典,每条记录都储存了什么信息?
4、中午饭谁请?
 

IOC演义1