[SWIFT] Trois problèmes dans lesquels les débutants sont susceptibles de tomber lors du développement d'applications iOS et de leur analyse

introduction

Bonjour, c'est l'ingénieur iOS dayossi.

Je veux offrir un service qui rend ma famille heureuse Nous avons publié une application de journal familial appelée HaloHalo.

Cette fois, j'ai senti que je développais une application via un test unitaire. J'ai analysé trois problèmes.

Le livrable cette fois est ici. https://github.com/Kohei312/TDD_Practice_Poker

Problème dans lequel je suis souvent tombé

Lors du développement sans écrire de test Je ne pouvais pas séparer les trois problèmes suivants et j'étais souvent troublé.

-Est-ce que la logique de construction de la vue centrée sur UIKit est étrange ...? -Est-ce que les données d'affichage de l'interface utilisateur sont étranges ...? ・ Le transfert de données est-il étrange ...?

En écrivant un test unitaire La séparation ici peut se faire en douceur, Il est plus facile d'analyser le problème.

Lors de l'analyse du problème, les trois perspectives suivantes J'ai senti que c'était efficace en commun.

1. Êtes-vous capable de séparer correctement les responsabilités?

L'un des principes SOLID, qui est un principe orienté objet, `` principe de responsabilité unique '', Ceci est une question pour confirmer si vous êtes au courant de la politique et de l'intention de conception.

Cette fois, je l'ai utilisé avec la nuance de "clarifier le but de chaque couche et processus".

2. Est-il possible de visualiser le changement d'état?

Il y a une partie qui chevauche le premier point, Il s'agit de clarifier quel processus est appelé à quel moment et quel type de changement se produit.

Surtout quand il y a beaucoup d'arguments et que vous essayez d'exécuter plusieurs processus en même temps, il est facile de se confondre. Par conséquent, j'ai senti que des erreurs de code étaient susceptibles de se produire.

Si vous énumérez les états avec enum ou définissez le processus pour modifier la propriété uniquement dans une couche spécifique, Il était facile de saisir le changement d'état.

3. Les couches sont-elles divisées de manière appropriée?

Cela chevauche également le premier point, J'ai vérifié un par un si les couches fonctionnaient selon la conception générale.

Tout en vérifiant le but de l'état géré par chaque couche La séparation progressive des responsabilités a facilité la révision.

Ci-dessous, nous reprendrons les cas sur lesquels nous avons réellement travaillé dans le développement de cette application.

Cas: l'état n'est pas partagé

Parmi les jeux de poker créés cette fois Nous avons établi une règle selon laquelle les joueurs peuvent échanger des cartes jusqu'à 3 fois. (Il y a deux joueurs, un utilisateur et un CPU)

Parmi eux, selon le nombre de fois où chaque joueur a échangé des cartes Une pierre d'achoppement s'est produite dans la mesure où l'état du jeu a changé.

Le nombre d'échanges est une variable appelée changeCount Conservez-le comme une propriété d'une structure avec un état de joueur appelé Player J'essayais de contrôler selon les règles.

Si le joueur est un utilisateur ou un ordinateur dans l'énumération appelée PlayerType J'essaye de distinguer.

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
}

Initialement dans une couche supérieure appelée PokerInteractor qui gère la logique de l'ensemble du jeu J'ai placé et géré directement une instance de type Player.


 // MARQUE: -Gérer le nombre d'échanges de cartes et le statut de chaque joueur
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
       }
    }

Positionné comme une couche qui organise la logique métier, Ici, je contrôlais l'état du joueur et la progression du jeu.

Cependant, il y avait un piège ici.

Lors du tour d'un joueur, le décompte des échanges de cartes de ce joueur était correctement compté. Le décompte des échanges de cartes de l'autre joueur n'est pas partagé.

Au tour de player_me, le changeCount de player_me diminue certainement Au tour de player_other, le changeCount de player_me est revenu à sa valeur initiale.

public struct PokerInteractor{

# AVERTISSEMENT ("L'état n'est jamais partagé ...")
    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
       }
    }

Problème: il y a un problème dans la gestion des données logiques

Dans le test, il a été confirmé qu'il était effectivement calculé. Je n'ai vu aucune erreur de construction du côté Affichage, j'ai donc pensé qu'il y avait un problème de gestion des données logiques.

Analyse: impossible d'envisager de changer le pointeur de mémoire

changeCount est une valeur immuable Lors de la modification de la valeur, il est nécessaire d'instruire la modification à partir de l'instance de type de lecteur générée.

Cependant, si vous mettez à jour les propriétés du lecteur correspondant au type de valeur, la valeur de l'ensemble du lecteur sera mise à jour. La couche supérieure PokerInteractor avec Player en tant que propriété est également mise à jour.

Par conséquent, les deux joueurs gérés par PokerInteractor En conséquence, une nouvelle instance sera régénérée,

Le décompte des échanges de cartes a été réinitialisé à la valeur initiale à chaque fois Il n'était plus possible de partager le statut de tous les joueurs.

Par conséquent, ajoutez un type de référence PlayerStatus pour saisir le statut de tous les joueurs. Il a été modifié pour que le statut du joueur puisse être partagé.

