Entity Factory 设计模式(.NET版)






4.84/5 (26投票s)
一种能够实现极高性能的实体模式。
引言
与其他允许您从基类创建派生对象的工厂模式类似,Entity Factory 模式允许您从基实体或“元素”创建派生实体。元素包含特定数据片段的规则,以及包含数据本身的“数据包装器”。这使得在验证或分析实体对象时可以实现极高的性能。
为了实现这一级别的性能,我们需要以一种不同于标准实践的方式来定义我们的实体。实体类中的属性 getter 和 setter 不再直接操作私有字段,而是通过 `BaseHash` 类中的 `Hashtable` 来操作“规则”字段,“实体”字段则通过 `EntityAbstract` 类中的 `Elements` 集合来操作。这并不是一个难以实现的调整,并且非常值得付出努力。除了性能之外,此模式还提供了一种在所有应用程序中维护数据约束的简单方法。
UML 类图
BaseHash 类
当将一个实体转换为另一个实体时,新实体中不会填充属性值。这可以通过在派生类中包含一个接受基类实例作为参数的构造函数来解决。派生类然后将该参数传递给基类的构造函数。
public ElementBaseData(ElementBase elementBase) : base(elementBase)
public ElementBase(ElementBase elementBase)
在基类构造函数中,您可以硬编码要设置的字段以匹配参数的值,或者使用反射来遍历参数中的字段并填充新对象中相应的字段。第一种方法速度很快,但也很死板。后一种方法提供了通用的灵活性,但速度非常慢。反射不仅慢,而且随着字段的增加,其速度会与第一种方法不成比例地下降。
基准测试(10万次迭代)
1 个字段 |
10 个字段 |
硬编码 ~20ms | 硬编码 ~50ms |
反射 ~500ms | 反射 ~2800ms |
通过使用 `Hashtable` 来管理字段值,我们可以兼顾这两种方法的优点。现在,我们的 `ElementBase` 构造函数如下所示。
public ElementBase(ElementBase elementBase)
{
Hash = elementBase.Hash;
}
我们的基准测试结果比硬编码方法稍高(1 个字段 = ~110ms),但随着字段数量的增加,哈希方法的处理效率比其他两种方法都高(10 个字段 = ~210ms)。十个字段的处理时间不到一个字段的两倍。
首先,我们需要创建 `BaseHash` 类。它只包含一个 `Hashtable` 类型的属性和一个名为 `InitializeProperties` 的方法。 `InitializeProperties` 方法的用途将在下一节“ `ElementBase` 类”中介绍。
public class BaseHash
{
private Hashtable _hash = new Hashtable();
public BaseHash()
{
}
public Hashtable Hash
{
get { return _hash; }
set { _hash = value; }
}
protected void InitializeProperties(Type enumerator)
{
int i = 0;
Enum.GetNames(enumerator).ToList().ForEach(e => _hash.Add(i++, null));
}
}
ElementBase 类
数据约束存储在 `ElementBase` 类中。下面的示例仅包含一小部分字段。您可能对约束有其他要求,例如“ `IsNumeric` ”或“ `MatchCode` ”。您可以简单地将这些属性添加到 `ElementBase` 中,并在 `ElementBaseData` 类中定义它们的行为。为简单起见,我只创建几个字段。
请注意下面示例中属性 getter 和 setter 的编写方式。它们操作的是 `BaseHash` 中的 `Hashtable`,而不是私有字段。这将为我们带来前面部分提到的速度和灵活性。我们需要在实例创建时初始化属性,否则属性会因为 `Hashtable` 中的索引尚未设置而产生错误。
public class ElementBase : BaseHash, IBase
{
public enum ElementBaseFields : int
{
ID,
Name,
MaxLength,
MinLength
}
public ElementBase()
{
InitializeProperties(typeof(ElementBaseFields));
}
public ElementBase(ElementBase elementBase)
{
Hash = elementBase.Hash;
}
[DataMember]
public int? ID
{
get
{
if (Hash[(int)ElementBaseFields.ID] == null)
return null;
return int.Parse(Hash[(int)ElementBaseFields.ID].ToString());
}
set { Hash[(int)ElementBaseFields.ID] = value; }
}
[DataMember]
public string Name
{
get
{
if (Hash[(int)ElementBaseFields.Name] == null)
return null;
return Hash[(int)ElementBaseFields.Name].ToString();
}
set { Hash[(int)ElementBaseFields.Name] = value; }
}
[DataMember]
public int? MaxLength
{
get
{
if (Hash[(int)ElementBaseFields.MaxLength] == null)
return null;
return int.Parse(Hash[(int)ElementBaseFields.MaxLength].ToString());
}
set { Hash[(int)ElementBaseFields.MaxLength] = value; }
}
[DataMember]
public int? MinLength
{
get
{
if (Hash[(int)ElementBaseFields.MinLength] == null)
return null;
return int.Parse(Hash[(int)ElementBaseFields.MinLength].ToString());
}
set { Hash[(int)ElementBaseFields.MinLength] = value; }
}
}
ElementBaseData 类
`ElementBaseData` 是 `ElementBase` 的一个数据包装器。在这里,我们可以以传统的方式创建 `Data` 属性。我们将创建一个名为 `_data` 的私有变量字段,并创建一个使用它的名为 `Data` 的属性。
使用此模式时,一个重要考虑因素是克隆。如果我们只是克隆此项,它将继续与原始项共享 `Hashtable`。这可能会导致严重问题。包含一个 `Clone` 方法至关重要,该方法也将为我们克隆 `Hashtable`。
我还包含了一个 `Validation` 方法。这将演示此模式带来的高性能。您可以看到我们如何利用 `ElementBase` 中公开的属性来执行我们的验证。
`Validation` 方法公开的异常是字符串而不是 `Exception` 对象。由于问题不是堆栈问题,因此不需要 `Exception` 对象的开销。
public class ElementBaseData : ElementBase, IBaseData, ICloneable
{
private object _data = null;
public ElementBaseData() : base()
{
}
public ElementBaseData(ElementBase elementBase)
: base(elementBase)
{
}
[DataMember]
public object Data
{
get { return _data; }
set { _data = value; }
}
[OperationContract]
public object Clone()
{
ElementBaseData elementBaseData = this.MemberwiseClone() as ElementBaseData;
elementBaseData.Hash = Hash.Clone() as Hashtable;
return elementBaseData;
}
[OperationContract]
public List<string> Validate()
{
List<string> exceptions = new List<string>();
if (MaxLength != null && _data.ToString().Length > MaxLength)
exceptions.Add(String.Format("Property: {0}{1}{2}", Name, Environment.NewLine, "Data is too long"));
if (MinLength != null && _data.ToString().Length < MinLength)
exceptions.Add(String.Format("Property: {0}{1}{2}", Name, Environment.NewLine, "Data is too short"));
return exceptions;
}
}
EntityAbstract 类
现在,我们可以创建实体的抽象类了。我们实体的属性值将存储在 `EntityAbstract` 的 `Elements` 属性中,并从中检索。 `Elements` 属性是 `ElementBaseData` 对象的集合。 `EntityAbstract` 的另一个属性是 `Exceptions` 属性。这是一个由 `ElementBaseData` 对象上的 `Validation` 方法生成的异常字符串集合。
我们遇到了与 `ElementBaseData` 类中相同的克隆问题。为避免共享相同的元素集合,我们需要一个 `Clone` 方法来为我们克隆元素。此外,在 `EntityAbstract` 类中,我们还需要创建一个 `Validation` 方法。这将使我们的每个实体都具备快速验证实体内所有元素的功能。验证后,我们可以调用 `Exceptions` 属性来获取生成的任何异常。
public abstract class EntityAbstract : ICloneable
{
private List<ElementBaseData> _elements = new List<ElementBaseData>();
private List<string> _exceptions = new List<string>();
public List<ElementBaseData> Elements
{
get { return _elements; }
set { _elements = value; }
}
public List<string> Exceptions
{
get { return _exceptions; }
set { _exceptions = value; }
}
public virtual object Clone()
{
List<ElementBaseData> elements = new List<ElementBaseData>();
Elements.ForEach(e => elements.Add(e.Clone() as ElementBaseData));
return elements;
}
public virtual bool Validate()
{
Exceptions.Clear();
return Elements.FindAll(e => !Validate(e)).Count == 0;
}
private bool Validate(ElementBaseData elementBaseData)
{
List<string> elementExceptions = elementBaseData.Validate();
elementExceptions.ForEach(ex => _exceptions.Add(ex));
return elementExceptions.Count == 0;
}
}
实体类和 ElementIndex
`ElementIndex` 是元素规则的内存存储库。在此示例中,我使用了一个静态类,但单例模式同样有效。如果您需要跟踪使用情况或想要对 `ElementIndex` 进行负载均衡,则需要使用单例模式。此演示不需要这些,因此我选择展示静态类以保持简单。
`ElementIndex` 的一个优点是它允许您在实体和系统之间维护数据标准。例如,您可能有一个系统可以接受 25 个字符长的姓氏,而另一个系统可以接受 35 个字符长的姓氏。使用 `ElementIndex`,您可以确保始终使用最宽松的通用标准,并避免在收集数据后进行截断。
public static class ElementIndex
{
public readonly static ElementBase FirstName = new ElementBase()
{
Name = "First Name",
MinLength = 1,
MaxLength = 25
};
public readonly static ElementBase LastName = new ElementBase()
{
Name = "Last Name",
MinLength = 1,
MaxLength = 25
};
public readonly static ElementBase Age = new ElementBase()
{
Name = "Age",
MinLength = 1,
MaxLength = 3
};
}
现在我们可以开始创建实体了。在此演示中,我将创建一个扩展 `EntityAbstract` 的 `TestEntity` 类。在构造函数中,我将每个字段添加到 `EntityAbstract` 的 `Elements` 属性中。字段是 `ElementBaseData` 对象,它由 `ElementIndex` 中定义的 `ElementBase` 构建。接下来,我为构造函数中创建的每个元素创建一个属性。
同样,我需要考虑克隆方法。我可以通过重写基类的 `Clone` 方法来处理此问题,同时仍然使用基类方法来重建我的元素集合。
public class TestEntity : EntityAbstract
{
public enum TestEntityFields : int
{
FirstName,
LastName,
Age
}
public TestEntity()
{
Elements.Add(new ElementBaseData(ElementIndex.FirstName)
{
ID = (int)TestEntityFields.FirstName
});
Elements.Add(new ElementBaseData(ElementIndex.LastName)
{
ID = (int)TestEntityFields.LastName
});
Elements.Add(new ElementBaseData(ElementIndex.Age)
{
ID = (int)TestEntityFields.Age
});
}
public string FirstName
{
get { return Elements[(int)TestEntityFields.FirstName].Data.ToString(); }
set { Elements[(int)TestEntityFields.FirstName].Data = value; }
}
public string LastName
{
get { return Elements[(int)TestEntityFields.LastName].Data.ToString(); }
set { Elements[(int)TestEntityFields.LastName].Data = value; }
}
public int Age
{
get { return int.Parse(Elements[(int)TestEntityFields.Age].Data.ToString()); }
set { Elements[(int)TestEntityFields.Age].Data = value; }
}
public override object Clone()
{
List<ElementBaseData> elements = base.Clone() as List<ElementBaseData>;
TestEntity testEntity = this.MemberwiseClone() as TestEntity;
testEntity.Elements = elements;
return testEntity;
}
}
测试模式
我编写了一个简单的控制台应用程序来测试和基准测试我们模式的性能。它将遍历 `TestEntity` 类中的元素,并就每个元素向用户询问输入。收集完所有输入后,它将执行 1000 次验证,并以毫秒为单位报告持续时间。它还会报告数据是否有效,或者显示遇到的所有错误。
class Program
{
private static void GetUserInput(ElementBaseData elementBaseData)
{
Console.Write(String.Format("{0}: ", elementBaseData.Name));
elementBaseData.Data = Console.ReadLine();
}
static void Main(string[] args)
{
while(true)
{
TestEntity testEntity = new TestEntity();
testEntity.Elements.ForEach(e => GetUserInput(e));
System.Diagnostics.Stopwatch bench = new System.Diagnostics.Stopwatch();
bench.Start();
int iEnd = 1000;
for (int i = 0; i < iEnd; i++)
{
testEntity.Validate();
}
bench.Stop();
Console.WriteLine(String.Format("Validated {0} times in {1} milliseconds.",
iEnd.ToString(), bench.ElapsedMilliseconds.ToString()));
if (testEntity.Validate())
Console.WriteLine("Data is valid");
else
testEntity.Exceptions.ForEach(ex => Console.WriteLine(ex));
}
}
}
结论
此模式提供了两个主要优点:高性能和合并约束变量的能力。它还创建了一个框架,可用于持久化和报告等更高级的功能。
尽管我们改变了属性 getter 和 setter 操作值的方式,但实体的属性和方法仍然像传统实体暴露其成员一样暴露。这将允许您继续像以前一样使用接口和 LINQ to Entities。任何使用 `GetField` 的反射都需要修改为使用 `Elements` 属性。这将为您带来更好的性能。
在我的下一篇文章中,我将演示如何使用与 Entity Factory Pattern 完美集成的 Memento Pattern,为您的应用程序提供“Ctrl-Z”、“Ctrl-Y”功能。