Sunday, May 22, 2016

SOLID - F# vs. C#


Do you want to write code that’s more testable, maintainable, extensible? Of course you do!

However much you think you already code in such a fashion, how many of you have worked on a shared codebase where:

  • a) there are so many thousands of classes, layers of indirection or abstraction that you just don’t know what does what?
  • b) a crisp, clean piece of architecture that you originally constructed has been slowly mangled by less experienced team members?
  • c) everything important goes through one massive ‘God class’ and you just know that what a change needs making, that class is the one to look at first?

I’m going to guess that most of you would put your hand up.

What if I told you there was a way to stop any of these things happening in your codebases? The traditional answer might be SOLID. Mine is functions. By the end of this post, I hope to have convinced you!


Background

Last week, I watched Mark Seemann’s excellent Pluralsight course on Encapsulation and SOLID.

It in, he talks about there being some duality between applying Uncle Bob’s SOLID principles of object-oreitned design, and using functional programming. He, and others, have also blogged in similar veins:

http://blog.ploeh.dk/2014/03/10/solid-the-next-step-is-functional/
http://gorodinski.com/blog/2013/09/18/oop-patterns-from-a-functional-perspective/
https://neildanson.wordpress.com/2014/03/04/it-just-works/

These posts have been pretty good in giving some idea as to why the two concepts are similar, but I want to approach the issue from another angle: learning.


The Woes of the Junior Developer

As someone that’s recently had to learn object-oriented programming (self-directed, from scratch), I can safely say I haven’t seen a ‘beginners’ guide to SOLID that has any chance of teaching an inexperienced developer how to write code that adheres to these principles.

It’s hard enough trying to write code in an existing codebase that matches whatever million-tiered architecture the original authors came up with, and to learn the right time to create an abstraction, how to use polymorphism effectively, ensure you are encapsulating things properly, etc.

Then, someone tells you that objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program, and you think that’s all well and good, but what on earth does that mean in terms of real code?

The same applies in less or greater measure to the remaining four principles — they clearly work and result in maintainable, flexible, testable code, but when you’re starting down the barrel of a couple of million lines of someone else’s handiwork, it’s nigh-on impossible to figure out how to ‘solidify’ what you’re looking at.

On the other hand, I said before that there is a duality between SOLID and functional programming. So how easy is it to teach the relevant concepts in a functional language? The rest of this post will show you that the answer is: very!


Why does this matter?

I’ve just told you that you can get the same results (flexibility, testability, extensibility, maintainability, …) that applying the SOLID principles give, but do so in a way that’s


What’s up next?

I will now:

  • Give a very quick introduction to each of the SOLID principles.
  • Show an example of violating the principle in C#.
  • See how we can fix the problems using object-oriented code.
  • Present an alternative, functional approach in F#.
  • Explain why the functional approach is easier to learn and teach.

All of the code for the examples below can be found at https://github.com/douglasbruce88/SOLID


Single Responsibility Principle

What is it?

A class or module should one do one thing (and hopefully do it well!)

Give me an example of breaking it

The class below both counts the number of commas in a given string, and logs that number. It thus does two things.

public class DoesTwoThings
{
    public int NumberOfCommas (string message)
    {
        int count = 0;
        foreach (char c in message)
        {
            if (c == '/') count++;
        }
        return count;
    }

    public void Log (string message)
    {
        Console.WriteLine ($"The string you gave me has { NumberOfCommas(message) } commas");
    }
}
How can I fix it in C#?

Assuming that we actually wanted the class to do the comma counting, we use dependency injection to stick in a logger and delegate the logging functionality to that. This involves creating an interface, a non-default constructor, and a class field.

public class DoesOneThing
{
    readonly ILogger Logger;

    public DoesOneThing (ILogger logger)
    {
        this.Logger = logger;
    }

    public int NumberOfCommas (string message)
    {
        int count = 0;
        foreach (char c in message) {
            if (c == '/') count++;
        }
        return count;
    }

    public void Log (string message)
    {
        this.Logger.Log($"The string you gave me has { NumberOfCommas (message) } commas");
    }
}

public interface ILogger
{
    void Log (string message);
}
What about F#?

I will admit that the code to count the number of commas is not strictly in functional style, but I wanted to keep it as similar to the C# variant as I could. The more functional approach using recursion can be found with the rest of the blog code.

All we do to solve the SRP issue is use higher-order functions, i.e. the logger. This logger can be any function with the required signature (in this case string -> unit). It doesn’t need to implement an interface (which can be tough if the code doesn’t adhere to the ISP principle, more on which to follow), we don’t need a constructor or a field in the module.

module DoesOneThing = 

  let numberOfCommas (s : string) = 
     let mutable count = 0
     for c in s do if c = '/' then count <- count + 1
     count

  let log logger (message : string) = 
      logger (sprintf "The string you gave me has %i commas" (numberOfCommas message))
      |> ignore
