简体   繁体   中英

Can you model an interface where only it's descendants can be implemented?

Assume the following code:

namespace Example {
  public interface IBase {
    string CommonMember { get; set; }
  }

  public interface IDerived : IBase {
    void DoSomething();
  }

  public interface IComplexDerived : IBase {
    IEnumerable<object> Junk { get; }
  }
}

I have a similar structure in the project I'm currently working on. The interface IBase primarily serves the purpose to be able to keep instances of IDerived and IComplexDerived in the same container (like a List<IBase> ) and also not having to repeat common interface member definitions (like CommonMember in the example).

One way this would then be used would be something like this:

public class Foo {
  public void Bar( IEnumerable<IBase> instances ) {
    foreach( IBase instance in instances ) {
      if( instance is IDerived ) { /* do something */ } 
      else if( instance is IComplexDerived ) {  /* do something else */ }
    }
  }
}

So, nothing would stop the user from implementing IBase and passing instances of that class into the system. But doing that would be completely useless because the whole library only expects to deal with classes that implement interfaces that were derived from IBase .

This concept is of course fully documented and shouldn't cause any problems. However, I was wondering if it would be possible to communicate this through means of the language itself. Like having an abstract class, but for interfaces.

You might ask why not simply use an abstract class then. The reason for that is that we don't want to impose the requirement to inherit from our class.

I'm not sure if this is feasible in your actual case, but I think you could have

  • IComplexDerived inherit from IDerived instead of IBase .
  • You would then have a list of IDerived instead of IBase , so even a new implementation of IBase would not type-check (since you require an IEnumerable<IDerived> )
  • Your classes inheriting from IComplexDerived would simply implement DoSomething() in a different way. By doing this you would let your Bar method decide polymorphically what DoSomething it needs to call (and avoid checking on the type)

I mean something like this:

  public interface IBase {
    string CommonMember { get; set; }
  }

  public interface IDerived : IBase {
    void DoSomething();
  }

  //IComplexDerived isnow a IDerived    
  public interface IComplexDerived : IDerived { 
    IEnumerable<object> Junk { get; }
  }

public class Foo 
{
  // Bar requires IEnumerable<IDerived> so you can't call it with a collection
  // of classes implementing IBase
  public void Bar( IEnumerable<IDerived> instances ) {
    foreach( IDerived instance in instances ) {
        instance.DoSomething(); // DoSomething will "do something else" in 
                                // classes implementing IComplexDerived
    }
  }
}

One possibility is to remove the common interface from IDerived and IComplexDervied and create a wrapper class which takes an instance of one of them and provides the common functionality:

public interface IDerived
{
    void DoSomething();
    string CommonMember { get; set; }
}

public interface IComplexDerived
{
    IEnumerable<object> Junk { get; }
    string CommonMember { get; set; }
}

public class EitherDerived : IBase
{
    private readonly IDerived derived;
    private readonly IComplexDerived complex;
    private readonly bool isComplex;

    public EitherDerived(IDerived derived)
    {
        this.derived = derived;
        this.isComplex = false;
    }

    public EitherDerived(IComplexDerived complex)
    {
        this.complext = complex;
        this.isComplex = true;
    }

    public string CommonMember
    {
        get
        {
            return isComplex ? complex.CommonMember : derived.CommonMember;
        }
        set
        {
            //...
        }
    }

    public TOut Either<TOut>(Func<IDerived, TOut> mapDerived, Func<IComplexDerived, TOut> mapComplex)
    {
        return isComplex ? mapComplex(complex) : mapDerived(derived);
    }
}

Then you can use this class instead of your IBase interface if you want to be sure you are dealing with one of those classes:

private object HandleDerived(IDerived derived) { ... }
private object HandleComplex(IComplexDerived complex) { ... }

public void Bar(IEnumerable<EitherDerived> instances)
{
    foreach(var either in instances)
    {
        object _ = either.SelectEither(HandleDerived, HandleComplex);
    }
}

The suggestion to look for a different design altogether was what I ended up doing. Since this is an open source project, we can look at the actual results.

  • IBase is ITimelineTrackBase and describes interface members that are common to all derived types.

  • IDerived is ITimelineTrack and it describes a track on a timeline which consists of a single element with a start and end.

  • IComplexDerived is IMultiPartTimelineTrack and it describes a track on a timeline which consists of multiple elements that each have a start and an end.

Contrary to my earlier plans, I'm not storing these in a List<IBase> , but I'm using List<IComplexDerived> . Or, in terms of the application, a List<IMultiPartTimelineTrack> .

Now I decided to not accept an IBase anywhere if that's not what I actually want to support in that method. So the ITimelineTrackBase is used purely as a base interface and isn't offered as an accepted parameter type anywhere in the library.

Instead the whole library deals either with single track elements ( ITimelineTrack ) or a collection of those ( IMultiPartTimelineTrack ). As needed, the former is wrapped into the latter by a helper construct SingleTrackToMultiTrackWrapper .

So instead of making it impossible to implement the interface, I just made it pointless to implement it.

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