Swiftly

La review du code, des idées pour s’améliorer!

La revue de code constitue l’un des piliers pour assurer la qualité du produit développé. Ce processus est vraiment délicat et peut engendrer même des conflits énormes entre développeurs qui dépassent parfois le cadre professionnel. On peut juger le niveau technique d’un développeur à travers la review de son code mais il faut bien préparer le terrain pour faire une évaluation « cartésienne ». Dans cet article, j’essayerai de vous présenter ma vision de la review de code à travers des exemples que j’ai tiré de mon expérience professionnelle personnelle.

C’est quoi la review du code

La revue du code est un processus de qualité logicielle dans lequel le code écrit par un ou plusieurs développeurs peut être analysé « manuellement » par le reste de l’équipe. Lors de la review, les développeurs essayeront de s’assurer que le code réalise l’objectif du ticket créé et aussi respecte les règles de codage établis au sein de l’équipe. La review d’une nouvelle fonctionnalité assez consistante prendra la plupart du cas plus de temps de vérification que la correction d’un bug simple. La review du code dépendra toujours de la culture et la rigueur de l’équipe. J’ai travaillé avec des développeurs qui font la review par commit (une maitrise de Git est nécessaire dans ce genre d’équipe) et d’autres qui analysent le code verticalement rapidement sans entrer dans les détails du ticket. Pour moi, la review du code est sacrée! (c’est pas le job du lead dev ou tech lead seulement). Non pas pour embêter les gens mais surtout pour avoir une visibilité sur tout le code qui existe et de comprendre ce qu’on voudra livrer pour nos clients. Imaginons que le développeur qui a créé la fonctionnalité n’est pas là avec nous. Il n’y a pas de soucis, les gens qui ont fait la review ou du pair programming sont ici pour prendre un sujet relié à son développement.

Comment je review le code

Je pars du principe que l’équipe a bien établi un code styling clair. Imaginons que je dois faire la review pour un nouveau collègue sénior, confirmé ou junior. Mon approche est toujours de faire du pair programming avec lui, l’appeler et discuter directement à vif voix me permettent de comprendre mieux son code et s’assurer vraiment qu’on a développé tous les cas d’utilisations possibles et qu’on a respecté les critères d’acceptation définis dans le ticket JIRA par exemple. Si on parle d’échange directe, l’expérience m’a appris que c’est toujours une question de modestie et comment la personne se voit lui même. Il est plus facile de faire du pair programming avec quelqu’un d’humble intelligent qui ne se voit pas monsieur programmation ou Turing du développement. En effet, faire de la review avec ces développeurs, te permettent de monter en compétence, d’aimer leur façon de penser et de s’inspirer d’eux. Malheureusement, ils ne sont pas beaucoup. Ils sont des perles rares!

Je décompose généralement ma review en deux grandes parties:

Une première review verticale du code ou j’essaye de regarder si le code respecte le code styling. Néanmoins, on utilise l’outil Swift Lint au quotidien.En effet, ça nous aide énormément pour avoir du code qui respecte notre code styling. Aussi, une solution comme SonarQube permet de gagner du temps et faire de la review statique avant de lancer la pull ou merge request.

La deuxième étape est de prendre en détail le ticket JIRA (On commence le sérieux). J’essaye de lire très attentivement le besoin métier (j’ai déjà une idée sur tout les tickets qui sont dans le sprint alors je découvre pas mais c’est juste pour s’assurer qu’entre temps rien n’a changé! On sait jamais :)). Je vérifie surtout que la solution proposée pour gérer le cas nominale colle bien avec l’architecture et comment surtout je ferai si je codais moi même cette feature!

C’est ici que les choses peuvent déraper malheureusement. On est d’accord que l’architecture assure une bonne partie de la qualité du code et permet aux développeurs de ne pas diverger et de créer une sorte de pattern de comment on pense lorsqu’on code mais ce n’est pas suffisant. Il faut créer ce que je l’appelle un modèle de pensée dans l’équipe tout en gardant l’imagination et la créativité. Ce pattern ou façon ergonomique de penser se crée avec le temps dans l’équipe et tout le monde sentira qu’il y a une ADN ou une marque de pensée algorithmique et technique qui s’installe au fil du temps. C’est pour cela, je répète toujours qu’un bon développeur généralement se crée dans l’équipe non pas tout seul à moins qu’il est sur-doué ou très expérimenté!. Pour anticiper, avec les nouveaux développeurs, on fait énormément du pair programing à tour de rôle. Ceci permet de souder le relationnel humain et aussi de s’aligner tous et de créer cette ADN ou timbre de pensée. En effet, La deuxième partie peut me prendre des jours (pas la totalité mais deux heures au max par journée) et des allers retours entre moi, le développeur, le métier (PO et équipe UX) et les testeurs aussi. Dans l’idéale, l’approche BDD pourra nous gagner beaucoup de temps mais je fais avec et je m’adapte au contexte du travail :).