Why is this easier?

Contrast the OO approach:

Oh, you didn’t know about dependency injection? Here, let me explain using about twelve open-source frameworks and a 400-page book. Once you’re finished with that (not on company time, mind!), you can easily solve the problem we had.

… and by the way, the ILogger interface actually has eight methods that you need to implement, not just the one. Enjoy!

… with the functional approach

Use a higher-order function for the logger. As long as the signature matches, you’re good to go!

FP: one; OO: nil.


Open/Closed Principle

What is it?

A class or module should be open for extension, but closed for modification.

Give me an example of breaking it

Let’s say we are playing Monopoly. The rules say that if we roll a double, we get to roll again, so we write the code below. Looking more closely at the rules, three doubles in a row sends us to jail.

To make the code adhere to this rule, we have to modify the CanRollAgain method, so the class isn’t closed for modification.

public class OpenForModification
{
    public bool CanRollAgain (int firstDieScore, int secondDieScore)
    {
        return firstDieScore == secondDieScore;
    }
}
How can I fix it in C#?

One way is to make the class and implementation of the rules abstract, and then allow someone to extend the functionality by inheriting from the class.

public abstract class ClosedForModification
{
    public bool CanRollAgain (int firstDieScore, int secondDieScore)
    {
        return CanRollAgainImpl (firstDieScore, secondDieScore);
    }

    public abstract bool CanRollAgainImpl (int firstDieScore, int secondDieScore);
}

public class ThreeDoubles : ClosedForModification
{
    readonly bool LastTwoRollsAreDoubles;
    public ThreeDoubles (bool lastTwoRollsAreDoubles)
    {
        this.LastTwoRollsAreDoubles = lastTwoRollsAreDoubles;
    }

    public override bool CanRollAgainImpl (int firstDieScore, int secondDieScore)
    {
        return !LastTwoRollsAreDoubles && (firstDieScore == secondDieScore);
    }
}
What about F#?

Use a higher-order function. We allow the caller to inject an arbitrary rule function that takes in two integers and returns a bool.

let canRollAgain (firstRoll : int) (secondRoll : int) ruleSet : bool = 
    ruleSet firstRoll secondRoll
Why is this easier?

Contrast the OO approach:

So you’ll need to be able to figure out whether it’s best to use an abstract base class with an abstract implementation method, or a non-abstract base class with a virtual method and some default behaviour. Maybe go and learn about inheritance first, and then ask yourself why we didn’t use polymorphism instead.

… with the functional approach

Use a higher-order function for the rules. As long as the signature matches, you’re good to go!

FP: two; OO: nil.


Liskov Substitution Principle

What is it?

You should be able to swap an interface with any implementation of it without changing the correctness of the program.

Give me an example of breaking it

Let’s use the ILogger interface that we introduced in the Single Responsibility Principle section. Clearly the two implementations do quite different things.

public class ConsoleLogger : ILogger
{
    public void Log (string message)
    {
        Console.WriteLine (message);
    } 
}

public class NewLogger : ILogger
{
    public void Log (string message)
    {
        throw new NotImplementedException ();
    }
}
How can I fix it in C#?

In a way, you can’t. Whilst you can do something more sensible in NewLogger, it doesn’t stop someone coming along a few months later and writing

public class AnotherLogger : ILogger
{
    public void Log (string message)
    {
        throw new NotImplementedException ();
    }
}
What about F#?

Going back to the F# version, we had this injected function logger with a signature of string -> unit that we had to match.

It gets a little woolly here — there is nothing stopping you from writing

let logger : string -> unit = raise (new NotImplementedException())

In a more pure functional language such as Haskell, you wouldn’t be able to implicitly throw such an exception. However in F#, the real solution is to use something like Railway-Oriented Programming which uses the Either monad (also known as the Exception monad) to ensure that you always return something from your functions.

This relies on the developer adhering the the design principle of not throwing exceptions. Whilst this is certainly easier to do in a functional language, it’s still something else to remember.

Why is this easier?

It’s a little harder to justify here, but to me the solution is much firmer in F# — the concept of not throwing exceptions is more natural in a language where we can create constructs (such as the Either monad) to show us another way of handling exceptional situations.


Interface Segregation Principle

What is it?

A client of an interface should not be made to depend on methods it doesn’t use.

Give me an example of breaking it

Using the logging example again (and thinking about to NUnit’s version of ILogger), if we have an interface as below and want to do some kind of tracing on the method, we have no need for the LogError method on ILogger

interface ILogger
{
    void LogError (string message);

    void LogDebug (string message);
}

public class ClientWithLogging
{
    readonly ILogger Logger;

    public ClientWithLogging (ILogger logger)
    {
        this.Logger = logger;
    }

