我正在努力解决一个困扰了我几个月的难题。
我的同事和我在一个技术问题上意见完全不一致,我想听听我们亲爱的社区对这件事的看法。
简单来说:
是使用枚举(带有诸如"Description"之类的潜在属性)还是使用实体(带有Name和Description属性)更可取?
在细节:
在我们的领域模型中,我们有很多只包含Id、Name和Description的迷你实体。95%的情况下,"描述"等于"名称"。
为了便于解释,我将举一个例子:在我们的Security实体中,我们有一个AssetClass属性。AssetClass有一个静态的值列表("Equity","Bond"等),并且不会从接口或其他任何东西中改变。问题是,当你想获得资产类别为"Bond"的所有证券时,NHibernate将不得不加入AssetClass表…考虑到AssetClass不是唯一的属性,你可以想象所有这些连接对性能的影响。
我们目前的解决方案:(我不同意):
我们的代码中有一些硬编码的AssetClass实例,它们具有各自的值和Id(即Id为1的股权,Id为2的债券等),它们与数据库中的内容相匹配:
public partial class AssetClass
{
static public AssetClass Equity = new AssetClass(1, "Equity", "Equity");
static public AssetClass Bond = new AssetClass(2, "Bond", "Bond");
static public AssetClass Future = new AssetClass(3, "Future", "Future");
static public AssetClass SomethingElse = new AssetClass(4, "Something else", "This is something else");
}
我们还做了一个特殊的NHibernate类型(如果你感兴趣的话,下面的代码),它允许我们通过加载硬编码实例来避免NHibernate进行连接,而不是去数据库获取它:
using System;
using System.Data;
using System.Data.Common;
using NHibernate.Dialect;
using NHibernate.SqlTypes;
using NHibernate.Type;
namespace MyCompany.Utilities.DomainObjects
{
public abstract class PrimitiveTypeBase<T> : PrimitiveType where T : class, IUniquelyNamed, IIdentifiable
{
private readonly PrimitiveTypeFactory<T> _factory;
public PrimitiveTypeBase() : base(SqlTypeFactory.Int32)
{
_factory = new PrimitiveTypeFactory<T>();
}
public override string Name
{
get { return typeof(T).Name; }
}
public override Type ReturnedClass
{
get { return typeof(T); }
}
public override Type PrimitiveClass
{
get { return typeof (int); }
}
public override object DefaultValue
{
get { return null; }
}
public override void Set(IDbCommand cmd, object value, int index)
{
var type = value as T;
var param = cmd.Parameters[index] as DbParameter;
param.Value = type.Id;
}
public override object Get(IDataReader rs, int index)
{
return GetStaticValue(rs[index]);
}
public override object Get(IDataReader rs, string name)
{
return GetStaticValue(rs[name]);
}
private T GetStaticValue(object val)
{
if (val == null)
{
return (T)DefaultValue;
}
int id = int.Parse(val.ToString());
T entity = _factory.GetById(id); // that returns, by reflection and based on the type T, the static value with the given Id
if (entity == null)
{
throw new InvalidOperationException(string.Format("Could not determine {0} for id {1}", typeof (T).Name, id));
}
return entity;
}
public override object FromStringValue(string xml)
{
return GetStaticValue(xml);
}
public override string ObjectToSQLString(object value, Dialect dialect)
{
var type = value as T;
return type.Id.ToString();
}
}
}
我的解决方案:(我同意:-))
用枚举替换这些实体,如果我们需要Description字段,使用属性。我们还可以对数据库进行约束,以确保不能存储与enum不匹配的随机值。
它们反对使用枚举的理由:
- 这不是一个对象,所以你不能扩展它,它不是面向对象的,等等。
- 你不能轻易获得描述,或者有"适当的英文"名称(带有空格或符号),如"我的值"(在enum上将是"MyValue")
- 枚举了
- 属性吸
我反对当前解决方案的理由:
- 代码中的id和数据库中的id可能不匹配
- 它很难维护(我们需要绝对确保我们拥有的每个硬编码值也在数据库中)
- 属性和枚举如果使用得当,对于这些静态值不会很糟糕
- 对于"正确的英文"名称,我们也可以使用属性,并使用一些扩展方法来消费它。
现在,你怎么想?
我个人更喜欢第一种解决方案,可能有一个额外的属性,返回所有的值。
更多的是OO -枚举基本上是"命名数字",仅此而已。在代码的其他地方,状态都存储在属性中——那么为什么要使用属性呢?正如Eric Lippert在一篇比较两者的博客文章中所写的那样,"属性是关于机制的事实"。你基本上是把它们作为一种提供关于值的数据的方式,而不是,这对我来说是错误的。
关于代码和数据库之间潜在的不匹配,你对使用poco的前两个反对意见也不正确——因为它们对枚举也是完全相同的,不是吗?此外,编写一个验证数据的测试非常容易——如果你愿意,甚至可以在启动时这样做。
不清楚AssetClass
的其余部分是做什么的,但如果它只有一个私有构造函数,那么您就可以在有限的,众所周知的值集方面获得枚举的许多好处,但没有枚举基本上只是数字的问题。
FooEnum invalid = (FooEnum) 12345;
你要到处检查吗?一般来说,空引用比无效的枚举值更早出现,也更容易检查。
我能想到的POCO方法的一个缺点是你不能打开它。有一些方法可以解决这个问题,但它们不是很令人愉快-你基本上必须也必须有一组众所周知的数字(当然可以是enum)以便某些属性会返回它并且你可以打开它
我真的不喜欢这两个选项,因为代码和数据库之间的ID可能不匹配。你实际上指出了这一点,但出于某种原因,你似乎认为如果你使用枚举,这个问题就会消失,而实际上你会得到完全相同的东西?
如果我必须在两者之间做出选择,我会选择当前实现,原因是Jon Skeet提到的。
然而,我认为最好的方法是创建一个正常的类并从数据库中获取项目,并根据属性编写程序。我不久前写了这篇文章来解释我的意思:针对属性的程序我个人更喜欢将枚举作为字符串存储在数据库中。您可能需要的任何其他信息都可以在它自己的表中使用enum类型和value作为键。不过,我不会将描述存储为属性。另外,转换Enum也很简单。一个包含"Some Value"的字符串
我更喜欢枚举,主要是因为它们给了你编译器的支持,并且很容易通过简单的重构来修改添加新值,而字符串值在值改变时可能会产生一些隐藏的bug。
if(obj.EnumValue == Enum.Value)
比这个好
if(obj.Value == "SomeValues")
我使用GenericEnumMapper自带的fluent-nhibernate,但如果你使用hibernate,你可以使用EnumString类型。这提供了一种很好的方式将枚举作为数据库中的字符串,这样您的数据对于没有访问代码的人来说是可读的。