The curious case of the publicity-seeking interface and the shy abstract class
Noda Time has a guilty secret, and I'm not just talking about the fact that there's been very little progress on it recently. (It's not dead as a project - I have high hopes, when I can put some quality time into it.) This secret is called LocalInstant, and it's a pain in the neck.
One of the nice things about giving talks about an API you're currently writing is that you can see which concepts make sense to people, and which don't - as well as seeing which concepts you're able to explain and which you can't. LocalInstant has been an awkward type to explain right from day 1, and I don't think it's improved much since then. For the purpose of this blog post, you don't actually need to know what it means, but if you're really interested, imagine that it's like a time-zone-less date and time (such as "10:58 on July 2nd 2015" but also missing a calendar system, so you can't really tell what the month is etc. The important point is that it's not just time-zone-less, but it's actually local - so it doesn't represent a single instant in time. Unlike every other concept in Noda Time, I haven't thought of any good analogy between LocalInstant and the real world.
Now, I don't like having types I can't describe easily, and I'd love to just get rid of it completely... but it's actually an incredibly powerful concept to have in the library. Not for users of course, but for the implementation. It's spattered all over the place. Okay, the next best step to removing it is to hide it away from consumers: let's make it internal. Unfortunately, that doesn't work either, because it's referred to in interfaces all the time too. For example, almost every member of ICalendarSystem has LocalInstant as one of its parameters.
The rules around interfaces
Just to recap, every member of an interface - even an internal interface - is implicitly public. That causes some interesting restrictions. Firstly, every type referred to in a public interface must be public. So this would be invalid:
internal struct LocalInstant {}
public interface ICalendarSystem
{
LocalInstant GetLocalInstant(int year, int month, int day);
}
So far, so good. It's entirely reasonable that a public member's declaration shouldn't refer to an internal type. Calling code wouldn't understand what LocalInstant was, so how could it possibly use ICalendarSystem sensibly? But suppose we only wanted to declare the interface internally. That should be okay, right? Indeed, the compiler allows the following code:
internal struct LocalInstant {}
internal interface ICalendarSystem
{
LocalInstant GetLocalInstant(int year, int month, int day);
}
But hang on... isn't GetLocalInstant public? That's what I said earlier, right? So we're declaring a public member using an internal type... which we thought wasn't allowed. Is this a compiler bug?
Well, no. My earlier claim that "a public member's declaration shouldn't refer to an internal type" isn't nearly precise enough. The important aspect isn't just whether the member is declared public - but its accessibility domain. In this case, the accessibility domain of ICalendarSystem.GetLocalInstant is only the assembly, which is why it's a valid declaration.
However, life becomes fun when we try to implement ICalendarSystem in a public class. It's perfectly valid for a public class to implement an internal interface, but we have some problems declaring the method implementing GetLocalInstant. We can't make it a public method, because at that point its accessibility domain would be anything referring to the assembly, but the accessibility domain of LocalInstant itself would still only be the assembly. We can't make it internal, because it's implementing an interface member, which is public.
There is an alternative though: explicit interface implementation. That comes with all kinds of other interesting points, but it does at least compile:
internal struct LocalInstant {}
internal interface ICalendarSystem
{
LocalInstant GetLocalInstant(int year, int month, int day);
}
public class GregorianCalendarSystem : ICalendarSystem
{
LocalInstant ICalendarSystem.GetLocalInstant(int year, int month, int day);
{
}
}
So, we've got somewhere at this point. We've managed to make a type used within an interface internal, but at the cost of making the interface itself internal, and requiring explicit interface implementation within any public classes implementing the interface.
That could potentially be useful in Noda Time, but it doesn't solve our real LocalInstant / ICalendarSystem problem. We need ICalendarSystem to be public, because consumers need to be able to specify a calendar when they create an instance of ZonedDateTime or something similar. Interfaces are just too demanding in terms of publicity.
Fortunately, we have another option up our sleeves...
Abstract classes to the rescue!
I should come clean at this point and say that generally speaking, I'm an interface weenie. Whenever I need a reusable and testable abstraction, I reach for interfaces by default. I have a general bias against concrete inheritance, including abstract classes. I'm probably a little too harsh on them though... particularly as in this case they do everything I need them to.
In Noda Time, I definitely don't need the ability to implement ICalendarSystem and derive from another concrete class... so making it a purely abstract class will be okay in those terms. Let's see what happens when we try:
internal struct LocalInstant {}
public abstract class CalendarSystem
{
internal abstract LocalInstant GetLocalInstant(int year, int month, int day);
}
internal class GregorianCalendarSystem : CalendarSystem
{
internal override LocalInstant GetLocalInstant(int year, int month, int day)
{
}
}
Hoorah! Now we've hidden away LocalInstant but left CalendarSystem public, just as we wanted to. We could make GregorianCalendarSystem public or not, as we felt like it. If we want to make any of CalendarSystem's abstract methods public, then we can do so provided they don't require any internal types. There's on interesting point though: types outside the assembly can't derive from CalendarSystem. It's a little bit as if the class only provided an internal constructor, but with a little bit more of an air of mystery... you can override every method you can actually see, and still get a compile-time error message like this:
OutsideCalendar.cs(1,14): error CS0534: 'OutsideCalendar' does not implement inherited abstract member
'CalendarSystem.GetLocalInstant(int, int, int)'
I can just imagine the author of the other assembly thinking, "But I can't even see that method! What is it? Where is it coming from?" Certainly a case where the documentation needs to be clear. Whereas it's impossible to create an interface which is visible to the outside world but can't be implemented externally, that's precisely the situation we've reached here.
The abstract class is a little bit like an authentication token given by a single-sign-on system. From the outside, it's an opaque item: you don't know what's in it or how it does its job... all you know is that you need to obtain it, and then you can use it to do other things. On the inside, it's much richer - full of useful data and members.
Conclusion
Until recently, I hadn't thought of using abstract classes like this. It would possibly be nice if we could use interfaces in the same way, effectively limiting the implementation to be in the declaring assembly, but letting the interface itself (and some members) be visible externally.
A bigger question is whether this is a good idea in terms of design anyway. If I do make LocalInstant internal, there will be a lot of interfaces which go the same way... or become completely internal. For example, the whole "fields" API of Noda Time could become an implementation detail, with suitable helper methods to fetch things like "how many days are there in the given month." The fields API is an elegant overall design, but it's quite complicated considering the very limited situations in which most callers will use it.
I suspect I will try to go for this "reduced API" for v1, knowing that we can always make things more public later on... that way we give ourselves a bit more flexibility in terms of not having to get everything right first time within those APIs, too.
Part of me still feels uncomfortable with the level of hiding involved - I know other developers I respect deeply who hide as little as possible, for maximum flexibility - but I do like the idea of an API which is really simple to browse.
Aside from the concrete use case of Noda Time, this has proved an interesting exercise in terms of revisiting accessibility and the rules on what C# allows.