Architecture & Agility: 03 - AppCore & UI

AppCore: Connecting the dots

In the last two articles we have seen how UseCases encapsulate our business logic and how Features group and coordinate them.

Now let’s connect the Features which results into the AppCore. This AppCore then will receive messages from the UI and other sources.

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

func createAppCore(
          store: StateStore,
      receivers: [Input],
    rootHandler: @escaping Output) -> Input
{
    store.updated {
        #if targetEnvironment(simulator)
        print("\(store.state())")
        #endif
    }

    let features: [Input] = [
        createTodoListFeature  (store: store, output: rootHandler),
        createTaggingFeature   (store: store, output: rootHandler),
        createJournalingFeature(store: store, output: rootHandler)
    ]

    return { msg in
        (receivers + features).forEach { $0(msg) }
    }
}

Exactly like our features, AppCore is just a function that we will use as an Input.

{ msg in
    (receivers + features).forEach { $0(msg) }
}

All it does is iterating over all Features and passing each one the message it just received. And as we have seen before, a Feature will pattern match to check if it is interested in this message and use its UseCases accordingly.

Connecting the UI

Core|UI is fully decoupled from the UI, it isn’t part of the core and must be hooked up from the outside. In this example I’ll use SwiftUI, but as it is fully decoupled using any other UI framework on any platform should work — the core is easily portable, thanks to its decoupled nature.

import SwiftUI

@main
final
class TodosApp: App {

    private let store: StateStore = createStore()
    private lazy var viewState: ViewState = ViewState(store: store)
    private lazy var rootHandler: (Message) -> ()
        = createAppCore(store: store, receivers: [viewState.handle(msg:)], rootHandler: { self.rootHandler($0) })

    var body: some Scene { WindowGroup { ContentView( viewState: viewState, rootHandler: rootHandler ) } }
}

First, we create a StateStore, which we will pass into a ViewState object and the core. The ViewState translates our app state for SwiftUI.

import SwiftUI

final
class ViewState: ObservableObject  {

    @Published var items  : [ TodoItem ] = []
    @Published var entries: [ Entry    ] = []
    @Published var tags   : [ Tag      ] = []
    @Published var deleted: TodoItem?

    init(store: StateStore, dismissToastAfter: Double = 2) {
        self.dismissToastAfter = dismissToastAfter
        store.updated {
            self.process(appState: store.state())
        }
        process(appState: store.state())
    }

    private let dismissToastAfter: Double

    func handle(msg: Message) {
        if case .todo(.response(.wasDeleted(item: let item))) = msg {
            deleted = item
            DispatchQueue.main.asyncAfter(deadline: .now() + dismissToastAfter) {
                self.deleted = nil
            }
        }
    }

    private func process(appState: AppState) {
        items   = appState.items  .sorted(on: \.creationDate, by: <)
        entries = appState.entries.sorted(on: \.creationDate, by: <)
        tags    = appState.tags   .sorted(on: \.name,         by: <)
    }
}

if the store gets updated, ViewState will process the new state

init(store: StateStore, dismissToastAfter: Double = 2) {
    //…
    store.updated {
        self.process(appState: store.state())
    }
    //…
}

private func process(appState: AppState) {
    items   = appState.items  .sorted(on: \.creationDate, by: <)
    entries = appState.entries.sorted(on: \.creationDate, by: <)
    tags    = appState.tags   .sorted(on: \.name,         by: <)
}

As items, entries and tags are tagged @Published, SwiftUI can listen to those change and react accordingly.

So every time our state changes, the UI will pick up the changes automatically.

And how does the App handle UI events?

Quite simple: it just sends messages, which in SwiftUI looks like

Button { rootHandler(.todo(.add(TodoItem(text:text)))) } label: { Text("add") }

or

.onDelete {
  if let idx = $0.first { rootHandler(.todo(.delete(viewState.items[idx]))) }
}

The rootHandler here is the Input function that we also refer to as AppCore.

class TodosApp: App {
    //...
    private lazy var rootHandler: (Message) -> ()
        = createAppCore(store: store, receivers: [viewState.handle(msg:)], rootHandler: { self.rootHandler($0) })

    var body: some Scene { WindowGroup { ContentView( viewState: viewState, rootHandler: rootHandler ) } }
}

So all UI events are handled the same way:

  • an event occurs
  • it is send as a message to the AppCore
  • the AppCore processes it into a new state
  • the state changes trigger the UI to refresh itself

I chose this way, as it resembles the same unidirectional flow that I have in each UseCase and Feature, but another way, that might feel more natural for SwiftUI could also be to only have bindings in the view code and have the ViewState generate the message once the UI writes to a binding.

The Code

At this point I want to share my code with you, though I have not yet highlighted all aspects of Core|UI.

Real World Code Snippets

I think, the following code line demonstrate quite strongly the strength of the combination of a Command Interpreter and a powerful DSL.
At first it might look strange, but just read it. Literally read it:

rootHandler( .chat(.send(ChatMessage(from: user, to: receiver, body: message).alter(.append(image)))) )

the same code formatted differently:

rootHandler(
    .chat(
        .send(
            ChatMessage(
                from: user,
                to: receiver,
                body: message
            ).alter(
                .append(image)
            )
        )
    )
)

It calls rootHandler with the command that reads:

«Chat Feature, send the message from <user> to <receiver> with <image> append to it»

Isn’t that impressive? We encoded non-trivial behaviour in just one line of code, resembling near-to-natural English.

Not convinced? let’S try another example

rootHandler(
    .contact(.replace((contact: contactToEdit!, with: contactToEdit.alter(.nickname(newNickName)))))
)

And the long form if you find that easier to read:

rootHandler(
    contact(
        .replace(
            (contact: contactToEdit,
                with: contactToEdit
                    .alter(
                        .nickname(newNickName)
                )
            )
        )
    )
)

This calls rootHandler with the command:

«Contact Feature, replace the contact with itself having an altered nickname»

Again: A complex behaviour encoded in just one statement. We are actually telling the AppCore, what we what it to do — not how to do it. Which is a big difference to the procedural style which is mostly used in OOP. This code is more reminiscent of Functional Programming.

Next…

Though I don’t consider it an integral part of the architecture I want to show you how we can take inspiration from Functional Programming and use immutable state. If you look at the code you might expect the StateStore (which is also a gateway in our UseCases) to be a class, but it is actually a tuple. I also want to explain you, why I have chosen to use that over a class.

As I mentioned earlier there will be an article on how to do BDD with Core|UI

I also want to dig a little into how this architecture helps during agile processes, though I will cover this briefly, as this will be the focus of a book I am currently writing.

Another topic I want to cover is: How to scale (and shrink) this architecture for project of different complexities. Turns out this architecture is fractal.

Stay tuned.

Part 4: Immutable State

Discuss

Please share your thoughts.