Uncategorized

Solving Diamond Inheritance with Functional Programming

Note: this article is based on a talk I gave at iOSDevCampDC in August, 2019. That talk, in turn, was based in large part upon the excellent PointFree video talking about similar concepts, though that video did not have the same focus on comparing OO with FP. That said, the video is an amazing one (of many from PointFree), and you should go watch it and also subscribe to PointFree.

Let’s consider a common task in iOS development: building styling code we can easily reuse throughout an app. Suppose, for the sake of argument, we want to build an interface something like this:

Cool! So, how do we start? Well, let’s look at the properties of the buttons here:

  • Each button has rounded corners
  • Two buttons have a blue border
  • Two buttons have a grey background

Great! How, normally, would we start to build this in an Object Oriented fashion? Well, it’d be reasonable to start with a super class that has rounded corners, but here we already run into an issue:

open class DiamondButton: UIButton {
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        layer.cornerRadius = 4
    }

    override public init(frame: CGRect) {
        super.init(frame: frame)
        layer.cornerRadius = 4
    }
}

We have a required initializer which requires us to repeat our code. That’s ok though, we can just add a function that both initializers call:

open class DiamondButton: UIButton {
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupCorners()
    }

    override public init(frame: CGRect) {
        super.init(frame: frame)
        setupCorners()
    }

    func setupCorners() {
        layer.cornerRadius = 4
    }
}

This is a little redundant, but that’s ok – we’ve isolated the code appropriately and are just calling it from the initializers. What’s next? How about handling that grey background, like in the solitary button:

open class BgButton: DiamondButton {
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupLightGreyBackground()
    }

    override public init(frame: CGRect) {
        super.init(frame: frame)
        setupLightGreyBackground()
    }

    func setupLightGreyBackground() {
        backgroundColor = UIColor(displayP3Red: 0.8, green: 0.8, blue: 0.8, alpha: 1)
    }
}

Well, ok, now we see another problem: we have to implement both initializers each time we subclass it. So, we’ll go back to the super class and change it to call a single configuration function that our subclasses can override, obviating the need for the duplicate initializers:

open class DiamondButton: UIButton {
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        configure()
    }

    override public init(frame: CGRect) {
        super.init(frame: frame)
        configure()
    }

    open func configure() {
        setupCorners()
    }

    func setupCorners() {
        layer.cornerRadius = 4
    }
}

And now our subclass is much simpler:

open class BgButton: DiamondButton {
    override open configure() {
        super.configure()
        setupLightGreyBackground()
    }

    func setupLightGreyBackground() {
        backgroundColor = UIColor(displayP3Red: 0.8, green: 0.8, blue: 0.8, alpha: 1)
    }
}

That’s… ok, but it does feel like we’re having to override 3 functions and implement another two, just to allow two behaviors we want. But whatever, let’s press on, let’s handle the cancel button, which has a border and rounded corners:

open class BorderButton: DiamondButton {
    open override func configure() {
        super.configure()
        setupBorder()
    }

    func setupBorder() {
        layer.borderWidth = 1
        layer.borderColor = UIColor.blue.cgColor
    }
}

That’s another class and two functions in order to enable one behavior; feels like overkill, but it works, so let’s not worry about it. Alright, last button! Let’s implement the button that needs rounded corners, a blue border, and a grey background:

…except we have a problem. Both our BorderButton and our BgButton subclass DiamondButton to get the rounded corners. We want our new button to include all of those things, but we can only subclass one thing. If we subclass BorderButton, we won’t get the grey background. If we subclass BgButton, we won’t get the blue border. This is called the Diamond Inheritance problem, and it’s called that because when we diagram it out it kinda looks like a diamond shape; we want the features of two different super classes, but we can only subclass one:

    Diamond
   /       \
Border   Background
   \      /
    Paired

So what can we do? If you’re like me, you’re getting a twitch in your eye because you know you’re going to have to reimplement something. The best we can do is perhaps use an extension on UIButton that holds all our setup functions, and then refactor our code to call those functions appropriately:

extension UIButton {
    func setupCorners() {
        layer.cornerRadius = 4
    }

