“造轮运动”之 OOM框架
一、什么是OOM框架?
OOM 的全拼是 Object-Object-Map,意思是对象与对象之间的映射,OOM框架要解决的问题就是对象与对象之间数据的自动映射。
举一个具体的例子:用过MVC模式开发Web后台的小伙伴们都知道EO(Entity Object,实体对象)与DTO(Data Transfer Object,数据传输对象)之间需要进行一个转换。使用最原始的方法,我们会像这样去进行转换操作:
1 /// <summary> 2 /// EO类 3 /// </summary> 4 public class Student_EO 5 { 6 public string Id { get; set; } 7 8 public string Name { get; set; } 9 10 public int Age { get; set; } 11 } 12 13 /// <summary> 14 /// DTO类 15 /// </summary> 16 public class Student_DTO 17 { 18 public string Id { get; set; } 19 20 public string StudentName { get; set; } 21 22 public int Age { get; set; } 23 } 24 25 /// <summary> 26 /// EO对象转DTO对象 27 /// </summary> 28 /// <param name="eo"></param> 29 /// <returns></returns> 30 public Student_DTO _toDTO(Student eo) 31 { 32 Student_DTO dto = new Student_DTO(); 33 dto.Id = eo.Id; 34 dto.StudentName = eo.Name; 35 dto.Age = eo.Age; 36 37 return dto; 38 } 39
看起来也不难嘛,这个代码很容易写。但是要注意的是,你的项目中可能有几十上百个EO类与DTO类需要进行这样的映射转换,作为一个懒惰的程序员这是最不能容忍的事情。那么,想偷懒,那就必须先动动脑子......
于是,我们希望有一个框架能帮我们避免这枯燥乏味重复的代码,有一点想法的程序员很快就会想到:我们可以使用反射去获取类的字段将它们对应的去赋值。当然,我也是这么想,并且也是这么做的。于是废话不多说,开始动手,创造属于你自己的OOM框架吧,奥利给!!!!!
二、现成的OOM框架
在即将自己动手实现OOM框架之前,当然是要先了解了解目前有哪些OOM框架在流行,毕竟你能想到的问题大部分人已经想到了,并且可能已经有了很好的解决方案,比如AutoMapper,EmitMapper...这些框架,不得不承认,这些轮子已经很好用了,它们已经提供了丰富而全面的功能,但是,这并不妨碍我探索的激情。最后我也会总结一下自己“土法炮制”的OOM框架与AutoMapper框架对比的情况。废话不多说,往下看。
三、自制OOM框架的需求分析及技术选择
决定要做一件事情之前,你最好要清楚地知道你将完成的东西最后具体是什么样的,这样的一个好处是能理清你需要的功能点,再一个就是你要确定你想要的是不是这样一个东西。
Ok,我现在想要做一个OOM框架,就把它叫做CoffeeMapper框架吧,那么我脑海里CoffeeMapper的模样是这样的:
1、实现任意两个类的对象之间的属性的映射,默认相同属性名的属性之间进行映射,也可指定要映射的属性名
2、可以自由设定映射的逻辑
3、映射转换的速度尽可能的快
其中第一条最好理解,那就是实现OOM框架最基本的功能,值的映射关系绑定,我们可以通过添加Attribute标签的方式进行绑定标注。
第二条也很好理解,默认的映射逻辑是等值映射,也就是映射类与被映射类之间对应的属性值是相等的;自由设定映射逻辑也就是说我可以根据需要设置我自己的映射逻辑,比如:原来的值加个前缀或后缀再映射...
最后一条是最模糊的,什么叫做映射转换的速度尽可能的快?那我们就要清楚,如果使用的是反射的方法先创建一个目标的对象,然后再逐个对对象的属性值进行赋值操作,那么你必须要知道的一点是利用反射去创建一个对象的性能是不理想的,就像这样子:
Form1 form1 = Activator.CreateInstance(typeof(Form1)) as Form1;
看大神关于反射性能对比的分析:https://www.cnblogs.com/7tiny/p/9861166.html(再看ExpressionTree,Emit,反射创建对象性能对比)
那么如何避免这个性能的瓶颈呢?在这里我选择运用表达式树的技术来提升框架整体的性能。
四、源代码展示与思路分析
代码也不多,就一起放在这了,方便小伙伴们查看
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class MapperAttribute:Attribute { /// <summary> /// the Property Name that map to /// </summary> public string MapTo { get; set; } public MapperAttribute() { } public MapperAttribute(string mapTo) { this.MapTo = mapTo; } public static string GetMapToPropertyName(PropertyInfo propertyInfo) { object[] mapperAttrs = propertyInfo.GetCustomAttributes(typeof(MapperAttribute), false); MapperAttribute mapperAttr; if (mapperAttrs != null && mapperAttrs.Count() >= 1) { mapperAttr = mapperAttrs[0] as MapperAttribute; return mapperAttr.MapTo; } else { return propertyInfo.Name; } } }
1 [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 2 public class NoMapperAttribute:Attribute 3 { 4 }
1 public static class TypeExtension 2 { 3 /// <summary> 4 /// Get All level inherit class types of current type 5 /// </summary> 6 /// <param name="currentType"></param> 7 /// <param name="outInheritClassList">storage result of type</param> 8 public static void GetInheritClassTypes(Type currentType, ref List<Type> outInheritClassList) 9 { 10 outInheritClassList.Add(currentType); 11 12 if (currentType.BaseType.Name != "Object") 13 { 14 GetInheritClassTypes(currentType.BaseType, ref outInheritClassList); 15 } 16 else 17 { 18 return; 19 } 20 } 21 }
1 public sealed class CoffeeMapper<TIn, TOut> where TIn:class where TOut:class 2 { 3 private static readonly Func<TIn, TOut> funcCache = FuncFactory(); 4 public static TOut AutoMap(TIn InData, Action<TOut, TIn> action = null) 5 { 6 TOut _out = funcCache(InData); 7 8 if (null != action) action(_out, InData); 9 10 return _out; 11 } 12 private static Func<TIn, TOut> FuncFactory() 13 { 14 #region get Info through Reflection 15 16 var _outType = typeof(TOut); 17 var _inType = typeof(TIn); 18 var _outTypeProperties = _outType.GetProperties(BindingFlags.Instance | BindingFlags.Public); 19 var _outTypePropertyNames = _outTypeProperties.Select(p => p.Name); 20 21 #endregion 22 23 #region some Expression class that can be repeat used 24 25 //Student in 26 var _inDeclare = Expression.Parameter(_inType, "_in"); 27 //StudentDTO _out 28 var _outDeclare = Expression.Parameter(_outType, "_out"); 29 //new StudentDTO() 30 var new_outEntityExpression = Expression.New(_outType); 31 //default(StudentDTO) 32 var default_outEntityValue = Expression.Default(_outType); 33 //_in == null 34 var _inEqualnullExpression = Expression.Equal(_inDeclare, Expression.Constant(null)); 35 36 #endregion 37 38 var set_inEntityNotNullBlockExpressions = new List<Expression>(); 39 40 #region _out = new StudentDTO(); 41 set_inEntityNotNullBlockExpressions.Add(Expression.Assign(_outDeclare, new_outEntityExpression)); 42 #endregion 43 44 PropertyInfo[] needMapPropertys = ScanAllPropertyNeedMap(); 45 46 foreach (var propertyInfo in needMapPropertys) 47 { 48 string mapToName = MapperAttribute.GetMapToPropertyName(propertyInfo); 49 50 //no contain, no map 51 if (!_outTypePropertyNames.Contains(mapToName)) 52 continue; 53 54 //no type equal, no map and expection 55 if (_outTypeProperties.First(p => p.Name == mapToName).PropertyType.FullName != propertyInfo.PropertyType.FullName) 56 continue; 57 58 if (propertyInfo.CanWrite) 59 { 60 //_out.Id 61 var _outPropertyExpression = Expression.Property(_outDeclare, _outTypeProperties.First(p => p.Name == mapToName)); 62 //_in.Id 63 var _inPropertyExpression = Expression.Property(_inDeclare, propertyInfo); 64 65 //_out.Id = _in.Id; 66 set_inEntityNotNullBlockExpressions.Add( 67 68 Expression.Assign(_outPropertyExpression, _inPropertyExpression) 69 ); 70 } 71 } 72 73 var checkIf_inIsNull = Expression.IfThenElse( 74 _inEqualnullExpression, 75 Expression.Assign(_outDeclare, default_outEntityValue), 76 Expression.Block(set_inEntityNotNullBlockExpressions) 77 ); 78 79 var body = Expression.Block( 80 81 new[] { _outDeclare }, 82 checkIf_inIsNull, 83 _outDeclare //return _out; 84 ); 85 86 return Expression.Lambda<Func<TIn, TOut>>(body, _inDeclare).Compile(); 87 } 88 89 /// <summary> 90 /// Get All Property Info that need be mapped 91 /// </summary> 92 /// <typeparam name="TIn"></typeparam> 93 /// <param name="InData"></param> 94 /// <returns></returns> 95 private static PropertyInfo[] ScanAllPropertyNeedMap() 96 { 97 List<PropertyInfo> propertyInfoList = new List<PropertyInfo>(); 98 99 //取得包括當前層級類在內的所有繼承的每一層祖先類型 100 List<Type> inheritClassList = new List<Type>(); 101 TypeExtension.GetInheritClassTypes(typeof(TIn), ref inheritClassList); 102 103 foreach (Type classType in inheritClassList) 104 { 105 var attrs = classType.GetCustomAttributes(typeof(MapperAttribute), false); 106 if (null == attrs || attrs.Count() <= 0) continue; 107 108 PropertyInfo[] currentClassPropertyInfos = classType.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance) 109 .Where(proInfo => proInfo.GetCustomAttributes(typeof(NoMapperAttribute)).Count() <= 0) 110 .ToArray(); 111 propertyInfoList.AddRange(currentClassPropertyInfos); 112 } 113 114 return propertyInfoList.ToArray(); 115 } 116 117 #region 118 //#region 實驗類型 119 //public class EntityBase 120 //{ 121 // /// <summary> 122 // /// storage QueryField's Result 123 // /// </summary> 124 // private Dictionary<string, object> queryFieldsDictionary = new Dictionary<string, object>(); 125 126 //} 127 128 //[Mapper] 129 //public class BaseEO : EntityBase 130 //{ 131 // public string id { get; set; } 132 //} 133 //[Mapper] 134 //public class Student : BaseEO 135 //{ 136 // [Mapper("studentName")] 137 // public string name { get; set; } 138 // public int age { get; set; } 139 // [NoMapper] 140 // public string nomapField { get; set; } 141 //} 142 143 //public class BaseDTO 144 //{ 145 // public string id { get; set; } 146 //} 147 //public class StudentDTO : BaseDTO 148 //{ 149 // public string studentName { get; set; } 150 // public int age { get; set; } 151 //} 152 //#endregion 153 154 //public StudentDTO Student_AutoMapTo_StudentDTO(Student _in) 155 //{ 156 // StudentDTO _out; 157 158 // if (_in == null) 159 // _out = default(StudentDTO); 160 // else 161 // { 162 // _out = new StudentDTO(); 163 164 // _out.id = _in.id; 165 // _out.age = _in.age; 166 // _out.studentName = _in.name; 167 // } 168 169 // return _out; 170 //} 171 #endregion 172 173 }
重要优化点思路:
每一个映射类都会生成一个进行值映射的方法委托,并将其以类静态字段的方式缓存下来,除第一次需要执行表达式树生成映射方法之外,以后的每一次调用都是直接调用缓存下来的委托,其执行效率和直接执行一个方法的效率几乎是一样的,这就达到了映射转换的速度尽可能的快的要求。
现在,我们可以看一下如何使用“土炮”—CoffeeMapper进行对象之间的属性值映射:
1 public class EntityBase 2 { 3 } 4 5 [Mapper] 6 public class BaseEO:EntityBase 7 { 8 public string id { get; set; } 9 } 10 11 [Mapper] 12 public class Student:BaseEO 13 { 14 [Mapper("studentName")] 15 public string name { get; set; } 16 public int age { get; set; } 17 } 18 19 public class BaseDTO 20 { 21 public string id { get; set; } 22 } 23 public class StudentDTO:BaseDTO 24 { 25 public string studentName { get; set; } 26 public int age { get; set; } 27 } 28 29 class Program 30 { 31 32 static void Main(string[] args) 33 { 34 Student s = new Student { id = "123456", name = "wuqiansen", age = 18 }; 35 //一行代码实现映射 36 StudentDTO t = CoffeeMapper<Student, StudentDTO>.AutoMap(s, (t1, t2)=> { t1.studentName = t2.name+"default"; }); 37 } 38 39 }
我们看到程序中使用了一行代码就完成了对象之间的属性值映射,程序员的头发又可以少掉几根了!!!!
五、总结收获
通过这次OOM框架的造轮子,更加熟练了表达式树技术的使用。这是一个非常简单的轮子,其实与AutoMapper对比起来,这个框架就显得过于简单了,还有很多需要提高和完善的地方:比如,只可以进行扁平类之间的映射、只实现了属性的映射、只实现了一对一的类对象映射。但是,优点也恰恰是来源于此,由于它功能的简单,所以它能在满足日常开发需求的前提下达到轻量级、性能优秀的要求。最重要的还是在这个过程中学到了很多!!!!!