Architecture & Agility: 02 - Features

Features: Managing UseCases and connecting with each other

In my last article I have shown you how my Core|UI architecture uses UsesCases. UseCases get grouped into Features. Features receive app wide messages and they will translate it to UseCase request if necessary. So where UseCases have their own set of requests/responses, Features share one message type. Now a question might arise why not have all UseCases listen to that message type. If we would do that we would encourage to just do everything inside one or few UseCases. We would see the rise of side effects and bugs. This would create a new mess of Barb Wire Code. By using to systems that are quite similar yet distinct, we encourage the proper implementation of separation of concerns.

Feature’s Anatomy

As mentioned before: the Feature coordinates the UseCases usage, so it must be a datatype that holds those UseCases. Naturally classes come to mind and indeed also structs would work, as once the feature is instantiated, it won’t be copied or moved, there-for structs value-semantics are as good as classes’ reference-semantics. But both have problems the bring with them which might lead to code deteriorating over time — and bugs:

Classes: They have the ability to be subclassed which we don’t need. Actually it might bring issues as coders less familiar with this architecture might try to change behaviour through subclassing, breaking out of the skeleton the architecture offers.

Classes and Structs: Both offer properties, but for similar reasons why subclassing is not wanted for a Feature, so we don’t want to have the ability to create public properties — as again this would allow to install different communication paths. And lead to deterioration.

So what to use?

In a different discussion I once told a colleague that when developing this architecture, that over time I had slimmed down the interfaces of the different parts to a point where they had one sending method and one receiving callback — and that this feels more like a function with one input and one output. My colleague then challenged me to use functions instead of classes or structs.

Of course, I just couldn’t use one function, as there is no logic relationship between the Feature’s Message type and the responses from any UseCase. There isn’t even a strict rule that an UseCase must respond to a request with a response. A request might trigger a number of responses (progress updates), or none as it might resemble a sink (logger) or no request is needed to get responses (chat message). I didn’t want to throw everything away and go full pure functional, as I wanted to keep the UseCases and everything as it was — I just wanted to replace the problematic structs I had used at that point.

The solution:

A function that takes and output callback as parameter and returns another function, which is the input.

typealias  Input = (Message) -> ()
typealias Output = (Message) -> ()

func createTodoListFeature(store: StateStore, output: @escaping Output) -> Input {
    let itemAdder   = AddTodoItemUseCase   (store: store, responder: handle(output: output))
    let itemDeleter = DeleteTodoItemUseCase(store: store, responder: handle(output: output))
    let itemChecker = CheckTodoItemUseCase (store: store, responder: handle(output: output))

    return { msg in
        if case .todo(.add    (let item)) = msg {   itemAdder.request( .add    (item: item) ) }
        if case .todo(.delete (let item)) = msg { itemDeleter.request( .delete (item: item) ) }
        if case .todo(.check  (let item)) = msg { itemChecker.request( .check  (item: item) ) }
        if case .todo(.uncheck(let item)) = msg { itemChecker.request( .uncheck(item: item) ) }
    }
}

˘ The function createTodoListFeature, takes an StateStore object and an Output callback which it uses to instantiate its UseCases. Then it returns a closure (or unnamed function), which is actually the feature itself. This function gets saved by the AppCore and every message is forwarded by the core to every feature. The feature just pattern matches to call the correct UseCases. I did not invent this pattern, it is called “Partially Applied Functions” (“PAFs”), or (in a normalised form with just one parameter per partially applied function) “Curried Functions”. Even though it might not be used very often in OOP, it is well-known and understood, and has more math behind it to back it up than all OOP together.

Note that in the previous code block the handle functions are missing:

private func handle(output: @escaping Output) -> (AddTodoItemUseCase.Response) -> () {
    return { response in
        switch response {
        case .wasAdded(item: let item): output(.todo(.response(.wasAdded(item))))
        }
    }
}

private func handle(output: @escaping Output) -> (DeleteTodoItemUseCase.Response) -> () {
    return { response in
        switch response {
        case .wasDeleted(item: let item): output(.todo(.response(.wasDeleted(item))))
        }
    }
}

private func handle(output: @escaping Output) -> (CheckTodoItemUseCase.Response) -> () {
    return { response in
        switch response {
        case .wasChecked  (item: let item): output(.todo(.response(.wasChecked  (item))))
        case .wasUnchecked(item: let item): output(.todo(.response(.wasUnchecked(item))))
        }
    }
}

Did you noticed? These are actually curried functions. As we can see each UseCase has its own handle function, distinguished by the returned function parameter type. These functions task is to translate a UseCase response into one or more feature messages to be broadcasted by the AppCore to the whole app.

So by using functions as module building type, we managed to get the optimal behaviour:

  • one input, one output
  • minimal surface area: you can only enter the feature through its input. there is no other way. No need to declare anything private.
  • compile time messaging: as this is swift, in- and output are typed, you cannot hand in wrong datatypes.

Of course, you can use classes or structs instead — but they offer more room for error, as you must manually declare all properties private and don’t forget to finalise the classes. When I switched from struct to PAFs, the features file sizes where reduced by 30 to 40% — simply because of all the private declarations.

Functions — so is it Functional Programming?

No, this is not FP. For a simple reason: In FP functions are required to be pure (as in: No side effects) and return once for each time being called. As I showed above: We use partially applied functions to actually create state — and there-fore side effects. And the feature function (the unnamed one) does not return, but it calls the output — at any time in any order. This is behaviour we know from OOP. I’d rather describe it as a variation of OOP, Message Orientated Programming — MOP.

If you feel this isn’t any form of OOP, please argue with the master Alan Kay himself.

But when we talk about state keeping, I will come back to the topic of OOP & FP.

Next…

We have seen the UseCases that encode the business logic, the Features, responsible to listen for messages and coordinate the UseCases, the next logical step will be to combine the features into an AppCore and connect that to an UI. Here we will also meet the Message type.

Part 3: AppCore & UI

Discuss

Please share your thoughts.