Swiftly

Principes modernes de développement: Revoir le DRY

Qui parmi nous lors de revue du code de l’un de ses collègues ou même au moment du développement d’une nouvelle fonctionnalité ou la résolution d’un bug n’a pas constaté qu’une ou une petite ou grande partie du code se répète et quelque part il faut supprimer la duplication (question du bon sens humain 😀). Le principe DRY ou ‘dont repeat yourself‘ entre dans ce cadre de suppression de duplication. Mais est ce qu’on peut affirmer que cette définition est correcte?

On n’est pas obligé parfois de dupliquer notre code pour des raisons de bonnes pratiques d’architectures, maintenance, une évolution imprévisible de la demande métier?… Dans cet article, j’essayerai à travers des exemples de vous montrer comment le DRY peut être mal compris et que la duplication n’est pas toujours mauvaise.

Un peu d’histoire

Dans leur livre the pragmatic programmer, Andy Hunt et Dave Thomas, définissent le DRY de cette manière:

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

The pragmatic programmer

Je pense que l’ambiguïté provient de la compréhension de la première partie de la définition à savoir (piece of knowledge) ou au sens DDD le domaine de connaissance (domain of knowledge).

Prenons un exemple d’une structure Address qui constitue l’adresse d’un utilisateur. On peut dire que Address constitue un objet DAO.


// MARK: - Address

public struct Address: Codable {
    // MARK: - Properties

    public let id: String?
    
    public let address: String?
    
    public let city: String?
    
    public let firstName: String?
    
    public let lastName: String?
  
}

Imaginons que nous sommes dans un design pattern MVVM, MVP, MVC… Est ce qu’on va utiliser le modèle Address pour afficher la liste d’address d’un utilisateur? Il y a des développeurs qui vont dire mais oui on veut pas répéter le modèle etc et on veut respecter le DRY!. Oui, c’est cohérent si on comprend la définition de DRY comme étant la non duplication du code. Mais on n’a oublié la notion du ‘piece of knowledge‘ ou ce que Martin Fowler appelle BoundedContext. Je cite ici un passage important dans l’article de Martin Fowler:

As you try to model a larger domain, it gets progressively harder to build a single unified model. Different groups of people will use subtly different vocabularies in different parts of a large organization. The precision of modeling rapidly runs into this, often leading to a lot of confusion

Martin Fowler Bounded Context

En effet, dans une application complexe non monolothique , utiliser un modele DAO au niveau de l’affichage de données est signe d’un couplage fort et d’une vision qui rend l’affichage de données reliés à l’infrastructure. Imaginons qu’un jour, on veut consommer nos données d’un autre modèle DAO. A ce moment je dois faire le tour de l’application pour remplacer le mode Address par un nouveau modele Address2.

La duplication n’est pas mauvaise!

Alors, l’idéal c’est d’avoir un modèle DTO (Data Transfert Object) et on effectue le mappage depuis notre codable vers ce DTO.

public protocol CustomerAddressDTOProtocol {
    // MARK: - Properties

    var id: String { get }

    var address: String { get }

    var city: String { get }

    var countryCode: String { get }

    var firstName: String { get }

    var lastName: String { get }
}

L’idée est d’implémenter le DTO

public struct CustomerAddressDTO: CustomerAddressDTOProtocol {
    public var id: String

    public var address: String

    public var city: String

    public var countryCode: String
    
    public var firstName: String
    
    public var lastName: String
}

Ensuite de faire le mapping.

extension Address {
    var toCustomerAddressDTO: CustomerAddressDTO {
        return .init(
            id: id.orEmpty,
            address: address1.orEmpty,
            city: city.orEmpty,
            firstName: firstName.orEmpty,
            lastName: lastName.orEmpty
       )
}

D’un point de vue Domain, la structure Address et CustomerAddressDTO n’ont pas le même rôle et objectif même si elles partagent les mêmes attributs. Un changement dans la structure Address n’impactera pas la couche présentation de données vu celle ci utilisent seulement les données qui proviennent du DTO qui est agnostic des détails Codable, base de données Core Data, Swift Data, realm…

Conclusion

  • Le principe DRY concerne la connaissance du domaine.
  • Il y a des cas où la duplication de code est parfaitement correcte.

Références

https://enterprisecraftsmanship.com/posts/dry-revisited

https://martinfowler.com/tags/domain%20driven%20design.html

Le monde des Kata de programmation: jeu de la vie en iOS (création graphique en UIKIT et CoreGraphics)- Partie 2

Introduction

Dans le premier article de cette série sur les kata de programmation, nous avons mis en place à travers une approche TDD, les structures de données et la logique métier qui constitue la base de notre solution. En effet, les deux structures de données Cell et Game sont normalement indépendants de toute implémentation graphique (UIKIT, SwiftUI).

Dans cet article, nous proposons une implémentation graphique de notre solution à base de UIKIT et CoreGraphics. Nous tenons pas à utiliser une approche TDD au niveau de la construction de notre interface. Nous dédions un autre article prochainement sur la construction graphique à base de TDD.

Bonne lecture 🙂

Simulation finale

Création Graphique avec UIKIT et CoreGraphics

Le jeu de la vie est composé d’un ensemble de cellules qui vivent et meurent à l’intérieur d’une grille. On commencera par la création d’une classe GameView qui constitue la grille. Cette classe héritera de UIView. Pour créer des objects graphiques à l’intérieur de la vue, on doit redéfinir la méthode draw(_ rect: CGRect) et ajouter la logique de création des cellules selon les dimensions de la GameView.

import UIKit

final class GameView: UIView {
    override func draw(_ rect: CGRect) {
        // add drawing logic there
    }
}

Afin de connaître les dimensions et aussi déterminer la couleur des cellules, on doit injecter une instance de la classe Game à l’intérieur de GameView et faire appel au constructeur parent UIView afin d’initialiser la vue.

final class GameView: UIView {
    private let game: Game
    