Un ticket bien écrit doit présenter dans un langage techno-naturel par exemple (GHERKIN) les scénarios simples, complexes et aussi les erreurs possibles qui peuvent écouler. Il faut pas penser seulement au cas idéal au moment d’écrire le ticket, cela impactera la review ensuite. Jusqu’à maintenant, malgré les efforts fournis dans l’équipe, on arrive pas à cerner dés l’écriture du ticket tout les cas qu’on découvre au moment du développement et aussi lors de la review. Pour moi, cela est normal vu que l’ingénierie informatique est un travail empirique scientifique assez complexe dans un langage machine avec beaucoup de dépendances et de side effets non connus tous à l’avance.

Dans certaines organisations, on ajoute la review UX avec l’équipe design pour s’assurer qu’on est au pixel près. Cela nous conduit à ajouter peut être le snapshot testing au niveau de notre CI/CD. Afin de s’assurer avant même de faire la review avec l’équipe UX que notre code respecte le rendu design attendu.

Avec le temps, on a senti que la review s’est amélioré et que la plupart des développeurs de l’équipe ont bien compris l’utilité du code review et s’épanouit à le faire. Je pense que la raison principale que chacun de nous a contrôlé son égo et a compris les avantages de s’intéresser à ce sujet qui apporte beaucoup au produit. La review permet à un développeur junior de passer au niveau supérieur s’il est fait avec des gens expérimentés et humbles.

Conclusion

Le sujet de la review du code est pour moi un tabou dans plusieurs organisations. c’est rare de trouver des boites de tech qui le font bien avec de l’humanisme. A mon avis, même les gens qui prétendent être des crafters ont contribué à rendre ce processus une source pour faire plaisir à leur arrogance non contrôlée (Il y a toujours des êtres humains qui aiment juger et se montrer mieux que les autres). Je prétends pas que le processus de review que j’applique est le meilleur. C’est mon approche, il y a surement d’autres manières plus meilleurs de faire que je suis toujours prenant à découvrir et appliquer.

Code kata exercise – Game of life 

Before I begin, I want to express my gratitude to Swiftly for its excellent article on the Game of Life. His insights and guidance have inspired me to approach this code kata in a fresh and exciting way.

In this article, I present my own perspective on the Game of Life, hoping to extend and contribute to the knowledge he has shared.

You can check the complete code on my GitHub repo.

https://github.com/brahim-lmr/GameOfLife

Kata in software development 

« Kata » is a term that originates from Japanese martial arts, specifically from disciplines such as karate, judo, and aikido. In this context, a kata refers to a sequence of movements performed in a specific order. It is a form of practice that allows practitioners to train and perfect their techniques, movements, and overall martial arts skills.

Outside of martial arts, the term « kata » has been adapted and applied to various fields, including software development. In the context of software development, « kata » refers to structured exercises or practices that help developers improve their coding skills, problem-solving abilities, and understanding of programming concepts, algorithms, design patterns, and best practices.

There are several popular code kata exercises that developers often practice to improve their software development skills such as FizzBuzz, String Reversal, TDD (Test-Driven Development) Katas

Those are just some examples of common kata code exercises, In this article, we will try to solve a popular code kata exercise called the « Game of Life ». So let’s go 😁

The « Game of Life »

The « Game of Life » is a popular programming kata that involves creating a simulation of an evolving population of cells. This exercise is typically used to practice writing clean, modular code that is easy to read and maintain.

The « Game of Life » is a mathematical game devised by the British mathematician John Horton Conway in 1970. Despite its name, it is not a typical game in the conventional sense with players and rules. Instead, it is a simulation or « game » that models the behavior of cells on a two-dimensional grid, each cell can either be « alive » or « dead ». 

