Maîtriser les principes de conception : guide du KISS, DRY et SOLID en développement logiciel

Explorez les principes fondamentaux de conception KISS, DRY et SOLID pour améliorer les pratiques de développement logiciel. Apprenez à écrire un code clair et maintenable pour une meilleure collaboration en équipe et le succès des projets.
Maîtriser les principes de conception : guide du KISS, DRY et SOLID en développement logiciel

En pénétrant dans le monde au-delà des murs de l’université, là où la théorie rencontre la réalité, vous réalisez rapidement le fossé qui sépare l’apprentissage en salle de classe des exigences de l’industrie. Si le milieu académique vous introduit à la programmation et aux fondamentaux comme les mathématiques discrètes et le calcul, l’industrie demande une compréhension fine des principes clés qui simplifient le code et renforcent la collaboration. Ici, nous explorons la trifecta transformatrice : les principes KISS, DRY et SOLID.

Adopter la Simplicité : Le Principe KISS

Keep It Simple, Stupid!

Dans la sphère collaborative du développement logiciel, la clarté prime sur la complexité. Imaginez-vous au sein d’une équipe, chaque membre façonnant des aspects différents d’un projet. Si vous devez superviser le code de votre collègue, un script clair et bien documenté est préférable à un fouillis logico-complexe. Le même principe s’applique à vos projets personnels. Après une pause dû à une maladie par exemple, revenir sur un projet avec un code opaque ne fait qu’aggraver le défi.

Considérez le processus de débogage : une base de code enchevêtrée transforme un débogage mineur en une tâche insurmontable. Ainsi, les sages choisissent la simplicité, faisant écho à la sagesse de Martin Fowler :

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”

En pratique, la simplicité garantit qu’après une période d’absence, on peut retourner au développement et comprendre le travail sans perdre le rythme.

Maîtriser les Techniques KISS

Réfléchissez à une classe Student, chargée de stocker les informations des modules et les notes correspondantes. Voici une représentation simple :

data class Student(
  val name: String,
  val age: Int,
  val moduleMarks: Map<Pair<String, String>, Pair<Int, Int>>
)

Pour l’efficacité, une fonction identifie les élèves doués :

fun scholars(students: List<Student>): Map<String, List<String>> {
  val scholars = mutableMapOf<String, List<String>>() 
  students.forEach { student ->
    scholars[student.name] = student.moduleMarks
      .filter { (_, (a, m)) -> a / m > 0.8 }
      .map { ((n, _), _) -> n }
  }
  return scholars
}

Avec un peu de recul, simplifier ceci améliore la lisibilité. Séparez les préoccupations en introduisant plus d’abstractions :

data class Student(
  val name: String,
  val age: Int,
  val moduleMarks: Map<Module, Mark>
)

data class Module(
  val name: String,
  val id: String
)

data class Mark(
  val achieved: Double,
  val maximum: Double
) {
  fun isAbove(percentage: Double): Boolean {
    return achieved / maximum * 100 > percentage
  }
}

fun scholars(students: List<Student>): Map<String, List<String>> {
  val scholars = mutableMapOf<String, List<String>>() 
  students.forEach { student ->
    val modulesAbove80 = student.moduleMarks
      .filter { (_, mark) -> mark.isAbove(80.0) }
      .map { (module, _) -> module.name }

    scholars[student.name] = modulesAbove80
  }
  return scholars
}

Les structures additionnelles rendent la compréhension et le débogage beaucoup plus gérables. Chaque partie du code se lit maintenant comme un récit logique.

Efficacité par le Principe DRY

Don’t Repeat Yourself

Dans un monde où la redondance conduit à la fragilité, évitez le code répétitif. Supposons que vous développez une application de tableur nécessitant souvent de récupérer des objets de données par emplacement. Initialement, votre approche pourrait être répétitive :

public class Spreadsheet implements BasicSpreadsheet {
  private final Set<Cell> cells;

  @Override
  public double getCellValue(CellLocation location) {
    Cell cell = cells.stream()
        .filter(cell -> cell.location.equals(location))
        .findFirst()
        .orElse(null);
    
    return cell == null ? 0d : cell.getValue();
  }

  @Override
  public String getCellExpression(CellLocation location) {
    Cell cell = cells.stream()
        .filter(cell -> cell.location.equals(location))
        .findFirst()
        .orElse(null);
    
    return cell == null ? "" : cell.getExpression();
  }

  @Override
  public void setCellExpression(CellLocation location, String input) throws InvalidSyntaxException {
    Cell cell = cells.stream()
        .filter(cell -> cell.location.equals(location))
        .findFirst()
        .orElse(null);

    // ...
  }

  // ...
}

L’abstraction est votre alliée. Extrayez la logique commune en composants réutilisables :

public class Spreadsheet implements BasicSpreadsheet {
  private final Set<Cell> cells;

  @Override
  public double getCellValue(CellLocation location) {
    return getFromCell(location, Cell::getValue, 0d);
  }

  @Override
  public String getCellExpression(CellLocation location) {
    return getFromCell(location, Cell::getExpression, "");
  }

  @Override
  public void setCellExpression(CellLocation location, String input) throws InvalidSyntaxException {
    Cell cell = findCell(location);

    // ...
  }

  // ...

  private Cell findCell(CellLocation location) {
    return cells.stream()
        .filter(cell -> cell.location.equals(location))
        .findFirst()
        .orElse(null);
  }

  private <T> T getFromCell(CellLocation location,
                            Function<Cell, T> function,
                            T defaultValue) {
    Cell cell = findCell(location);
    return cell == null ? defaultValue : function.apply(cell);
  }
}

Ces changements garantissent que lorsqu’un bug apparaît, une seule modification le corrige dans tous les contextes affectés.

Principes SOLID : Les Piliers d’un Design Robuste

Comprendre SOLID ne consiste pas à mémoriser des acronymes, mais à internaliser des principes qui favorisent un code réactif et maintenable.

S — Responsabilité Unique

Chaque classe ou fonction doit porter une seule responsabilité, améliorant la clarté et réduisant le chevauchement des fonctionnalités. Par exemple :

class Repository(
  private val api: MyRemoteDatabase,
  private val local: MyLocalDatabase
) {
  fun fetchRemoteData() = flow {
    // Fetching API data
    val response = api.getData()

    // Saving data in the cache
    var model = Model.parse(response.payload)
    val success = local.addModel(model)
    if (!success) {
      emit(Error("Error caching the remote data"))
      return@flow
    }

    // Returning data from a single source of truth
    model = local.find(model.key)
    emit(Success(model))
  }
}

Ce code couple le réseautage et la mise en cache. La séparation des préoccupations suggère des responsabilités distinctes :

class Repository(
  private val api: MyRemoteDatabase,
  private val cache: MyCachingService
) {
  fun fetchRemoteData() = flow {
    // Fetching API data
    val response = api.getData()

    val model = cache.save(response.payload)

    // Sending back the data
    model?.let {
      emit(Success(it))
    } ?: emit(Error("Error caching the remote data"))
  }
}

class MyCachingService(
  private val local: MyLocalDatabase
) {
  suspend fun save(payload: Payload): Model? {
    var model = Model.parse(payload)
    val success = local.addModel(model)
    return if (success)
      local.find(model.key)
    else
      null
  }
}

O — Ouvert/Fermé

Votre code doit être facile à étendre tout en résistant à la modification. Concevez vos interfaces pour une expansion facile :

interface PageTag {
  val width: Int
  val height: Int
}

class ParagraphTag(
  override val width: Int,
  override val height: Int
) : PageTag

class AnchorTag(
  override val width: Int,
  override val height: Int
) : PageTag

class ImageTag(
  override val width: Int,
  override val height: Int
) : PageTag

infix fun PageTag.tallerThan(other: PageTag): Boolean {
  return this.height > other.height
}

L — Substitution de Liskov

Le comportement de sous-types doit remplacer sans heurt les types de base, préservant la cohérence du système. Considérez la différenciation des capacités de vol :

open class FlightlessBird {
  open fun eat() {
    // ...
  }
}

open class Bird : FlightlessBird() {
  open fun fly() {
    // ...
  }
}

class Penguin : FlightlessBird() {
   // ...
}

class Eagle : Bird() {
  // ...
}

I — Ségrégation d’Interface

Évitez les dépendances forcées en segmentant les interfaces :

interface SystemRunnable {
  fun turnOn()
  fun turnOff()
}

interface Drivable() {
  fun drive()
}

interface Flyable() {
  fun fly()
}

interface Pedalable() {
  fun pedal()
}

class Car : SystemRunnable, Drivable {
  override fun turnOn() { /* Implementation */ }
  override fun turnOff() { /* Implementation */ }
  override fun drive() { /* Implementation */ }
}

class Aeroplane : SystemRunnable, Flyable {
  override fun turnOn() { /* Implementation */ }
  override fun turnOff() { /* Implementation */ }
  override fun fly() { /* Implementation */ }
}

class Bicycle : Pedalable {
  override fun pedal() { /* Implementation */ }
}

D — Inversion de Dépendance

Construisez des systèmes s’appuyant sur des abstractions plutôt que sur des implémentations concrètes :

interface CachingService {
  suspend fun save(): Model?
}

interface SomeLocalDb() {
  suspend fun add(model: Model): Boolean
  suspend fun find(key: Model.Key): Model
}

class Repository(
  private val api: SomeRemoteDb,
  private val cache: CachingService
) { /* Implementation */ }

class MyCachingService(
  private val local: SomeLocalDb
) : CachingService { /* Implement methods */ }

class MyAltCachingService(
  private val local: SomeLocalDb
) : CachingService { /* Implement methods */ }

class PostgreSQLLocalDb : SomeLocalDb { /* Implement methods */ }
class MongoLocalDb : SomeLocalDb { /* Implement methods */ }

Chaque étape dans la maîtrise de ces principes trace le chemin vers un design logiciel plus intuitif et résilient. Que vous soyez dans les tranchées du débogage ou que vous preniez des décisions architecturales, ces principes vous arment de la clarté et de l’agilité nécessaires pour répondre avec adresse aux besoins évolutifs.


Je vous remercie d’avoir consacré votre temps précieux à cette exploration des principes logiciels essentiels. J’espère que ces perspectives transformeront votre approche des tâches de codage, vous guidant vers l’écriture non seulement de code, mais de solutions durables.