Abstract classes are isomorphic to Dependency Injection.

This article is part of a series of articles about software design isomorphisms.

The introduction to Design Patterns states:

Program to an interface, not an implementation.
When I originally read that, I took it quite literally, so I wrote all my C# code using interfaces instead of abstract classes. There are several reasons why, in general, that turns out to be a good idea, but that's not the point of this article. It turns out that it doesn't really matter.

If you have an abstract class, you can refactor to an object model composed from interfaces without loss of information. You can also refactor back to an abstract class. These two refactorings are each others' inverses, so together, they form an isomorphism.

Abstract class on the left, concrete class with injected interfaces on the right; arrow between boxes.

When refactoring an abstract class, you extract all its pure virtual members to an interface, each of its virtual members to other interfaces, and inject them into a concrete class. The inverse refactoring involves going back to an abstract class.

This is an important result, because upon closer inspection, the Gang of Four didn't have C# or Java interfaces in mind. The book pre-dates both Java and C#, and its examples are mostly in C++. Many of the examples involve abstract classes, but more than ten years of experience has taught me that I can always write a variant that uses C# interfaces. That is, I believe, not a coincidence.

Abstract class #

An abstract class in C# has this general shape:

public abstract class Class1
{
    public Data1 Data { getset; }
 
    public abstract OutPureVirt1 PureVirt1(InPureVirt1 arg);
 
    public abstract OutPureVirt2 PureVirt2(InPureVirt2 arg);
 
    public abstract OutPureVirt3 PureVirt3(InPureVirt3 arg);
 
    // More pure virtual members...
 
    public virtual OutVirt1 Virt1(InVirt1 arg)
    {
        // Do stuff with this, Data, and arg; return an OutVirt1 value.
    }
 
    public virtual OutVirt2 Virt2(InVirt2 arg)
    {
        // Do stuff with this, Data, and arg; return an OutVirt2 value.
    }
 
    public virtual OutVirt3 Virt3(InVirt3 arg)
    {
        // Do stuff with this, Data, and arg; return an OutVirt3 value.
    }
 
    // More virtual members...
 
    public OutConc1 Op1(InConc1 arg)
    {
        // Do stuff with this, Data, and arg; return an OutConc1 value.
    }
 
    public OutConc2 Op2(InConc2 arg)
    {
        // Do stuff with this, Data, and arg; return an OutConc2 value.
    }
 
    public OutConc3 Op3(InConc3 arg)
    {
        // Do stuff with this, Data, and arg; return an OutConc3 value.
    }
 
    // More concrete members...
}

Like in the previous article, I've deliberately kept the naming abstract (but added a more concrete example towards the end). The purpose of this article series is to look at the shape of code, instead of what it does, or why. From argument list isomorphisms we know that we can represent any method as taking a single input value, and returning a single output value.

An abstract class can have non-virtual members. In C#, this is the default, whereas in Java, you'd explicitly have to use the final keyword. In the above generalised representation, I've named these non-virtual members Op1, Op2, and so on.

An abstract class can also have virtual members. In C#, you must explicitly use the virtual keyword in order to mark a method as overridable, whereas this is the default for Java. In the above representation, I've called these methods Virt1, Virt2, etcetera.

Some virtual members are pure virtual members. These are members without an implementation. Any concrete (that is: non-abstract) class inheriting from an abstract class must provide an implementation for such members. In both C# and Java, you must declare such members using the abstract keyword. In the above representation, I've called these methods PureVirt1, PureVirt2, and so on.

Finally, an abstract class can contain data, which you can represent as a single data object, here of the type Data1.

The concrete and virtual members could, conceivably, call other members in the class - both concrete, virtual, and pure virtual. In fact, this is how many of the design patterns in the book work, for example Strategy, Template Method, and Builder.

From abstract class to Dependency Injection #

Apart from its Data, an abstract class contains three types of members:

  • Those that must be implemented by derived classes: pure virtual members
  • Those that optionally can be overriden by derived classes: virtual members
  • Those that cannot be overridden by derived classes: concrete, sealed, or final, members
When refactoring to interfaces, you do the following:
  1. Extract an interface from the pure virtual members.
  2. Extract an interface from each of the virtual members.
  3. Implement each of the 'virtual member interfaces' with the implementation from the virtual member.
  4. Add a constructor to the abstract class that takes all these new interfaces as arguments. Save the arguments as class fields.
  5. Change all code in the abstract class to talk to the injected interfaces instead of direct class members.
  6. Remove the virtual and pure virtual members from the class, or make them non-virtual. If you keep them around, their implementation should be one line of code, delegating to the corresponding interface.
  7. Change the class to a concrete (non-abstract) class.
If you apply this refactoring to the above class, you should arrive at something like this:

public sealed class Class1
{
    private readonly IInterface1 pureVirts;
    private readonly IVirt1 virt1;
    private readonly IVirt2 virt2;
    private readonly IVirt3 virt3;
    // More virt fields...
 
    public Data1 Data { getset; }
 
    public Class1(
        IInterface1 pureVirts, 
        IVirt1 virt1, 
        IVirt2 virt2, 
        IVirt3 virt3
        /* More virt arguments... */)
    {
        this.pureVirts = pureVirts;
        this.virt1 = virt1;
        this.virt2 = virt2;
        this.virt3 = virt3;
        // More field assignments
    }
 
    public OutConc1 Op1(InConc1 arg)
    {
        // Do stuff with this, Data, and arg; return an OutConc1 value.
    }
 
    public OutConc2 Op2(InConc2 arg)
    {
        // Do stuff with this, Data, and arg; return an OutConc2 value.
    }
 
    public OutConc3 Op3(InConc3 arg)
    {
        // Do stuff with this, Data, and arg; return an OutConc3 value.
    }
 
    // More concrete members...
}

While not strictly necessary, I've marked the class sealed (final in Java) in order to drive home the point that this is no longer an abstract class.

This is an example of the Constructor Injection design pattern. (This is not a Gang of Four pattern; you can find a description in my book about Dependency Injection.)

Since it's optional to override virtual members, any class originally inheriting from an abstract class can choose to override only one, or two, of the virtual members, while leaving other virtual members with their default implementations. In order to support such piecemeal redefinition, you can extract each virtual member to a separate interface, like this:

public interface IVirt1
{
    OutVirt1 Virt1(InVirt1 arg);
}

Notice that each of these 'virtual interfaces' are injected into Class1 as a separate argument. This enables you to pass your own implementation of exactly those you wish to change, while you can pass in the default implementation for the rest. The default implementations are the original code from the virtual members, but moved to a class that implements the interfaces:

public class DefaultVirt : IVirt1IVirt2IVirt3

When inheriting from the original abstract class, however, you must implement all the pure virtual members, so you can extract a single interface from all the pure virtual members:

public interface IInterface1
{
    OutPureVirt1 PureVirt1(InPureVirt1 arg);
 
    OutPureVirt2 PureVirt2(InPureVirt2 arg);
 
    OutPureVirt3 PureVirt3(InPureVirt3 arg);
 
    // More pure virtual members...
}

This forces anyone who wants to use the refactored (sealed) Class1 to provide an implementation of all of those members. There's an edge case where you inherit from the original Class1 in order to create a new abstract class, and implement only one or two of the pure virtual members. If you want to support that edge case, you can define an interface for each pure virtual member, instead of one big interface, similar to IVirt1, IVirt2, and so on.

From Dependency Injection to abstract class #

I hope it's clear how to perform the inverse refactoring. Assume that the above sealed Class1 is the starting point:

  1. Mark Class1 as abstract.
  2. For each of the members of IInterface1, add a pure virtual member.
  3. For each of the members of IVirt1, IVirt2, and so on, add a virtual member.
  4. Move the code from the default implementation of the 'virtual interfaces' to the new virtual members.
  5. Delete the dependency fields and remove the corresponding arguments from the constructor.
  6. Clean up orphaned interfaces and implementations.
This refactoring assumes a class using Dependency Injection like the one shown in this article, above. The example code is the same as the above example code, although the order is reversed: you start with the Dependency Injection class and end with the abstract class.

Example: Gang of Four maze Builder as an abstract class #

As an example, consider the original Gang of Four example of the Builder pattern. The example in the book is based on an abstract class called MazeBuilder. Translated to C#, it looks like this:

public abstract class MazeBuilder
{
    public virtual void BuildMaze() { }
 
    public virtual void BuildRoom(int room) { }
 
    public virtual void BuildDoor(int roomFrom, int roomTo) { }
 
    public abstract Maze GetMaze();
}

In the book, all four methods are virtual, because:

"They're not declared pure virtual to let derived classes override only those methods in which they're interested."
When it comes to the GetMaze method, this means that the method in the book returns a null reference by default. Since this seems like poor API design, and also because the example becomes more illustrative if the class has both abstract and virtual members, I changed it to be abstract (i.e. pure virtual).

