How To Make a Card Game For iOS — Improving Architecture With Generics and Protocols (Part 2)
#iOS App Development
That's the second part of the series, devoted to implementation of the Card Game Engine.
- Card Game Engine with UIKit
- Generic Card Game - You are here.
- Card Game Internals - The Game Internals
- Part 4
This time, mostly about Swift Protocols and applying PoP principles. I guess, most of iOS devs do not often use many of Swift powerful features related to generic programming. This post is about applying generic programming to Card Game in hope to make it more flexible and extendable.
Too much want
I wanted to be able to build up the whole game cake like from Lego bricks. Where each brick conforms to some ~interface~ protocol and handles its role.
I wanted to have some bricks super-thin and game-specific, some reusable bricks and I also wanted to have some bricks with base implementation, depending only on other protocols.
I wanted to apply restrictions on some of my bricks types. Restrictions that look like "these bricks can work together, because some of their associated types match or mappable to each other", etc.
I wanted to perform all checks during compile time. 🤷♂️
Gratefully, Swift is powerful enough to build such kind of things.
PoP is not Poop. Is it?
They called it Protocol-Oriented Programming and they named it completely wrong.
To make the long story short, Swift Protocols mix up two different things.
Protocol aka. Interface
Simply a lists of properties and methods that your type should implement in order to conform to suggested protocol.
It's helpful to make our depend on protocols instead of exact types. We may have several implementations for the same protocols that may be replaced or changed more safely in future. While overall architecture will be more clean. L and D from SOLID, you know.
Protocol aka. Generic Constraint
If one has ever had experience with C++ Templates and Concepts - that's exactly the thing.
It's useful if you are working on implementation of some generic algorithm (Template) that works with some type T, and you need to add some restrictions (Concepts) for that type. That will be Protocol, applied as generic constraint.
Generic constraints protocols may be complex and include associated types, that may also have some constraints, etc.
Here is where the problem comes out.
*"Protocol can only be used as a generic constraint because it has Self or associated type requirements" - is the most disappointing compiler error ever.*😭
As soon as you add some associated type to a protocol, it cannot be used as a plain protocol aka. interface any more. You cannot simply pass it as a type to a func or use as a property type in your class.
Since that time it can only be used as a constraint for generic type. For example, in implementation of your generic algorithm.
Slathering Card Game with Protocols
How to initialize game in thirty one ~simple steps~ lines.
These 31 lines have a lot of magic underhood.
If we have a look at DeckPresenterProtocol
, we will see it has associated types:
FoundationPresenter
, TableauPresenter
, DeckWastePresenter
conform to DeckPresenterProtocol
. If we have a look at their implementation (probably it's declaration?) we will find constraints magic there.
The same thing with GameActionsPresenterProtocol
And GameActionsPresenter
implementation:
Guess what happens with GamePresenterProtocol
And its implementation:
What we get
Game, Presenters, DeckType, DeckViewType, and a lot of other things have many associated types, and when we combine them together, all necessary types are checked to match and support to work with each other. At compile time!😯
The design of the whole architecture turns out to be very flexible on the one hand allowing to replace parts of the Game or Game Presentation and UI easily. On the other hand, it is super strict with developer, forcing him to implement all protocols and satisfy all constraints to make it compile.
Superpower against runtime errors!
Type Erasure
As I've mentioned, adding associated types is Swift makes your protocol work as a generic constraint. That means that you cannot use it as a simple protocol, passing and putting it everywhere.
Thankfully there is a workaround pattern for it, called Type Erasure.
For example, AnySequence that can be met in Swift Standard Library.
The idea is to wrap generic protocol into a special box class, that itself conforms to that protocol.
Though we make the outer code depend on the box class type, not on the protocol, the initial flexibility goals is still preserved.
In fact it looks like box inside one more box. That's intentionally done so that it allows to do the following tricks:
Millennials reinvented overengineering
The whole thing is turning out to be rather complicated. Protocols with constraints, type-erased wrappers etc. It will be especially hard for a new developer to dive into that codebase for the first time.
In fact, we can make it much more "softer" by allowing our types to be much more dynamic: "Dictionary<Any: Any>", "AnyObject" with "isKindOfClass" order of things...
But. Even without all this additional generic stuff, the game is gonna be complicated.
So if to compare Generic Hell with Dynamic Ducks:
-
Generic Hell Cons:
- Complicated codebase
-
Dynamic Ducks Cons:
- Complicated codebase.
- Requires heavier test coverage
-
Generic Hell Pros:
- Some of the errors can be spotted at compile time
-
Dynamic Ducks Pros:
- Happy coding at the beginning
Instead of Conclusions
I'm gonna leave all the conclusions up to my readers, because this post is already super boring.
Comments