If you absolutely need to use a software pattern, do it the right way.
Design Patterns, Elements of Reusable Software by Gamma, Vlissides, et al.
Four authors, ominously referred to as “the Gang of Four”.
The original Gang of Four Book explains what a specific software pattern is and how to use it.
They list and explain, as a “catalog” a number of what nowadays we would call “traditional software patterns“. They are 23 in number, divided in three categories:
Before everything, the GoF explain what a pattern is, crediting Alexander (an architect, not a software one) with creating the concept. Historically, the first pattern to be invented was MVC, invented by Xerox doing research on Smalltalk programming, it is not listed in GoF.
A pattern is a solution to a problem, usually a recurring one, that is applicable to a context.
The GoF book is old; when it comes to the caveats of a pattern, they explain what was known at the time of writing the book.
We have, in the meantime, accumulated knowledge, so, for instance, we know that injecting a global state using a Singleton is not always going to be good.
For one thing, for instance, Singletons are difficult to mock unless you define them in ways so that they cease to be actual singletons.
Also, the GoF book tells absolutely nothing about concurrency problems you may encounter in modern times, as when the book was written, computer architectures were often single-core. The problem of race conditions in the example implementations of some patterns, including the humble Singleton was not apparent then. Now we know that in order to implement a Singleton you have to make its instantiation thread-safe.
The GoF book uses C++ as an example language and a predecessor of UML for diagrams called OMT.
The diagrams, however are quite similar to modern UML and are understandable to a modern reader that knows some UML superficially.
MISUSE OF SOFTWARE PATTERNS
We are assisting to a lot of “pattern-related thinking” that is doing more harm than the difficulties it claims to solve.
When we use a pattern, we ideally want to reduce the cognitive load necessary to understand code. Your objective when programming is not just writing code that works.
Our objective is being understood by the fellow humans that will read your code.
Being uncaring about your fellow humans means being a bad neighbour. You should try to be a good Caring Gardner instead.
Your patio should be nice, clean, and inviting.
How to be a Good Gardner
I have a few recommendations that will help you write better code.
Do study patterns, but do it seriously. Do not rely on some third-level understanding from a tutorial. Go and grab a copy of the original book and read it.
Also, read about what others have written about the patterns you are interested in, but do not settle for unreputable sources; bring with you a heavy dose of scepticism and critical thinking.
IF IT IS TOO GOOD TO BE TRUE, IT PROBABLY IS.
If a new pattern promises to solve all your problems, and it is claimed that this new magical pattern is a universal solution, that just by using that you will gain some magical reduction of complexity, good.
I would say that just from this description, it looks implausible, but maybe you should give it a try.
You need to see if this reduction of complexity is real.
Sometimes you will notice from the beginning that the promises look unreal.
Especially when it comes to mobile patterns, if none of the promises looks deliverable, or you need months of studies just to learn additional frameworks that are needed just to use that pattern, you should consider that Snake Oil and treat it as a potential poison.
E.g.: the pattern requires bindings that do not exist in the operating system you need to use, and in order to use that pattern you have to integrate a complex third-party framework that requires weeks or months to study it. So comes SwiftRx and having to study a complex functional approach to concurrency to implement MVVM “properly”.
Apple was probably misguided enough to have to implement its own take on reactive functional frameworks, Combine.
And it now seems Apple is focusing away from Combine, seeing that it was not mentioned in WWDC.
Problem is that a pattern such as MVVM came from a context where those bindings were supplied by the operating system, but has no reason to exist outside the context of the applicability of that pattern.
Poor MVVM! It makes no sense to use it in iOS if you are using UIKit because the binding functionality is absent from the operating system. It was there in the operating system that originated the pattern: WPF/.NET
On WPF/.NET, MVVM is the right tool to use; in iOS, it is a terrible choice.
Now the issue is, does it make sense to adopt MVVM in SwiftUI now that we have bindings (@State, @ObservableObject, @Binding, etc…)? Well, no.
Actually Apple says we should not. They probably reached their limit, after so many “thought leaders” started to earn their honest living by giving really bad advice to the unsuspecting masses, and Apple eventually published a very clear message titled “Stop using MVVM on SwiftUI”.
MV is Apple’s preferred framework and is called by Apple, “Post Reactive”.
You need a View and its corresponding model, and that’s it, bind state (you have ample choice on how to do that) and Bob’s your uncle.
The model will update when the view modifies something that it is bound to in the model.
When the model updates something that the view is bound to, the views that are bound to that content will refresh, using an ultra-efficient diffable approach, highly optimised, and that makes no sense for you to try to reproduce with your code.
I know because in the past I tried to be smarter than UIKit, and had to go past the time-consuming auto layout, and render some parts on the UI directly with Core Animation. I needed to be fast. Now I wouldn’t need to go to those lengths anymore because the operating system and SwiftUI do that automatically.
The difference between you and Apple, as the development team is probably three orders of magnitude in the number of top-notch world-class guys at their disposal. If they implement a pattern inside their operating systems, they do have the resources to do it spectacularly well.
And their implementation, you can bet, is going to be very heavily tuned to their hardware.
Can you do the same? Probably not.
Sorry for the folks who misspent their time learning all those complex frameworks and used them in huge applications because it was the fad of the moment. But their efforts were wrong from inception.
The reason why MVVM in SwiftUI is wrong is that you don’t need a VM bridge class, as that function “comes for free” and is already implemented natively by Apple’s View struct.
So, in an iOS context, if you are using MVVM in SwiftUI, you are shooting yourself in the foot because you are going to have double the necessary complexity in order to implement whatever you were planning to code. Even if you manage to get it to work, it is always more complex than the simple way of doing things, and it will therefore be slower and need more memory. Not needing to wade through non-native frameworks increases software developers’ freedom and helps them live happier lives.
MV / State
When you change anything via the bindings, you modify the application state. Model and View will receive these changes from each other bidirectionally.
You can also use actors and simple to reason with async / await concurrency in order to decouple things and reason in simple terms about complex concurrency.
No more callbacks, no more pyramids of hell.
Do you need local state? Use @State.
Do you need state sharing across a view and its children? Use @Binding.
Do you need to share the state from Model to View? @ObservableObject and @Published.
NB: You may want to use @StateObject to avoid re-instantiating objects marked with @StateObject whenever their associated view gets redrawn.
Do you want to have global state sharing? Yes, it makes sense if you need to share global events, such as notifications, displaying errors, keeping track of connection status, etc.
In that case, you should use @Environment (for apple system defined keys) and @EnvironmentObject (for your own).
If your way of building an app gets in the way of these binding mechanisms, you made an architectural error, creating more complexity than necessary.
Is the modest advantage of what you try to achieve worth the price?
No, it is not.
Sorry for those who see complexity as a way to defend their jobs; now is a good moment to use simpler approaches to achieve more.
Don’t do anything complicated if it is not needed.
Rather, use your time to decouple your system properly, partitioning it sensibly, preferably using a Clean Architecture approach.
It means using dependency injection to decouple your modules; again, no need to invent new patterns to solve problems that are not even there.
For Clean Architecture, I mean the real thing, as devised by Robert Martin.
But also, “est modus in rebus”, everything should be used, with moderation.
No reason to exceed with decoupling and create a mess so well decoupled that you need to wade through an excessive number of levels to achieve anything.
Related things should be kept together; there is a balance to be reached between decoupling and excessive coupling.
Coupling is the number of things you need to change if you change something in your code.
Is the change local? No problem. Suppose you can change the implementation of a module without changing a module that depends on that. In that case, you can say that the implementation of a service is independent of the module that uses the service.
Saying that your pattern, VIPER, implements Clean Architecture means you don’t understand architecture as a concept; you are either lying or are misguided at best.
VIPER is a failed architectural pattern from the beginning because it claims to be universally applicable. It claims to solve the “fat MVC controller problem”.
There is no fat MVC controller problem; the real problem is not partitioning an App because the developer doesn’t know software architecture.
The problem is exacerbated if developers don’t do TDD because TDD would FORCE them to decouple things. Otherwise, the developers would not be able to write tests. And, if writing unit tests is difficult on your app, your architecture is probably wrong.
MVC per se was never the main problem.
Arguably, doing things the Apple Way was always a better choice than third-party alternatives.
Whenever you have a pattern and want to use it ALWAYS, well, you have a problem, not a solution. A pattern is a tool that you would want to use when it makes sense:
A hammer is a terrible tool if I need to drill a hole.
VIPER is so decoupled that you need seven files to implement a simple screen.
Yes, but it is sooo decoupled! Well, it is way too decoupled; in order to implement a tiny change, you have to go through generic boilerplate code, too many levels, to achieve very little.
There is also no consensus on how to implement VIPER.
It is probably wrong if your pattern requires automated code generation tools producing seven files per screen.
Suggesting using the wrong tool as a panacea is selling snake oil.
Snake oil in IT is often the promise of a silver bullet, some tool or construct that will solve “everything”.
Snake oil is seldom innocuous; it is often poison that will make your project worse, make you work long hours to achieve less, and help you create more bugs rather than the opposite.
It will make your company lose money, and everybody will be unhappy when it happens.
Most of the suggestions you read on the Internet and especially as Tutorials are sometimes wrong, misguided and even dangerous.
The new “pattern” they want to sell you looked great on paper or in the slides at a conference, where it is demonstrated with a trivial example that bears no resemblance to the real problems you face. We already know that MVVM does not make any sense; what could be even more wrong than MVVM? MVVM/C, add a “coordinator” to the mix to “reduce complexity” by adding yet another entity.
Want to really screw up things? Throw in RX,
That said, rather than looking for a new pattern, try to see if an existing, traditional pattern fits the bill.
Do not Reinvent the wheel and give it a new name. There is no need to invent a pattern that is already listed among the traditional ones and give it a fancy new name.
What is it that the coordinator pattern tries to solve?
If you are creating a new pattern, DO DOCUMENT IT WELL.
You should give the following sections, the same that were used in the original GoF book:
- Intent: the problem you want to solve
- applicability: when it is appropriate to use the pattern
- structure: normally a class diagram illustrating the class relationship
- participants: who the subpart of your patterns are, e.g., classes, structs and what their ROLE.
- collaborations: a timing diagram, if needed, a textual description of how the participants “talk” to each other.
- consequences: benefits, possible bad consequences (caveats).
- implementation: an example, commented implementation, in a real enough but not too complex case.
Yes, you should use diagrams; someone told you that you should not use diagrams because it is not agile. That was some seriously misguided advice.
A diagram is useful when you have to explain a concept.
It is useless if you want to capture the full complexity of a complete system because the work needed to keep in sync the representation of the system and the system itself will be simply too much. UML failed there, Structured Analysis failed there even in a worse way, and C4 looks a lot like SA, to the point of using Context Diagrams (Data Flow Diagram Level Zero in the defunct Ed Yourdon/De Marco parlance).
And “diagrams as code” as a methodology is not going to end well. It never worked, you will die of analysis paralysis. It happened in the past, pity people do not know history:
Someone told you that you should aim at fully representing code with diagrams. That someone is mistaken; we already tried that approach and failed miserably from the end of the 80s to the 90s; that methodology was called Structured Analysis. The term Paralysis from Analysis comes from that attempt. C4 is not evil per se, but trying to use it as a sledgehammer is probably misguided.
Don’t use a pattern if it is not needed or if it adds more complexity rather than simplifying things.
The purpose of a pattern is solving a problem and helping fellow humans reading your code, not “being clever by showing you know a pattern”.
A pattern is a solution that is applicable to a context. It is not a recipe; it is not applicable “always” independently from context.
If you apply a traditional pattern to a problem that is outside the applicability scope, you are quite probably making a mistake.
If you spot a new pattern that has too broad a scope, and it is suggested you have to use it always, and it will solve all your problems, that is not a pattern; that is charlatanry and snake oil
A very misguided way of using patterns is trying to maximise the number of patterns you can
throw at a problem.
The naming of patterns is an important matter (Old Deuteronomy is not a good name for a pattern). Seriously, DO NOT use a pattern and call it by its pattern name, inside your code.
NAMING THINGS IS IMPORTANT. We name things because we want to be understood. We want to be easy on our colleagues and help them.
E.g: in the examples about State, in the original GoF Book, the example was about the state of a TCP connection. How did the GoF name it? TCPConnection. You don’t need to get fancy; call it the same as the problem you are trying to solve.
Now, suppose that you have determined that your problem requires a Facade.
The definition you find for Facade in the GoF book is:
Intent: “Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface and makes the system easier to use”
The authors go on to clarify that the purpose of the Facade is reducing the complexity of the interface:
E.g.: I have a single command interface to a compiler.
The compiler facade hides the various components it will call in stages: preprocessor, parser, code generator, optimiser, assembler, and linker.
The facade makes the programmer’s life easier without hiding the lower-level functionality from the few that need it.
So, if you use a Facade in your code, you should not make fellow programmers need to ask you:
“What do you want to achieve, and why don’t you document and explain it?”
And please call it in a way that your intent is obvious.
The example name for a Facade example is Compiler, not “Facade to code generating steps”.
it is not, and it would have been even worse, ParserFacade, mentioning only the first subcomponent.
This would eventually make sense to the poor souls reading the code and trying to grok what you were after, only after they eventually understand that there are other steps following parsing. The actual intent was to generate an executable, eventually.
So calling the whole thing “compiler” explains what you were after in no uncertain terms.
The Applicability section for Facade is telling: reducing complexity, hiding the fact that subsystems may evolve while their interface won’t, decoupling, and layering subsystems. (do yourself a favour and read the GoF Book!).
The GoF authors give little about the contraindications of Facade. Still, an obvious one is misusing the pattern trying to use it outside its intended applicability, and even worse actually hiding its true purpose.
Hiding the purpose you wanted to achieve is not “using a pattern”; that is called hiding dirt under the rug, and nothing good will come to a project if you use this approach.
Do you need an OSFacade in an application?
The name is problematic; it shows you did not really pay attention and probably you have the wrong or not clear enough “intent”.
You are being unclear, and the name you choose evidences the problem.
It is the OS loading the application, so a Facade allows the OS to “drive the application”?
But you are not simplifying the OS work by making it simpler via a Facade; because the required mechanisms are already there, and those mechanisms are not being provided by your Facade!
Maybe you want to access what happens as the application is invoked from the OS, but that is not a Facade. And whatever mechanism allows you to access that information is already there, so you should probably call it an Adapter, at best.
Would you call it a Facade just because it contains several other software objects in one place?
That looks fishy. Are you providing a common simple interface to external entities? Is this what you wanted to achieve? We will never know for sure without asking you in person, and we should not have had to. We have to ask you because your code is not clear.
Did you need to subscribe to the App lifecycle in SwiftUI?
Did you need to use deep linking?
Did you want to do that with UIKit in a SwiftUI context? And why would you do that from iOS15 on? There are better ways to achieve that, and those ways are SwiftUI native. Whatever your pattern of choice, it should be idiomatic; that is, it should feel natural in the context of your choice’s programming language and environment. You should integrate UIKit in a SwiftUI context only if you have a reason to do that, maybe if there is a bug and you have no other choice.
Is this the case?
Sorry for being repetitive, but you should not call “that” a Facade; rather, you should document what you want to achieve. We should not be left in the dark guessing about your motives.