In general, there are various other issues with this design, the most glaring of which is the implied sequence coupling between members: you're expected to call BuildMaze before any of the other methods. A better design would be to remove that explicit step entirely, or else turn it into a factory that you have to call in order to be able to call the other methods. That's not the topic of the present article, so I'll leave the API like this.

The book also shows a simple usage example of the abstract MazeBuilder class:

public class MazeGame
{
    public Maze CreateMaze(MazeBuilder builder)
    {
        builder.BuildMaze();
 
        builder.BuildRoom(1);
        builder.BuildRoom(2);
        builder.BuildDoor(1, 2);
 
        return builder.GetMaze();
    }
}

You use it with e.g. a StandardMazeBuilder like this:

var game = new MazeGame();
var builder = new StandardMazeBuilder();
 
var maze = game.CreateMaze(builder);

You could also, again following the book's example as closely as possible, use it with a CountingMazeBuilder, like this:

var game = new MazeGame();
var builder = new CountingMazeBuilder();
 
game.CreateMaze(builder);
 
var msg = $"The maze has {builder.RoomCount} rooms and {builder.DoorCount} doors.";

This would produce "The maze has 2 rooms and 1 doors.".

Both StandardMazeBuilder and CountingMazeBuilder are concrete classes that derive from the abstract MazeBuilder class.

Maze Builder refactored to interfaces #

If you follow the refactoring outline in this article, you can refactor the above MazeBuilder class to a set of interfaces. The first should be an interface extracted from all the pure virtual members of the class. In this example, there's only one such member, so the interface becomes this:

public interface IMazeBuilder
{
    Maze GetMaze();
}

The three virtual members each get their own interface, so that you can pick and choose which of them you want to override, and which of them you prefer to keep with their default implementation (which, in this particular case, is to do nothing).

The first one was difficult to name:

public interface IMazeInitializer
{
    void BuildMaze();
}

An interface with a single method called BuildMaze would naturally have a name like IMazeBuilder, but unfortunately, I just used that name for the previous interface. The reason I named the above interface IMazeBuilder is because this is an interface extracted from the MazeBuilder abstract class, and I consider the pure virtual API to be the core API of the abstraction, so I think it makes most sense to keep the name for that interface. Thus, I had to come up with a smelly name like IMazeInitializer.

Fortunately, the two remaining interfaces are a little better:

public interface IRoomBuilder
{
    void BuildRoom(int room);
}

public interface IDoorBuilder
{
    void BuildDoor(int roomFrom, int roomTo);
}

The three virtual members all had default implementations, so you need to keep those around. You can do that by moving the methods' code to a new class that implements the new interfaces:

public class DefaultMazeBuilder : IMazeInitializerIRoomBuilderIDoorBuilder

In this example, there's no reason to show the implementation of the class, because, as you may recall, all three methods are no-ops.

Instead of inheriting from MazeBuilder, implementers now implement the appropriate interfaces:

public class StandardMazeBuilder : IMazeBuilderIMazeInitializerIRoomBuilderIDoorBuilder

This version of StandardMazeBuilder implements all four interfaces, since, before, it overrode all four methods. CountingMazeBuilder, on the other hand, never overrode BuildMaze, so it doesn't have to implement IMazeInitializer:

public class CountingMazeBuilder : IRoomBuilderIDoorBuilderIMazeBuilder

All of these changes leaves the original MazeBuilder class defined like this:

public class MazeBuilder : IMazeBuilderIMazeInitializerIRoomBuilderIDoorBuilder
{
    private readonly IMazeBuilder mazeBuilder;
    private readonly IMazeInitializer mazeInitializer;
    private readonly IRoomBuilder roomBuilder;
    private readonly IDoorBuilder doorBuilder;
 
    public MazeBuilder(
        IMazeBuilder mazeBuilder,
        IMazeInitializer mazeInitializer,
        IRoomBuilder roomBuilder,
        IDoorBuilder doorBuilder)
    {
        this.mazeBuilder = mazeBuilder;
        this.mazeInitializer = mazeInitializer;
        this.roomBuilder = roomBuilder;
        this.doorBuilder = doorBuilder;
    }
 
    public void BuildMaze()
    {
        this.mazeInitializer.BuildMaze();
    }
 
    public void BuildRoom(int room)
    {
        this.roomBuilder.BuildRoom(room);
    }
 
    public void BuildDoor(int roomFrom, int roomTo)
    {
        this.doorBuilder.BuildDoor(roomFrom, roomTo);
    }
 
    public Maze GetMaze()
    {
        return this.mazeBuilder.GetMaze();
    }
}

