principles

star 0

Appliquer les principes de développement (YAGNI > KISS > DRY > SOLID) avant de proposer du code ou un refacto. À utiliser pour concevoir du nouveau code, proposer des abstractions, extraire des helpers/services, ou juger si un changement ajoute une complexité nécessaire. Particulièrement utile pour résister aux réflexes de sur-abstraction (service objects prématurés, injection de dépendances sans appelant, configuration générique). Inclut la Rule of Three (ne pas extraire à la 2e duplication) et le Semantic DRY (coïncidence de valeur ≠ même connaissance).

dinum-HubEE By dinum-HubEE schedule Updated 6/9/2026

name: principles description: Appliquer les principes de développement (YAGNI > KISS > DRY > SOLID) avant de proposer du code ou un refacto. À utiliser pour concevoir du nouveau code, proposer des abstractions, extraire des helpers/services, ou juger si un changement ajoute une complexité nécessaire. Particulièrement utile pour résister aux réflexes de sur-abstraction (service objects prématurés, injection de dépendances sans appelant, configuration générique). Inclut la Rule of Three (ne pas extraire à la 2e duplication) et le Semantic DRY (coïncidence de valeur ≠ même connaissance). globs: - "app//*.rb" - "lib//*.rb"

Principes de développement

Appliquer ces principes à tout le code produit. Priorité en cas de conflit : YAGNI > KISS > DRY > SOLID.

Pourquoi cet ordre : un principe pris isolément semble toujours sage, mais les principes entrent en conflit. Quand c'est le cas, privilégier celui qui garde la codebase plus petite et plus simple aujourd'hui. Appliquer SOLID à du code dont on n'a pas encore besoin (YAGNI) produit des abstractions qui seront de toute façon refactorées. Appliquer DRY à deux blocs qui se ressemblent mais signifient des choses différentes (Semantic DRY) couple des concepts indépendants et les rend plus difficiles à faire évoluer. La simplicité d'abord, la structure seulement quand elle paie.

SOLID

S — Single Responsibility

Une classe/méthode n'a qu'une seule raison de changer. Un model ne fait pas d'appels HTTP, un service ne rend pas de vues.

# ❌ le model gère la logique métier ET l'appel externe
class Subscription < ApplicationRecord
  def activate!
    update!(status: "active")
    HubeeApi.new.post("/subscriptions", id: id) # pas son rôle
  end
end

# ✅ chaque classe a une seule responsabilité
class Subscription < ApplicationRecord
  def activate!
    update!(status: "active")
  end
end

class SubscriptionActivator
  def call(subscription)
    subscription.activate!
    HubeeApi.new.post("/subscriptions", id: subscription.id)
  end
end

O — Open/Closed

Le code est ouvert à l'extension, fermé à la modification. Ajouter du comportement sans toucher au code existant.

# ❌ ajouter un format impose de modifier la méthode existante
class ExportService
  def call(format)
    if format == :csv then export_csv
    elsif format == :pdf then export_pdf # on touche au code existant
    end
  end
end

# ✅ on étend en ajoutant une nouvelle classe, pas en modifiant les existantes
class CsvExportService
  def call = export_csv
end

class PdfExportService
  def call = export_pdf
end

L — Liskov Substitution

Toute sous-classe doit pouvoir se substituer à sa classe parente sans altérer le comportement attendu. Ne pas redéfinir une méthode pour en changer le contrat.

# ❌ la sous-classe lève là où le parent retourne nil, ce qui casse les appelants
class ApiClient
  def find(id) = nil # retourne nil quand non trouvé
end

class StrictApiClient < ApiClient
  def find(id) = raise NotFoundError # change le contrat
end

# ✅ la sous-classe préserve le contrat
class CachedApiClient < ApiClient
  def find(id)
    cache.fetch(id) { super }
  end
end

I — Interface Segregation

Préférer plusieurs modules ciblés à un seul module fourre-tout. Une classe ne devrait pas être forcée d'implémenter des méthodes dont elle n'a pas besoin.

# ❌ un concern fourre-tout force tous les includers à porter des méthodes inutilisées
module Exportable
  def to_csv = ...
  def to_pdf = ...
  def to_xml = ...
end

# ✅ modules ciblés, on n'inclut que ce dont on a besoin
module CsvExportable
  def to_csv = ...
end

module PdfExportable
  def to_pdf = ...
end

class Subscription < ApplicationRecord
  include CsvExportable # uniquement ce qui est nécessaire
end

D — Dependency Inversion

Dépendre d'abstractions, pas d'implémentations concrètes. Injecter les dépendances plutôt que de les instancier directement.

# ❌ dépendance codée en dur, impossible à stubber dans les tests
class SubscriptionActivator
  def call(subscription)
    HubeeApi.new.post("/subscriptions", id: subscription.id)
  end
end

# ✅ dépendance injectée, facile à remplacer ou à stubber
class SubscriptionActivator
  def initialize(api_client: HubeeApi.new)
    @api_client = api_client
  end

  def call(subscription)
    @api_client.post("/subscriptions", id: subscription.id)
  end
end

# Dans spec/services/subscription_activator_spec.rb :
# SubscriptionActivator.new(api_client: double("api", post: true))

