泛型实现 - 非泛型接口模式是否存在缺陷?

时间:2017-02-02 20:06:37

标签: c# generics design-patterns interface

我开始看到这种模式经常出现在我的代码中:

class Foo { }

interface IBar
{
    Foo Foo { get; }
}

class Bar<TFoo> : IBar where TFoo : Foo
{
    public TFoo Foo { get; private set; }

    Foo IBar.Foo
    {
        get 
        { 
            return Foo; 
        }
    }
}

它的一些好处是:

  1. 检查对象是否为包装类型(if (something is IBar)
  2. 的简单方法
  3. 在封闭式构建的TFoo
  4. 中对Bar<>的强类型访问权限
  5. 对接口Foo s
  6. 中的IBar进行多态访问

    有人可能会争辩说这种模式在框架中无处不在(例如List<T> : IList),但我想知道这是否只是.NET 1.0的遗留物,当泛型不存在时。

    在我的头脑中,我主要担心的是IBar不一定是一个合适的合同,它定义了“酒吧”应该提供的成员;它只是访问一般类型成员的黑客。

    此外,如果我开始为此目的添加界面,我很快就会难以维护parallel inheritance hierarchies

    我是否应该担心在我的代码库中传播此模式?如果是这样,有哪些替代模式可以提供上面列出的3项好处中的部分或全部?

    由于不允许明确实现抽象成员,因此无法实现这种“理想”解决方案:

    class Foo { }
    
    class Bar
    {
        public abstract Foo Foo { get; }
    }
    
    class Bar<TFoo> : Bar where TFoo : Foo
    {
        private TFoo foo;
    
        Foo Bar.Foo
        {
            get
            {
                return foo;
            }
        }
    
        public new TFoo Foo
        {
            get
            {
                return foo;
            }
        }
    }
    

4 个答案:

答案 0 :(得分:3)

对我来说,总结是你不应该认为你实现接口只是为了通过更多的输入来扩充泛型类型参数。

AFAIK,您使用接口来提供与给定API一起使用的合同。泛型只是一种语言功能/工具,可以提供更多的打字,最终你会做很多演员表。因此,使用泛型,您可以限制API以期望实现一个或多个接口的参数,以及使用泛型约束的一些要求。

例如,如果您只想接受名为IWhatever的给定接口的实现,您会使用泛型吗?

public void DoStuff<T>(T whatever)
       where T : IWhatever
{
}

// versus

public void DoStuff(IWhatever whatever)
{
}

顺便说一句,没有泛型,你如何检查IWhatever的实现是class并且有public构造函数?与使用泛型相比,你最终会反思并且你的代码会闻起来:

public void DoStuff<T>()
       where T : class, IWhatever, new()
{
}

实际上,泛型参数可以约束T必须继承给定的类并实现一个或多个接口:

public void DoStuff<T>(T arg)
       where T : A, IEquatable<T>, IWhatever, IWhichever, IWherever
{
}

无论T是继承带有或不带泛型参数的类型,还是实现带或不带泛型参数的接口,它本身并不是一个好的或坏的设计,但同样,只是一个语言工具& #39;适用于特定情况。

因此,你的陈述......

  

在我的头顶,我主要担心的是IBar不是   必然是一个合适的合同,定义成员a&#34; bar&#34;应该   提供;它只是访问一般类型成员的黑客。

...描述了一个特定的设计缺陷,而不是使用接口奇迹输入泛型的实际问题。

结论:如果IBar不是合适的合同,那么您应该重新审视您的架构并重新考虑您的解决方案。

关于该主题的更多背景

实际上我认为我原来的答案暗示我发现整个解决方案都存在设计缺陷。

总之,您使用接口在某些类上公开关联,这些类使用泛型类型参数提供整个关联的类型。并且您认为这样做是为了能够在较少类型的上下文中访问此类关联:

  

然而,我有时需要一个&#34;更少&#34;类型安全的背景,因此我的问题。

然后当covariance开始行动时!请参阅以下代码示例:

public class SuperClass 
{
}

public interface IWhatever<out TAssociation>
    where TAssociation : SuperClass
{
    TAssociation Association { get; }   
}

public class SomeImplementation<TAssociation> : IWhatever<TAssociation>
    where TAssociation : SuperClass
{
        public TAssociation Association { get; set; }
}

现在让我们定义SuperClass的派生类:

public class DerivedClass : SuperClass
{
}

看看这是如何运作的魅力:

SomeImplementation<DerivedClass> someImpl = new SomeImplementation<DerivedClass>();

// Covariance: you decide the degree of specialization of TAssociation
// interfaces' type parameter. In our case, we'll upcast TAssociation to
// the SuperClass type.
IWhatever<SuperClass> whatever = someImpl;

显然,这是自C#4.0以来的方法。

我想说出表达要求的正确方法是,你需要一个不太专业的上下文而不是一个较少类型的上下文Covariance/contravariance是C#中最强大的功能之一,可以涵盖这种情况,当泛型涉及到等式时。

这种做法本身并不是代码味道。在我的情况下,当我真正需要访问一个或多个协会时,我只需要访问某些具有特定目的的成员。

例如,如果我构建一个树型层次结构,我会定义一个这样的接口:

public interface IHasParent<out TParent>
{
     TParent Parent { get; }
}

这使我能够做到这一点:

IHasParent<object> withParent = someObject as IHasParent<object>;

if(withParent != null)
{
     // Do stuff here if some given object has a parent object
}

但是我不会不加区别地创建界面,因为有一天我需要对某些属性进行较少类型的访问。应该有明确的目的。否则,您最终可以将一个很好的解决方案变成代码气味

你会说不要重复但我仍然觉得没有分析你的项目代码库并检查你是如何真正使用这种接口的,没有明确的答案解决具体问题。

所以,严格来说,如果你真的需要使用整个模式,它应该是一个很好的设计决策。

也许你想避免不可避免的

基于一些聊天我们既有OP又有我,我觉得最好的结论是OP希望避免不可避免的。

在面向对象的语言中,如C#接口是定义类型契约和公开实现某个接口的完整类型子集的正确工具。

此外,OP会喜欢C#中的一个功能,例如协议,其中一个隐式填充接口的类足以认为它实现了可以节省的接口如果C#具有此功能,可以使用许多代码行:

public interface IWhatever
{
       void DoStuff();
}

public class X
{
      void DoStuff();
}

public class Y
{
      public void HandleStuff(IWhatever whateverImpls)
      {
      }
}

Y y = new Y();
// Protocols would support passing an instance of X which may not implement
// IWhatever but it implicitly fulfills IWhatever:
y.HandleStuff(new X());
BTW,C#缺乏此功能。因此,考虑到拥有这样的功能会有多么甜蜜,这是浪费时间。你需要处理C#已经提供的内容。

无论如何,如果您只需要在对象图中公开一些关联并有选择地获取它们,您可以使用比您更简化的方法来使用接口的奇迹。 您是否知道如果其通用参数不同,您可以多次显式实现相同的界面?

为什么不设计这样的界面:

public interface IHasAssociation<out TAssociation>
{
    TAssociation Association
    {
        get;
    }
}

public interface IHasManyAssociation<out TEnumerable, out TAssociation>
        where TEnumerable : IEnumerable<TAssociation> 
        where TAssociation : Entity
{
    TEnumerable Association
    {
        get;
    }
}

public class Entity
{
}

public class Company : Entity
{
}

public class CustomerProfile : Entity
{
}

public class Contact : Entity
{
}

public class Customer : 
    IHasAssociation<Company>, 
    IHasAssociation<CustomerProfile>, 
    IHasManyAssociation<IList<Contact>, Contact>
{
    public Company Company
    {
        get;
        set;
    }

    public CustomerProfile Profile
    {
        get;
        set;
    }

    public IList<Contact> Contacts
    {
        get;
        set;
    }

    Company IHasAssociation<Company>.Association => Company;
    CustomerProfile IHasAssociation<CustomerProfile>.Association => Profile;
    IList<Contact> IHasManyAssociation<IList<Contact>, Contact>.Association => Contacts;
}

这肯定会让事情更简单(KISS!),因为您不需要并行接口对象图定义,您只需定义一个接口即可获得给定类型的关联:

var customer = new Customer();
customer.Profile = new CustomerProfile();
customer.Company = new Company();
customer.Contacts = new List<Contact>();

var withCompany = customer as IHasAssociation<Company>;
var withCustomerProfile = customer as IHasAssociation<CustomerProfile>;
var withContacts = customer as IHasManyAssociation<IList<Contact>, Contact>;

if (withCompany != null)
{
    Company company = withCompany.Association;
    Console.WriteLine("This object has an associated company!");
}

if (withCustomerProfile != null)
{
    CustomerProfile profile = withCustomerProfile.Association;
    Console.WriteLine("This object has a profile!");
}

if (withContacts != null)
{
    IList<Contact> contacts = withContacts.Association;
    Console.WriteLine("This object has contacts!");
}

另外,请参阅行动中的协方差:

    if(customer is IHasManyAssociation<IEnumerable<Contact>, Contact>)
    {
        Console.WriteLine("This object has an enumerable of contacts!");
    }

或者在这里,您将获得一个或多个IHasAssociation<out TAssociation>接口实现的实现者的所有关联值:

    var entityAssociations = typeof(Customer)
                            .GetInterfaces()
                            .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IHasAssociation<>))
                            .Select(i => i.GetProperty("Association").GetValue(customer));


    foreach(var entityAssociation in entityAssociations)
    {
        Console.WriteLine($"{entityAssociation.GetType().FullName}");
    }

这是通用编程的真正美! 请记住:您不需要不分青红皂白地实施IHasAssociation<out TAssociation> / IHasManyAssociation<out TEnumerable, out TAssociation>。也就是说,您需要在某个地方实施需要提取关联的类,而您不关心谁是该关联的具体所有者,您只需要协会本身

答案 1 :(得分:0)

我在开发API之后发现自己处于类似的地方。 我有一些问题给你,没有答案。 但是一旦你能回答这些问题,也许你对如何解决这种情况有了更多了解。

我想知道有多少个类实现了IBar。 有足够的理由吗? 这是一个API,你将它暴露给客户端代码? 在多少代码点中,您是否利用了接口的多态性?

也许......那些答案会让你质疑界面的用处。

这种结构出现了多少次? 你确定它确实存在吗?

我的意思是,你说你这样做了:

  • 实施大量代码(A);
  • 在一个地方重构它以清理它;
  • 实施更多代码(B);
  • 重构它以清理它;
  • 注意B看起来类似于A;
  • 实现更多代码(C);
  • 重构它以清理它;
  • 注意到C看起来与B类似(通过传递性也与A相似);
  • 重复...

结构是否真的出现了,还是你认为以同样的方式塑造代码? 哪个先来?

  • 出现;
  • 思考。

这&#34;冲洗&#34;并且&#34;重复&#34;方法可能很好,但也许......你已经从这种方法中发展出来,应该接近另一种方法:

  