At this point, you may decide to keep the old MazeBuilder class around, because you may have other code that relies on it. Notice, however, that it's now a concrete class that has dependencies injected into it via its constructor. All four members only delegate to the relevant dependencies in order to do actual work.

MazeGame looks like before, but calling CreateMaze looks more complicated:

var game = new MazeGame();
var builder = new StandardMazeBuilder();
 
var maze = game.CreateMaze(new MazeBuilder(builder, builder, builder, builder));

Notice that while you're passing four dependencies to the MazeBuilder constructor, you can reuse the same StandardMazeBuilder object for all four roles.

If you want to count the rooms and doors, however, CountingMazeBuilder doesn't implement IMazeInitializer, so for that role, you'll need to use the default implementation:

var game = new MazeGame();
var builder = new CountingMazeBuilder();
 
game.CreateMaze(new MazeBuilder(builder, new DefaultMazeBuilder(), builder, builder));
 
var msg = $"The maze has {builder.RoomCount} rooms and {builder.DoorCount} doors.";

If, at this point, you're beginning to wonder what value MazeBuilder adds, then I think that's a legitimate concern. What often happens, then, is that you simply remove that extra layer.

Mazes without MazeBuilder #

When you delete the MazeBuilder class, you'll have to adjust MazeGame accordingly:

public class MazeGame
{
    public Maze CreateMaze(
        IMazeInitializer initializer, 
        IRoomBuilder roomBuilder, 
        IDoorBuilder doorBuilder,
        IMazeBuilder mazeBuilder)
    {
        initializer.BuildMaze();
 
        roomBuilder.BuildRoom(1);
        roomBuilder.BuildRoom(2);
        doorBuilder.BuildDoor(1, 2);
 
        return mazeBuilder.GetMaze();
    }
}

The CreateMaze method now simply takes the four interfaces on which it relies as individual arguments. This simplifies the client code as well:

var game = new MazeGame();
var builder = new StandardMazeBuilder();
 
var maze = game.CreateMaze(builder, builder, builder, builder);

You can still reuse a single StandardMazeBuilder in all roles, but again, if you only want to count the rooms and doors, you'll have to rely on DefaultMazeBuilder for the behaviour that CountingMazeBuilder doesn't define:

var game = new MazeGame();
var builder = new CountingMazeBuilder();
 
game.CreateMaze(new DefaultMazeBuilder(), builder, builder, builder);
 
var msg = $"The maze has {builder.RoomCount} rooms and {builder.DoorCount} doors.";

The order in which dependencies are passed to CreateMaze is different than the order they were passed to the now-deleted MazeBuilder constructor, so you'll have to pass a new DefaultMazeBuilder() as the first argument in order to fill the role of IMazeInitializer. Another way to address this issue is to supply various overloads of the CreateMaze method that uses DefaultMazeBuilder for the behaviour that you don't want to override.

Summary #

Many of the original design patterns in Design Patterns are described with examples in C++, and many of these examples use abstract classes as the programming interfaces that the Gang of Four really had in mind when they wrote that we should be programming to interfaces instead of implementations.

The most important result of this article is that you can reinterpret the original design patterns with C# or Java interfaces and Dependency Injection, instead of using abstract classes. I've done this in C# for more than ten years, and in my experience, you never need abstract classes in a greenfield code base. There's always an equivalent representation that involves composition of interfaces.

Next: Inheritance-composition isomorphism.


Comments

While the idea is vey interesting I think it is not exactly an isomorphism.

The first reason I think it is not an isomorphism is language-specific since Java and C# allow implementing multiple interfaces but not multiple abstract classes. It can make a reverse transformation from interfaces back to an abstract class non-trivial.

The second reason is that abstract class guarantees that whatever class implements the pure virtual members and overrides virtual members share the same state between all its methods and also with the abstract base class. With the maze Builder example there must be a state shared between GetMaze, BuildMaze, BuildRoom and BuildDoor methods but the dependency injection does not seem to reflect it.

Perhaps there should be some kind of Data parameter passed to all injected interfaces.

2018-03-05 19:03 UTC

Max, thank you for writing, and particularly for applying critique to this post. One of my main motivations for writing the entire article series is that I need to subject my thoughts to peer review. I've been thinking about these things for years, but in order to formalise them, I need to understand whether I'm completely wrong (I hope not), of, if I'm not, what are the limits of my findings.

I think you've just pointed at one such limit, and for that I'm grateful. The rest of this answer, then, is not an attempt to refute your comment, but rather an effort to identify some constraints within which what I wrote may still hold.