    init(game: Game) {
        self.game = game
        super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func draw(_ rect: CGRect) {
        // add drawing logic there
    }
}

Passons à l’implémentation de la méthode draw. On aura besoin maintenant de parcourir notre game et de créer pour chaque cellule un carré représentatif avec une bordure et un arrière plan selon l’état de la cellule morte ou vivante.

override func draw(_ rect: CGRect) {
   let context = UIGraphicsGetCurrentContext()
   game.cells.forEach { cell in
      context?.setFillColor(fillColorForCell(state: cell.state).cgColor)
      context?.addRect(frameForCell(cell: cell))
      context?.fillPath()
   }
}

func fillColorForCell(state: State) -> UIColor {
   switch state {
   case .Alive:
   return UIColor.green
   case .Dead:
   return UIColor.white
   }
}

func frameForCell(cell: Cell) -> CGRect {
    let dimensions = CGFloat(game.dimensions)
    let cellSize = CGSize(
        width: self.bounds.width / dimensions,
        height: self.bounds.height / dimensions
    )
    return CGRect(
      x: CGFloat(cell.x) * cellSize.width,
      y: CGFloat(cell.y) * cellSize.height,
      width: cellSize.width,
      height: cellSize.height
     )
}

La méthode fillColorForCell(state: State) permet de déterminer la couleur de la cellule selon le state (Alive, Dead)qu’on recalcule à chaque génération. Ensuite la méthode frameForCell(cell: Cell) construira la cellule et retournera à chaque fois un CGRect. Voyons de près cette méthode.

Tout d’abord, on calcule la dimension de la cellule par rapport à la dimension de la grille (notre grille est un carré) et le nombre de cellules choisit ensuite lorsqu’on créera notre GameOfLifeViewController. Alors, la largeur de notre cellule sera égale à au self.bounds.width / dimensions, de la même manière pour la hauteur.

Une fois, on connait les dimensions de notre cellule, il faut déterminer ces coordonnées au niveau de la grille par rapport aux cordonnées x et y définis dans la structure de données Cell. La position X de la cellule sur la grille sera la multiplication de ‘sa position logique‘ par la hauteur ou largeur (CGFloat(cell.x) * cellSize.width par exemple)

On a maintenant la cellule et son background, on utilisera la méthode context?.fillPath() pour dessiner la cellule.

Pour plus d’informations sur CoreGraphics et comment l’utiliser pour dessiner en iOS, cet article de Paul Hudson sera une très bonne entrée.

Je pense qu’on a terminé la construction de la vue. Mettons alors la vue à l’intérieur de notre GameOfLifeViewController.

import UIKit

final class GameOfLifeViewController: UIViewController {

    private let game = Game()
    private let gameView: GameView

    required init?(coder: NSCoder) {
        self.gameView = GameView(game: game)
        super.init(coder: coder)
        initializeGame()
    }

    // MARK: - helpers
    
    private func initializeGame() {
        for _ in 0...200 {
            let x = getRandomLocation()
            let y = getRandomLocation()
            game[x,y]!.state = .Alive
        }
    }

    private func getRandomLocation() -> Int {
        return Int(arc4random()) % game.dimensions
    }
}

Notre GameOfLifeViewController a besoin de la GameView et aussi notre Game. L’idée est de remplir aléatoirement la moitié de la grille (200 cellules) par des cellules vivantes. rappelons que la dimension de la grille est égale à 20*20 (400 cellules) qu’on a définit dans le premier article au niveau de la classe Game.

La méthode initializeGame() permet de réaliser cette initialisation en exploitant la méthode getRandomLocation(). Cette dernière retournera X et Y deux entiers entre 0 et 19 représentant les coordonnées logiques de la cellule.

On a initialisé notre jeu mais on n’a pas encore inséré notre gameView à l’intérieur de notre GameOfLifeViewController. Pour ce faire on peut ajouter ce code dans le ViewDidLoad()

override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        setupGameView()
}

private func setupGameView() {
  let margin: CGFloat = 20.0
  let size = view.frame.width - margin * 2
  let frame = CGRect(
      x: margin,
      y: (view.frame.height - size) / 2.0,
      width: size,
      height: size
    )
   gameView.frame = frame
   gameView.layer.borderColor = UIColor.blue.cgColor
   gameView.layer.borderWidth = 2
   view.addSubview(gameView)
}

On veut garder une marge à droite et à gauche de 20 pixels. La hauteur et la largeur de toute la grille sera égale à la view.frame.width – 40. Afin de centrer la grille, la position y sera égale à (view.frame.height – size) / 2.0.

Si nous lançons maintenant notre code, on aura l’affichage de la grille mais seulement pour une seule génération. Il faut tout simplement appeler chaque ‘n’ secondes la méthode computeNextGeneration() qu’on a définit dans la première partie de cette série d’articles.

Pour ce faire, on peut utiliser les schedulers de la bibliothèque Combine.

var cancellable: Cancellable?

override func viewDidLoad() {
 cancellable = Timer.publisher(every:2, on: .main, in: .default)
     .autoconnect()
     .subscribe(on: DispatchQueue.main)
     .sink { [weak self] in 
        self?.tick()
     }
}

func tick() {
  game.computeNextGeneration()
  gameView.setNeedsDisplay()
}
   

Conclusion

Nous jugeons que l’implémentation graphique de notre jeu de la vie était simple vu que la logique métier était assez indépendante. Vous retrouvez pas aucune utilisation graphique au niveau des class Cell et Game. Néanmoins que l’algorithme de détermination des cellules voisins doit être revu afin de l’optimiser et ça sera l’objectif du prochain article.

A très bientôt.

Le monde des Kata de programmation: L’exemple du jeu de la vie en iOS (une approche fonctionnelle)- Partie 1

Dans la culture japonaise, les parents partageaient leur connaissances et savoir faire avec leur enfants. L’enfant commence à apprendre ce qu’on appelle des Kata (synonyme de « forme » ou « cadre » en français). On peut penser qu’un enfant d’un menuisier apprendra les Kata du métier du menuisier, et un enfant d’un pianiste apprendra les cadres de l’art pratiqué par ses parents. Un Kata est généralement un jeu dans lequel on passera des connaissances que les enfants s’apprenaient du monde de travail des adultes. En effet, les Kata sont très connus dans le monde des arts martiaux (Karaté, Judo..). Ce concept a attiré l’attention des informaticiens ensuite surtout avec la monté de la méthode agile d’extrême programming (XP) de Kent Beck, dont je suis très enthousiaste :).