The cells change their state over time based on a set of simple rules. these rules indicate whether a cell will be alive or dead in the next generation as follows:

  • Any live cell with fewer than two live neighbors dies as if caused by underpopulation.
  • Any live cell with two or three live neighbors lives on to the next generation.
  • Any live cell with more than three live neighbors dies, as if by overpopulation.
  • Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.

One important aspect of this Kata is writing code that is modular by breaking the program down into smaller parts, each of which is responsible for a specific aspect of the game.

Another important consideration is testing. In order to ensure that the program is working correctly we will use the TDD approach, ensuring correct functioning by examining various input scenarios. Creating test cases covering all possible scenarios, such as a small set of cells, helps identify bugs and ensures the program’s functionality on a larger scale.

Let’s write some code 

Our program will be composed of two separate parts:

The UI: 

We will use SwiftUI to create the UI of our game.

The domain: 

It contains all the different logic parts of the game such as the calculation of the next generation of cells, how to determine the neighbors of a given cell, and so on. As we said previously we will use the TDD approach so this part will be driven by unit tests. 

A cell is represented by a struct called Cell, each cell has two coordinates (x, y) in the grid and a state of type CellState either .alive or .dead.

struct Cell {
    
    let x: Int
    let y: Int
    
    var state: CellState = .dead
    
    var position: Position {
        (x: x, y: y)
    }
}

enum CellState {
    case alive
    case dead
}

In the Game class, we will create the grid for the game and put the necessary logic for it. 

typealias Row = [Cell]
typealias Grid = [Row]
typealias Position = (x: Int, y: Int)

final class Game {
  var numberOfRows: Int
  var numberOfColums: Int
    
  var grid = Grid()
    
  init(numberOfRows: Int, numberOfColums: Int) {
      self.numberOfRows = numberOfRows
      self.numberOfColums = numberOfColums
      grid = createGrid()
   }
}

The creation of the grid is simple, we can use two for loops to create cells and append them to the grid. The reason why we use two for loops is that the grid has two dimensions ie: a matrix. 

private func createGrid() -> Grid {
        var grid = Grid()
        for x in 0..<numberOfRows {
            var row = Row()
            for y in 0..<numberOfColums {
                row.append(Cell(x: x, y: y, state: .dead))
            }
            grid.append(row)
        }
        return grid
 }

That’s it! Now let’s write our first test.

the rules of the game are based on neighborhood, so we need to create an empty function getNeighborsForCell(at position: Position) -> [Cell] that determines all the neighbors of a given cell. 

Cell’s neighbors are all cells around our main cell, see the image below. 

We can begin with a test like this

func test_ifTwoCellsAreNeighbors() {
        // GIVEN
        let cell = Cell(x: 2, y: 2)
				
				// Offset = (x-x', y-y')				

        let cell1 = Cell(x: 1, y: 1) // Offset between cell et cell1 = (1, 1)
        let cell2 = Cell(x: 1, y: 2) // Offset between cell et cell2 = (1, 0)
        let cell3 = Cell(x: 1, y: 3) // Offset between cell et cell3 = (1, -1)
        let cell4 = Cell(x: 2, y: 1) // Offset between cell et cell4 = (0, 1)
        let cell5 = Cell(x: 2, y: 2) // Offset between cell et cell5 = (0, 0)
        let cell6 = Cell(x: 2, y: 3) // Offset between cell et cell6 = (0, -1)
        let cell7 = Cell(x: 3, y: 1) // Offset between cell et cell7 = (-1, 1)
        let cell8 = Cell(x: 3, y: 2) // Offset between cell et cell8 = (-1, 0)
        let cell9 = Cell(x: 3, y: 3) // Offset between cell et cell9 = (-1, -1)

        let cell10 = Cell(x: 1, y: 4) // Offset between cell et cell10 = (1, -2)

        // THEN
        XCTAssertTrue(sut.areNeighbors(rhd: cell, lhd: cell1))
        XCTAssertTrue(sut.areNeighbors(rhd: cell, lhd: cell2))
        XCTAssertTrue(sut.areNeighbors(rhd: cell, lhd: cell3))
        XCTAssertTrue(sut.areNeighbors(rhd: cell, lhd: cell4))
        XCTAssertTrue(sut.areNeighbors(rhd: cell, lhd: cell6))
        XCTAssertTrue(sut.areNeighbors(rhd: cell, lhd: cell7))
        XCTAssertTrue(sut.areNeighbors(rhd: cell, lhd: cell8))
        XCTAssertTrue(sut.areNeighbors(rhd: cell, lhd: cell9))
        
        XCTAssertFalse(sut.areNeighbors(rhd: cell, lhd: cell5))
        XCTAssertFalse(sut.areNeighbors(rhd: cell, lhd: cell10))
    }

We see clearly that two cells are neighbors if the absolute offset between the x-axis is 1 or 0 and the offset between the y-axis is 1 or 0. With this logic in mind, we can implement our function like this:

func areNeighbors(rhd: Cell, lhd: Cell) -> Bool {
        let range = [0, 1]
                
        let xOffset = abs(rhd.x - lhd.x)
        let yOffset = abs(rhd.y - lhd.y)

        return range.contains(xOffset) && range.contains(yOffset)
}

We notice that all assertions have passed, except for cell5 because it has the same position as cell.In other words, it’s the cell itself. so we need to add this line of code to our function in the first line.

if rhd == lhd { return false }

Now the test should pass 😁

To determine the next generation of the game we need to get all the living neighbors for a given cell, let’s add a function func getAliveNeighbordForCell(at position: Position)-> [Cell] which returns an empty array. 

As usual, we begin by writing the test first:

func test_getLivingNeighborsForCell_ShouldBeZeroInEmptyGameGrid() {

        // GIVEN
        let position: Position = (x:2, y: 3)
        
        let aliveCells = sut.getAliveNeighbordForCell(at: position)
        // THEN
        XCTAssertEqual(aliveCells.count, 0)
        
}

This test should succeed, now what if our cell has two living neighbors, our second test should be like this:

func test_foundTwoLivingNeighbors() {
        
        // GIVEN
        sut.changeStateOfCellAt(position: (x:2, y: 1), to: .alive)
        sut.changeStateOfCellAt(position: (x:2, y: 3), to: .alive)
 
        let position: Position = (x:2, y: 2)
        let aliveCells = sut.getAliveNeighbordForCell(at: position)

        // THEN
        XCTAssertEqual(aliveCells.count, 2)
        
 }

This test fails because we return an empty table. we need to return cells that are neighbors and have the state of .alive

func getAliveNeighbordForCell(at position: Position) -> [Cell] {
        getNeighborsForCell(at: position)
            .filter{ $0.state == .alive }
}

And Voilà! test succeed.

Let’s move on to the second part of our Kata. The question at hand is: How can I determine the state of my game for the next generation? We need to consider implementing a function func computeNextGeneration() that allows us to determine the state of each game cell for the next generation. As usual, let’s think about the tests involved.

The idea is to write a test for each rule of the game, let’s begin with the REPRODUCTION rule:

  • If a dead cell has exactly three living neighbors, it becomes alive (reproduction).
// REPRODUTION
    func test_deadCellWithThreeNeighbors_getsAlive() {
        
        // GIVEN
        sut.changeStateOfCellAt(position: (x:1, y: 1), to: .alive)
        sut.changeStateOfCellAt(position: (x:2, y: 1), to: .alive)
        sut.changeStateOfCellAt(position: (x:3, y: 1), to: .alive)
                
        // WHEN
        sut.computeNextGeneration()
        
        let state = sut.getCell(at: (x: 2, y: 2))?.state
        
        // THEN
        XCTAssertEqual(state, .alive)
    }

According to rule number one, we will first focus on determining the dead cells, find those that have exactly three living neighbors, and then apply a transformation function to obtain a new matrix of cells.

func computeNextGeneration() {
        _ = grid
            .flatMap { $0 }
            .filter { cell in
                isReproductionCondition(cell)
            }
            .map { cell in
								// Change the state from .alive to .dead and vice versa 
                grid[cell.x][cell.y].state.toggle()
            }
    }

private lazy var isReproductionCondition = { (cell: Cell) -> Bool in
        
        return (cell.state == .dead)
        &&
        self.getAliveNeighbordForCell(at:  cell.position).count == 3
 }

Now let’s run our test. And the green color appears ✅

Let us now move on to the second rule OVERPOPULATION:

  • If a living cell has more than three living neighbors, it dies (overpopulation).

The test should be something like this:

// OVERPOPULATION
    func test_cellWithMoreThanThreeLivedNeighbors_dies() {
        
        // GIVEN
        sut.changeStateOfCellAt(position: (x:1, y: 1), to: .alive)
        sut.changeStateOfCellAt(position: (x:1, y: 2), to: .alive)
        sut.changeStateOfCellAt(position: (x:1, y: 3), to: .alive)
        sut.changeStateOfCellAt(position: (x:2, y: 1), to: .alive)
        sut.changeStateOfCellAt(position: (x:2, y: 2), to: .alive)

        // WHEN
        sut.computeNextGeneration()

        let cell1 = sut.getCell(at: (x: 1, y: 2))
				let cell2 = sut.getCell(at: (x: 2, y: 2))
        // THEN
        XCTAssertEqual(cell1?.state, .dead)

    }

We should update now the computeNextGeneration() function to make this test green without impacting our old tests. 

func computeNextGeneration() {
        _ = grid
            .flatMap { $0 }
            .filter { cell in
                isReproductionCondition(cell)
								||
								isOverpupulationCondition(cell)
            }
            .map { cell in
								// Change the state from .alive to .dead and vice versa 
                grid[cell.x][cell.y].state.toggle()
            }
    }

private lazy var isOverpupulationCondition = { (cell: Cell) -> Bool in
        
        return (cell.state == .alive)
        &&
		    self.getAliveNeighbordForCell(at:  cell.position).count > 3
 }

The same logic goes with the third rule UNDERPOPULATION

  • If a living cell has fewer than two living neighbors, it dies (underpopulation).
// UNDERPOPULATION
    func test_cellWithLessThanTwoLivedNeighbors_dies() {
        
        // GIVEN
        sut.changeStateOfCellAt(position: (x:1, y: 2), to: .alive)
        sut.changeStateOfCellAt(position: (x:2, y: 3), to: .alive)

        // WHEN
        sut.computeNextGeneration()
        
        let cell1 = sut.getCell(at: (x: 1, y: 2))
        let cell2 = sut.getCell(at: (x: 2, y: 3))
        
        // THEN
        XCTAssertEqual(cell1?.state, .dead)
        XCTAssertEqual(cell2?.state, .dead)

    }
func computeNextGeneration() {
        _ = grid
            .flatMap { $0 }
            .filter { cell in
                isReproductionCondition(cell)
								||
								isOverpopulationCondition(cell)
								||
								isUnderpopulationCondition(cell)
            }
            .map { cell in
								// Change the state from .alive to .dead and vice versa 
                grid[cell.x][cell.y].state.toggle()
            }
    }

private lazy var isUnderpupulationCondition = { (cell: Cell) -> Bool in
        
        return (cell.state == .alive)
        &&
		    self.getAliveNeighbordForCell(at:  cell.position).count < 2
 }

We could refactor our code and make only one condition for overpopulation and underpopulation conditions since they share the same condition (cell.state == .alive)

We end up with this code: 

func computeNextGeneration() {
        _ = grid
            .flatMap { $0 }
            .filter { cell in
                isReproductionCondition(cell)
                ||
                isOverpupulationOrUnderpopulationCondition(cell)
            }
            .map { cell in
                grid[cell.x][cell.y].state.toggle()
            }
    }
    
    /// If a dead cell has exactly three living neighbors,
    /// it becomes alive (reproduction).
    private lazy var isReproductionCondition = { (cell: Cell) -> Bool in
        
        return (cell.state == .dead)
        &&
        self.getAliveNeighbordForCell(at:  cell.position).count == 3
    }
    
    /// If a living cell has more than three living neighbors, it dies (overpopulation).
    /// Or
    /// If a living cell has fewer than two living neighbors, it dies (underpopulation).
    private lazy var isOverpupulationOrUnderpopulationCondition = { (cell: Cell) -> Bool in
        
        return (cell.state == .alive)
        &&
        (
            self.getAliveNeighbordForCell(at:  cell.position).count > 3
            ||
            self.getAliveNeighbordForCell(at:  cell.position).count < 2
        )
    }

The last rule will be applied automatically since our filter treats all three first conditions.

  • Any live cell with two or three live neighbors lives on to the next generation.

and our last tests will succeed.