Your second objection doesn't worry me that much, because you also suggest a way around it. I admit that I faked the Maze Builder code somewhat, so that the state isn't explicit. I feel that fudging the code example is acceptable, as the Gang of Four code example is also clearly incomplete. In any case, you're right that an abstract class could easily contain some shared state. When refactoring to interfaces, the orchestrating class could instead pass around that state as an argument to all methods, as you suggest. Would it be reasonable to conclude that this, then, doesn't prevent the translations from being isomorphic?

There's still your first objection, which I think is significant. That's the reason I decided to cover your second objection first, because I think it'll require more work to address the first objection.

First, I think we need to delimit the problem, since your comment slightly misrepresents my article. The claim in the article is that you can refactor an abstract class to a concrete class with injected dependencies. Furthermore, the article claims that this translation is isomorphic; i.e. that you can refactor a concrete class with injected dependencies to an abstract class.

If I read your comment right, you're saying that a class can implement more than one interface, like this:

public class MyClass : IFooIBar

I agree that you can't use the transformations described in this article to refactor MyClass to an abstract class, because that's not the transformation that the article describes.

That doesn't change that your comment is uncomfortably close to an inconvenient limitation. You're right that there seems to be a limitation when it comes to C#'s and Java's lack of multiple inheritance. As your comment implies, if a translation is isomorphic, one has to be able to start at either end, and round-trip to the other end and back. Thus, one has to be able to start with a concrete class with injected dependencies, and refactor to an abstract class; for example:

public class MyClass
{
    public MyClass(IFoo foo, IBar bar)
    {
        // ...
    }
}

As far as I can tell, that's exactly the shape of the sealed version of Class1, above, so I'm not convinced that that's a problem, but something like the following does look like a problem to me:

public class MyClass
{
    public MyClass(IFoo foo1, IFoo foo2)
    {
        // ...
    }
}

It's not clear to me how one can refactor something like that to an abstract class, and still retain the distinction between foo1 and foo2. My claim is not that this is impossible, but only that it's not immediately clear to me how to do that. Thus, we may have to put a constraint on the original claim, and instead say something like this:

An abstract class is isomorphic to a concrete class with injected dependencies, given that all the injected dependencies are of different types.
We can attempt to illustrate the claim like this:

The set of abstract classes juxtaposed with the set of dependency injection, the latter with a subset for which arrows go both ways between the subset and the set of abstract classes.

This is still an isomorphism, I think, although I invite further criticism of that claim. Conceptual Mathematics defines an isomorphism in terms of categories A and B, and as far as I can tell, A and B can be as specific as we want them to be. Thus, we can say that A is the set of abstract classes, and B is the subset of concrete classes with injected dependencies, for which no dependency share the same type.

If we have to constrain the isomorphism in this way, though, is it still interesting? Why should we care?

To be perfectly honest, what motivated me to write this particular article is that I wanted to describe the translation from an abstract class to dependency injection. The inverse interests me less, but I thought that if the inverse translation exists, I could fit this article in with the other articles in this article series about software design isomorphisms.

The reason I care about the translation from abstract class to dependency injection is that I often see code where the programmers misuse inheritance. My experience with C# is that one can completely avoid inheritance. The way to do that is to use dependency injection instead. This article shows how to do that.

The result that one can write real, complex code bases in C# without inheritance is important to me, because one of my current goals is to teach people the advantages of functional programming, and one barrier I run into is that people who come from object-oriented programming run into problems when they no longer can use inheritance. Thus, this article shows an object-oriented alternative to inheritance, so that people can get used to the idea of designing without inheritance, even before they start looking at functional programming.

Another motivation for this article is that it's part of a much larger article series about design patterns, and how they relate to fundamental abstractions. In Design Patterns, all the (C++) patterns are described in terms of inheritance, so I wrote this article series on isomorphisms in order to be able to represent various design patterns in other forms than they appear in the book.

2018-03-08 9:57 UTC
Ciprian Vilcan #

This idea is a very interesting and useful one, but as I found out in one of my toy projects and as Max stated above, it is unfortunately not language agnostic.
As far as C# is concerned, you can have operator overloading in an abstract class, which is a bit of logic that I see no way of extracting to an interface and thus remove the need for inheritance. Example below.
(You could define Add, Subtract, Multiply and Divide methods, but to me they seem like reinventing the square wheel. They seem much less convenient than +-*/)

I tried creating some nice Temperature value objects, similar to the money monoid you presented and I came up with 5 classes:
Temperature, Kelvin, Celsius, Fahrenheit and TemperatureExpression.
Temperature is the abstract class and its +Temperature and -Temperature operators are overloaded so that they return a TemperatureExpression, which can then be evaluated to a Maybe<TTemperature> where TTemperature : Temperature.
A TemperatureExpression is nothing more than a lazily evaluated mathematical expression (for example: 12K + 24C - 32F).
Also, it's a Maybe because 0K - 1C won't be a valid temperature, so we have to also take this case into consideration.

For further convenience, the Kelvin class has its own overloaded + operator, because no matter what you do, when adding together two Kelvin values you'll always end up with something that is greater than 0.

These being said, if you want to leverage the C# operators there are some limitations regarding this transformation that keep you from having this abstract class -> DI isomorphism.
That is, unless you're willing to add the methods I've spoken of in the first paragraph.
But, in an ideal programming language, IMHO, arithmetic and boolean operators should be part of some public interfaces like, ISubtractable, IDivideable and thus allow for a smooth transition between abstract classes and interface DI.

2018-03-08 10:16 UTC

Ciprian, thank you for writing. I agree that this isn't language agnostic. It's possible that we need to add further constraints to the conjecture, but I still anticipate that, with appropriate constraints, it holds for statically typed 'mainstream' object-oriented languages (i.e. C# and Java). It may also hold for other languages, but it requires detailed knowledge of a language to claim that it does. For instance, it's been too long since I wrote C++ code, and I can't remember how its object-oriented language features work. Likewise, it'd be interesting to investigate if the conjecture holds when applied to JavaScript, Ruby, Python, etc., but I've been careful not to claim that, as I know too little about those languages.

Regarding your temperature example, I think that perhaps I understand what the issue is, but I'm not sure I've guessed right. In the interest of being able to have an unambiguous discussion about a code example, could you please post the pertinent code that illustrates your point?

2018-03-09 8:07 UTC
Ciprian Vilcan #

I've added the code to github. I think it's better than copy-pasting here as you can gain some extra context.

It's a playground project where I put to practice various ideas I find on the web (so if you see something you consider should be done otherwise, I don't mind you saying it). I've added a few comments to make it a bit easier to understand and also removed anything that doesn't pertain to the issue with abstract class -> DI.

In short, the bit of logic that I can't see a way of extracting to some sort of injectable dependency are the +- operators on the Temperature class due to the fact that they are static methods, thus cannot be part of an interface (at least in C#, I don't know about programming languages).

2018-03-09 11:14 UTC

Ciprian, thank you for elaborating. I'd forgotten about C#'s ability to overload arithmetic operators, which is, I believe, what you're referring to. To be clear, I do believe that it's a fair enough critique, so that we'll have to once again restrict this article's conjecture to something like:

There exists a subset A of all abstract classes, and a subset of all concrete classes with injected dependencies I, such that an isomorphism A <-> I exists.
In diagram form, it would look like this:

By its shape, the diagram suggests that the size of abstract classes not isomorphic with Dependency Injection is substantial, but that's not really my intent; I just had to leave room for the text.

My experience suggests to me that most abstract classes can be refactored as I've described in this article, but clearly, as you've shown, there are exceptions. C#'s support for operator overloading is one such exception, but there may be others of which I'm currently ignorant.

That said, I would like to make the case that arithmetic operators aren't object-oriented in the first place. Had the language been purely object-oriented from the outset, addition would more appropriately have had a syntax like 40.Add(2). This would imply that the language would have been based on the concept of objects as data with behaviour, and the behaviour would exclusively have been defined by class members.

Even at the beginning, though, C# was a hybrid language. It had (and still has) a subset of language features focused on more low-level programming. It has value types in addition to reference types, it has arithmetic operators, it has special support for bitwise Boolean operators, and it even has pointers.

There are practical reasons that all of those features exist, but I would claim that none of those features have anything to do with object-orientation.

Specifically when it comes to arithmetic operators, the operators are all special cases baked into the language. The selection of operators is sensible, but when you get to the bottom of it, arbitrary. For instance, there's a modulo operator, but no power operator. Why?

As an aside, languages do exist where arithmetic is an abstraction instead of a language feature. The one I'm most familiar with is Haskell, where arithmetic is defined in terms of type classes. It's worth noting that the operators +, *, and - are defined in an abstraction called Num, whereas the 'fourth' arithmetic operator / is defined in a more specialised abstraction called Fractional.

Not that Haskell's model of arithmetic is perfect, but there's a rationale behind this distinction. Division is special, because it can translate two integers (e.g. 2 and 3) into a rational number (e.g. 2/3), while both addition and multiplication are monoids. This is where Haskell starts to fall apart itself, though, because subtraction can also translate two numbers out of the set in which they originally belonged. For example, given two natural numbers 2 and 3, 2 - 3 is no longer a natural number, since it's negative.

But all of that is an aside. Even in C#, one has to deal with low-level exceptional cases such as integer overflow, so even addition isn't truly monoidal, unless you use BigInteger.

2018-03-11 9:35 UTC

Mark, thank you for the detailed response. I didn't mean to refute the usefulness of the refactoring you described. I now see how I tried to apply the refactoring of DI to abstact class where it was not claimed to be possible.

I've been thinking about the example with the injection of distinct instances of the same type. I think we can use simple wrapper types for this kind of problem:

public sealed class Foo1 : IFoo
{
    private readonly IFoo impl_;
 
    public Foo1 (IFoo impl)
    {
        impl_ = impl;
    }
 
    public Out1 Op1 (In1 in1)
    {
        return impl_.Op1 (in1);
    }
}

Foo1 implements IFoo to keep the ability to e.g. create a list of foo1 and foo2.

Given such wrappers are created for all dependencies of type IFoo, MyClass can be rewritten like this:

public class MyClass
{
    public MyClass (Foo1 foo1, Foo2 foo2)
    {
        // ...
    }
}

Now MyClass can be refactored into an abstract class without losing the distinction between foo1 and foo2.

I also belive that, given the wrappers are used only in the context of MyClass, the use of wrappers and diffent fields or parameters is an isomorphism. While its usefulness as a stand-alone refactoing is limited, it may come handy for reasoning similar to the one you did in Composite as a monoid.

Thin wrappers can be used to create an almost convenient way to define operators on interfaces. Unfortunately C# does not allow user-defined conversions to or from an interface so one have to explicitly wrap all instances of the interface in an expression that do not have wrapper on one side of an expression. While it can be acceptable in some cases (like Wrap(a) + b + c + d ...), it can make more complex expressions very cumbersome (like (Wrap(a) + b) + (Wrap(c) + d) ... hence it does not solve the problem that Ciprian described.

2018-03-22 21:33 UTC

Max, thank you for writing back. That's an ingenious resolution to some of the problems you originally pointed out. Thank you!

As far as I can tell, this seems to strengthen the original argument, although there's still some corner cases, like the one pointed out by Ciprian. We can use Decorators as concrete dependencies as you point out, as an argument that even two (or n) identical polymorphic dependencies can be treated as though they were distinct dependencies.

What if we have an arbitrary number of dependencies? One example would be of a Composite, but it doesn't have to be. Consider the ShippingCostCalculatorFactory class from this example. It depends on a list of IBasketCalculator candidates. Could such a class, too, be refactored to an abstract class?

I suppose it could, since the dependency then really isn't an arbitrary number of IBasketCalculator, but rather the dependency is on a collection. Would it be enough to refactor to an abstract class with a single Factory Method that returns the candidates?

2018-03-27 5:50 UTC

Mark, you're welcome! Admittedly, my solution is heavily inspired by the strong-typing as promoted by "if a Haskell program compiles, it probably works" and the original purpose of Hungarian notation.

As for an abrbitrary number of dependencies, you have already pointed out that the dependency is the collection itself, not its elements. So I think ShippingCostCalculatorFactory can be refactored to an abstract class with an abstract factory method to provide a collection of IBasketCalculator.

While abstract class would be more complex and less elegant than DI implementation, I find the reversibility of refactorings very important. Reversability means that the changes to the code are not changing the behavior of the compiled program. It allows to refactor even obscure and undertested legacy code without fear of breaking it. I find the two-way nature of changes to the code the most interesting about your concept of software isomorphisms.

I think the reason abstract classes are usually difficult to reason about and often considered to be a bad choice is that an abstract class and it's inheritors create an implicit composition and the relationships of the parts of this composition can be very different. A base class can serve as a collection of helper methods, or derived classes can serve as dependencies or specify dependencies like in the ShippingCostCalculatorFactory example, or inheitors can serve as a configuration to the base class like custom configuration element classes in .NET derived from ConfigurationElement. Abstract base class can be even used to implement disciminated unions (and in fact F# compiler does).

Perhaps different kinds of hierarchies can be enumerated with some formal ways to recognize a specific kind of hierarchy and refactor it into an explicit compistion?

P.S. One way to implement discriminated unions with C# abstract base classes and guarantee exhaustive matching:

public interface IOptionVisitor<T>
{
    void Some(T value);
 
    void None();
}
 
public abstract class Option<T>
{
    private sealed class SomeImpl : Option<T>
    {
        private T _value;
 
        public SomeImpl(T value)
        {
            _value = value;
        }
 