Dépends de ce qui change moins souvent que toi : une dépendance est saine quand la cible est plus stable et plus abstraite. Trois leviers : (1) graphe acyclique — jamais de cycle ; (2) injecter les dépendances volatiles plutôt que les instancier en dur ; (3) isoler ce qui change le plus (API/client tiers) derrière une frontière. Concret-Rails : un service dépend des models ; un model ne dépend jamais d'un controller/service ; le code métier ne dépend pas directement d'un client HTTP tiers (cf. archi 3-couches api-client).

Law of Demeter — ne parle qu'à tes amis immédiats

N'invoque que des méthodes de tes voisins directs : pas de chaîne order.customer.address.zip qui traverse trois objets et te couple à leur structure interne.

# ❌ chaîne qui traverse customer puis address
order.customer.address.zip

# ✅ l'objet expose ce dont l'appelant a besoin
class Order
  def customer_zip = customer.address_zip
end
order.customer_zip

YAGNI

N'implémenter que ce qui est nécessaire maintenant, pas ce dont on pourrait avoir besoin un jour.

# ❌ système de config générique construit « au cas où »
class Subscription < ApplicationRecord
  def self.find_with_options(id, cache: false, fallback: nil, locale: :fr)
    # logique complexe que personne n'a demandée
  end
end

# ✅ on implémente exactement ce qui est nécessaire
class Subscription < ApplicationRecord
  def activate! = update!(status: "active")
end

KISS

Privilégier la solution la plus simple qui fonctionne, éviter la complexité accidentelle.

# ❌ astucieux mais difficile à lire
active_orgs = orgs.each_with_object({}) { |o, h| h[o.id] = o if o.status == "active" }

# ✅ simple et explicite
active_orgs = orgs.select(&:active?)

DRY

Chaque élément de connaissance a une représentation unique et non ambiguë — mais sans abstraction prématurée.

# ❌ même logique dupliquée à deux endroits
def admin_label = "[#{siret}] #{name}"
def export_label = "[#{siret}] #{name}"

# ✅ source de vérité unique
def label = "[#{siret}] #{name}"
alias admin_label label
alias export_label label

Rule of Three — attendre de l'avoir vu trois fois

Ne pas extraire à la deuxième occurrence. La duplication coûte peu, la mauvaise abstraction coûte cher : une fois un helper extrait, chaque variation future doit soit s'y conformer, soit le casser.

Deux occurrences peuvent partager une forme par coïncidence. Une troisième occurrence est la preuve d'un motif.

# Premier usage — on l'écrit, c'est tout
def admin_label = "[#{siret}] #{name}"

# Deuxième usage — on duplique, on n'extrait pas encore
def export_label = "[#{siret}] #{name}"

# Troisième usage avec la même forme — maintenant on extrait
def label = "[#{siret}] #{name}"
alias admin_label label
alias export_label label
alias search_label label

Corollaire : si la troisième occurrence montre que la forme est presque la même mais avec une variante (séparateur différent, champ conditionnel), c'est un signal que l'abstraction n'est pas prête — continuer à dupliquer jusqu'à ce que la bonne forme émerge.

Semantic DRY — coïncidence de valeur ≠ même connaissance

Deux choses qui partagent la même valeur ne sont pas la même chose si elles ont des rôles sémantiques distincts. DRY s'applique à la connaissance (une règle métier → un endroit), pas aux coïncidences structurelles.

Avant de factoriser deux valeurs qui se ressemblent, se demander : « représentent-elles le même concept ? » Si non → les garder séparées, même si leurs valeurs actuelles coïncident.

# ❌ même URL aujourd'hui, fusionnées en une seule (faux DRY)
KEYCLOAK_URL = ENV.fetch("KEYCLOAK_BASE_URL") # utilisée à la fois pour le login portail et la gestion des utilisateurs

# ✅ deux rôles sémantiques, deux variables, évoluables indépendamment
KEYCLOAK_OIDC_URL  = ENV.fetch("KEYCLOAK_BASE_URL")  # authentifie les admins du portail (OIDC)
KEYCLOAK_ADMIN_URL = ENV.fetch("KEYCLOAK_BASE_URL")  # gère les comptes utilisateurs finaux HubeeV1

Auto-vérification avant de proposer du code

Tu reconnais des « best practices » issues de tes données d'entraînement et tu as tendance à sur-abstraire : des service objects là où une méthode de model suffirait, de l'injection de dépendances là où le codage en dur convient, de la configuration générique là où une valeur littérale fait l'affaire. Ces principes existent pour contrer ce réflexe.

Avant de proposer du code, vérifie-toi :

  • YAGNI d'abord. Cette abstraction est-elle nécessaire aujourd'hui par un appelant réel, ou prépare-t-elle un futur hypothétique ? Si c'est pour le futur, supprime-la.
  • KISS plutôt que SOLID. Un controller Rails qui appelle .update! directement vaut souvent mieux qu'un service object enrobant la même ligne. Résiste au réflexe de découper.
  • Rule of Three avant DRY. Si seulement deux endroits partagent une forme, ne les factorise pas encore.
  • Semantic DRY. Deux valeurs ayant la même chaîne ne sont pas la même connaissance.

Si tu es sur le point d'extraire une classe, d'ajouter un strategy pattern ou d'introduire une gem pour éviter trois lignes de duplication — contre-toi et garde la version la plus simple.

Install via CLI
npx skills add https://github.com/dinum-HubEE/hubee-claude-plugin --skill principles
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator