简体   繁体   English

使用泛型为C#中的可调后端键入安全性?

[英]Type safety with generics for modulable backends in C# ?

Consider writing a piece of software relying on a backend which must be modular. 考虑编写一个依赖于后端的软件,该后端必须是模块化的。 As long as the backend fits into a single class, it is easy to achieve by defining some interface and letting several backend classes implement it: 只要后端适合单个类,通过定义一些接口并让几个后端类实现它很容易实现:

interface IBackend { ... }
class BackendA : IBackend { ... }
class BackendB : IBackend {...}

Now, suppose working with the backend requires holding intermediate pieces of data, whose internals are backend-dependant. 现在,假设使用后端需要保存中间数据,其内部依赖于后端。 Again, it is represented by an interface and concrete classes: 同样,它由接口和具体类表示:

interface IFoo { ... }
class FooA : IFoo { ... }
class FooB : IFoo { ... }

The constructors of the Foo are not exposed, and we can instead rely on factory methods in the IBackend interface: Foo的构造函数没有公开,我们可以依赖IBackend接口中的工厂方法:

interface IBackend {
    IFoo createFoo(some arguments)
}

class BackendA : IBackend {
    override FooA createFoo(some arguments) {
        return new FooA(some arguments)
    }
}

} }

Using the interfaces from the common abstraction layer everywhere works, but now code from BackendA is now forced to accept IFoo instead of FooA when it needs it, and cast it to FooA : 使用来自公共抽象层的接口无处不在,但现在BackendA的代码现在被迫在需要时接受IFoo而不是FooA ,并将其转换为FooA

interface IBackend {
    void soSomethingWithFoos(IFoo f1, IFoo f2);
}
class BackendA : IBackend {
    override void doSomethingWithFoos(IFoo f1, IFoo f2) {
        FooA fa1 = (FooA)f1;
        FooA fa2 = (FooA)f2;
        // do something
    }
}

Besides, this is not type safe, as a careless user could instanciate different backends at once and pass objects from one backend to the other under the nose of the compiler: 此外,这不是类型安全的,因为粗心的用户可以立即实现不同的后端,并在编译器的鼻子下将对象从一个后端传递到另一个后端:

BackendA ba = new BackendA();
BackendB bb = new BackendB();
ba.doSomethingWithFoo(bb.createFoo(some args)); // Typechecks but is clearly incorrect

To improve this, we can add a type parameter to all the common interfaces: IBackend<T> , IFoo<T> , etc... and use it to distinguish the backends: BackendA : IBackend<A> , BackendB : IBackend<B> , FooA : IFoo<A> . 为了改善这一点,我们可以为所有常见接口添加一个类型参数: IBackend<T>IFoo<T>等...并用它来区分后端: BackendA : IBackend<A>BackendB : IBackend<B>FooA : IFoo<A> Then we can have: 然后我们可以:

interface IBackend<T> {
    public IFoo<T> createFoo(some arguments)
}

This way, mixing objects from different backends is disallowed by the compiler. 这样,编译器就不允许混合来自不同后端的对象。 However this is not quite satisfactory: code from some backend BackendA must still cast IFoo<A> to FooA , even though that would be type-safe as long as FooA is the only class implementing IFoo<A> . 然而,这并不十分令人满意:来自某些后端的代码BackendA仍然必须将IFoo<A>FooA ,即使这是类型安全的,只要FooA是实现IFoo<A>的唯一类。 Besides, code willing to be backend-independant now has generics creeping up all over the place. 此外,愿意独立于后端的代码现在已经遍布整个地方的泛型。

To address this, if we assume the amount of classes we need to represent our backend is finite and constant, we can avoid the cast by parametrizing the interface with all the concrete classes: 为了解决这个问题,如果我们假设我们需要表示后端的类数量是有限且常量的,我们可以通过使用所有具体类参数化接口来避免强制转换:

interface IBackend<T, FooT, BarT> where FooT : IFoo<T>, BarT : IBar<T> {
    FooT createFoo();
    ...
}

I don't even know if this is valid syntax, nor if this actually solves the problem. 我甚至不知道这是否是有效的语法,也不知道这是否真的解决了问题。

Is this madness ? 这是疯了吗? is there a type-safe way out of there ? 那里有一种类型安全的方式吗? Or is there a better way without using generics ? 或者没有使用泛型有更好的方法吗?

You might be able to solve this by reversing the roles a bit. 您可以通过稍微颠倒角色来解决这个问题。 Instead of having the IBackend take an IFoo like this and trusting that an IFoo is always used with the corresponding IBackend , eg 而不是让IBackend像这样采用IFoo并相信IFoo总是与相应的IBackend ,例如

interface IBackend {
    IFoo createFoo();
    void doSomethingWithFoo(IFoo foo);
}

You can change this to let the IFoo remember which IBackend it came from. 您可以更改此项以让IFoo记住它来自哪个IBackend

interface IBackend {
    IFoo createFoo();
}

interface IFoo {
    void doSomethingWithBackend();
}

class BackendA : IBackend {
    IFoo createFoo() {
         return new FooA(this);
    }

    void doSomethingWithFoo(FooA foo) { ... }
}

class FooA : IFoo {
    private BackendA backend;

    FooA(BackendA backend) {
        this.backend = backend;
    }

    void doSomethingWithBackend() {
        backend.doSomethingWithFoo(this);
    }
}

If your design is supposed to be modular as you describe, then the casts should be entirely unnecessary. 如果您的设计应该按照您的描述进行模块化,则演员应该完全没必要。 Casts are (often) a sign of a less-than-modular system. 演员(通常)是低于模块化系统的标志。

To make things completely modular, the interface IFoo should capture the operations necessary to use instances of implementations of IFoo . 为了使事物完全模块化,接口IFoo应捕获使用IFoo实现实例所必需的操作。 The tricky part would be finding a high enough abstraction for your problem. 棘手的部分是为您的问题找到足够高的抽象。 After that, providing an interface to represent that abstraction becomes trivial. 在那之后,提供一个表示该抽象的接口变得微不足道。 Of course, this is often easier said than done, but if you are looking for complete type-safe modularity, it's the way to go, in my opinion. 当然,这通常说起来容易做起来难,但如果你正在寻找完全类型安全的模块化,那么我认为这是最佳选择。

What you want is something called "virtual types", which basically makes type parameters virtual things that can be overriden by derived classes. 你想要的是一种叫做“虚拟类型”的东西,它基本上使类型参数成为可以被派生类覆盖的虚拟东西。 The typical use case used to promote the idea is when you have a family of types , all of which must be overriden in concert. 用于推广这一想法的典型用例是当您拥有一系列类型时,所有类型都必须一致地覆盖。

They look something like this: 他们看起来像这样:

class Foo {
    virtual type T = Bar;
    virtual type R = Baz;

    abstract T CreateT() {
    }
}

class FooDerived : Foo {
    override type T = BarDerived;
}

When you use the type member T on a foo, you see a Bar. 当你在foo上使用类型成员T时,你会看到一个Bar。 On a FooDerived you see a BarDerived. 在FooDerived上,您会看到BarDerived。 It's all very convenient for the programmer. 这对程序员来说非常方便。

Obviously C# doesn't have this feature though. 显然C#没有这个功能。

The one language I know of that supports virtual types is Beta. 我所知道的支持虚拟类型的一种语言是Beta。

You can read about it here: 你可以在这里读到它:

http://en.Wikipedia.org/wiki/BETA http://en.Wikipedia.org/wiki/BETA

One important thing to take away is that this isn't staticly type safe. 要带走的一件重要事情是,这不是静态类型安全的。 When it is supported by the compiler it requires lots of runtime checks to be actually type safe. 当编译器支持它时,它需要大量的运行时检查才能实际上是类型安全的。