func test_cellWithTwoLivedNeighbors_lives() {
        
        // GIVEN
        sut.changeStateOfCellAt(position: (x:0, y: 3), to: .alive)
        sut.changeStateOfCellAt(position: (x:0, y: 4), to: .alive)
        sut.changeStateOfCellAt(position: (x:1, y: 4), to: .alive)
        sut.changeStateOfCellAt(position: (x:2, y: 5), to: .alive)
        
        sut.printGrid()
        
        // WHEN
        sut.computeNextGeneration()
        
        let cell = sut.getCell(at: (x: 1, y: 4))
        
        // THEN
        XCTAssertEqual(cell?.state, .alive)
        
        sut.printGrid()
    }
    
    func test_cellWithThreeLivedNeighbors_lives() {
        
        // GIVEN
        sut.changeStateOfCellAt(position: (x:0, y: 3), to: .alive)
        sut.changeStateOfCellAt(position: (x:0, y: 4), to: .alive)
        sut.changeStateOfCellAt(position: (x:0, y: 5), to: .alive)
        sut.changeStateOfCellAt(position: (x:1, y: 4), to: .alive)
        
        sut.printGrid()
        
        // WHEN
        sut.computeNextGeneration()
        
        let cell = sut.getCell(at: (x: 1, y: 4))
        
        // THEN
        XCTAssertEqual(cell?.state, .alive)
        
        sut.printGrid()
    }

We finish building the core logic of the Kata Game of Life and now let’s plug this logic with a graphic interface and see the game in practice, this time we will use SwiftUI to create the interface. 

The UI part

The game of life consists of a set of cells that live and die within a grid.

In the ContentView we will create an instance of our Game class to be able to use the grid property. 

For this, the Game class needs to conform to ObservableObject and the grid property should be marked by the property wrapper @StateObject , with this SwiftUI will allows us to observe any change on the grid property and it will update the UI accordingly. 


final class Game: ObservableObject {
    
    var numberOfRows: Int
    var numberOfColums: Int
    
    @Published var grid = Grid()
}

struct ContentView: View {
    
    @StateObject private var game = Game(
       numberOfRows: 10,
       numberOfColums: 10
    )
}

Creating a cell using swiftUI is very simple we can achieve that by using the Rectangle shape like this: 

Rectangle()
   .fill(.green)
    .frame(
           width: 50,
           height:50
     )
     .border(.gray.opacity(0.5), width: 1)

For now, the width and the height have a hard-coded value of 50, later we will make this dynamic by calculating the width and the height according to our screen and the number of cells. 

To create the grid we will loop over our matrix using the ForEash and use a VStack for the rows and an HStack for the columns in this way: 

VStack(spacing: .zero) {
 ForEach(game.grid, id: \.self) { row in
  HStack(spacing: .zero) {
    ForEach(row, id: \.self) { cell in
      Rectangle()
        .fill(fillColorForCell(state: cell.state))
        .frame(
           width: 50,
           height: 50
    )
    .border(.gray.opacity(0.5), width: 1)
   }
  }
 }
}

To make the width and the height of a cell dynamic we should embed our VStack in a container view called GeometryReader that provides information about the geometry of its parent view.

We will use this information to create a function calculateCellWidth(in: geometry) and use it for both width and height to obtain a square. 

GeometryReader { geometry in
   VStack(spacing: .zero) {
     ForEach(game.grid, id: \.self) { row in
       HStack(spacing: .zero) {
         ForEach(row, id: \.self) { cell in
            Rectangle()
              .fill(.green)
              .frame(
                  width: calculateCellWidth(in: geometry),
                  height: calculateCellWidth(in: geometry)
               )
              .border(.gray.opacity(0.5), width: 1)
           }
       }
    }
  }
}

// Outside the body view 

private func calculateCellWidth(in geometry: GeometryProxy) -> CGFloat {
       (geometry.size.width) / CGFloat(game.numberOfColums)
 }

For the fill color, we will use blue for the .alive state and clear for .dead, to determine the color we add a helper function private func fillColorForCell(state: CellState) -> Color like this: 

Rectangle()
  .fill(fillColorForCell(state: cell.state))
  .frame(
      width: calculateCellWidth(in: geometry),
      height: calculateCellWidth(in: geometry)
   )
   .border(.gray.opacity(0.5), width: 1)

