8

I've got something like this:

// This gets implemented by plugin authors to get callbacks about various things.
public interface ExternalPlugin
{
    // This gets called by the main application to tell the plugin some data
    // is available or similar.
    void DoStuff(SomeDataBlob blob);
}

// Data blob for v1 of API
public class SomeDataBlob
{
    internal SomeDataBlob(string prop) { Prop = prop; }
    // Some piece of data that V1 plugins need
    public string Prop { get; private set; }
}

// FUTURE!
// Data blob API v2 of API
public class SomeDataBlobV2 : SomeDataBlob
{
    // Can be passed to clients expecting SomeDataBlob no problem.
    internal SomeDataBlobV2(string prop, string prop2) :base(prop) { Prop2 = prop2; }
    // Some piece of data that V2 plugins need. V2 plugins can cast to this from
    // SomeDataBlob, but still can load successfully into older versions that support
    // only V1 of the API
    public string Prop2 { get; private set; }
}

I have to make SomeDataBlob public so that it can be used as a member of the public interface method ExternalPlugin.DoStuff. However, I would not like to allow clients to inherit from that class and thus be susceptible to the brittle base class problem. (All derivatives of that class should be kept in the same assembly)

Marking the class sealed goes too far because I believe removing sealed is a breaking API change; and even if that isn't, once I ship SomeDataBlobV2 clients could still do the wrong thing and inherit from SomeDataBlob directly.

Is there a way to enforce this kind of pattern?

Billy ONeal
  • 104,103
  • 58
  • 317
  • 552
  • just make it internal... previously answered here... http://stackoverflow.com/questions/1223873/namespace-only-class-visibility-in-c-net – phillip Jul 02 '13 at 20:42
  • 4
    Interfaces. Interfaces for the win. – It'sNotALie. Jul 02 '13 at 20:43
  • @Guru: Then clients using "V2" of the API can't determine if they can use the new features of "V2". – Billy ONeal Jul 02 '13 at 20:43
  • 1
    @newStackExchangeInstance: I'm unclear on what that means. Can you write an answer demonstrating what you're saying? – Billy ONeal Jul 02 '13 at 20:44
  • @BillyONeal I am already. – It'sNotALie. Jul 02 '13 at 20:46
  • Is there any reason your API's clients should *care* that the object being passed to their `DoStuff()` method is an instance of `SomeDataBlob`? Why not declare an `IDataBlob` interface instead? – Brant Bobby Jul 02 '13 at 20:49
  • 3
    Out of curiosity, can't a class just have all its constructors marked as `internal`? Then a derived class out of your assembly shouldn't be able to chain its constructor. Or am I missing something? – Theodoros Chatzigiannakis Jul 02 '13 at 20:54
  • 2
    Unsealing a class isn't a breaking change. Doing the opposite is, obviously. – EricLaw Jul 02 '13 at 20:56
  • Your class `SomeDataBlob` already has all its instance constructors `internal`. So that prevents classes in other assemblies from deriving from `SomeDataBlob` in a meaningful way. – Jeppe Stig Nielsen Jul 02 '13 at 20:56
  • @EricLaw I don't know if there's a precise definition of "breaking change", but suppose I have a lot of methods which take in a `System.String` parameter. These methods implicitly assume that the object they get is actually "just" a string. If the authors of .NET one day removed `sealed` from the `string` type, someone might pass a `DerivedString` to my methods where `DerivedString` had a new implementation of `Equals(object)` and `GetHashCode()`. That might "break" my methods. Breaking change? – Jeppe Stig Nielsen Jul 02 '13 at 21:43
  • Wouldn't disallowing derived classes be a "breaking change" if the API has supported it in the past? – RQDQ Jul 02 '13 at 22:11
  • @RQDQ: Yes, presumably it would be. (I'm defining an API from scratch here though and trying to do the right thing to allow forwards and backwards compatibility between plugins and the main app) – Billy ONeal Jul 02 '13 at 22:47
  • @Jeppe: Not really -- only if such a derived type were passed to code expecting the old interface. Of course, having a derived type like that breaks the rules -- easy to see violation of LSP. – Billy ONeal Jul 03 '13 at 19:04

6 Answers6

9

Make the class internal, and expose an interface instead. Then use the factory pattern to create the correct implementation.

public interface ISomeDataBlob
{
}

internal class SomeDataBlob : ISomeDataBlob
{
}

public class BlobApiFactory
{
    ISomeDataBlob Create();
}

You hide the implementation, but still give the user access to everything. You even make unit tests easier for your users ;)

Edit (answer to a comment from the OP)

What I effectively want is some method taking some parameters. I want to be able to add parameters that the main application can provide in a future version if the API without breaking clients. But I don't want clients to be able to create instances of the "parameters" class/interface or otherwise interact with it beyond receiving an instance of it as a parameter

Instead of hiding the APIs you can make sure that all object passed to your library originates from your assembly:

public class YourCoolService
{
    public void DoSomething(ISomeDataBlob blob)
    {
        if (blob.GetType().Assembly != Assembly.GetExecutingAssembly())
            throw new InvalidOperationException("We only support our own types");
    }
}

Edit2

