【问题标题】:C# Interfaces & Generics working together [closed]C#接口和泛型一起工作[关闭]
【发布时间】:2019-12-29 04:37:03
【问题描述】:

我有一个域对象,Address,它可以从各种数据源中填充,这需要大量的映射代码。为了“Closed to Modification”的利益,我希望能够为每个数据源创建单独的“映射器”。然后我可以将映射器传递到 Address 的实例中,瞧!获取适当的数据实体作为响应。反之亦然,我还想在该 Address 上实现一个方法,该方法允许我将实体映射到新实体或填充 Address 的现有实例。

我创建了我的 Address 对象...

public class Address
{
    public string Street1 { get; set; }
    public string Street2 { get; set; }
    public string Street3 { get; set; }
    public string Street4 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Country { get; set; }
    public string PostalCode { get; set; }
}

现在我创建了几个类,它们将有助于将特定的数据实体对象映射到这个地址对象。

//
// Maps to and from a Database object (DB1_ADDRESS)
//
public class DB1AddressMapper
{
    property DB1_ADDRESS _entity;

    public DB1AddressMapper()
    {

    } 

    public DB1AddressMapper(DB1_ADDRESS entity)
    {
        _entity = entity;
    }

    public DB1_ADDRESS MapModelToEntity(Address model)
    {
        DB1_ADDRESS ret = new DB1_ADDRESS();

        <... mapping logic goes here>

        return ret;
    }

    public Address MapEntityToModel()
    {
        Address ret = new Address();

        <... mapping logic goes here>

        return ret;
    }
}

//
// Maps to and from a WebService response (WS_ADDRESS)
//
public class WSAddressMapper
{
    property WS_ADDRESS _entity;

    public WSAddressMapper()
    {

    } 

    public WSAddressMapper(WS_ADDRESS entity)
    {
        _entity = entity;
    }

    public WS_ADDRESS MapModelToEntity(Address model)
    {
        WS_ADDRESS ret = new WS_ADDRESS();

        <... mapping logic goes here>

        return ret;
    }

    public Address MapEntityToModel()
    {
        Address ret = new Address();

        <... mapping logic goes here>

        return ret;
    }
}

现在我有了映射器,我在 Address 上创建了一个方法,我可以将它们传递到其中,以便于转换数据。所以你可以在下面的代码中看到我不得不重载这些方法,因为每个映射器都有自己的类型。这意味着每次我想添加一个新的数据源来填充地址对象时,我都必须重新打开 Address 并添加新的重载方法。 Ugghhh ...不,谢谢(“关闭修改”发生了什么?)

public class Address
{
    public string Street1 { get; set; }
    public string Street2 { get; set; }
    public string Street3 { get; set; }
    public string Street4 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Country { get; set; }
    public string PostalCode { get; set; }
    //
    // Populate "this" instance of the Address object from data found in the mapper.
    // The "mapper" argument would have to have been instantiated with the entity it expects to map 
    // to the Domain object, Address
    //
    public Address MapToModel(DB1AddressMapper mapper)
    {
        return mapper.MapEntityToModel();
    }

    //
    // Map "this" instance of address to a new DB1_ADDRESS instance
    //
    public DB1_ADDRESS MapToEntity(DB1AddressMapper mapper)
    {
        return mapper.MapModelToEntity(this);
    }


    //
    // And now again for WSAddressMapper
    //
    public Address MapToModel(WSAddressMapper mapper)
    {
        return mapper.MapEntityToModel();
    }

    //
    // Map "this" instance of address to a new WS_ADDRESS instance
    //
    public WS_ADDRESS MapToEntity(WSAddressMapper mapper)
    {
        return mapper.MapModelToEntity(this);
    }

} 

这让我想到了接口和泛型……我已经涉足多年,但缺乏必要性并没有迫使我加深对它们的理解(我相信这阻碍了我)。

回到手头的问题...我只想要 Address 中的两个映射方法,它们将“关闭以供修改”。他们需要为我遇到的任何数据源提供任何映射器。映射器封装了所有特定的映射逻辑,而 Address 并不真正关心细节。它只想“映射到”。

伪代码解决方案看起来像这样......

public class Address 
{
    public Address MapToModel(EntityMapper mapper)
    {
        ...
    }

    public EntityAddress MapToEntity(EntityMapper mapper)
    {
        ...
    }
}

似乎我可以为映射器创建一个接口,以便所有映射器都实现相同的两种方法...

MapModelToEntity();
MapEntityToModel();

我从那个开始......

public interface IEntityAddressMapper
{
    Address MapEntityToModel();
    T MapModelToEntity<T>(Address model);
}

您也许可以看到我开始遇到麻烦的地方。由于“MapModelToEntity”的返回类型因数据源而异,我不知道该怎么做。我选择使它成为通用的;这些在其他领域对我有用。我继续在我的映射器中实现它,希望答案会自行显现。

public class DB1AddressMapper : IEntityAddressMapper
{
    Address MapEntityToModel()
    {
        Address ret = new Address();

        <... mapping logic goes here>

        return ret;
    }

    //
    // This is what I want but, does NOT satisfy interface
    //
    DB1_ADDRESS MapModelToEntity(Address model)  <!-- DOES NOT SATISFY INTERFACE
    {
        DB1_ADDRESS ret = new DB1_ADDRESS();

        <... mapping logic goes here>

        return ret;
    }
    //
    // This satisfies interface but is silly. The mapper already KNOWS the TYPE, that's the point.
    // Besides this means that the consumer will have to pass in the types, which is EXACTLY what 
    // I am trying to avoid.
    //
    T MapModelToEntity<T>(Address model)  
    {
        DB1_ADDRESS ret = new DB1_ADDRESS();

        <... mapping logic goes here>

        return ret;
    }   
}

I've tried a million different permutations so it's impractical to list them all here but suffice to say the closest I have come so far is the following ...



public interface IEntityAddressMapper<EntityType>
{

    EntityType MapModelToEntity(Address mode);

    void MapModelToEntity(Address model, ref EntityType entity);

    Address MapEntityToModel(EntityType entity);

    void MapEntityToModel(EntityType entity, ref Address model);
}



public class DB1AddressMapper : IEntityAddressMapper<DB1_ADDRESS>
{
    Address MapEntityToModel()
    {
        Address ret = new Address();

        <... mapping logic goes here>

        return ret;
    }

    T MapModelToEntity(Address model)  
    {
        DB1_ADDRESS ret = new DB1_ADDRESS();

        <... mapping logic goes here>

        return ret;
    }   
}

这似乎让我现在可以毫无问题地实现接口,但我似乎已经将负担转移到现在正在破坏的方法上......

public class Address 
{
    // *********************************************
    // ERROR - Using generic type 'IEntityAddressMapper<EtityType>' requires one type argument
    // *********************************************
    public Address MapToModel(EntityMapper mapper)
    {
        ...
    }

    // *********************************************
    // ERROR - Using generic type 'IEntityAddressMapper<EtityType>' requires one type argument
    // *********************************************
    public EntityAddress MapToEntity<EntityType>(EntityMapper mapper)
    {
        ...
    }
}

我一直在转圈,多年来一直在做这件事。我需要解决这个问题!!任何帮助将不胜感激。

谢谢

【问题讨论】:

  • 'mapper 怎么知道类型'?它没有,你有一个带有泛型参数的函数。也许您可以尝试使用 IEntityAddressMapper 而不是 IEntityAddressMapper。在这种情况下 MapModelToEntity 不需要 T 参数。
  • 基本上,您的 MapModelToEntity 所说的是它可以将 Address 映射到您放入 中的任何类型,这显然不是真的。例如,您可能无法处理 var s = MapModelToEntity...
  • 我很愿意在这里犯错,但是拥有单独的映射器的重点是每个数据库(实体类型)都有一个。因此,他们知道他们正在使用的类型是有道理的。我错过了什么吗?
  • 好吧,在您的代码中,您有一个映射器,其函数可以将 Address 映射到任何类型。这与为每种实体类型使用一个映射器不同。尝试在接口而不是方法上放置通用约束。
  • 我认为这让我走上了正确的道路!

标签: c# generics design-patterns interface open-closed-principle


【解决方案1】:

您需要从一个由两个类型参数而不是一个类型参数变化的通用接口开始。这将解决您当前的问题,但在解决之后,我想提出一种不同的方法。

解决方案

考虑这个界面

public interface IMapper<TModel, TEntity>
{
    TEntity MapModelToEntity(TModel source);

    TModel MapEntityToModel();
}

它允许您创建可以传递给Address 类的特定实现:

public class DatabaseAddressMapper : IMapper<Address, DB1_ADDRESS>
{
    public DB1_ADDRESS MapModelToEntity(Address source) { ... }

    Address MapEntityToModel()
}

public class WSAddressMapper : IMapper<Address, WS_ADDRESS>
{
    public WS_ADDRESS MapModelToEntity(Address source) { ... }

    Address MapEntityToModel()
}

并修改Address,使其具有一些可以接受您的映射器的通用方法

// only need TEntity for this generic method because we know we are an Address
public Address MapToModel<TEntity>(IMapper<Address, TEntity> mapper)
{
    return mapper.MapEntityToModel();
}

// only need TEntity for this generic method because we know we are an Address
public TEntity MapToEntity<TEnity>(IMapper<Address, TEntity> mapper)
{
    return mapper.MapModelToEntity(this);
}

通过此设置,您的Address 类现在遵循Open/Closed Principal,因为它可以接受任何支持Address 和任意类型的映射器。这也将您的 Address 与其他类型完全分离,这很好。

替代方案

这里有改进的余地,而且相当简单。问问自己:为什么Address 需要了解有关映射的任何信息?这只是一个地址。映射是其他事情的关注点:也许是调用者本身?

您可以从 Address 中完全删除这些方法,并在您调用这些方法的调用者中删除这些方法

var mapper = new WSAddressMapper();
var model = address.MapToModel<WS_ADDRESS>(mapper);
var entity = address.MapToEntity();

您可以直接调用映射器。

var model = mapper.MapEntityToModel<WS_ADDRESS>();
var entity = mapper.MapModelToEntity(address);

这意味着您的Address 现在也服从Single Responsibility Principle!毕竟是你的调用者发起了映射; 应该有这个责任,而不是地址本身。

让我们继续前进!

为什么要一个接口可以双向映射?毕竟,您更有可能为一段代码映射一个方向(例如,保存到数据库)并将另一个方向映射到另一段代码(例如从数据库中读取)。

让我们把接口分成两个,每个只有一个方法。我将把这个实现留给你,但这是你得到的:另一个SOLID 支柱,Interface Segregation Principle。呜呼!

【讨论】:

  • 太棒了!谢谢!这正是我想去的地方。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-04-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多