Le concept des Kata a été présenté pour le monde de développement informatique par Dave Thomas dans son livre « The pragmatic programmer« . Beaucoup de piliers du craft logiciels comme oncle Bob dans son livre « The clean coder » ou principalement dans son article « The programming Dojo« , Sandro Mancuso dans son livre « The software Craftsman« , et d’autres…, encourageaient les développeurs à pratiquer les kata d’une manière continue afin de perfectionner leur compétences.

Il existe plusieurs Kata, la plus connu que j’enseignais à mes étudiants aux lycées et à l’université c’était les nombres romains. Malheureusement, J’enseignais les problèmes sans savoir qu’ils sont des katas et aussi sans approche TDD, sans pair programming. Bref, l’idée est de résoudre le problème avec une approche impérative sans tests unitaires. Malheureusement, cela ne permettra pas au développeur (étudiant, enseignant) de comprendre l’utilité du problème et de penser aux différents cas de tests possibles.

Ils existent d’autres Kata, comme le célèbre Fizz Buzz, Gilded Rose et plein d’autres. Le site codingDojo, présente une liste assez intéressante de Kata que je vous laisse le temps de découvrir.

On s’intéressera à un kata un peu particulier vu qu’il provient du monde des mathématiques et principalement de la calculabilité et la décidabilité à savoir le jeu de la vie.

Dans cet article, on présentera le jeu de la vie, l’histoire, l’origine et les règles. Ensuite, on commencera le développement en utilisant une approche fonctionnelle basé sur la TDD.

Bonne lecture…

Le jeu de la vie, l’origine et les règles

Le jeu de la vie ou Game Of Life est un automate cellulaire inventé par le mathématicien anglais John Conway en 1970. Le jeu se joue sur un échiquier infinie dont chaque cellule peut avoir deux états (vivant ou mort). On peut jouer tout seul en partant d’un état initial quelconque. Les règles sont très simples et on peut les résumer comme suit:

  • Une cellule morte peut être vivante lorsqu’elle possède exactement 3 cellules voisines vivantes.
  • Une cellule vivante peut devenir morte lorsqu’elle possède moins que deux cellules voisines vivantes (on parle d’un phénomène d’isolement ou underpopulation) ou plus de 3 cellules voisines vivantes(on parle de surpopulation)

Le plus surprenant au sens mathématiques que Conway a pu dégager à partir de certains états initiaux et après certaines générations des formes stables que Conway les appels des natures mortes (serpent, oscillateur, pendule…). Le net est riche de plusieurs articles mathématiques qui cherchent à expliquer et comprendre comment on arrive à partir de ces deux règles simples à avoir ces formes.

Commençons le Kata

Essayons maintenant de penser une solution qui permet de résoudre d’une manière fonctionnelle et Swifty cet exercice.

On peut commencer par un premier test qui permet de vérifier pour une grille de jeu composée d’une cellule morte, on peut pas trouver des voisins vivants.

On peut imaginer un test de ce genre:


final class GameOfLifeTests: XCTestCase {

    func testNumberOfLivingNeighboursShouldBeZeroInEmptyGameGrid() {
        let game = Game()
        let cell = Cell(x: 2, y: 2)
        XCTAssertEqual(game.livingNeighboursForCell(cell), 0)
    }

}

Nous pensons simplement à deux structures de données un jeu qu’on appelle Game et une cellule possédant deux coordonnées x et y. Le jeu doit être capable pour une cellule bien particulière, de calculer le nombre de ces voisins vivants

Commençons par la création de la classe ou la structure Game

final class Game {
    var cells: [Cell]

init() {
        cells = [Cell]()
    }
}

Puis la structure ou la classe Cell

final class Cell {

    let x: Int
    let y: Int
    
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

Pensons maintenant à implémenter une fonction ou une closure qui permet de calculer le nombre de cellules voisines vivantes par rapport à une cellule bien particulière. Pour répondre à cette question, on doit tout d’abord connaitre quels sont les cellules voisines?

ll y a cinq ans, on m’a proposé ce Kata comme test et je me suis bloqué sur la détermination des cellules voisines qui pour moi était la question la plus difficile 😃. Vu que nous sommes pas dans un entretien, essayons de penser calmement et trouver une bonne formule qui permet de déterminer les voisins.

On peut raisonner sur les indices des cellules et on remarquera que la différence en valeur absolue entre une cellule et ces voisins peut se résumer en ces trois cas au max (1,1),(1,0),(0,1). Comment on a aboutit à ça, regardons cette grille

En effet, si nous prenons par exemple, une cellule au milieu, la delta de différence entre sa position x et la position x de ses voisins ne dépassera pas 1 en valeur absolue et pareil pour sa position y. En effet, la cellule en haut à gauche et à une ligne de moins par rapport à notre cellule de milieu et une colonne de moins par rapport à notre cellule de milieu. Alors, on a cet offset de (-1,-1). On peut penser à une fonction qui cherchera les voisins en se basant sur cette logique et on aura:

private let cellsAreNeighbours = {
  (rhs:Cell, left: Cell) -> Bool in
  let delta = (left.x - rhs.x,left.y - rhs.y)
  switch delta {
  case (-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1): return true
  default: return false
  }
}

Utilisons cette closure pour chercher les voisins d’une cellule quelconque

lazy var neighboursForCell = { (cell: Cell) -> [Cell] in
   self.cells.filter{ self.cellsAreNeighbours(cell, $0) }
}

Vous remarquez que l’écriture fonctionnelle rend notre code plus lisible vu qu’on utilise la fonction filtrer sur la séquence (tableau cells) afin de déterminer seulement les voisins. Autre chose, l’utilisation de la position x et y, nous évitera de créer une matrice et faire un parcours avec deux boucles. Alors notre parcours est toujours à O(n) en terme de complexité algorithmique temporelle.

Lançons notre premier test.

Passons à un deuxième test maintenant.

L’idée de ce deuxième test, c’est de vérifier pour une cellule bien particulière, qu’elle doit contenir après l’initialisation de jeu, un voisin vivant.

func testFoundOneLivingNeighbours() {
  let game = Game()
  let cell = Cell(x: 2, y: 2)
        
  game[2,1]?.state = .Alive
  XCTAssertEqual(game.livingNeighboursForCell(cell), 1)
}

Pour ce cas, la cellule (2,1) est voisine de la cellule (2,2). Comment peut on ajouter pour une cellule un état (state) et comment je peux accéder pour initialiser la cellule (2,1)?

Ecrivons le code qui permet d’ajouter l’attribut state à une cellule.

enum State {
    case Alive
    case Dead
}

final class Cell {

    let x: Int
    let y: Int
    var state: State
    
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

On a ajouté un type énuméré State qui contient 2 valeurs possibles:

