Hello, this is the iOS engineer dayossi.
I want to provide a service that makes my family happy We have released a family diary app called HaloHalo.
This time, I felt that I was developing an application through unit tests. I analyzed three problems.
The deliverable this time is here. https://github.com/Kohei312/TDD_Practice_Poker
When developing without writing tests, I couldn't separate the following three problems, and I was often troubled.
・ Is the logic of View construction centered on UIKit strange ...? -Is the data for displaying the UI strange ...? ・ Is the data transfer strange ...?
By writing unit tests Separation around here can be done smoothly, It's easier to analyze the problem.
When analyzing the problem, the following three perspectives I felt that it was effective in common.
One of the SOLID principles, which is an object-oriented principle, `` `single responsibility principle ```, This is a question to confirm whether you are aware of the design policy and intention.
This time, I used it with the nuance of "clarify the purpose for each layer and process".
There is a part that overlaps with the first point, It is to clarify which process is called at what timing and what kind of change is occurring.
Especially when there are many arguments and you try to perform multiple processes at the same time, it is easy to get confused. Therefore, I felt that code mistakes were likely to occur.
If you enumerate the states with enum or define the process to change the property only in a specific layer, It was easy to grasp the state change.
This also overlaps with the first point, I checked sequentially whether the layers were working according to the overall design.
While checking the purpose of what state each layer manages Gradually separating the responsibilities made it easier to review.
Below, we will take up the cases that we actually worked on in this application development.
Among the poker games I created this time We have set a rule that players can exchange cards up to 3 times. (There are two players, a user and a CPU)
Among them, according to the number of times each player exchanged cards A stumbling block occurred in that the state of the game changed.
The number of exchanges is a variable called changeCount Hold it as a property of a structure that has a player state called Player, I was trying to control according to the rules.
Whether the player is a user or a computer in an enum called PlayerType I try to distinguish.
public enum PlayerType{
case me
case other
}
struct Player{
var playerType:PlayerType
init(playerType:PlayerType){
self.playerType = playerType
}
var playerStatement:PlayerStatement = .thinking
var changeCount = 3
}
Initially in a higher layer called PokerInteractor that manages the logic of the entire game I placed and managed a Player type instance directly.
// MARK:-Manage the number of card exchanges and status of each player
public struct PokerInteractor{
var player_me:Player
var player_other:Player
mutating func changePlayerCount(_ playerType:PlayerType){
switch playerType{
case .me:
player_me.changeCount -= 1
case .other:
player_other.changeCount -= 1
}
}
Positioned as a layer that organizes business logic, Here I was controlling the state of the player and the progress of the game.
However, there was a pitfall here.
On one player's turn, that player's card exchange count was properly counted. The other player's card exchange count is not shared.
At player_me's turn, player_me's changeCount is certainly decreasing At the turn of player_other, the changeCount of player_me has returned to the initial value.
public struct PokerInteractor{
# WARNING ("State is never shared ...")
var player_me:Player
var player_other:Player
mutating func changePlayerCount(_ playerType:PlayerType){
switch playerType{
case .me:
player_me.changeCount -= 1
case .other:
player_other.changeCount -= 1
}
}
In the test, it was confirmed that the calculation was actually possible. I didn't see any build errors on the View side, so I thought there was a problem managing the logic data.
Because changeCount is an immutable value When changing the value, it is necessary to instruct the change from the generated instance of Player type.
However, if you update the property of Player which is a value type, the value of the entire Player will be updated. The upper layer PokerInteractor with Player as a property is also updated.
Therefore, the two players managed by PokerInteractor As a result, a new instance will be regenerated,
The card exchange count has been reset to the initial value every time It was no longer possible to share the status of all players.
Therefore, add one reference type PlayerStatus to grasp the status of all Players. It has been changed so that the player status can be shared.
Because the memory area that refers to PlayerStatus is always the same Even if the value of each player is updated and the memory pointer is changed The aim was to always be able to scope the changed value.
final class PlayerStatus{
var players:[Player] = []
var interactorInputProtocol:InteractorInputProtocol?
subscript(playerType:PlayerType)->Player{
get{
return players.filter({$0.playerType == playerType}).last!
}
set(newValue){
if let player = players.filter({$0.playerType == playerType}).last{
for (index,p) in players.enumerated() {
if p.playerType == player.playerType{
players.remove(at: index)
players.insert(newValue, at: index)
}
}
}
}
}
func decrementChangeCount(_ playerType:PlayerType){
self[playerType].changeCount -= 1
interactorInputProtocol?.checkGameStatement(playerType)
}
}
I stored the Player class in an array so that I can extract the required properties with subscript. Since the calculation cost is high and the nesting is difficult to read deeply,
In the case where the characters are limited like this app, It is better to keep each instance separately I'm glad it was easy to understand.
As a caveat, change the PlayerStatus property from anywhere Because you can share that state The calculation process is also unified so that it is performed from Player Status.
With the above, it is also clarified that Player Status is responsible for managing the status of each player. From PokerInteractor, I just instructed to change the state.
In other words, in PokerInteractor, which manages business logic It can be said that he overlooked that the responsibilities could be dispersed.
Through the test, each process of business logic Because I was able to confirm that it was working properly
The responsibilities of the PokerInteractor layer are becoming more complex I think I was able to notice it.
In my experience, I extracted 3 patterns that are easy to fall into. It's really embarrassing because it's just the basics.
I was able to realize once again that the majority of the parts were out of principle and difficult. We will do our best to make better use of the design principles.
We look forward to your warm tsukkomi.
[TDD Boot Camp.TDDBC Sendai 07 Challenge: Poker] (http://devtesting.jp/tddbc/?TDDBC%E4%BB%99%E5%8F%B007%2F%E8%AA%B2%E9%A1%8C)
[Kenji Tanaka. TDD (2018). Impress R & D.] (https://nextpublishing.jp/book/10137.html)
[keyword] TDD Driven Design: [Dan Chaput, Lee Lambert, Rich Southwell. What is an Enterprise Business Rule Repository?. MODERA analyst.com.] (http://media.modernanalyst.com/New_Wisdom_Software_Webinar_-_PRINT.pdf) Value Semantics: What is Yuta Koshizawa. Value Semantics. Heart of Swift Yuta Koshizawa. Types without Value Semantics and Workarounds. Heart of Swift [Yuta Koshizawa. Why Swift has become a value-centric language and how to use it. Heart of Swift](https://heart-of-swift.github.io/value-semantics/how-to-use-value- types) Copy-on-Write: (I don't think Copy-on-Write is a problem in Swift) [https://qiita.com/koher/items/8c22e010ad484d2cd321] (Explanation of how to implement Copy on Write in Swift) [https://qiita.com/omochimetaru/items/f32d81eaa4e9750293cd] (Design that is easy to test using the principle of dependency reversal in swift) [https://qiita.com/peka2/items/4562456b11163b82feee]
VIPER: (Summary of making iOS app of product from 1 with VIPER architecture) [https://qiita.com/hirothings/items/8ce3ca69efca03bbef88]
SOLID analysis: (SOLID principle understood by Swift iOSDC 2020) [https://speakerdeck.com/k_koheyi/swifttewakarusolidyuan-ze-iosdc-2020] (Explanation of the SOLID principle in the case of iOS development) [https://zenn.dev/k_koheyi/articles/019b6a87bc3ad15895fb]
memory: (Examine Swift's memory layout) [https://qiita.com/omochimetaru/items/64b073c5d6bcf1bbbf99] (Great Swift pointer type commentary) [https://qiita.com/omochimetaru/items/c95e0d36ae7f1b1a9052] (Memory Safety) [https://docs.swift.org/swift-book/LanguageGuide/MemorySafety.html#//apple_ref/doc/uid/TP40014097-CH46-ID571]
Value type: Let's know the difference between mutable type and immutable type [Pure value type Swift] (https://qiita.com/koher/items/0745415a8b9842563ea7)
subscript: About Swift Subscript
protocol oriented: Making better apps with value types at WWDC 2015 Swift
enum: Swift enum (enum) review [Swift] enums are protocol compliant, so you can simply compare them, for example with Comparable