简体   繁体   中英

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:

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 :

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> . 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> . 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

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

You can change this to let the IFoo remember which IBackend it came from.

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 . 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. On a FooDerived you see a BarDerived. It's all very convenient for the programmer.

Obviously C# doesn't have this feature though.

The one language I know of that supports virtual types is Beta.

You can read about it here:

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. Referencing the interface would be really noisy ( IBackend<T1,...,T10> ). More importantly, a variant parameter can either be used as an argument or as a return type. 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.

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. 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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