    func setupLightGreyBackground() {
        backgroundColor = UIColor(displayP3Red: 0.8, green: 0.8, blue: 0.8, alpha: 1)
    }

    func setupBorder() {
        layer.borderWidth = 1
        layer.borderColor = UIColor.blue.cgColor
    }
}

open class DiamondButton: UIButton {
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        configure()
    }

    override public init(frame: CGRect) {
        super.init(frame: frame)
        configure()
    }

    open func configure() {
        setupCorners()
    }
}

open class BgButton: DiamondButton {
    override open configure() {
        super.configure()
        setupLightGreyBackground()
    }
}

open class BorderButton: DiamondButton {
    open override func configure() {
        super.configure()
        setupBorder()
    }
}

This would minimize the amount of duplication we need to do for the PairedButton:

open class PairedButton: BorderButton {
    open override func configure() {
        super.configure()
        setupLightGreyBackground()
    }
}

However, that still makes me feel icky because PairedButton and BgButton are basically the same, only the name and superclass are different. Now imagine if this was a more complicated example, and much more code had to be duplicated – yikes!

Let’s look at how we’d do this with functional programming. Since functional programming (FP for short) is all about functions, let’s write some!

func setupCorners(_ object: UIButton) {
    object.layer.cornerRadius = 4
}

func setupLightGreyBackground(_ object: UIButton) {
    object.backgroundColor = UIColor(displayP3Red: 0.8, green: 0.8, blue: 0.8, alpha: 1)
}

func setupBorder(_ object: UIButton) {
    object.layer.borderWidth = 1
    object.layer.borderColor = UIColor.blue.cgColor
}

These are all just the functions from our extension, except they have a parameter instead of referring to self.

So how do we use them? Well, we can just pass in the buttons to the functions:

class MyViewController: UIViewController {
  @IBAction var solitaryButton: UIButton!
  @IBAction var cancelButton: UIButton!
  @IBAction var pairedButton: UIButton!

  override public func viewDidLoad() {
    super.viewDidLoad()

    setupCorners(solitaryButton)
    setupLightGreyBackground(solitaryButton)

    setupCorners(cancelButton)
    setupBorder(cancelButton)

    setupCorners(pairedButton)
    setupBorder(pairedButton)
    setupLightGreyBackground(pairedButton)
  }
}

Whoa! That’s way easier! Fewer lines of code, and very easy to understand. And it gets better: we can generalize these into more free standing functions, so we can use them anywhere in our app (which is like the whole point of styling code):

func styleSolitary(_ solitaryButton: UIButton) {
  setupCorners(solitaryButton)
  setupLightGreyBackground(solitaryButton)
}

func styleBordered(_ cancelButton: UIButton) {
  setupCorners(cancelButton)
  setupBorder(cancelButton)
}

func stylePaired(_ pairedButton: UIButton) {
  setupCorners(pairedButton)
  setupBorder(pairedButton)
  setupLightGreyBackground(pairedButton)
}

Which makes our view controller even shorter:

class MyViewController: UIViewController {
  @IBAction var solitaryButton: UIButton!
  @IBAction var cancelButton: UIButton!
  @IBAction var pairedButton: UIButton!

  override public func viewDidLoad() {
    super.viewDidLoad()

    styleSolitary(solitaryButton)
    styleBordered(cancelButton)
    stylePaired(pairedButton)
  }
}

Very short, very clear, very reusable.

So, why does this work?

Fundamentally, functions work to expand the capability of your code, while classes restrict it. Sometimes that’s good – it helps with modularization and SRP – but the restrictions here are hurtful, not helpful. There’s no reason to restrict only BorderedButtons to having a border, or only BgButtons to having a grey background – in fact, that was the entire problem. Object Oriented programming often unnecessarily restricts functionality to certain classes when there’s no reason to.

FP, however, makes it easy to write programs that can act on the broadest possible set of concepts. Because functions are about adding functionality, you have much better control over the scope of their parameters and outputs.

Realizing this (by watching the PointFree videos) is what convinced me to do more FP in my own work. This has led me to being able to do in hours what it used to take weeks to do. I have more code I can reuse between projects, my code is easy to stub, easy to refactor, easy to add new things to, and easy to test, all because of the power, flexibility, and reusability that FP provides me.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s