Slow architecture in Swift
Godamn it, I was playing Diablo 4
Saw this when waiting my breakfast.
Published in Better Programming, in whch he touted Clean Architecture as a solution to be more Agile.
I laughed my ass off. What a brilliant comedy piece from our friend at Better Programming.
I would describe Clean Architecture as clumpsy, awkward, rigid, sluggish and stupid. In fact I have to look up dictionary for adjectives, as shown in the picture above.
Dude, I came from Chaos development flow. Let me provide my two cents on this.
Introducing: Slow Architecture
As he pointed out:
For most, “agile” had become a meaningless word, just describing a marketing term for a certification industry.
You know what else is a marketing term for a certification industry?
Clean Architecture.
What are the steps you take in a certification industry?
- Hero worship
So Bob is deified becasue he used the word “Clean” in his book. Then it must be clean, right?
2. A brand
Next step is to create your own brand, and promote it in the name of said hero. In his case,
I recently developed an Agile Architecture “Khipu” in Swift. If this interests you and your team and you want to learn more about it, feel free to contact me.
But it is doomed to fail, because the word has no meaning.
You need something that has “Clean” or “View Model” in it. You are in certification industry after all. This is the part where they will piggyback dumb shit under hero’s name.
3. Marketing campaign
Of course you need visibility. So you will publish it under Better Programming, with memebership exclusive. They will publish any dumb shit under the sun from what I can tell. I have no doubt ChatGPT can write an article to be published on Better Programming at this point. Or is it already?
Why do I bother breaking down these steps? To prepare you for the mis-direction that follows:
Using protocols with attributed types and nested enums to implement Robert C. Martin’s use cases
In this article, I want to show you how I combine protocols with attributed types by building expressive yet simplistic modules that offer their own flexible and safe DSL. These are constructed by nesting enums, associating model objects as values where needed.
Hero worship, brand promotion in a marketing campaign with an emphasis on the words “flexible” and “safe”.
This is all to distract you from asking: why nested enums?
And you won’t ask proof or comparison that it is as good as claimed either. Because it’s Robert C. Martin! OMFG!
Be extra cautious when someone uses “nesting” as a feature not bug. And guess in what “architecture” the usage of “nesting” is normalized?
But let’s not dwell upon small techinical details. What makes it agile?
The idea is to modularize everything, to break things into smaller use cases. E.g.;
struct AddTodoItemUseCase: UseCase {
enum Request {
case add (item: TodoItem)
}
enum Response {
case wasAdded(item: TodoItem)
}
typealias RequestType = Request
typealias ResponseType = Response
init(store: StateStore, responder: @escaping ((Response) -> ())) {
self.interactor = Interactor(store: store, responder: responder)
}
func request(_ request: Request) {
switch request {
case .add(item: let item):
interactor.add(item: item)
}
}
private let interactor: AddTodoItemUseCase.Interactor
}
Now, can you tell you what makes this idea so slow?
- You don’t have clearly-defined request-response most of the time
In Chaos development, everything is changing, all the time. The last thing you need is a strict type system that incurs more costs to changing.
If his use case does only one thing, like he claimed:
Use cases should only do one thing, like adding items.
It’s just a function!
func add(_ items: [Item]) -> isSuccess {...}
// request response
He said it himself:
The goal of this architecture is to be reasonable, meaning that at any time, you can deduct its state and logic.
You can deduct its state and logic from function signature at any time!
Robert C. Martin’s use cases, ladies and gentlemen.
You may argue, but there’s interactor and store and shit. OK, let’s go into more details.
I stand by my principle until I don’t
Let’s get more context:
Use cases should only do one thing, like adding items. Therefore, the request often has one command. And the response will often have two cases: one success and one failure case. But this is neither a law nor a rule, i.e., you might want to combine symmetric commands in one use case, such as opening and closing files.
So use cases should only do one thing, but this is not a rule. You can do more than one thing if you need it.
WTF?
He is establishing and demolishing his own rule in the same paragraph. It’s quite impressive really.
It is ChatGPT writing this, isn’t it? It is a reverse-Turing test. Only qualified human developer can pass it.
What’s stopping you from writing two functions?
func open(...) -> FileStatus
func close(...) -> FileStatus
What Agile benefits are you providing when you can easily do the same using functions?
If you want to consider interactor and store, then I first ask you, why is use case a value type?
struct AddTodoItemUseCase: UseCase
In the context of SwiftUI, this value type doesn’t conform to View
. You can’t just put a reference type in it and pretend to be a value type.
You need to justify it.
Otherwise what’s the point of having value type if you can bypass it by accessing its member reference type?
Didn’t Robert C. Martin cover this in his book? Oh right, he didn’t.
Does he know you are writing dumb shit in his name?
So this use case is bullshit from the get-go. It should be reference type if it can change app state, which is a reference type passed in as parameter.
Then the logical “use case” would be
class AddItemInteractor {...}
It doesn’t need the affix Interactor
at all, I’m just making it consistent with Clean Architecture terms.
So all the Request
, Response
, UseCase
can be consolidated without loss of deductability.
This will definitely NOT SCALE. Which is exactly why Clean Architecture will market it as SCALABLE.
The overheads of creating a type for every task will SLOW you down.
Function over object.
Funny thing is, he knows it!
typealias Input = (Message) -> ()
typealias Output = (Message) -> ()
func createTodoListFeature(store: StateStore, output: @escaping Output) -> Input {...}
And he even talked about functional programming.
All these are extra complexity that you don’t need.
Agile. Robert C. Martin. Clean.
Why is his return type Input
… nevermind.
Agile. Robert C. Martin. Clean.
Oh I’m sorry, he used another guy’s name
This is behaviour we know from OOP. I’d rather describe it as a variation of OOP — Message Orientated Programming or MOP.
If you feel this isn’t any form of OOP, please argue with the master Alan Kay himself.
I have no doubt Alan Kay is a master in OOP. But is he? And how valuable is OOP in a POP language?
Dude, you can’t even create a reference type object right. You know what, let’s review his code.
If your theory is so right, why is your code so shit
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) ) }
}
}
Handling cases without swtich
means he didn’t want to handle default
.
store
is repeated so many times, it needs to be refactored out. It’s not like you are passing in different store
at runtime, at least not in here.
This “message”-oriented programming that he implemented using enum
is not new. Remember in Objective-C you are sending “message” all the time via functions? This nostalgia?
[receiver message]
Use function as message? Which is what I just said?
It’s dynamic, run-time, and didn’t require strict type overheads?
Not saying we go back to Objective-C, but is this redundant boilerplate the best we can do in Swift? dynamicMemberLookup
might be a way to go, but let’s keep things simple.
What about
func createTodoList(...) {
// store is a member property, so no need to pass it repeatedly
try {
addItem(...)
checkItem(...)
} catch {...}
}
Straight-forward.
Note that @Escaping
closure is not a harmless decorator. It specifically WARNED you that your closure may implicitly capture something.
What did he do? Use it as a sign of skill, functional programming and shit.
Pass it around like candies which make it hard to track and debug.
Use Output
to generate Input
via escaping closure is confusing and dangerous.
The golden rule of thumb of Agile is this:
Always assume your co-workers are dumb idiots
There’s no fking possible way you can teach them currying without backfiring. And it is a very situational technique with a lot to consider, e.g.; abuse of escaping closures.
I’ll reject this submit from code replication alone, let along readability.
Oh I’m sorry. Agile. Robert C. Martin. Clean.
Not only is he slowing himself down, he has to spend time bringing his team up to speed, hence slowing the team down.
These can be considered captital investment if the architecture is actually good, but code duplication alone would kill it, including redundant request
— response
and nested enum messsage.
One more example:
private init (_ text: String, _ completed: Bool, _ id: UUID, _ dueDate:Date?, _ creationDate: Date, _ alterDate:Date?) {
self.text = text
self.completed = completed
self.id = id
self.creationDate = creationDate
self.dueDate = dueDate
self.alterDate = alterDate
}
private func alter(_ change:Change) -> TodoItem {
let alterDate = Date()
switch change {
case .text (let text ): return TodoItem( text, completed, id, dueDate, creationDate, alterDate )
case .completed(let completed): return TodoItem( text, completed, id, dueDate, creationDate, alterDate )
case .due (let dueDate ): return TodoItem( text, completed, id, dueDate, creationDate, alterDate )
}
}
Use the default initializer man. So you don’t need init()
.
Use default value man. So you don’t repeat every property. E.g.;
var item = TodoItem()
switch change {
case .text(let text):
item.text = text
case .due(let dueDate):
item.dueDate = dueDate
// ...
}
return item
This will come in handy when you CHANGE PROPERTY.
You could probably get away from auto-refactoring, but keep it a good practice.
Again, he is asking for trouble. He didn’t get it right himself, how do you expect his team that he trained to get it right?
I mentioned nested enum from the beginning. Here’s why:
struct AppState: Codable {
enum Change {
case add(_Add)
case update(_Update)
case remove(_Remove)
case replace(_Replace)
enum _Add {
case item(TodoItem)
case entry(Entry)
case tag(Tag)
}
enum _Update { case item(TodoItem) }
enum _Replace { case tags([Tag] ) }
enum _Remove { case item(TodoItem) }
}
// ...
}
Enum-inception, which is nested inside a struct
.
When you can flatten it with
enum Action {
case addItem(TodoItem)
case addEntry(Entry)
case updateItem(TodoItem)
...
}
Or you are a puzzle enjoyer?
case add(_Add)
Did Robert C. Martin teach you this in his book?
This means you can’t just use a case. You need to prefix it with the whole family history. E.g.;
case AppState.Change.add(let add: _Add)
which SLOWS YOU DOWN!
Also wtf is this?
private func alter(_ change:Change) -> AppState {
switch change {
case .add (let msg): return add (msg)
case .update (let msg): return update (msg)
case .remove (let msg): return remove (msg)
case .replace(let msg): return replace(msg)
}
}
State change logic shouldn’t be inside value type. But this seems too trivial for that.
But on the other hand, if it didn’t do shit, why is it here?
This is as boilerplate as it gets, and you don’t have time to write redundant shit. And guess what happens when you add / remove cases… how many places you have to re-visit?
All this setup so he can write this?
private func remove(_ change: Change._Remove) -> AppState {
switch change {
case .item(let item): return AppState( items.filter{ $0.id != item.id }, entries, tags.map( { $0.alter(.remove(item)) } ) )
}
}
Wtf knows what he is writing about?
I speak it from a perspective of a dumb idiot co-worker that you might run in. Wtf is $0
???
Dude you have a reference type AppStore
that you pass in everywhere. Use it!!!!!! E.g.;
store.change(_ newState: .remove(item))
Dependency injection!!!
What Robert C. Martin’s book is all about!!!!!!!!!!!!!
How the F do you fail the one thing noteworthy in his book? The thing Clean Architecture never shuts up about?
I can’t. The shit is too stupid.
Uh, I mean as in the opposite of Agile.
It’s in the dictionary, look:
I don’t have time to review every bit of his code. Because it would take away more of my time playing Diablo 4.
I’m going to wrap up by just looking at one of his tests. That he is so proud of and throws buzzwords like BDD around.
class TodoItemSpec : QuickSpec {
override func spec() {
describe("TodoItem") {
let orig = TodoItem(text:"Hey Ho")
context("creation default") {
let now = Date()
it("has custom text" ) { expect( orig.text ) == "Hey Ho" }
it("has random id" ) { expect( orig.id ).toNot(beNil()) }
it("is not completed" ) { expect( orig.completed ) == false }
it("has the creation date set") { expect( orig.creationDate ).to(beCloseTo(now)) }
it("has no due date set" ) { expect( orig.dueDate ).to(beNil()) }
it("has no alter date set" ) { expect( orig.alterDate ).to(beNil()) }
}
}
}
}
What’s wrong with this?
You have to know precisely how TodoItem
is defined.
You don’t. Models change all the time, behaviors change all the time, features change all the time. That is, spec changes all the time.
If spec changes, you now have at least one more place to modify, which cannot be done by auto-refactoring. This manual work SLOWS you down.
What value does this test provide?
No spec will tell you what variables you should use and their initial values. Dude they are written by PM. They focus on features. It is your job to handle implementation.
So you are glimping yourself by dictating what variables and their inital values should be. Nobody gives a shit.
Is the feature complete or not, do they pass QA or not. That’s the things that matter.
For an architecture that builds around state management, this is just stupid.
The only thing you should check is state. E.g.;
func testUpdate() {
assert(store.state == .initial)
store.update(...)
assert(store.state == .updateSuccess)
}
The rest follows from state. This decouples you from implementation details. You don’t check every property at every time like an idiot!
This is just part of the “tests” of ONE model type at differnt times:
override func spec() {
describe("TodoItem") {
let orig = TodoItem(text:"Hey Ho")
context("default") {
// ...
}
context("altering") {
let new = orig.alter(.completed(true))
context("check") {
it("changes completed" ) { expect( new.completed == true ) != orig.completed }
it("doesnt changes the id" ) { expect( new.id ) == orig.id }
it("doesnt changes the text" ) { expect( new.text ) == orig.text }
it("changes the alterDate" ) { expect( new.alterDate ).toNot(beNil()) ; expect( orig.alterDate ).to(beNil()) }
it("doesn't change the due date" ) { expect( new.dueDate ).to (beNil()) ; expect( orig.dueDate ).to(beNil()) }
it("doesn't change creation date") { expect( new.creationDate ) == orig.creationDate }
}
context("uncheck") {
let orig = new
let new = orig.alter(.complete(false))
it("changes completed" ) { expect( new.completed ) != orig.completed }
it("doesnt changes the id" ) { expect( new.id ) == orig.id }
it("doesnt changes the text" ) { expect( new.text ) == orig.text }
it("changes the alterDate" ) { expect( new.alterDate ) > orig.alterDate! }
it("doesn't change the due date" ) { expect( new.dueDate ).to (beNil()) ; expect( orig.dueDate ).to(beNil()) }
it("doesn't change creation date") { expect( new.creationDate ) == orig.creationDate }
}
}
}
}
You will never get to the end of it. Too many possibilities. What if you change parameters? E.g.; add a different item. And all these are worthless if design changes.
Really? This is the BDD Clean Architecture? Did they cover this in the book or…?
And is this the “readability” I’ve heard so much about?
let fork = orig.alter(.fork([.due(orig.dueDate?.dayAfter), .completed(!orig.completed), .fork([.text("Let's go")])]))
I especially love that he had store
and everything, but he gets to change state without going through store
. Not that I can see from above.
Finally, let’s summarize what did we learn.
What Did We Learn From This Exercise?
First of all, adding features is purely additive in this architecture.
We didn’t need to go through jungle-like code paths and alter complex statements.We didn’t even have to code anything where our todo items were changed. We listen for the message that the change occurred. We are using true black boxes.
We have seen that feature can resemble a sink. They can also resemble a source, i.e., chat inboxes, heartbeat clocks.
It took me mere minutes to implement this. I have seen teams struggle with the same task for months.
First of all, he went through jungle-like NESTED code paths and alter complex statements. E.g.;
let fork = orig.alter(.fork([.due(orig.dueDate?.dayAfter), .completed(!orig.completed), .fork([.text("Let's go")])]))
He coded EVERYTHING where his todo items were changed.
context("uncheck") {
let orig = new
let new = orig.alter(.complete(false))
it("changes completed" ) { expect( new.completed ) != orig.completed }
it("doesnt changes the id" ) { expect( new.id ) == orig.id }
it("doesnt changes the text" ) { expect( new.text ) == orig.text }
it("changes the alterDate" ) { expect( new.alterDate ) > orig.alterDate! }
it("doesn't change the due date" ) { expect( new.dueDate ).to (beNil()) ; expect( orig.dueDate ).to(beNil()) }
it("doesn't change creation date") { expect( new.creationDate ) == orig.creationDate }
}
It took him HOURS at MINIMUM to implement this. Look at above.
I have seen teams struggle with the same task for months.
Because they run Clean Architecutre?
Oh I’m sorry.
Agile. Robert C. Martin. Clean.