如何处理看起来相同但没有共享接口的对象

时间:2018-01-13 16:46:24

标签: c# inheritance interface

我已经写了细微差别和详细信息here,但这里是我正在努力解决的问题的主旨:

我有一个第三方组件,它返回基本上看起来相同的各种POCO对象中的任何一个。看起来一样,我的意思是如果你浏览对象的JSON,你会认为它们是完全相同的类。但他们不是同一类。尽管具有相同的形状,但它们不实现接口或从基类继承。像这样:

// Order V1
public class ThirdPartyOrderV1
{
  public decimal Amount { get; set; }
  public ThirdPartyOrderItemV1[] LineItems { get; set; }
  public ThirdPartyOrderAddressV1 BillingAddress { get; set; }
  public ThirdPartyOrderAddressV1 ShippingAddress { get; set; }
  public string SomeV1ThingIDontCareAbout { get; set; }
  public decimal AnotherV1ThingIDontCareAbout { get; set; }
  // ...
}

public class ThirdPartyOrderItemV1
{
  public int ItemID { get; set; }
  public int Quantity { get; set; }
  // ...
}

public class ThirdPartyOrderAddressV1
{
  public string Name { get; set; }
  public string Street { get; set; }
  // ...
}

// Order V2
public class ThirdPartyOrderV2
{
  public decimal Amount { get; set; }
  public ThirdPartyOrderItemV2[] LineItems { get; set; }
  public ThirdPartyOrderAddressV2 BillingAddress { get; set; }
  public ThirdPartyOrderAddressV2 ShippingAddress { get; set; }
  public object SomeV2ThingIDontCareAbout { get; set; }
  // ...
}

public class ThirdPartyOrderItemV2
{
  public int ItemID { get; set; }
  public int Quantity { get; set; }
  // ...
}


public class ThirdPartyOrderAddressV2
{
  public string Name { get; set; }
  public string Street { get; set; }
  // ...
}

该库有3个或4个这些版本。现在当我从库中获取对象时,我会对它们进行类型检查并将它们传递给方法以处理特定类型:

public static void Main()
{
    var parser = new ThirdPartyOrderParser();
    var order = parser.ParseOrder("text");
    if (order is ThirdPartyOrderV1)
    {
        IngestOrderV1(order as ThirdPartyOrderV1);
    }
    else if (order is ThirdPartyOrderV2)
    {
        IngestOrderV2(order as ThirdPartyOrderV2);
    }
    // ...
}

这并不理想,因为我最终为每种类型复制了我的方法。我希望有一个方法可以处理任何看起来像订单(或行项目或地址等)的任意对象。