Just noticed that @RQDQ already provided that solution (didn't notice when answering your comment). If that's the solution you want, accept his answer instead.

jgauffin
  • 99,844
  • 45
  • 235
  • 372
  • This has the same problem though. The external assembly could implement ISomeDataBlob and I don't want to allow that. – Billy ONeal Jul 02 '13 at 20:47
  • 6
    If you don't even want to expose the contract how would the users ever be able to use your api? – jgauffin Jul 02 '13 at 20:49
  • What I effectively want is some method taking some parameters. I want to be able to add parameters that the main application can provide in a future version if the API without breaking clients. But I don't want clients to be able to create instances of the "parameters" class/interface or otherwise interact with it beyond receiving an instance of it as a parameter. – Billy ONeal Jul 02 '13 at 20:52
3
  /// <summary>
  /// This is a dummy constructor - it is just here to prevent classes in other assemblies
  /// from being derived from this class. 
  /// See http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2971840&SiteID=1
  /// </summary>
  internal MhlAdminLayer() { }

The idea is to have a constructor with no parameters internal. Then the class can't be derived from.

Edit: Sorry, the link in the comment doesn't work any more.

Edit 2: http://msdn.microsoft.com/en-us/library/vstudio/ms173115.aspx

"You can prevent a class from being instantiated by making the constructor private ... "

RenniePet
  • 11,420
  • 7
  • 80
  • 106
  • The class he shows in his question already has all its instance constructors `internal`, so he might have solved his "problem" already. – Jeppe Stig Nielsen Jul 02 '13 at 20:59
  • I think the trick depends on it being a parameterless constructor. Unfortunately I can't remember the details now, just thought it was a neat trick when I discovered it several years ago on MSDN - and now they've removed that thread or something. – RenniePet Jul 02 '13 at 21:05
  • Well, when a class derives from another class, some of its instance constructors have to "chain" some instance constructor of the base class (there's a pathological counter-example which I shall not mention). So if you make sure _all_ instance constructors of the other class are `internal` or `private`, no class in a foreign assembly can inherit in a meaningful way. If you see no instance constructors at all in the source code of a (non-static) class, the compiler will create one default constructor for you, and that will be accessible from the outside, so you want to avoid that. – Jeppe Stig Nielsen Jul 02 '13 at 21:29
  • @JeppeStigNielsen: I'm sure you're right. I'm also sure that I HATE it that Microsoft is constantly reorganizing their websites, leaving millions of broken links all over the Internet. – RenniePet Jul 02 '13 at 21:38
2

If you are hell bent on not using sealed AND still using classes, you can enforce this at runtime. In otherwords, at your API boundary, inspect the classes involved and make sure they come from your assembly.

A simple example:

public void Bar(Foo foo)
{
    if (foo.GetType().Assembly != this.GetType().Assembly)
        throw new InvalidOperationException("It's not one of mine!");
}

public class Foo
{
}
RQDQ
  • 15,461
  • 2
  • 32
  • 59
1

As far as I know, interfaces are the way to do this. It would be an API breaking change, but it would mean you could do what you want.

public interface ExternalPlugin
{
    void DoStuff(ISomeDataBlob blob);
}
// Interfaces:
public interface IDataBlob
{
    string Prop { get; }
}
public interface IDataBlobV2 : IDataBlob
{
    string Prop2 { get; }
}
// Data blob for v1 of API
internal class SomeDataBlob : IDataBlob
{
    internal SomeDataBlob(string prop) { Prop = prop; }
    public string Prop { get; private set; }
}
// FUTURE!
// Data blob API v2 of API
public class SomeDataBlobV2 : SomeDataBlob, IDataBlobV2
{
    // Can be passed to clients expecting SomeDataBlob no problem.
    internal SomeDataBlobV2(string prop, string prop2) : base(prop) { Prop2 = prop2; }
    public string Prop2 { get; private set; }
}

And then to make the objects use the factory pattern, e.g.

public static class DataBlobFactory
{
    public IDataBlob GetDataBlob(string prop)
    {
        return new SomeDataBlob(prop);
    }
    // so on.
}
It'sNotALie.
  • 22,289
  • 12
  • 68
  • 103
0

What I would do is make some sort of factory class that exposes an interface that would pass an instance of whatever version for the specific API your client is using, and hide the implementation with an internal class

You can also use constraints to make it little easier to use, then the client can just put the Type of object they are looking for

public interface IBlobV1 { /*public stuff for V1 here*/ } 
internal class BlobV1: IBlobV1 {/*v1 implementation*/ }

public interface IBlobV2 : IBlobV1 {/*public interface for V2 here*/ }
internal class BlobV2:BlobV1,IBlobV2 {/*v2 implementation*/} 



public sealed BlobFactory 
{
    public IBlobV1 CreateVersion1Blob(){/* implementation */} 
    public IBlobV2 CreateVersion2Blob(){/* implementation */}
    public T CreateBlob<T>() 
           where T: IBlobV1
    { /* implementation */}
}
konkked
  • 3,161
  • 14
  • 19
0

SomeDataBlob can not be inherited because its only constructor is internal. If you try to implement a derived class in a client application:

class SomeDataBlobClient : SomeDataBlob
{
    SomeDataBlobClient():base("TEST")
    {

    }
}

You will get the following error:

The type 'ClassLibrary1.SomeDataBlob' has no constructors defined

It seems that you solved your own problem.

fcuesta
  • 4,429
  • 1
  • 18
  • 13