Solution de contournement: ajout d'une couche de référence entre Player et PokerInteractor

Parce que la zone de mémoire qui fait référence à PlayerStatus est toujours la même Même si la valeur de chaque lecteur est mise à jour et le pointeur de mémoire est modifié L'objectif était de toujours pouvoir mesurer la valeur modifiée.


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)
        
    }
}

J'ai mis la classe Player dans un tableau pour pouvoir extraire les propriétés nécessaires avec un indice. Le coût de calcul étant élevé et l'imbrication difficile à lire en profondeur,

Dans le cas où les personnages sont limités comme cette application, Il est préférable de conserver chaque instance séparément Je suis content que ce soit facile à comprendre.

En guise de mise en garde, modifiez la propriété PlayerStatus de n'importe où Parce que vous pouvez partager cet état Le processus de calcul est également unifié afin qu'il soit effectué à partir du statut du joueur.

Discussion: la séparation des responsabilités de chaque module était ambiguë

Avec ce qui précède, il est également précisé que le statut du joueur est responsable de la gestion du statut de chaque joueur. De PokerInteractor, je viens de demander de changer d'état.

En d'autres termes, dans PokerInteractor, qui gère la logique métier On peut dire qu'il a oublié que les responsabilités pouvaient être réparties.

Grâce aux tests, chaque processus de la logique métier Parce que j'ai pu confirmer que cela fonctionnait correctement

Les responsabilités de la couche PokerInteractor deviennent de plus en plus complexes Je pense que j'ai pu le remarquer.

Sommaire

J'ai extrait 3 modèles dans lesquels je tombe souvent dans mon expérience. C'est vraiment embarrassant car ce ne sont que les bases.

J'ai pu me rendre compte une fois de plus que la majorité des parties étaient par principe et difficiles. Nous ferons de notre mieux pour mieux utiliser les principes de conception.

Nous nous réjouissons de votre chaleureux tsukkomi.

Ouvrages / articles de référence

[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)

Kazuaki Matsuo, Yusuke Hosonuma, Kenji Tanaka et al. IOS Test Complete Book (2019). PEAKS Publishing.

Yoshitaka Seki, Shoshin Fumi, Kenji Tanaka et al. Introduction aux modèles de conception d'applications iOS (2019). PEAKS Publishing.

[Kenji Tanaka. TDD (2018). Impress R & D Co., Ltd. écrit en Swift.] (https://nextpublishing.jp/book/10137.html)

[mot-clé] Conception du lecteur TDD: [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: Qu'est-ce que Yuta Koshizawa. Value Semantics. Heart of Swift Yuta Koshizawa. Problèmes de type et solutions de contournement sans Value Semantics. Heart of Swift [Yuta Koshizawa. Pourquoi Swift est devenu un langage centré sur les valeurs et comment l'utiliser. Heart of Swift](https://heart-of-swift.github.io/value-semantics/how-to-use-value- les types) Copy-on-Write: (Je ne pense pas que la copie sur écriture soit un problème dans Swift) [https://qiita.com/koher/items/8c22e010ad484d2cd321] (Explication de la mise en œuvre de la copie lors de l'écriture dans Swift) [https://qiita.com/omochimetaru/items/f32d81eaa4e9750293cd] (Conception avec swift pour être facile à tester en utilisant le principe de l'inversion des dépendances) [https://qiita.com/peka2/items/4562456b11163b82feee]

VIPER: (Résumé de la création de l'application iOS du produit à partir de 1 avec l'architecture VIPER) [https://qiita.com/hirothings/items/8ce3ca69efca03bbef88]

Analyse SOLIDE: (Principe SOLID compris par Swift iOSDC 2020) [https://speakerdeck.com/k_koheyi/swifttewakarusolidyuan-ze-iosdc-2020] (Explication du principe SOLID pour le cas de développement iOS) [https://zenn.dev/k_koheyi/articles/019b6a87bc3ad15895fb]

Mémoire: (Examinez la disposition de la mémoire de Swift) [https://qiita.com/omochimetaru/items/64b073c5d6bcf1bbbf99] (Commentaire de type pointeur Great Swift) [https://qiita.com/omochimetaru/items/c95e0d36ae7f1b1a9052] (Memory Safety) [https://docs.swift.org/swift-book/LanguageGuide/MemorySafety.html#//apple_ref/doc/uid/TP40014097-CH46-ID571]

Type de valeur: Faisons la différence entre le type mutable et le type immuable [Type de valeur pure Swift] (https://qiita.com/koher/items/0745415a8b9842563ea7)

subscript: À propos de Swift Subscript

orienté protocole: Créer une meilleure application avec un type de valeur dans WWDC 2015 Swift

enum: Examen du type d'énumération Swift (énumération) [Swift] enum est conforme au protocole, vous pouvez donc simplement le comparer, par exemple avec Comparable

Recommended Posts

Trois problèmes dans lesquels les débutants sont susceptibles de tomber lors du développement d'applications iOS et de leur analyse
À propos de "Injection de dépendances" et "Héritage" qui sont faciles à comprendre lorsqu'ils sont mémorisés ensemble