我知道我不可能是唯一一个遇到过这类问题的人。我有一些想法,比如将它转换为dynamic对象并以这种方式引用属性。或者将其序列化为JSON并遍历[JObject表示](https://www.newtonsoft.com/json/help/html/QueryingLINQtoJSON.htm(或XML或YAML或其他)

但是这些解决方案仍然感觉很脆弱,只是错误。有没有更好的方法来处理这个?也许甚至一些可以生成类的工具代表类的共性并在实际类和这个“常见”类之间来回转换?

谢谢!

1 个答案:

答案 0 :(得分:1)

使用dynamic反对强类型工作是一个不错的主意。不幸的是,你必须处理所有这些类型,但至少它们是定义的,所以你知道它们各自代表什么。

我首先要定义自己的类,以满足您的需求。它们可能与此第三方库中的类相似也可能不相似。如果您收到任何其他形式的输入,请立即将其转换为您自己的模型。这样你就可以避免与其他人的课程紧密联系。如果它们发生了变化,或者您想要与其他库集成,那么您只需要将模型中的映射添加到模型中。

假设这些是外部模型:

public class ThirdPartyOrderV1
{
    public decimal Amount { get; set; }
    public ThirdPartyOrderItemV1[] LineItems { get; set; }
}

public class ThirdPartyOrderItemV1
{
    public int ItemID { get; set; }
    public int Quantity { get; set; }
}

这些是您的模型,它允许您处理来自多个来源的项目:

public class MyOrder
{
    public Money Amount { get; set; }
    public MyOrderItem Lines { get; set; }
}

public class MyOrderItem
{
    public ItemId ItemId { get; set; }
    public int Quantity { get; set; }
}

public class ItemId
{
    public OrderItemType ItemType { get; set; }
    public string Value { get; set; }
}

public enum OrderItemType
{
    Internal,
    Acme,
    BobsThirdPartyItems
}

您可以使用Automapper,或者甚至只编写自己的方法来转换它们。如果不是真的很明显1:1映射我宁愿写自己的。你必须以同样的方式编写相同的逻辑。

public static class BobsThirdPartyItemExtensions
{
    public static MyOrder ToMyOrder(this ThirdPartyOrderV1 source)
    {
        return new MyOrder
        {
            Amount = new Money(source.Amount, Currency.USD),
            Lines = source.LineItems.Select(item => item.ToMyOrderItem()).ToArray()
        };
    }

    public static MyOrderItem ToMyOrderItem(this ThirdPartyOrderItemV1 source)
    {
        return new MyOrderItem
        {
            ItemId = new ItemId
            {
                ItemType = OrderItemType.BobsThirdPartyItems,
                Value = source.ItemID.ToString("0")
            },
            Quantity = source.Quantity
        };
    }
}

var myOrder = someThirdPartyV1order.ToMyOrder();

到目前为止,这可以保护您的应用程序免受不必要的外部复杂性的影响,确保在内部您只需要处理由您为应用程序定义的一组类。这仍然留下了处理所有冗余V1V2类型的细节。你可以为那些编写额外的扩展方法,这些方法是相同的。这不是真正的代码重复。它们都是需要映射的不同类型。我可能会倾向于那个。

如果它们真的相同,那么您可以使用Automapper将一种外部类型转换为另一种外部类型。

假设你有

public class ThirdPartyOrderV2
{
    public decimal Amount { get; set; }
    public ThirdPartyOrderItemV2[] LineItems { get; set; }
    public string DontNeedThis { get; set; }
}

public class ThirdPartyOrderItemV2
{
    public int ItemID { get; set; }
    public int Quantity { get; set; }
}

您可以像这样配置自动映射:

Mapper.Initialize(config =>
{
    config.CreateMap<ThirdPartyOrderItemV2, ThirdPartyOrderItemV1>();
    config.CreateMap<ThirdPartyOrderV2, ThirdPartyOrderV1>()
        .ForSourceMember(source => source.DontNeedThis, option => option.Ignore());
});

这基本上表示将所有相同的属性从V2映射到V1,并忽略我们不关心的属性。

现在,给定V2的实例,您可以这样做:

var v2 = new ThirdPartyOrderV2
{
    Amount = 5.00M,
    DontNeedThis = "Who cares",
    LineItems = new ThirdPartyOrderItemV2[]
    {
        new ThirdPartyOrderItemV2 {ItemID = 5, Quantity = 200},
        new ThirdPartyOrderItemV2 {ItemID = 4, Quantity = 50}
    }
};
var v1 = Mapper.Map<ThirdPartyOrderV1>(v2);
var myOrder = v1.ToMyOrder();

你无法绕过做一些映射,但是我会保持强类型并且避免对dynamic做任何事情,除非你别无选择。

你的评论提到这些类很大,所以创建自己的类将需要很多额外的工作来维护。无论如何,我会考虑这样做。如果维护意味着某些事情正在发生变化,那么您将不得不在某处更改某些代码来解决这个问题。我宁愿在我的应用程序边界处理这种变化,在那里我收到输入而不是内部逻辑断开并且必须在那里进行维护。当您将外部输入映射到您已定义并控制的内容时,至少可以完成维护。

如果您在强类型类和属性之间进行映射,那么即使它稍微繁琐,对于必须维护它的下一个人来说也是可以理解的。它也说明了这样一个现实:虽然今天的课程看起来很相似,但你却无法控制它。 (如果第三方图书馆做出了理性的选择,那么也许他们不会复制那么多的课程。你能相信他们做明天有意义的吗?)如果你偏离那个那么那么既费时又难以弄清楚什么是继续