// Outside the body view 
private func fillColorForCell(state: CellState) -> Color {
        switch state {
        case .alive:
            return .blue
        case .dead:
            return .clear
        }
}

Now time to lunch the game, first we need to initialize the game randomly. so we add two helper functions getXRandomLocation() -> Int and getYRandomLocation() -> Int and use them in the initializeGame() function, the idea is to randomly populate half of the grid (50 cells) with living cells.: 

private func initializeGame() {

// numberOfRows x numberOfColums =  10 x 10 = 100 
// Fill the half of the grid 100 / 2 = 50
        for _ in 0...50 {
            let x = getXRandomLocation()
            let y = getYRandomLocation()
            
            game.grid[x][y]
                .state
                .toggle()
        }
    }

    private func getXRandomLocation() -> Int {
        return Int(arc4random()) % game.numberOfRows
    }
    
    private func getYRandomLocation() -> Int {
        return Int(arc4random()) % game.numberOfColums
    }

Make the call of the initializeGame() in the .onAppear modifier.

For now, the grid display only the first generation, To continuously update the grid, we simply need to call the computeNextGeneration() method every ‘n’ seconds, to achieve that we will use the Timer of the Combineframework and compute the next generation every two seconds: 

@State private var cancellable: Cancellable?

private func launchTheGame() {
        cancellable = Timer.publish(
            every: 2,
            on: .main,
            in: .default
        )
        .autoconnect()
        .subscribe(on: DispatchQueue.main)
        .sink { _ in
            game.computeNextGeneration()
        }
}

To stop the game we need simply to cancel our subscription like this: 

private func stopTheGame() {
        cancellable?.cancel()
}

Add two button to our view one for lunching the game and the other to stop the game:

Button(action: {
                    launchTheGame()
                }) {
                    Text("LAUNCH THE GAME")
                        .frame(maxWidth: .infinity)
                        .font(.headline)
                }
                .padding(
                    .horizontal,
                    Constant.horizontalInset
                )
                .tint(.blue)
                .buttonStyle(.borderedProminent)
                .buttonBorderShape(.automatic)
                .controlSize(.large)

 Button(action: {
                    stopTheGame()
                }) {
                    Text("Stop")
                        .frame(maxWidth: .infinity)
                        .font(.headline)
                }
                .padding(
                    .horizontal,
                    Constant.horizontalInset
                )
                .tint(.blue)
                .buttonStyle(.bordered)
                .buttonBorderShape(.automatic)
                .controlSize(.large)

That it! We can watch now the simulation of the Game of Live.

Conclusion

Overall, the Game of Life is a great way to practice programming skills and improve your ability to write clean, maintainable, and testable code. 

By breaking the program down into smaller, more manageable parts and testing each section thoroughly, you can ensure that the program is working correctly. 

Whether you’re a beginner or an experienced developer, this kata is sure to challenge and inspire you. So why not give it a try and see what you can create?

You can follow the new series about modern iOS development on SwiftWithWalid Youtube Channel

Gestion des logs à base de OSLog

OSLog est une nouvelle framework native Apple qui permet de récupérer des messages de log au niveau de votre application mobile. OSLog constitue un remplacement pour des solutions comme print, debugPrint.. Elle permet d’afficher les logs très clairs avec plusieurs types(trace, notice, warning, critical..) surtout avec la console actuelle de Xcode15.

Dans cet article, on découvrira à travers un exemple pratique comment utiliser OSLog.

Utilisation pour tracker des calls api

Dans cet exemple, on essaye de récupérer une liste de repos swift depuis une API Github connu

https://api.github.com/search/repositories?q=language:swift

final class SwiftReposListViewModel: ObservableObject {
    @Published var repos: [RepoDTO] = []
    private let httpClient: HTTPClient

    init(httpClient: HTTPClient) {
        self.httpClient = httpClient
    }


    func fetch() async throws {
        guard let url = URL(string: Constants.SwiftRepoUrl.rawValue) else { return }
        do {
            debugPrint("Start fetching data")
            let result = try await httpClient.get(from: url)
            debugPrint("Finish fetching data")
            switch result {
            case let .success(data, _) :
                do {
                    let apiResult = try JSONDecoder().decode(GithubSearchResponse.self, from: data)
                    if let items = apiResult.items {
                        let repos = items.toRepoDTOs
                        DispatchQueue.main.async {
                            self.repos = repos
                        }
                    }
                }
                catch {}
            case let .failure(error):
                debugPrint(error)
                
            }
        } catch {
            debugPrint(error)
        }
    }
}

