Agile Software Development: Architecture Patterns for Responding to Change: Part 1





Avatar


Responding to change is a key tenant of the Agile Manifesto. Whether or not you prescribe to agile, change is undeniably inevitable and predicting that change is hard, if not impossible.

Every software project of sufficient age will undergo change. We cannot predict how it will change, but accepting that change will happen can improve our decision-making process. We have to ship, we have to make money – it’s a business, after all. But resources like time, money, and effort are finite; thus it’s better to be wiser and more efficient in how we spend those resources. There are ways to develop (write) software and architect (plan) code that facilitates responding to change and strengthens stewardship with stakeholders.

This article series explores a coding approach we use at Big Nerd Ranch that enables us to more easily respond to change. It will start by laying out an example and explaining why some “traditional” approaches make change-response difficult. Part 2 will introduce the approach, and Part 3 will complete it.

My app

To help illustrate and understand how we can manage change, let’s look at a simplified Contacts app. I have chosen to use the common Master-Detail style interface, with a TableViewController showing the master list of Persons. Selecting a Person shows a ViewController displaying the Person’s details. The implementation is typical and likely familiar:

import UIKit

/// My `Person` model.
struct Person {
    let name: String
}

/// Shows a single Person in detail.
class PersonViewController: UIViewController {
    var person: Person?

    // Imagine it has functionality to display the Person in detail.
}

/// Shows a master list of Persons.
class PersonsViewController: UITableViewController {
    let persons = [
        Person(name: "Fred"),
        Person(name: "Barney"),
        Person(name: "Wilma"),
        Person(name: "Betty")
    ]

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let selectedPerson = persons[indexPath.row]
        // `instantiateFromStoryboard()` is poetic convenience for this example
        let personVC = PersonViewController.instantiateFromStoryboard()
        personVC.person = selectedPerson
        navigationController?.pushViewController(personVC, animated: true)
    }
    
    // Other TableView delegate and data source functions omitted for brevity
}

When my app is launched, the PersonsViewController is the initial ViewController loaded and displayed. A user can tap on a row/cell of a Person, and the app will navigate to display the details of that Person. This is a fairly common scenario in mobile apps.

Requirements change

As it is now, the app has a single view showing a list of Persons; tap a Person and the Person’s detail is shown. The product stakeholders want to expand the app to support a new feature: Groups. To support this new feature, they want a view showing a list of Persons, and when you tap a person it shows a list of the Person’s Group memberships. The app should change from a single-view UI to a tabbed-UI, with the first tab for Persons feature and the second for the Groups feature. How can we implement this change request in a manner that provides good stewardship of time, money, and resources, and also leads to a more robust, more maintainable code base?

Consider the commonalities: both tabs start by showing a list of Persons. Our PersonsViewController shows a list of Persons, so we can use it to implement both tabs, right? While it does show persons, it’s not able to satisfy our requirements: the data to display is hard-coded within the PersonsViewController, and the tight coupling to the PersonViewController doesn’t support showing Group membership.

How can we solve this?

These aren’t the solutions you’re looking for

I’ll immediately disqualify three approaches.

First, duplication. This is creating a new class, such as PersonsGroupViewController that replicates PersonsViewController in full (perhaps by select-all, copy, paste) and edits tableView(_:didSelectRowAt:) to transition to a GroupsViewController instead of PersonViewController. Duplicating code in such a manner might work but will become a maintenance nightmare to maintain essentially the same code in multiple places.

Second, subclassing. This is creating a common base class which then PersonsViewController and PersonsGroupViewController inherit from, varying just in how the didSelect is handled. This isn’t necessarily a bad option (and in some cases may be the right approach), but in addition to creating additional maintenance overhead, it’s also not quite the correct model. The display of a list of persons is the same regardless of the action taken when tapping a cell, so to subclass just to change the tap-action is a little much.

Third, to expand PersonsViewController with some sort of “behavior” or “configuration” enum such as:

class PersonsViewController: UITableViewController {
    enum SelectionMode {
        case personDetail
        case groupMembership
    }
    var selectionMode: SelectionMode
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        switch selectionMode {
        case .personDetail:
            let personVC = PersonViewController.instantiateFromStoryboard()
            navigationController?.pushViewController(personVC, animated: true)
            
        case .groupMembership:
            let groupVC = GroupsViewController.instantiateFromStoryboard()
            navigationController?.pushViewController(groupVC, animated: true)
        }
    }    
}

// when filling in tab 1…
let personsVC = PersonsViewController.instantiateFromStoryboard()
personsVC.selectionMode = .personDetail

// when filling in tab 2…
let personsVC = PersonsViewController.instantiateFromStoryboard()
personsVC.selectionMode = .groupMembership

This approach is problematic because, in addition to making the couplings tighter and more complex, it does not scale well when more modes are added. You might be tempted to think “Oh, it’s just two cases,” but remember we are trying to position the codebase to be to respond to future, and unknown, changes. The reality of codebase maintenance is that, once you establish a pattern, future developers are likely to maintain that pattern. If more changes are required, a future developer is more likely to expand the enum and lead the ViewController down the road to the ill-fated “Massive View Controller” problem.

There is a better way, which I’ll introduce in part 2.



Avatar






Source link

Leave a Reply