  • Vivant: Alive
  • Mort: Dead

Surement, vous avez remarqué que je peux accéder à une cellule [x,y] en utilisant l’objet game et non pas faire game.cells[2,1]. En effet, j’utilise un subscript sur Game qui me permet d’accéder à une cellule de mon jeu directement.

subscript (x: Int, y: Int) -> Cell? {
   return cells.filter { $0.x == x && $0.y == y }.first
}

Les subscripts sont des raccourcis pour accéder au membres d’une classe, structure ou type énumère. On peut définir plus qu’un subscript sur un type. Dans notre example, le subscript permet de filtrer sur les cellules du Game et retourner une cellule dans les coordonnées sont égales à ceux passés en paramètres. Le résultat peut être nil, c’est pour cela on a ce type de retour Cell?.

Lançons notre deuxième test maintenant.

Dans le processus de développement TDD, il y a une étape très importante, c’est le refactoring (red -> Green -> refactor). Ce que nous pouvons remarqué dans la propriété cellsAreNeighbours qu’on peut améliorer la code en raisonnant en valeur absolue et on peut tomber sur 3 cas au lieu des 8 cas possibles

private let cellsAreNeighbours = {
   (rhs:Cell, left: Cell) -> Bool in
   let delta = (abs(left.x - rhs.x),abs(left.y - rhs.y))
   switch delta {
   case (1,1),(1,0),(0,1): return true
     default: return false
   }
}

Relancez les tests, normalement rien n’est cassé 🙂

On peut sécuriser notre code par l’ajout d’autres tests qui calcule le nombre de voisins pour une cellule s’il est égale à 2, 3…

func testFoundTwoLivingNeighbours() {

   let game = Game()

   let cell = Cell(x: 2, y: 2)

   game[2,1]?.state = .Alive

   game[2,3]?.state = .Alive

   XCTAssertEqual(game.livingNeighboursForCell(cell), 2)

}

Passons maintenant à la deuxième partie de notre Kata. La question qui se pose, comment je peux déterminer l’état de mon jeu pour la génération suivante.Il faut penser à implémenter le code qui permet de déterminer l’état de chaque cellule de jeu pour la génération suivante. Comme d’habitude, pensons au tests.

L’idée de notre test, si j’ai une cellule morte et qui est entourée de 3 cellules voisines vivantes, notre cellule deviendra vivante.

Notre test aura cette allure:

func testADeadCellWithThreeNeighboursGetsAlive() {
   let game = Game()
        
   game[0,3]?.state = .Alive
   game[0,4]?.state = .Alive
   game[0,5]?.state = .Alive
   game[1,4]?.state = .Dead
        
   game.computeNextGeneration()
        
   XCTAssertEqual(game[1,4]?.state, .Alive)
}

On est dans l’étape red en TDD, on doit penser à implémenter cette fonction computeNextGeneration().

On va s’intéresser tout d’abord à déterminer les cellules mortes, chercher ceux qui ont des voisins vivants puis appliquer une fonction de transformation pour avoir une nouvelle matrice de cells.

func computeNextGeneration() {
  let deadCells = cells.filter { $0.state == .Dead }

  let newLifeCells = deadCells.filter { livingNeighboursForCell($0) == 3 }
  // update state
  newLifeCells.forEach{ cell in
    cell.state = .Alive
  }
}

Vous remarquez que je change dans le tableau newLifeCells qui est une référence du tableau cells vu que notre Cell est une classe (chaque modification dans le newLifeCells impactera le tableau d’origine cells)

Lançons maintenant notre test.

Passons au test sur les cellules vivantes qui deviennent mortes suite à l’isolement ou la surpopulation.

Commençons par la surpopulation.

func func testACellWithMoreThanThreeLivedNeighboursDies() {
   let game = Game()
        
   game[0,3]?.state = .Alive
   game[0,4]?.state = .Alive
   game[0,5]?.state = .Alive
   game[1,3]?.state = .Alive
   game[1,4]?.state = .Alive
        
   game.computeNextGeneration()
        
   XCTAssertEqual(game[1,4]?.state, .Dead)
 }

La cellule (1,4) vivante deviendra morte dans la génération suivante. Ajoutons le code nécessaire pour faire cette transformation au niveau de la méthode computeNextGeneration()

func computeNextGeneration() {
.....
let liveCells = cells.filter{ $0.state == .Alive }
let dyingCells = liveCells.filter { livingNeighboursForCell($0) > 3 }

dyingCells.forEach{ cell in
    cell.state = .Dead
}

lançons notre test

Passons au test du cas d’isolement

func testACellWithLessThanTwoLivedNeighboursDies() {
   let game = Game()
        
   game[0,3]?.state = .Alive
   game[1,4]?.state = .Alive
        
   game.computeNextGeneration()
        
   XCTAssertEqual(game[1,4]?.state, .Dead)
}

Il suffit d’ajouter cette condition au niveau du filtre des cellules qui vont être mortes.

let dyingCells = liveCells.filter { livingNeighboursForCell($0) > 3 || livingNeighboursForCell($0) < 2 }

Conclusion

Le jeu de la vie constitue un Kata simple et intéressante pour perfectionner notre manière d’appréhender des problèmes algorithmiques et éventuellement se préparer pour des entretiens d’embauche. Dans cette première partie, nous avons présenté comment l’utilisation du TDD pour écrire petit à petit un code testable à 100% peut être si facile. Dans la partie suivante, nous allons s’intéresser à relier la logique algorithmique avec la création UI en utilisant UIKIT, et comment on peut améliorer notre solution en terme de complexité algorithmique.

A très bientôt.

Références:

Extreme programming explained: Kent Beck.

Software craftsmanship: Sandro Mancuso.

ARTE: Voyages au pays des maths: Le jeu de la vie

Le kata et le jeu : l’éducation par le jeu dans une « société sans cadre » ?

Démystifier des problèmes du test iOS CodinGame (approche TDD)

C’est quoi le CodinGame?

Bonjour les développeurs,

Durant le processus de recrutement, plusieurs entreprises d’IT (généralement des ESN, grandes entreprises de développement) vous envoient comme première étape du processus de recrutement le CodinGame. Si vous êtes familiarisé avec des sites de genre HackerRank, LeetCode, Codeforces…, le test CodinGame ne sera pas difficile normalement pour vous :).

Nous ne sommes pas ici pour juger le test en matière de questions posées. L’idée consiste à faire connaître ce test aux développeurs qui ne le connaissent pas et d’essayer par la suite de simplifier la compréhension des exercices de type « problème algorithmique » qui ont un poids énorme au niveau de l’évaluation finale attribuée.

Dans cet article, nous allons essayer de résoudre en Swift, deux problèmes de ce test en suivant l’approche TDD.

Problème: Trouver le noeud terminal

L’objectif de ce problème est de trouver le noeud terminal d’un réseau simple.

Dans ce réseau:

  • Chaque nœud a seulement un successeur.
  • Le dernier nœud ne possède aucun successeur.
  • Le réseau est unidirectionnel.

Dans l’exemple c-dessus, le noeud terminal en partant par exemple du noeud 8 est le 7.

Dans l’exercice, on nous demande d’implémenter une méthode qui renvoie le dernier nœud de ce réseau à partir d’un nœud initial startNodeId en suivant les liens unidirectionnels du réseau.

findNetworkEndPoint(startNodeId, fromIds, toIds) -> Int

Notre réseau est unidirectionnel, chaque élément du tableau formIds doit avoir un successeur dans le tableau toIds, cela se traduit par la relation fromIds[i]toIds[i]. De ce fait, ces deux tableaux doivent avoir la même longueur.

Selon le schéma ci-dessus le contenu des deux tableaux est le suivant:

let fromIds = [2,5,3,8,14,10]
let toIds = [3,3,14,10,7,7]

En suivant l’approche TDD (Test-driven development), nous commençons par écrire nos tests puis nous développons notre algorithme avec un minimum de codes pour réussir nos tests.

Côté CodinGame, on suppose que la saisie est correcte. Alors, nous ne pouvons pas tomber sur un cas où nous cherchons un élément qui n’existe pas dans le tableau fromIds.

Tout d’abord, nous voulons commencer par tester ces deux cas,

  • Les deux tableaux fromIds et toIds sont vides.
  • Le tableau fromIds contient un seul élément et le tableau toIds est vide.

Voici à quoi resemble notre playground dans un premier lieu :

import UIKit
import XCTest

final class SimpleNetworkTests: XCTestCase {
    override func setUp() {
        super.setUp()
    }
    
    override func tearDown() {
        super.tearDown()
    }
    
    func testEmptyToIdsArrayShouldReturnStartNode() {
        // Given
        let fromIds = [2]
        let toIds = [Int]()
        let sut = SimpleNetwork()

        // THEN
        XCTAssertEqual(
            sut.findNetworkEndPoint(
                2,
                fromIds,
                toIds
            ),
            2
        )
        
    }
    
    func testEmptyFromIdsAndToIdsArraysShouldReturnStartNode() {
        // Given
        let fromIds = [Int]()
        let toIds = [Int]()
        let sut = SimpleNetwork()

        // THEN
        XCTAssertEqual(
            sut.findNetworkEndPoint(
                2,
                fromIds,
                toIds
            ),
            2
        )
        
    }
    
}

final class SimpleNetwork {
    
    init() {}
    
    func findNetworkEndPoint(
        _ startNodeId: Int,
        _ fromIds: [Int],
        _ toIds: [Int]
    ) -> Int {
        return startNodeId
    }
}

SimpleNetworkTests.defaultTestSuite.run()

Si nous réalisons les deux tests, les tests passeront, étant donné que notre algorithme renvoie toujours startNodeId.

Examinons maintenant le cas où nous avons deux tableaux fromIds et toIds qui ne sont pas vides, écrivons notre test.

func testNoEmptyFromIdsAndToIdsArraysShouldReturnStartNode() {
        // Given
        let fromIds = [2,5,3,8,14,10]
        let toIds = [3,3,14,10,7,7]
        let sut = SimpleNetwork()

        // THEN
        XCTAssertEqual(
            sut.findNetworkEndPoint(
                2,
                fromIds,
                toIds
            ),
            7
        )
        
    }

En partant du noeud 2, on arrivera au noeud 7. Ce test ne passera pas vu que notre code de production ne gère pas encore ce cas.

Sur Playground, vous aurez sur votre console cet affichage.

Ajoutons le stricte minimum du code pour passer ce test.

func findNetworkEndPoint(
        _ startNodeId: Int,
        _ fromIds: [Int],
        _ toIds: [Int]
    ) -> Int {
        var copyStartNodeId = startNodeId
        while(fromIds.contains(copyStartNodeId)) {
            let startIdToIdIndex = fromIds.firstIndex(of: copyStartNodeId) ?? 0
            copyStartNodeId = toIds[startIdToIdIndex]
        }
        return copyStartNodeId
    }

L’idée est de déterminer ou se trouve le startNodeId dans le tableau fromIds, ensuite d’écraser le startNodeId par son successeur au niveau du tableau toIds.

Lançons les test maintenant 🙂 , on remarque que notre premier test est impacté pour ce nouveau code

Rappelons que dans notre premier test, on a le tableau fromIds qui contient seulement 2 et que le tableau toIds est vide. Vu que la première instruction dans notre code est fromIds.contains(copyStartNodeId), on entrera dans l’itération sauf que l’erreur provient de cette instruction copyStartNodeId = toIds[startIdToIdIndex] ou on cherche le successeur de 2 dans le tableau toIds déjà vide.

La solution simple pour s’assurer, est de vérifier que le tableau toIds n’est pas vide avant de remplacer le copyStartNodeId.

func findNetworkEndPoint(
        _ startNodeId: Int,
        _ fromIds: [Int],
        _ toIds: [Int]
    ) -> Int {
        var copyStartNodeId = startNodeId
        while(fromIds.contains(copyStartNodeId)) {
            if let startIdToIdIndex = fromIds.firstIndex(of: copyStartNodeId),
               toIds.contains(startIdToIdIndex) {
                copyStartNodeId = toIds[startIdToIdIndex]
            }
        }
        return copyStartNodeId
  }

Tout nos tests passent maintenant.

Passons maintenant au deuxième problème que nous avons choisi de te montrer, c’est la recherche d’un élément dans une arbre binaire ordonné ABR.

Problème: Trouver un élément dans une arbre binaire de recherche(ABR)

Un arbre binaire de recherche est composé de noeuds qui respectent les règles suivantes:

  • Un noeud a une valeur de type entier.
  • Un noeud n’a pas plus de deux enfants, appelés noeud droite et noeud gauche.
  • La valeur de tout enfant du sous arbre gauche est inférieur à la valeur de son parent.
  • La valeur de tout enfant du sous arbre droite est supérieur à la valeur de son parent.

La hauteur de l’arbre (la distance entre le noeud le plus éloigné et la racine) est comprise entre 0 et 100000 noeuds.

Le candidat doit développer une méthode find(_ v: Int) qui retourne le noeud tenant la valeur v s’il existe.

On nous propose une classe Node qui contient trois attributs.

class Node {
    var value: Int
    var left: Node?
    var right: Node?
    
    init(_ value: Int) {
        self.value = value
    }
    
    func find(_ v: Int) -> Node? {
        return nil
    }
}

Pareil au problème précédent, on commencera par écrire une classe de test qu’on l’appelle NodeTests

class NodeTests: XCTestCase {
    override func setUp() {
        super.setUp()
    }
    