Le code permet de récupérer une liste de repos depuis l’api Github et de convertir vers un tableau de RepoDTO utilisé ensuite au niveau la vue SwiftUI pour afficher une liste de repos qui parlent de Swift etc.

On utilise un httpClient qui permet de faire un appel à base de Async Await.

Pour plus de détails sur la nouvelle framework concurrency, vous pouvez consulter notre article

Async Await et DI

Revenons à notre code, vous remarquez qu’on a ajouté un debugPrint pour tracer l’exécution de l’appel API. Imaginons qu’on peut catégoriser nos logs par type genre API Call tracking logs, Analytics logs, persistant data Logs … Imaginons encore que je peux distinguer graphiquement par des des couleurs les types des messages de logs afin de distinguer entre des messages simples, des erreurs, des warnings… En effet, OSLog permet de faire ça et on va voir comment.

extension Logger {
    private static var subsystem = Bundle.main.bundleIdentifier!
    static let apiCalls = Logger(subsystem: subsystem, category: "API calls life Cycle")
}

J’ai ajouté deux nouvelles propriétés subsystem et apiCalls. En effet, subsystem représente le bundle identifier qui nous permettra ensuite au niveau de la console de déboggage de distinguer les logs de notre application. Logger (une structure utiliser par OSLog pour émettre des logs) nous permet de définir une ou plusieurs types domaines de Log. Par exemple, j’ai choisi pour les call API le nom apiCalls.

Changeons maintenant le code pour ajouter l’appel vers Logger.

 do {
      Logger.apiCalls.trace("Swift repos start fetching")
      let result = try await httpClient.get(from: url)
      Logger.apiCalls.notice("Swift repos fetching is finished")
////
. catch {
      Logger.apiCalls.warning("\(error.localizedDescription, privacy: .public)")
}

Utilisation de la console

Tout d’abord pour afficher les logs sur la console, il faut l’afficher 😀. Pour se faire, il faut sélectionner manage Run Destinations depuis Xcode

Puis choisissez votre appareil et ensuite cliquer sur open Console

Vous aurez à ce moment l’interface de la console

Vous allez remarquez que notre console log tout (logs systems, applications). Afin de faciliter l’affichage de logs. Pour filter seulement sur notre application, on va utiliser notre bundle identifier fr.walidSASSI.SwiftRepos et choisissez Sous-système au niveau des filtres de recherche de la console.

Maintenant filtrons par le nom de notre domaine de recherche de logs à savoir API calls life Cycle

Si vous cliquez sur le message de log par exemple ‘Swift repos Start fetching’, vous retrouvez les informations sur son la catégorie API calls life cycle, le sous sytème ou on trouve notre bundle identifier, le numéro de processus pid associé, le thread.

Jouons avec les types des messages de logs

Logger avec plusieurs fonctions qui permettent de customiser vos messages de logs

public func trace(_ message: OSLogMessage)
public func debug(_ message: OSLogMessage)
public func info(_ message: OSLogMessage)
public func notice(_ message: OSLogMessage)
public func warning(_ message: OSLogMessage)
public func error(_ message: OSLogMessage)
public func critical(_ message: OSLogMessage)

Faisons les tests maintenant sur un simulateur iOS 17 sous Xcode 15. Vous allez remarquez que nos messages de logs prennent une forme assez stylé incluant plusieurs informations qu’on a vu au niveau de la console.

Xcode LLDB debugger distingue entre les différents type de messages de logs à travers des icônes spécifiques

Warning log
Trace log
Critical log

Une petite astuce de la WWDC 2023, vous pouvez cliquer sur le message de log avec la touche espace et vous voyez en détail les informations relatives au log event avec l’avantage de connaître la méthode dans laquelle s’est déclenché le log

Références

Debug With structured WWDC 2023

https://developer.apple.com/documentation/os/logger

https://swiftwithmajid.com/2022/04/19/exporting-data-from-unified-logging-system-in-swift/

https://swiftwithmajid.com/2022/04/06/logging-in-swift/

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.

fr_FRFrench