使用元数据类型安全地装饰对象

问题描述

我有某些实体(类)。每个实体可以具有与其关联的元数据。一个实体最多只能有一个每种类型的元数据对象。

简化示例:

// 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;

但是,方法2的一个主要优点是:类型正确。

请考虑以下两种方法都需要同时具有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在编译期间失败。

我想要一种使用方法1的方法,但仍然具有类型安全的方法参数。

我尝试过这种方法

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,反射(例如,向类添加属性以指示其作用)以及扩展方法或这些技术的组合来实现此目的,但是我认为上述方法是非常干净,如果您希望将引用类型作为元数据的可能值,则删除结构约束。