    override func tearDown() {
        super.tearDown()
    }
}
// Lancement des tests
NodeTests.defaultTestSuite.run()

On commencera par un simple test qui permet de vérifier si la recherche d’un élément non existant dans notre arbre avec un seul élément retournera nil.

func test_Search_No_Existing_Value_ON_MonoTree_Elements_ShouldReturnNil() {
     // GIVEN
     let root = Node(10)
        
     // THEN
     XCTAssertNil(root.find(3))
}

Ce test passera systématiquement vu que notre code de production retournera maintenant nil dans tout les cas 🙂 .

Passons à un deuxième TU.

func test_Search_Existing_Value_ON_MonoTree_Elements_ShouldNotReturnNil() {
    // GIVEN
    let root = Node(10)
        
    // THEN
    XCTAssertNotNil(root.find(10))
}

Ce test ne passera pas (phase rouge du TDD 🧪), essayons de passer ce test avec le minimum du code possible.

func find(_ v: Int) -> Node? {
     if value == v {
        return self
     }
     return nil
 }

Voila nos tests passent ✅

Passant ensuite au cas où notre arbre a un seul enfant droit (Une seule génération)

func test_Search_Existing_Value_ON_Tree_With_One_Right_Child_ShouldNotReturnNil() {
        // GIVEN
        let root = Node(10)

        let rightElementRoot = Node(14)

        root.right = rightElementRoot

        // THEN
        XCTAssertNotNil(root.find(14))
 }

Ce test ne passera pas aussi! vu que le noeud 14 existe et l’algorithme retourne nil

Ici la solution est simple, nous devons revenir a la condition:

💡 La valeur de tout enfant du sous-arbre droite est supérieure à la valeur de son parent.

Ce qui se traduit en code:

func find(_ v: Int) -> Node? {
        
     if value == v {
            return self
     }
        
     if v > value, let rightNode = right {
            return rightNode
     }
}

Nous faisons la même chose avec le côté gauche.

func test_Search_Existing_Value_ON_Tree_With_One_Left_Child_ShouldNotReturnNil() {
     // GIVEN
     let root = Node(10)

     let leftElementRoot = Node(7)

     root.left = leftElementRoot

     // THEN
     XCTAssertNotNil(root.find(7))
 }

💡La valeur de tout enfant du sous-arbre gauche est inférieure à la valeur de son parent.

func find(_ v: Int) -> Node? {
        
    if value == v {
            return self
     }
        
    if v > value, let rightNode = right {
            return rightNode
    }

    if v < value, let leftNode = left {
            return leftNode
    }
        
     return nil
}

Passons à la recherche dans une arbre à plusieurs générations ou niveaux.

func test_Search_Existing_Value_ON_Multi_levels_Tree_ShouldNotReturnNil() {
        // GIVEN
        let root = Node(10)

        let node7 = Node(7)
        let node14 = Node(14)

        root.left = node7
        root.right = node14

        let node4 = Node(4)
        let node9 = Node(9)

        node7.left = node4
        node7.right = node9

        let node1 = Node(1)
        let node5 = Node(5)

        node4.left = node1
        node4.right = node5

        // THEN
	let node = root.find(1)
        XCTAssertNotNil(root.find(1))
	XCTAssertEqual(node?.value, 1)
  }

Pour ce cas, nous allons utiliser la récursivité. On remarque bien que dans la recherche d’un nœud dans tout l’arbre, l’idée consiste à faire la recherche soit dans la génération gauche si la valeur est inférieure au nœud parent, ou la génération droite, si la valeur est supérieure au nœud parent et ainsi de suite jusqu’à nous tombons sur le critère d’arrêt if value == v { return self} ou bien return nil si le noeud n’existe pas.

Cherchant le noeud avec la valeur 1 par exemple:

func find(_ v: Int) -> Node? {
    if value == v {
            return self
    }
        
    if v > value,
       let rightNode = right {
            return rightNode.find(v)
    }

    if v < value,
       let leftNode = left {
            return leftNode.find(v)
    }
        
    return nil
  }

Techniques modernes de Hot Reloading en iOS/Mac OS

Introduction

Le Hot Reloading ou ‘le rechargement à chaud’ désigne l’affichage en temps réel des modifications UI sans devoir relancer l’application informatique. Cela, permet de gagner énormément en terme de productivité vu qu’on aperçoit en temps réel les effets de notre changement sans recompiler toute l’application.

Le Hot Reloading est connu au niveau des technologies mobiles hybride comme par exemple React Native, Flutter… étant donné que les langages derrière comme JS, Type Script ou Dart sont des langages interprétables. Le Hot Reloading est un peu méconnu dans les langages compilés tel que Swift, Objective-C, Kotlin… qui doivent passer par tout le cycle de compilation et d’édition de liens pour générer l’artefact final.

Dans cette article, j’essayerai d’explorer les techniques actuels de Hot reloading en iOS native. On explorera à travers des projets démos en iOS et Mac OS des frameworks comme InjectionIII et Inject.

Historique des solutions natives

Il n’y a pas de solutions natives offerte par Apple pour réaliser le hot reloading. Si nous comparons l’effort fait par Google à travers Instant run dite maintenant Live Edit, on peut dire que Xcode n’offre rien en ce sujet.

Cependant, la communauté a pu créer plusieurs frameworks dont l’idée de réaliser ce challenge.

  • Dynamic code injection(DyCI): un outil à base d’Objective-C crée par Paul Taykalo.
  • Vaccine: une framework développé par Christoffer Winterkvist et Richard Topchii
  • Inject: un outil crée par Krzysztof Zabłocki.

Toutes ces solutions passent par un logiciel qui permet d’injecter en temps réel les changements du code au niveau de l’application et communiquer avec le système d’exploitation pour réaliser ce challenge. Cette application est InjectorIII Injection for Xcode (InjectionIII): un outil crée par John Holdsworth.

Dans la suite de cette article, on va essayer de prendre un exemple d’application Mac OS à base de SwiftUI et comment on peut utiliser Inject et InjectorIII pour faire du live changes sur notre code.

Installation et configuration de InjectionIII et Inject