        public override void AcceptVisitor(IOptionVisitor<T> visitor)
        {
            visitor.Some(_value);
        }
    }
 
    private sealed class NoneImpl : Option<T>
    {
        public override void AcceptVisitor(IOptionVisitor<T> visitor)
        {
            visitor.None();
        }
    }
 
    private Option ()
    {
 
    }
 
    public abstract void AcceptVisitor(IOptionVisitor<T> visitor);
 
    public static Option<T> Some(T value)
    {
        return new SomeImpl(value);
    }
 
    public static Option<T> None { get; } = new NoneImpl();
}

While I don't see how this kind of abstract base class can be refactored to DI, I can not call this a constraint on the abstact class isomorphism because semantically it is not an asbtract class in first place.

2018-04-03 18:20 UTC

Max, once again thank you for writing. I've never seen that article by Joel Spolsky before, but I particularly liked your enumeration of the various different roles an abstract class can have.

It's seems that we're generally in agreement about the constraints of the described refactoring.

When it comes to your option implementation, I think you could fairly easy split up Option<T> into an interface that defines the AcceptVisitor method, and two classes that implements that interface. This is, however, closely related to a series of articles I'll publish in the future.

2018-04-04 19:20 UTC

Mark, thank you for pointing out the alternative option implementation.

The key trick in my Option implementation is the use of private constructor in an abstract class with nested sealed implementation classes. Nested classes can access the private contructor while any class "outside" Option would be unable to call the base constructor. Now I think that enforcing that there are no implementation of Option except for SomeImpl and NoneImpl is redundant as long as the implementations are correct.

Perhaps I should have made an example with public nested classes which can be matched by their type but then it could be refactored into Visitor pattern too. Does it mean that Visitor is isomorphic to discriminated unions then?

2018-04-05 18:50 UTC

Max, I agree that using a nested, private, sealed class is a good way to ensure that no-one else can add rogue implementations of an interface like IOptionVisitor<T>.

Additionally, I think that you're correct that it isn't possible to lock down the API to the same degree if you redefine Option<T> to an interface. Just to be clear, I'm thinking about something like this:

public interface IOptionVisitor<T>
{
    void Some(T value);
 
    void None();
}
 
public interface IOption<T>
{
    void AcceptVisitor(IOptionVisitor<T> visitor);
}
 
public static class Option
{
    public static IOption<T> Some<T>(T value)
    {
        return new SomeImpl<T>(value);
    }
 
    private sealed class SomeImpl<T> : IOption<T>
    {
        private T value;
 
        public SomeImpl(T value)
        {
            this.value = value;
        }
 
        public void AcceptVisitor(IOptionVisitor<T> visitor)
        {
            visitor.Some(value);
        }
    }
 
    public static IOption<T> None<T>()
    {
        return new NoneImpl<T>();
    }
 
    private sealed class NoneImpl<T> : IOption<T>
    {
        public void AcceptVisitor(IOptionVisitor<T> visitor)
        {
            visitor.None();
        }
    }
}

With a design like that, rogue implementations of IOption<T> are possible, and I admit that I can't think of a way to prevent that.

Usually, that doesn't concern me that much, but if one were to publish a type like that as, say, a public NuGet package, that degree of lock-down could, in fact, be desirable. So, it looks like you've identified another constraint on the isomorphism. I admit that I may have been too focused on the ability to implement behaviour in various different ways, whereas I haven't given too much thought to accessibility.

To be frank, one of the reasons for that is that I tend to not consider accessibility modifiers too much in C#, as I tend to design classes in such a way that they protect their invariants. When classes do that, I'm happy to make most methods public.

Another reason that I've been vague on accessibility is that this could easily get implementation-specific. The way C#'s access modifiers work is different from Java's and C++'s.

That doesn't change the fact, though, that it looks like you've identified another constraint on the isomorphism, and for that I'm very grateful. Perhaps we ought to say something like:

Abstract classes are isomorphic with dependency injection up to accessibility.
Again, there may be other constraints than that (and operator overloads), but I'm beholden to you for fleshing out those that you've already identified.

About the relationship between discriminated unions and the Visitor design pattern, then yes: those are isomorphic. That's a known property, but I'm going to publish a whole (sub-)series of articles about that particular topic in the future, so I think it'd be better to discuss that when we get there. I've already written those articles, but it'll take months before I publish them, according to the publishing schedule that I currently have in mind. Very prescient of you, though.

2018-04-06 7:36 UTC


Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 19 February 2018 13:10:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 19 February 2018 13:10:00 UTC