Agile Software Development: Architecture Patterns for Responding to Change – Part 2





Avatar


This article series explores a coding approach we use at Big Nerd Ranch that enables us to more easily respond to change. In Part 1, I presented the example of a simplified Contacts app and how it might traditionally be implemented, along with disqualifying three common approaches used to respond to change. Here in Part 2, I’ll introduce the first step in how code can be architected to be better positioned to respond to change. In Part 3, I’ll complete the approach.

Single Responsibility ViewControllers

While the Contact app example is simple, it shows how business logic creeps into display logic and makes reuse difficult. The problem is that the ViewController knows too much about the world around it. A ViewController should be designed to be unaware of the world around it.

  • ViewController should understand only how to make itself operate and go; it only controls itself.
  • If a ViewController needs something from the outside world to make it go, such as the data to display, those things (dependencies) must be provided (injected).
  • If a ViewController needs to communicate something back to the outside world, it should use a broadcast mechanism such as delegation.

There is also the implication a ViewController should only publicly expose an API that is truly public. Properties such as IBOutlets, delegates, and other data members, along with internal implementation functions should be declared private. In fact, it’s good practice to default to private and only open up access when it must (Principle of Least Privilege).

Reworking the code

The ViewController will be implemented in standard ways: in code, via storyboard, or whatever the project’s convention may be.

That which the ViewController needs to work – dependencies, such as data, helpers, managers, etc. – must be injected at or as close to instantiation time as possible, preferably before the view is loaded (viewDidLoad() is called). If the ViewController is instantiated in code, the dependency injection could be performed via an overloaded initializer. If instantiating a ViewController from a storyboard, a configure() function is used to inject the dependencies, calling configure() as soon as possible after instantiation. Even if injection at initialization is possible, adopting configure() may be desirable to ensure a consistent pattern of ViewController initialization throughout the app. That which the ViewController needs to relay to the outside world – for example, the user tapped something – must be relayed to the outside world, preferably via delegation.

Here’s the Contacts code, reworked under this design:

/// Shows a single Person in detail.
class PersonViewController: UIViewController {
    // Person is an implicitly unwrapped optional, because the public API contract for this
    // class requires a `Person` for the class to operate properly: note how `configure()` 
    // takes a non-optional `Person`.. The use of an Implicity Unwrapped Optional simplifies and 
    // enforces this contract.
    //
    // This is not a mandated part of this pattern; just something it enables, if appropriate
    // for your need.
    private var person: Person!

    func configure(person: Person) {
        self.person = person
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        title = person.name // for example
    }
}

/// Delegate protocol for handling PersonsViewController actions
protocol PersonsViewControllerDelegate: AnyObject {
    func didSelect(person: Person, in viewController: PersonsViewController)
}

/// Shows a master list of Persons.
class PersonsViewController: UITableViewController {
    private let persons = [
        Person(name: "Fred"),
        Person(name: "Barney"),
        Person(name: "Wilma"),
        Person(name: "Betty")
    ]
    
    private weak var delegate: PersonsViewControllerDelegate?
    
    func configure(delegate: PersonsViewControllerDelegate) {
        self.delegate = delegate
    } 

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let selectedPerson = persons[indexPath.row]
        delegate?.didSelect(person: selectedPerson, in: self)
    }
}

PersonsViewController now supports a delegate. The delegate is provided to the PersonsViewController by means of a configure() function, when the PersonsViewController is instantiated (see Part 3 of this series). Because the delegate property is an implementation detail, it is declared private. The public API for accessing (setting) the delegate is the configure() function. Note the delegate argument is non-optional; the public API contract of this class is that a delegate is required (optionality andweakness is an implementation detail of delegate properties).

When the user taps a row, the delegate’s didSelect(person:in:) function is invoked. How should didSelect(person:in:) be implemented? However is appropriate for the context: showing the PersonViewController in the first app tab, and showing a GroupsViewController in the second app tab. This business logic is up to someone else to decide, not the PersonsViewController. I’ll show how this comes together in Part 3.

Now the PersonsViewController is more flexible and reusable in other contexts. Perhaps the next version of the app adds a third tab, showing a list of Persons and tapping shows their parents. We can quickly implement the third tab with the existing PersonsViewController and merely have a new delegate implementation.

Implementation Tidbits

(Details)

I did not create a delegate for the PersonViewController because it has no need to communicate with outside code. If PersonViewController would need to notify outside code of user action (e.g. it supports an Edit mode and the user tapped to edit), then a PersonViewControllerDelegate would be appropriate to define.

Additionally, the data was simply declared as a private data member of PersonsViewController. A better solution would be for the data to be injected in via the configure() function. Perhaps as [Person], or perhaps a DataSource type object that provided the [Person] from a network request, from a Core Data store, or wherever the app stores its data.

Delegation vs. Notification

It’s an intentional choice to prefer delegation over notification (Notification/NotificationCenter). In general, only one other thing cares to know about the changes to the ViewController, so delegation provides clear and focused handling. Notification is global and heavy-handed for our needs. It’s great for fire-and-forget operations, but inappropriate when receipt is required. As well, Notification permits others that may not be directly involved to tap into the event system of the View-Delegate, which is generally not desired.

This isn’t to say you cannot use Notification – you could if truly that was right. On the same token, if you find yourself needing a ViewController to notify more than one other thing, I would 1. consider your design to ensure this is truly needed and/or there isn’t another way to solve it, 2. consider instead using a MulticastDelegate type of approach, so it’s still delegation, just to explicit many objects (vs. NotificationCenter‘s global broadcast).

Consider as well, instead of using delegation or notification, you could provide closures to be executed as handlers for these actions. On one project, I implemented this design using closures because it made sense for what needed to be handled. There’s no hard-and-fast approach: it’s about knowing the options, and when it is and is not appropriate to use each.

Delegate Responses

Typically, a ViewController‘s communication with the outside world will be one-way: out-going messages; as such, return types will be Void. However, it is within the pattern to allow delegate functions to return a value. Perhaps during a ViewController‘s operation, someone outside must be queried then the ViewController responds accordingly. It’s perfectly acceptable to support this sort of query via delegation. This is another reason why delegation can work better than notifications. Note: this lends to synchronous queries. If you find the ViewController needs an async mechanisms via delegation, it may be worth considering another route (including accepting the complexity).

But wait… there’s more!

We’re not done yet! In Part 3, I’ll show how to make all of this… flow.



Avatar






Source link

Leave a Reply