首先设计,然后实施。

这是你的情况吗?你有没有成长,你最终可以在实施之前进行设计?

这里有一句可能适用的说法:

  

当你拿锤子时,一切看起来像钉子。

但是我们假设这不是你的情况。 你不会陷入思维周期,而潜在的问题真的有这种结构,因此你的代码反映了问题的结构。

如果你真的多次想出同样的事情,但问题就在于,而不是你的想法,那么以下可能是一个很好的建议。

停止编码一天,并远离键盘考虑它。

哪些部分是相同的,哪个不同? 您是否可以以更加抽象的方式(实际模式)实现这一点,而注入专用代码? 也许,最重要的是,它是一个像复合模式这样简单的东西,你可以一劳永逸地实现它,然后在整个地方重复使用它。

我发生的事情是类似的,我最终得到了一个依赖注入,一个抽象工厂,一个复合模式的抽象实现和一个信息专家,它采用了一个配置文件并组装了我需要的最终对象图。 / p>

  

这是对模式和架构的一个非常好的,令人羞愧的教训,但我后悔实际使用它。

编写文档几乎是不可能的,也是徒劳的。 代码变得非常难以遵循。 我总是不得不重新思考如何正确使用它。 最终结果并不令人惊讶。

所以,如果你想学习和锻炼,不要停止! 但是,如果你想完成它并继续前进,不要过度思考它。

  

简单就更好了!

您可能在某个地方尝试完善您的代码但实际上并不需要它。 您没有为M $编写新的API,是吗?

请接受以下建议:

  

在一两年内,您无法理解自己的代码。如果你把它复杂化,你必须记录它。如果您无法记录,那么您永远不会重复使用它。所以你不需要这种完美,它将是丢失的代码。

换句话说:

  

真正的价值不是代码,而是随附的文档。没有文档就没有重用..

答案 2 :(得分:0)

在你的问题中,你表达了对“通用”包装类型的需求(注意我在这里使用术语“通用”,独立于任何语言)。

好吧,我没有看到任何问题。如果你问我如何使用.NET,我会设计一次,适用于所有类型,更进一步:

interface IWrapper<T>
{
    T BaseObject { get; }
}

然后,包装类只是:

class Bar<TFoo> : IWrapper<TFoo> where TFoo : Foo
{
    public TFoo BaseObject { get; private set; }
}

我可以再进一步定义非泛型的,因为它有两个都是可取的,因为它有时难以使用泛型的clases /接口与元/反射代码(但这实际上是可选的):< / p>

interface IWrapper
{
    object BaseObject { get; }
}

如果您这样做,IWrapper<T>从[{1}}派生出来就很自然,就像这样:

IWrapper

课程就是这样:

interface IWrapper<T> : IWrapper
{
    new T BaseObject { get; }
}

PS:作为旁注,您可以查看WCF的ServiceModel class Bar<TFoo> : IWrapper<TFoo> where TFoo : Foo { public TFoo BaseObject { get; private set; } object IWrapper.BaseObject => BaseObject; } 类,该类具有与您的问题有些相关的通用/非通用组合层次结构。

答案 3 :(得分:0)

回想起来,我已经了解到我想要的正确术语是 return type covariance ,不幸的是not supported in C#,因为语言设计团队不考虑即使它保留了类型安全性,实现该功能的好处也超过了成本。 (proposal已经起草并完成,但似乎已被放弃了。

使用返回类型协方差,示例代码可以写为:

class Foo { }

class Bar
{
    public virtual Foo Foo { get; }
}

class Bar<TFoo> : Bar where TFoo : Foo
{
    public override TFoo Foo { get; }
}

Eric Lippert在该相关问题中提出的解决方法是:

class Foo { }

abstract class Bar
{
    protected abstract Foo foo { get; }
    public Foo Foo => foo;
}

class Bar<TFoo> : Bar where TFoo : Foo
{
    protected override Foo foo => this.Foo;
    public new TFoo Foo { get { ... } }
}

它的缺点是不是重复继承层次结构,而是每个协变模拟属性每个继承级别

有关模拟协变返回类型的杂乱程度可以为代码带来多少杂乱的进一步阅读,请考虑implementing ICloneable properly implies adding another virtual method per level of inheritance。我会把这作为我对语言特征的谦卑请求。