Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-05-27

Swift "Dispatch" Design Pattern

The parameter loaded enum in Swift makes an interesting pattern possible, I call this the "dispatch" pattern. The dispatch pattern converts a class interface from a function driven design to a data driven design. I.e. the dispatch pattern replaces a lot of API function calls with a single API function call.  The complexity is shifted from "selecting the right function" to "selecting the right parameter".

The signature of the dispatch function is thus:

func dispatch(action: Action) -> Result {}

In other languages (including Objective-C) this pattern is only possible by giving up on static type safety and using "id" or "NSObject" for the type of "Action" and "Result". Type safety is then a dynamic burden on the runtime. The biggest disadvantage from this is that you won't know until program execution if your code is correct.

Swift's parameterised Enum does away with the dynamic checking: it is now possible to determine at compile time if the code is correct. This in turn allows us to use the other advantages of the dispatch pattern:
  • Separating the user interface from the maintenance interface. I.e. the initialisation, setup, configuration and other management calls are clearly separated from the primary functionality the class/object offers to the rest of the application.
  • It becomes much easier to implement undo/redo functionality.
  • The scriptability of the application improves.
  • The API of a class is reduced, as a result it becomes easier to read, use and maintain.
  • A dispatcher is an excellent place to make your classes thread safe.
  • This is of course personal, but I find that the resulting code is much easier to read.
An example, suppose we have a text editing app, and want to create a line on which we can execute editing functions. Without a dispatcher the class would look like:

class Line {
    
    var string: String = ""
    
    func insert(str: String) -> Bool { ... }
    
    func makeUppercase() -> Bool { ... }
    
    func copySelection() -> String { ... }
}

So far so good, but as we add more functionality the interface becomes unwieldy, and what is more important: it becomes ever more difficult to add something like undo/redo as we add more API's to the class.

With a dispatcher function the class (and supporting enums) look as follows:

enum Action {
    case UPPERCASE
    case INSERT(String)
    case COPY
}

enum Result {
    case UNCHANGED
    case CHANGED
    case COPY_RESULT(String)
}

class Line {
    
    var string: String = ""
    
    func dispatch(action: Action) -> Result {
        
        switch action {
        case .UPPERCASE: return makeUppercase()
        case let .INSERT(str): return insert(str)
        case .COPY: return copySelection()
        }
        
    }
    
    private func insert(str: String) -> Bool { ... }
    
    private func makeUppercase() -> Bool { ... }
    
    private func copySelection() -> String { ... }
}

The functions that do the real work are now private and are only called from the dispatcher.
It may be obvious that it is now much easier to add undo/redo functionality since we can implement that by updating the dispatch function and adding the undo and redo functions. We do not need to change any of the functions that implement the core functionality.
Notice how the parameterised Enum in Swift makes this all possible without using "Any" or "AnyObject".

If we were to add XML or JSON conversions to the Action enum, it would also become very easy to add macro recording. 

When using this pattern, make sure that all functions that update the internal data structure are routed through the dispatcher. Functions that return information from the internal data structure -without updating the internal data- do not need to be routed through the dispatcher. But there is also no reason not to use the dispatcher for that!

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment