Coincidental duplication costs can really add up

No matter what route you have taken to becoming a developer you will have spent a considerable period of time on that route improving your ability to think abstractly - to see how a set of real world requirements can be transformed into a working model that you can build a system around. Two key components in our abstraction thought training involve:

  • Looking for common functionality
  • Joining the dots between processes/functionality

By no means is the above a complete list of the competences of abstract thinking

Ideas

Every developer who has ever broken down a large method into two smaller ones so that another method could use some of the original larger method's functionality is using Looking for common functionality. Every developer who has ever created multiple classes in a system rather than one monolithic class to pass data around a system has used Joining the dots between processes/functionality.

This ability to think abstractly serves us well and we use it continuously in our day-to-day development tasks. However we can start to get into trouble when we use this ability too much when talking 😱 to non-developers (product, designers, translators, etc) about the system we are building together. This is because the people we are interacting with have a different mental mapping of how this functionality will interact with the different parts of the existing app. This different mental mapping and different areas of concern can lead to us joining dots and making assumptions about common functionality where none actually exists. I call this Coincidental Duplication and coupled with our desire to keep things DRY can lead to us spending a lot of effort and energy building something that we then need to refactor.

Now before we go on, you may be thinking:

"Coincidental duplication sounds a lot like premature optimisation 🤔"

And it does share some aspects in common with premature optimisation. However whereas premature optimisation is about improving the efficiency of a piece of before we know if it's actually a bottleneck, coincidental duplication is much more about abstracting functionality that is accidentally common across multiple locations in your app.

Chasing the rainbow 🌈

For the most part examples of coincidental duplication are found around the edges of our projects: UI and API as these are the parts were we are interacting with other teams and small breakdowns in communication can happen. I want to look at a very common example of coincidental duplication that we have probably all seen in our projects - a colour palette class. We often create a colour palette class at the start of a project as a way to consolidate the colours that we use in the project and allow us to change colours across the entire app by making one change. The example below is an extension of UIColor that adds custom colours.

extension UIColor {

    //MARK: - ColorPalette

    class func startingBlue() -> UIColor {

        return UIColor(red: 51/255, green: 136/255, blue: 255/255, alpha: 1)
    }

    class func startingRed() -> UIColor {

        return UIColor(red: 255/255, green: 68/255, blue: 68/255, alpha: 1)
    }

    class func startingGreen() -> UIColor {

        return UIColor(red: 153/255, green: 204/255, blue: 0, alpha: 1)
    }

    class func startingGrey() -> UIColor {

        return UIColor(red: 173/255, green: 173/255, blue: 173/255, alpha: 1)
    }

    class func startingPurple() -> UIColor {

        return UIColor(red: 170/255, green: 102/255, blue: 255/255, alpha: 1)
    }
}

starting is used here to differenate it from the later examples - intermediate and finished

It's a nice short class with easy to understand method names that are consistent. It allows us to shortcut the colours that we use and prevents duplication in code. The trouble is that we often either don't tell the designers that we have abstracted out their colour palette or got agreement that they will only use those colours. What ends up happening is that as the app grows in features the designers want to add more subtle changes to better express their intentions 😒 So we end up with something like:

extension UIColor {

    //MARK: - Blue

    class func intermediateLightBlue() -> UIColor {

        return UIColor(red: 160/255, green: 192/255, blue: 255/255, alpha: 1)
    }

    class func intermediateBlue() -> UIColor {

        return UIColor(red: 51/255, green: 136/255, blue: 255/255, alpha: 1)
    }

    // MARK: - Red

    class func intermediateRed() -> UIColor {

        return UIColor(red: 255/255, green: 68/255, blue: 68/255, alpha: 1)
    }

    // MARK: - Green

    class func intermediateLightestGreen() -> UIColor {

        return UIColor(red: 238/255, green: 238/255, blue: 204/255, alpha: 1)
    }

    class func intermediateLightGreen() -> UIColor {

        return UIColor(red: 221/255, green: 238/255, blue: 153/255, alpha: 1)
    }

    class func intermediateGreen() -> UIColor {

        return UIColor(red: 153/255, green: 204/255, blue: 0, alpha: 1)
    }

    class func intermediateDarkGreen() -> UIColor {

        return UIColor(red: 68/255, green: 119/255, blue: 0, alpha: 1)
    }

    // MARK: - Grey

    class func intermediateLightGrey() -> UIColor {

        return UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1)
    }

    class func intermediateGrey() -> UIColor {

        return UIColor(red: 173/255, green: 173/255, blue: 173/255, alpha: 1)
    }

    class func intermediateDarkGrey() -> UIColor {

        return UIColor(red: 26/255, green: 26/255, blue: 26/255, alpha: 1)
    }

    // MARK: - Purple

    class func intermediateLightPurple() -> UIColor {

        return UIColor(red: 238/255, green: 224/255, blue: 238/255, alpha: 1)
    }

    class func intermediatePurple() -> UIColor {

        return UIColor(red: 170/255, green: 102/255, blue: 255/255, alpha: 1)
    }
}

In the above code snippet, we have added in a few more shades to our colours with the following scheme:

  • darkest
  • dark
  • standard
  • light
  • lightest

A slightly longer list of colours than before and some missing gaps between the different colours but still just about sticking to an abstract colour palette.

But the designers are still not finished 😡. They go on and use even more shades , all the time striving to create a better user experience - the evil bastards! At this point you can add in names like: lightDark, darkLight, etc or you can start to acknowledge the fact that you have a percentage of your colours only being used in one location throughout your project and begin to name them in more detailed fashion:

extension UIColor {

    //MARK: - Blue

    class func finishedSettingsBlue() -> UIColor {

        return UIColor(red: 9/255, green: 145/255, blue: 255/255, alpha: 1)
    }

    class func finishedLightBlue() -> UIColor {

        return UIColor(red: 160/255, green: 192/255, blue: 255/255, alpha: 1)
    }

    class func finishedBlue() -> UIColor {

        return UIColor(red: 51/255, green: 136/255, blue: 255/255, alpha: 1)
    }

    class func finishedIntroductionBlue() -> UIColor {

        return UIColor(red: 51/255, green: 106/255, blue: 235/255, alpha: 1)
    }

    // MARK: - Red

    class func finishedDangerRed() -> UIColor {

        return UIColor(red: 255/255, green: 18/255, blue: 88/255, alpha: 1)
    }

    class func finishedRed() -> UIColor {

        return UIColor(red: 255/255, green: 68/255, blue: 68/255, alpha: 1)
    }

    // MARK: - Green

    class func finishedLightestGreen() -> UIColor {

        return UIColor(red: 238/255, green: 238/255, blue: 204/255, alpha: 1)
    }

    class func finishedLightGreen() -> UIColor {

        return UIColor(red: 221/255, green: 238/255, blue: 153/255, alpha: 1)
    }

    class func finishedGreen() -> UIColor {

        return UIColor(red: 153/255, green: 204/255, blue: 0, alpha: 1)
    }

    class func finishedDarkGreen() -> UIColor {

        return UIColor(red: 68/255, green: 119/255, blue: 0, alpha: 1)
    }

    // MARK: - Grey

    class func finishedLightGrey() -> UIColor {

        return UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1)
    }

    class func finishedGrey() -> UIColor {

        return UIColor(red: 173/255, green: 173/255, blue: 173/255, alpha: 1)
    }

    class func finishedDarkGrey() -> UIColor {

        return UIColor(red: 26/255, green: 26/255, blue: 26/255, alpha: 1)
    }

    class func finishedTitleGrey() -> UIColor {

        return UIColor(red: 26/255, green: 26/255, blue: 26/255, alpha: 1)
    }

    // MARK: - Purple

    class func finishedButtonPurple() -> UIColor {

        return UIColor(red: 238/255, green: 204/255, blue: 238/255, alpha: 1)
    }

    class func finishedCoinPurple() -> UIColor {

        return UIColor(red: 238/255, green: 204/255, blue: 238/255, alpha: 1)
    }

    class func finishedLightPurple() -> UIColor {

        return UIColor(red: 238/255, green: 224/255, blue: 238/255, alpha: 1)
    }

    class func finishedPurple() -> UIColor {

        return UIColor(red: 170/255, green: 102/255, blue: 255/255, alpha: 1)
    }
}

How did we end up with that monster above? We, unsurprisingly ended up here because of coincidental duplication. We assumed that because the same shade of colour was used across different screens that they were related and that that relationship was somehow important so we mapped it into our project.

We have fallen for the common saying:

"to assume is to make an ass out of u and me"

When in fact, the choice of colours by the designer just happened to be the same but that designer wasn't adding any more particular weight to the fact that those colours were the same than they were to the ones that were not the same - it was purely coincidental. Now it's important to note that this won't always be true as any designer worth the job title, will attempt to create a consistent UI/UX and a the colour palette is an important part of that but for this example we will pretend we are working with a not-very-good designer (I'm sure we have all been there). So as the designer continues to change their colour palette from screen to screen and we add more fragmented colours into our colour palette class, let's look at the costs involved because we mistakenly seen a duplication in functionality were there wasn't actually one:

  • Increased initial development effort due to the unnecessary abstraction.
  • Increased maintenance effort as we switch between multiple classes and try to keep a consistent naming convention.
  • Increased deletion costs as we need to remember to not only delete the views, viewcontrollers and modal classes but also the colour methods.

And when eventually we do decide to refactor the colours back into our views and view controllers we get to pay for that as well:

  • Refactor colour palette class away.
There is no 💰 at the end of that rainbow

Now you may be thinking:

"🤷 - I use storyboards none of this applies to me"

but another fairly common example is where you use the same UITableViewCell subclass between different screens with only a few if statements to account for their differences in functionality, only for those cells to be further changed because they were never meant to be similar - it was just coincidental duplication of UI. Or for a non-UI related example, I think Emanuel Kantbronce sums it up best in this short video - the example is in Ruby but it's fairly simple, even a non-Ruby developer like me was able to get it. I'm sure you can think of or even remember many other examples of coincidental duplication so I won't bore you by listing any more.

While it is really common to share functionality between classes, and is often a very valid design, we need to make sure that when we do so we are not wasting effort that could be better spent elsewhere. Unfortunately it's fairly hard to spot coincidental duplication ahead of time as it means spotting when we are making an assumption. However if you can spot that it becomes fairly easy to fix it by making sure to discuss those assumptions that you are making about shared functionality with the person who created the potential for shared functionality - in our example that would be the designer. Armed with that information we can then make smart abstraction choices and understand when to apply the DRY principle and when to break it. So that if what was shared functionality is changed in the future, it won't come as a surprise that the changes will be more expensive to implement.

You can find the completed project with the colour example by heading over to https://github.com/wibosco/CoincidentalDuplication-Example.