.st0{fill:#FFFFFF;}

See the Magic of Iterative Refactoring: A Swift Case Study 

 September 19, 2023

by Jon Reid

0 COMMENTS

What’s a good strategy for tackling code design in Swift (or any programming language)? Do we have to master all the idioms, the design patterns, the things that people call “architecture”? Do we bring in an architect to advise us so we can figure out the right design upfront?

Or… what if we start from working code and refactor it, iterating our way to an improved design? What if could experiment to see what designs we like better, and allow the code to evolve?

Code snippet sample

Improve your test writing “Flow.”

Sign up to get my test-oriented code snippets.

This post was born from a live coding session you can watch YouTube. Scroll to the bottom if you want to jump straight to the video. Read on if you want to learn what iterative refactoring is before you see it.

Traditional Upfront Design Can Be Limiting

In upfront design, we optimize for getting the “right” design before coding. The thinking is that change is costly, so only do it once. This puts extra pressure on getting it “right” the first time. Developers deal with this need to be right by spending extra time on the design without coding. Because what if you get it wrong?

This fear can lead to over-engineering: you end up building capabilities you don’t need. This is a form of waste, and Lean practice seeks to eliminate waste. I’ve seen developers slavishly follow design patterns because “it’s best practice.” (And I have done that myself.) But at what cost?

There is no Platonic ideal in software design. A good design pattern describes when it’s helpful. It’s up to us to apply our judgment about whether it fits our particular context.

I’m not saying we shouldn’t sketch out ideas to understand possible solutions, or come up with plans. I’m saying, hold those plans loosely. Don’t be married to the ideas. Be open to change.

For more discussion, see Big Design Up Front on the original wiki.

The Power of Iterative Refactoring and Evolutionary Design

The Manifesto for Agile Software Development says, “We have come to value… responding to change over following a plan.” If we stay open to change, what does that mean for our software design?

It means we can change the design of the code as often as we want to, as long as it’s safe (and cheap). We keep it safe by applying refactoring principles, repeatedly.

Refactoring goes hand-in-hand with evolutionary design. We make small refactoring changes, over and over again. These smaller changes accumulate. Suddenly, you can see a possible larger change that wasn’t visible before. This happens in my coding regularly.

This means that while we can have a sketch of an idea going in, it’s not do-or-die. Instead, we:

  1. Apply some changes.
  2. Observe the effect of those changes on the shape of the code.
  3. Repeat, adjusting as we go.

This process lets us evolve the design of the code. A predetermined design is not the goal. Instead, the goal is code that cooperates with the change we intend to make next. We don’t try to solve for the future. We live in the moment and solve the problems we face now.

This process can feel spooky. But with good design sense, it’s exciting to see code almost shape itself! The small changes add up, a larger change becomes possible, and the code assumes a working design we didn’t predict.

I’ve seen this happen over and over. It always feels magical.

Getting Started with the Iterative Refactoring Approach

Here’s a guide for implementing this evolutionary, iterative refactoring approach. First, there’s a prerequisite: The code must have sufficient tests. If you’re working in test-driven code, congratulations. If you’re working in legacy code (meaning code without tests), use legacy code techniques to write microtests that cover its behavior. Avoid tests that check the implementation.

Once you have your code tested, apply these steps:

  1. Identify What Is Making the Code Hard to Change. Look for code smells in the area you intend to change. Get an idea of the design challenges you face.
  2. Choose an Initial Direction to Go. It’s an “initial direction” because you might change your mind later, but it gets us started. Look in the Industrial Logic Smells to Refactoring Cheatsheet to get an idea of what refactoring you might apply.
  3. Apply Small, Focused Refactorings. Start in green and end in green. That is, start with passing tests. Apply small changes, testing as you go. Commit when tests pass. If any tests ever fail, decide whether to fix forward (if you can do so in a short time) or reset to the last commit and try again.
  4. Iterate and Refine. Continuously repeat the process. Adapt as new insights emerge. Look for refactoring opportunities that weren’t obvious when you started. What can you see now that you couldn’t see before?

Case Study: A Swift Design Challenge

What does this look like in practice? Here’s a Swift coding question posed on Reddit.

I am creating a component for a button. The button has 3 different states and each state has 3 types so that makes 9 different variations of the button. Is there any way to use the State pattern effectively to avoid switch cases?

The code looks like this:

public final class ButtonComponent: UIButton {
    public enum ButtonState {
        case normal
        case disabled
        case pressed
    }
 
    public enum ButtonType {
        case primary
        case secondary1
        case secondary2
 
        func config(for state: ButtonState) -> ButtonModel {
            switch (self, state) {
            case (.primary, .normal):
                return ButtonModel(
                    backgroundColor: Constants.buttonPrimaryBackgroundColor,
                    titleColor: Constants.buttonPrimaryTitleColor)
            case (.primary, .pressed):
                return ButtonModel(
                    backgroundColor: Constants.buttonPrimaryPressedColor,
                    titleColor: Constants.buttonPrimaryTitleColor)
            case (.primary, .disabled):
                return ButtonModel(
                    backgroundColor: Constants.buttonPrimaryDisabledColor,
                    titleColor: Constants.buttonPrimaryDisabledTitleColor)
            case (.secondary1, .normal):
                return ButtonModel(
                    backgroundColor: Constants.buttonSecondaryBackgroundColor,
                    titleColor: Constants.borderedButtonTitleColor)
            case (.secondary1, .pressed):
                return ButtonModel(
                    backgroundColor: Constants.buttonSecondaryBackgroundColor,
                    titleColor: Constants.borderedButtonTitleColor)
            case (.secondary1, .disabled):
                return ButtonModel(
                    backgroundColor: Constants.buttonSecondaryDisabledColor,
                    titleColor: Constants.borderedButtonDisabledTitleColor)
            case (.secondary2, .normal):
                return ButtonModel(
                    backgroundColor: Constants.buttonSecondaryBackgroundColor,
                    titleColor: Constants.borderedButtonTitleColor)
            case (.secondary2, .pressed):
                return ButtonModel(
                    backgroundColor: Constants.buttonSecondaryBackgroundColor,
                    titleColor: Constants.borderedButtonTitleColor)
            case (.secondary2, .disabled):
                return ButtonModel(
                    backgroundColor: Constants.buttonSecondaryDisabledColor,
                    titleColor: Constants.borderedButtonDisabledTitleColor)
            }
        }
    }
}

Think about how you might approach this. (For non-Swift readers, this is a big switch statement where each case is the combination of two enumerated values.)

I wasn’t sure where to start. But I chose an initial direction, and an initial change that looked easy to apply. 62 micro-commits later, I ended up with a Multiple Dispatch solution:

public final class ButtonComponent: UIButton {
    public enum ButtonState {
        case normal
        case disabled
        case pressed
    }
 
    public enum ButtonType {
        case primary
        case secondary1
        case secondary2
 
        func config(for state: ButtonState) -> ButtonModel {
            let buttonModelMaker = makeButtonModelMaker(for: self)
            return buttonModelMaker.buttonModel(for: state)
        }
    }
}

The self passed to the makeButtonModelMaker factory method is the value of the ButtonType enumeration. That is, it uses the ButtonType to decide what sort of buttonModelMaker to create.

Once it has the buttonModelMaker, it asks it for a button model based on state, which is the ButtonState value.

So first we use the ButtonType to decide what type of button model maker to create:  a primary button, secondary1, or secondary2. Then we pass it the ButtonState (normal, disabled, or pressed), and ask it to make the button model for that state.

Here’s what I want to emphasize: This is not a design I had in mind when I started. Instead, the code evolved its way there over 62 small refactoring steps.

What I show above is like the finished product on a cooking show. “Look at this suddenly baked cake I had prepared in advance to save time on the show!” …But how did I reach this design?

A Video Demonstration of Iterative Refactoring

Would you like to see how I arrived at an unplanned solution by applying iterative refactoring?

I have a YouTube channel with quite a few videos I haven’t blogged about. That’s because I have two types of coding videos:

  • Planned and rehearsed coding. I want to communicate an idea in a short time, so I plan, rehearse, record, and edit. (I also handcraft the captions.)
  • Unrehearsed live coding. These are recordings of my live coding sessions on Twitch. I keep them an hour long. Most are part of a series, like my Swift Code Katas playlist. They’re unrefined, and I don’t write about them.

Well, I usually don’t write about unrehearsed live coding. But in this case, the live coding—pauses and all—demonstrates how I refactored the Swift code above.

Before we get to the video, I want to point out a couple of things about it:

  • The original poster wondered if the State pattern might be appropriate. I looked at the UML to remember how the State pattern works. But UML can look similar across different design patterns, so that’s not a great way to choose. Instead, read the Applicability section to check whether the pattern applies to your situation.
  • Before starting the live coding, I first got the code under test. Characterization testing is a powerful legacy code technique. You write assertions against obviously incorrect results. The test will fail, and the failure message will tell us the actual values.
  • Once I had the code under test, I used TCR. This is Test && Commit || Revert, an alternative TDD technique. It happens to work especially well for refactoring. TCR works like this: Make a change. Run the tests. If the tests pass, the system commits your change. Otherwise, it reverts your change.
  • With live coding, you get me rambling, interacting with commenters, and pausing to think about what to do. You also see any missteps I make along the way. Consider increasing the playback speed so that you can watch the entire hour of live coding in less time.

Click to play

Conclusion

Working code is a gift. Even if the code is ugly, it’s still a gift, because the cool thing we can do is keep it working as we reshape its design.

Once you have a good set of microtests, you don’t have to come up with a “correct” design up front. Instead, combine the power of:

  • Taking small steps, and
  • Verifying each step automatically.

The tests give you feedback. Commit on green, revert on red. Small refactoring steps add up to large ones you couldn’t see before. That’s the magic of iterative refactoring and part of evolutionary design.

Additional Resources

Disclosure: The book links below are affiliate links. If you buy anything, I earn a commission, at no extra cost to you.

All Articles in this series

Refactor Like a Pro
Refactor Like a Pro: The No-Brainer Guide to Converting to MVVM
Unlock Proven Steps of Refactoring to MVVM in Swift (Part 2)
Refactor like a Pro: Adopt the Null Object Pattern with Small, Verified Steps
See the Magic of Iterative Refactoring: A Swift Case Study

How to Automate Memory Leak Detection in Your Swift Code with XCTest

Jon Reid

Programming was fun when I was a kid. But working in Silicon Valley, I saw poor code led to fear, with real human costs. Looking for ways to make my life better, I learned about Extreme Programming, including unit testing, test-driven development (TDD), and refactoring. Programming became fun again! I've now been doing TDD in Apple environments for 20 years. I'm committed to software crafting as a discipline, hoping we can all reach greater effectiveness and joy. Now a coach with Industrial Logic!

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
>