  1. Installation de InjectionIII:

Pour pouvoir installer InjectionIII, vous pouvez installer depuis App Store ou depuis GitHub

Une fois installé (l’application est de type menu bar application), vous allez trouvé bien dans l’écran principal

InjectionIII

Vous devez ensuite sélectionner votre projet sur lequel InjectionIII va s’intégrer, pour notre cas ça sera un projet Mac OS, une application que j’ai appelé Reminders pour gérer les notes.

Pour pouvoir, vous cliquez sur l’icône d’InjectionIII ensuite Add directory et vous sélectionnez le dossier qui contient le fichier .xcodeproj

Ajouter un projet native à InjectionIII

Lorsque vous lancez votre application, vous devez avoir un message de connection de injectionIII avec votre application Mac OS

Connection de l’application Reminders avec InjectionIII

Vous remarquez aussi que la couleur de l’icône de InjectionIII a changé vers l’orange ce qui signifie que la liaison est bien faite avec l’application. Mais pour réaliser cette liaison, il faut passer par des étapes avant qu’on va découvrir ensemble.

Configuration d’un fichier AppDelegate.swift

On peut créer un fichier AppDelegate.swift et le lier ensuite avec le fichier de chargement principal de mon application Reminders

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        setupInjector()
    }
    
    private func setupInjector() {
        Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load()
    }
}

le code ne sera exécuté que si une instance de AppDelegate sera appelé dans le fichier RemindersApp.swift

@main
struct RemindersApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
}

La fonction load permet au système d’exploitation de charger une application externe genre InjectionIII au moment de l’exécution de l’application en cours. Dans notre cas, on va charger le bundle injectionIII Mac OS qui existe au niveau de ma machine à cette endroit /Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle

Vous pouvez explorer les différents types de bundles supportés, juste en accédant au dossier d’installation de InjectionIII ensuite l’ouvrir (clique droit -> ouvrir paquet )

InjectionIII bundles types

Pour pouvoir créer et lancer automatiquement l’appDelegate, on utilise une propriété wrapper de type @NSApplicationDelegateAdaptor. Vous pouvez consulter la documentation officielle sur ce sujet.

Mais ce n’est pas suffisant pour réaliser la liaison, il faut désactiver certaines capabilities au niveau du fichier entitlement de l’application qui sont:

com.apple.security.app-sandbox et com.apple.security.cs.disable-library-validation

<dict>
	<key>com.apple.security.app-sandbox</key>
	<false/>
	<key>com.apple.security.files.user-selected.read-only</key>
	<true/>
        <key>com.apple.security.cs.disable-library-validation</key>
        <false/>
</dict>

Dernier truc important au niveau de la configuration de InjectionIII avec le projet, il est nécessaire d’ajouter les flags -Xlinker et interposable afin de demander au compilateur d’authoriser l’action de recompilation d’un fichier qui existe en mémoire centrale et de remplacer en runtime celui qui est déjà en exécution sinon vous aurez ce message.

Vous ajoutez les deux au niveau du Build settings -> Other linker Flags

-XLinker et -interposable flags

Vu que notre projet est basé sur SwiftUI et non pas AppKit, il faut essayer de recharger l’interface swiftUI à chaque fois qu’il y un changement dans l’interface SwiftUI. A ce moment , on aura besoin de Inject la bibliothèque qui intercepte les changements et demande à InjectionIII de recompiler et recharger l’application.

  1. Installation de Inject

Inject est une framework open source à base Swift qu’on peut installer à travers CocoaPods ou SPM.

Inject via SPM

Il suffit ensuite d’importer au niveau de votre vue SwiftUI puis de déclarer un type par exemple dans notre cas un InjectionObserver qui notifiera InjectionIII à travers une notification spéciale connue par InjectionIII, le INJECTION_BUNDLE_NOTIFICATION, pour recompiler la vue en cours

public let injectionObserver = InjectionObserver()

public class InjectionObserver: ObservableObject {
    @Published var injectionNumber = 0
    var cancellable: AnyCancellable?
    let publisher = PassthroughSubject<Void, Never>()
    init() {
        cancellable = NotificationCenter.default.publisher(for:
            Notification.Name("INJECTION_BUNDLE_NOTIFICATION"))
            .sink { [weak self] _ in
            self?.injectionNumber += 1
            self?.publisher.send()
        }
    }
}

Appel de InjectionObserver dans une vue swiftUI

struct MyView: View {    
#if DEBUG
    @ObservedObject var iO = injectionObserver
#endif

Maintenant, on peut modifier dans notre code et vous allez remarquez les changements en temps réel.

Dans notre exemple, on a un fichier SideBarView.swift qui contient un text, une liste et un button en bas, on vas essayer de mettre en commentaire le code du Button de cette manière. On peut voir sans recompiler que le button se cache.

var body: some View {
        VStack(alignment: .leading) {
            Text("My list")
            MyListsView(viewModel: MyListsViewModel(context: context))
            Spacer()
            Button {
                isPresented = true
            }label: {
                HStack {
                    Image(systemName: Constants.Icons.plusCircle)
                    Text("Add List")
                }
            }.buttonStyle(.plain)
                .padding()
        }.sheet(isPresented: $isPresented) {
            // dismiss
        } content: {
            AddNewListView(viewModel: AddNewListViewModel(context: context))
        }.enableInjection()
    }
SideBar avant la modification

InjectionIII recompile et recharge la vue 🙂

Recompilation du fichier SideBarView.swift

Limites et contraintes

Lorsque j’ai essayé d’utiliser injectionIII et Inject sur un grand projet avec plusieurs frameworks et packages j’ai eu des problèmes à l’exploiter vu que le changement dans les couches inférieurs n’impactent pas instantanément la partie UI. J’étais obliger de faire un aller retour par exemple pour voir ma liste de produits se rafraîchir. En autre, j’ai pas pu l’exploiter sur un device apparemment et je suis pas le seul.

A très bientôt pour un nouveau sujet 🙂

fr_FRFrench