    public int NumberOfCommasWithLogging (string message)
    {
        this.Logger.LogDebug ($"Counting the number of commas in {message}");
        int count = 0;
        foreach (char c in message) {
            if (c == '/') count++;
        }

        this.Logger.LogDebug ($"{message} has {count} commas.");
        return count;
    }
}
How can I fix it in C#?

Prefer a Role Interface over a Header Interface. That is, define two interfaces doing one job each.

public interface IErrorLogger
{
    void LogError (string message);
}

public interface IDebugLogger
{
    void LogDebug (string message);
}

public class ClientWithLogging
{
    readonly IDebugLogger Logger;

    public ClientWithLogging (IDebugLogger logger)
    {
        this.Logger = logger;
    }

    public int NumberOfCommasWithLogging (string message)
    {
        this.Logger.LogDebug ($"Counting the number of commas in {message}");
        int count = 0;
        foreach (char c in message) {
            if (c == '/') count++;
        }

        this.Logger.LogDebug ($"{message} has {count} commas.");
        return count;
    }
}

There’s a slight twist to this one, though. These kind of single-method interfaces are essentially just delegates, and delegates are just encapsulations of function signatures.

The delegate form looks like this:

public delegate void DebugLogger (string message);

public class ClientWithLogging
{
    readonly DebugLogger LogDebug;

    public ClientWithLogging (DebugLogger debugLogger)
    {
        this.LogDebug = debugLogger;
    }

    public int NumberOfCommasWithLogging (string message)
    {
        this.LogDebug ($"Counting the number of commas in {message}");
        int count = 0;
        foreach (char c in message) {
            if (c == '/') count++;
        }

        this.LogDebug ($"{message} has {count} commas.");
        return count;
    }
}

The function form looks like this:

public class ClientWithLogging
{
    readonly Action<string> LogDebug;

    public ClientWithLogging (Action<string> debugLogger)
    {
        this.LogDebug = debugLogger;
    }

    public int NumberOfCommasWithLogging (string message)
    {
        this.LogDebug ($"Counting the number of commas in {message}");
        int count = 0;
        foreach (char c in message) {
            if (c == '/') count++;
        }

        this.LogDebug ($"{message} has {count} commas.");
        return count;
    }
}
What about F#?

We don’t have to change any of our code! The higher-order logger function with signature string -> unit looks suspiciously like the Action<string> method that we introduced in C#.

Why is this easier?

OO:

First, you need to read a few articles about interfaces, like those by Martin Fowler to which I linked earlier. Then, you need to know that single-method interfaces are the same as delegates. Then go and research delegates in C#.

Then realise that they are just a function encapsulation so you can use one of them instead.

Oh, but your method returns void, so you can’t use the flexible Func, you need to use the specific Action.

FP:

Higher-order functions, again. You already know about them, right?

Another win for FP!


Dependency Inversion Principle

What is it?
  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.
Give me an example of breaking it

You’ve seen this already in the very first code of the article — as well as doing two things, the high-level class below depends on the low-level behaviour of logging to the console.

public class DoesTwoThings
{
    public int NumberOfCommas (string message)
    {
        int count = 0;
        foreach (char c in message)
        {
            if (c == '/') count++;
        }
        return count;
    }

    public void Log (string message)
    {
        Console.WriteLine ($"The string you gave me has { NumberOfCommas(message) } commas");
    }
}
How can I fix it in C#?

You’ve seen this too! We use Inversion of Control to make sure that the logging implementation is abstracted

public class DoesOneThing
{
    readonly ILogger Logger;

    public DoesOneThing (ILogger logger)
    {
        this.Logger = logger;
    }

    public int NumberOfCommas (string message)
    {
        int count = 0;
        foreach (char c in message) {
            if (c == '/') count++;
        }
        return count;
    }

    public void Log (string message)
    {
        this.Logger.Log($"The string you gave me has { NumberOfCommas (message) } commas");
    }
}

public interface ILogger
{
    void Log (string message);
}
What about F#?

Well, this wasn’t a problem to begin with — we passed in a logger function that abstracted the logging bit.

Why is this easier?

I think I’ve covered most of this in the discussion of the Single Responsibility Principle. In essence, to satisfy the Dependency Inversion Principle you need to learn about several object-oriented principles, some of which are quite advanced such as Dependency Injection (yes, the book is 584 pages long).

In contrast, we only need to learn about one thing (higher-order functions) to solve the issues in F#.

Another win for FP — might just be a whitewash?


Conclusion

I hope I’ve shown in this post that the SOLID principles of object-oriented design can be satisfied in a functional programming using nothing but functions.

This is a huge contrast to an object-oriented language where a developer needs to understand a huge range of topics to implement the principles properly.

Now ask yourself which set of techniques you want to teach your next Junior Developer, and how much time, effort and money you could save by switching to a functional approach.

No comments:

Post a Comment