问题描述
我有某些实体(类)。每个实体可以具有与其关联的元数据。一个实体最多只能有一个每种类型的元数据对象。
简化示例:
// Approach 1
public interface Entity {}
public interface MetadataBase {}
public sealed class EntityPrice : MetadataBase {
public int Price{ get; }
public Price(int price) => Price = price;
}
public sealed class EntityAmount : MetadataBase {
public int Amount { get; }
public EntityAmount(int amount) => Amount = amount;
}
// somewhere in user code
Entity someEntity;
MetadataManager.Associate(someEntity,new EntityPrice(13));
MetadataManager.Associate(someEntity,new EntityAmount(17));
// somewhere else later
var price = MetadataManager.Get<EntityPrice>(someEntity).Price;
var amount = MetadataManager.Get<EntityAmount>(someEntity).Amount;
另一种方法是让具体的实体类实现许多接口(每个用例一个)。每个实体类都会相当快地增长,并违反一些SOLID原则:
// Approach 2 (I do not want this)
public interface Entity {}
public interface WithPrice : Entity {
int Price { get; }
}
public interface WithAmount : Entity {
int Amount { get; }
}
public interface ConcreteEntity : WithPrice,WithAmount { }
// somewhere in code
ConcreteEntity entity;
var price = entity.Price;
var amount = entity.Amount;
请考虑以下两种方法都需要同时具有Id和Amount的方法(请注意,Entity
引用不一定具有与其关联的价格或金额):
// Approach 1
public static int Calculatetotal(Entity entity) {
var amount = MetadataManager.Get<EntityAmount>(entity).Amount;
var price = MetadataManager.Get<EntityPrice>(entity).Price;
return amount * price;
}
// Approach 2
public static int Calculatetotal<TEntity>(TEntity entity) where TEntity : WithAmount,WithPrice {
return entity.Amount * entity.Price;
}
与元数据的解耦以类型安全性为代价。当元数据不存在时,方法1的Calculatetotal
在运行时抛出,而方法2在编译期间失败。
我尝试过这种方法:
public interface WithMetadata<out TMetadata> {
Entity Entity { get; }
TMetadata Metadata { get; }
}
如何将其扩展到两个或多个元数据对象,以便可以使用类型安全的参数实现Calculatetotal
?如何优雅地提取适当类型的元数据?
编辑:我刚刚意识到我可以使用通用类型约束来实现Calculatetotal
。但是,如果我想保存带有两个元数据对象的实体列表,以便以后可以计算价格怎么办?该列表的类型是什么?
解决方法
以下是使用合成和一些界面的示例:
public interface IMetadata
{
}
public interface IMetadataWithType<TMetadata> where TMetadata : struct
{
TMetadata Value { get; }
}
public interface IEntity
{
List<IMetadata> Metadata { get; }
}
public abstract class MetadataBase<TMetadata> : IMetadata,IMetadataWithType<TMetadata> where TMetadata : struct
{
protected MetadataBase(TMetadata value)
{
Value = value;
}
public TMetadata Value { get; private set; }
}
public class PriceMetadata : MetadataBase<decimal>
{
public PriceMetadata(decimal value) : base(value)
{
}
}
public class AmountMetadata : MetadataBase<int>
{
public AmountMetadata(int value) : base(value)
{
}
}
public class EntityWithAmount : IEntity
{
public EntityWithAmount()
{
Metadata.Add(new AmountMetadata(10));
}
public List<IMetadata> Metadata { get; } = new List<IMetadata>();
}
public class EntityWithPrice : IEntity
{
public EntityWithPrice()
{
Metadata.Add(new PriceMetadata(199));
}
public List<IMetadata> Metadata { get; } = new List<IMetadata>();
}
public class EntityWithPriceAndAmount : IEntity
{
public EntityWithPriceAndAmount()
{
Metadata.Add(new PriceMetadata(199));
Metadata.Add(new AmountMetadata(10));
}
public List<IMetadata> Metadata { get; } = new List<IMetadata>();
}
public static class Calculator
{
public static void Total(IEntity entity)
{
int? amount = null;
decimal? price = null;
foreach(var metadata in entity.Metadata)
{
if (metadata is PriceMetadata priceMetadata)
price = priceMetadata.Value;
else if (metadata is AmountMetadata amountMetadata)
amount = amountMetadata.Value;
}
if (amount.HasValue && price.HasValue)
Console.WriteLine($"Total: {amount.Value * price.Value}");
else
Console.WriteLine("Entity does not have price and amount metadata");
}
}
class Program
{
static void Main(string[] args)
{
var entityWithPrice = new EntityWithPrice();
var entityWithAmount = new EntityWithAmount();
var entityWithPriceAndAmount = new EntityWithPriceAndAmount();
Calculator.Total(entityWithPrice);
Calculator.Total(entityWithAmount);
Calculator.Total(entityWithPriceAndAmount);
Console.ReadLine();
}
}
结果:
Entity does not have price and amount metadata
Entity does not have price and amount metadata
Total: 1990
您也可以尝试使用dynamic type,反射(例如,向类添加属性以指示其作用)以及扩展方法或这些技术的组合来实现此目的,但是我认为上述方法是非常干净,如果您希望将引用类型作为元数据的可能值,则删除结构约束。