The closest statically typed equivalent would be variant interfaces (which C# does support), but you would need one type parameter for every "entity" type, which would be unwieldy. 最接近静态类型的等价物将是变体接口(C#支持),但是对于每个“实体”类型,您将需要一个类型参数,这将是不实用的。 Referencing the interface would be really noisy ( IBackend<T1,...,T10> ). 引用接口会非常嘈杂( IBackend<T1,...,T10> )。 More importantly, a variant parameter can either be used as an argument or as a return type. 更重要的是,variant参数既可以用作参数,也可以用作返回类型。 Not both. 不是都。 Given your sample code, that wouldn't work for you. 鉴于您的示例代码,这对您不起作用。

That gets to my main point. 这是我的主要观点。 What you want to do is inherently dynamically typed. 你想要做的是固有的动态类型。 So, I wouldn't try to make it staticly typed. 所以,我不会尝试静态输入。 That's just going to make things waaaay more complicated than they need to be. 这只是让事情变得比他们需要的更复杂。 Try being more dynamic. 尝试更有活力。 That will probably simplify the backend and entity interfaces. 这可能会简化后端和实体接口。 Also, your backend interface seems a little "chatty". 此外,您的后端界面似乎有点“健谈”。 That's usually indicative of "non optimal abstractions". 这通常表示“非最佳抽象”。 The best interfaces are broad and simple Think of IEnumerable, or IDisposable. 最好的界面是广泛而简单的想想IEnumerable或IDisposable。

Your backend interface should ideally look like: 理想情况下,您的后端界面应如下所示:

interface IBackend {
    void Run(IFrontEndStuff);
}

I'm not sure if this is what you are looking for, but one approach could be a an abstract base class Foo, then aa handler that dynamically dispatches to concrete handlers. 我不确定这是否是您正在寻找的,但一种方法可能是一个抽象基类Foo,然后是一个动态调度到具体处理程序的处理程序。 Something like: 就像是:

void FooHandler(dynamic foo)
{
   FooHandlerImpl(foo);
}

void FooHandlerImpl(FooA foo)
{
   //whatever you do with FooA
}

void FooHandlerImpl(FooB foo)
{
   //whatever you do with FooB
}

What about using discreet interfaces for separation of concerns ? 如何使用谨慎的接口来分离关注点 (Loose coupling) (松耦合)

Something about like this ... 像这样的东西......

class Program
{
    static void Main(string[] args)
    {
        IBackend addon = new FooA();

        Console.WriteLine("Enter something if you like");
        var more = Console.ReadLine();

        var result = Runtime(addon);
        Console.WriteLine("Result: {0}", result ?? "No Output :o(");
    }

    static object Runtime(IBackend addon, string more = null)
    {
        var need = addon as INeed;
        if (need != null)
            need.Input = more;

        addon.Execute();

        var give = addon as IGive;
        if (give != null)
            return give.Output;

        return null;
    }
}

public interface IBackend
{
    void Execute();
}
public interface INeed
{
    string Input { set; }
}
public interface IGive
{
    string Output { get; }
}
public class FooA : IBackend, INeed, IGive
{
    public void Execute()
    {
        Console.WriteLine(this.Input ?? "No input :o(");

        if (!string.IsNullOrWhiteSpace(this.Input))
            this.Output = "Thanks!";
    }

    public string Input { private get; set; }
    public string Output { get; private set; }
}

If your add-on/backend items don't provide the interface it will just be ignored by that part of the application. 如果您的加载项/后端项不提供接口,那么应用程序的该部分将忽略它。 You could tighten this up with generics, but I'm not sure that is really necessary. 你可以用泛型来收紧这个问题,但我不确定这是否真的有必要。

Using collections and delegates you could even make this more abstract allowing for more runtime flexibility. 使用集合和委托,您甚至可以使其更加抽象,从而提高运行时灵活性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM