Aucun point pour deviner qui m’a aidé
Nous avons eu un problème de base de code gonflé.
Et nous aimions en parler à chaque planification de Sprint – sans en réparer aucune partie.
C’était notre sujet de prédilection. Fatigués de parler des progrès toujours insaisissables dans les quotidiens, nous sommes allés la tête vide lors des réunions de planification de sprint de 90 minutes. Si nous n’avions rien à dire, cela reviendrait à dire que nous n’avions rien à apporter.
La base de code pléthorique était notre sauveur. Cela nous a donné le soulagement bien nécessaire et pas si comique. Relief? C’était sûrement le cas.
Cela nous a aidés à nous décharger.
C’était une opportunité pour nous, les développeurs de minions, de faire en sorte que les PO se sentent pathétiques envers eux-mêmes.
It’s you. You are the feature-slapping bots. (Yes, I mean it in every
possible meaning, but read on). You refuse to let us work on improving the code quality.
So here we are, with a million bloated LOC.
Find and replace is our only tool. And that's worse.
Can we have two sprints, just to ourselves?
In the private Teams chat:
(wink wink)
"If he ends up offering it, let us politely refuse.
It will rob us of our rights to make a fuss, forever."
Cela nous a aidés à éliminer le sentiment que nous possédions tous des développeurs, quelqu’un dont l’éternuement avait un effet exponentiel sur le tableau de qualité du produit.
Un passionné débutant l’a mentionné lors du sommet des développeurs à l’échelle de l’entreprise, et le couvercle a explosé.
La dette technique.
Tout le monde s’est soudainement senti passionné par l’amélioration du gros ballonnement. Et avec l’hystérie de masse qui l’entoure, il faudrait sûrement faire quelque chose.
Mon esprit a commencé à découvrir comment je pouvais éviter de réparer les péchés des autres développeurs. Celui que j’avais remplacé avait rejoint le côté obscur. Il était devenu PO lui-même. Je ne pourrais pas assez le blâmer.
Mais maintenant, si je refusais d’agir, le débutant serait le héros. Je n’avais pas beaucoup de problème avec ça, mais j’avais un problème avec le fait que je serais étiqueté comme un pécheur.
Mon esprit a couru pour trouver le fruit le plus bas.
Le stock Si les déclarations. Il y en avait beaucoup dans notre base de code. Mais quelque 500 d’entre eux étaient similaires.
C’étaient des drapeaux caractéristiques.
Martin Fowler les décrit ainsi :
« Feature Toggles (alias Feature Flags) est une technique puissante, permettant aux équipes de modifier le comportement du système sans changer de code. »
Leur utilisation typique (également tirée du site Web de Fowler) ressemble à ceci :
function reticulateSplines(){
if( featureIsEnabled("use-new-SR-algorithm") ){
return enhancedSplineReticulation();
}else{
return oldFashionedSplineReticulation();
}
}
Ils sont souvent connus sous le nom de bascules de version car les versions sont leur point d’utilisation le plus courant. Par exemple, si vous développez une fonctionnalité qui n’est pas si mature, vous souhaitez la désactiver dans la v1. Lorsque vous appuyez sur v2, vous l’activez.
Voici à quoi cela ressemble :
let v1Features = [“old-SR-algorithm”, ...]
let v2Features = ["use-new-SR-algorithm", ...]
let versionDictionaries = {"v1": v1Features, "v2": v2Features}function featureIsEnabled(key) {
let dictionary = versionDictionaries[currentVersion] // currentVersion = "v1", "v2"...
return dictionary[key] ? true: false
}
Le basculement se produit via des arguments de ligne de commande, juste avant le déploiement. De cette façon, c’est bénéfique.
« Lorsque l’indicateur de fonctionnalité affecte une très petite surface de votre logiciel, stocker un indicateur près du cœur devient un choix imprudent. »
Mais s’il s’agit d’un client de bureau/mobile, il doit de toute façon être intégré au binaire, donc ne pas changer le code n’est pas vrai après tout. Dans un tel scénario, ils se transforment en dette.
Un autre problème, plus crucial, avec les drapeaux de fonctionnalités est qu’en raison de leur logique de basculement, ils occupent de l’espace. Et cet espace sera découplé de leur logique d’évaluation. Encore une fois, le découplage, en général, est une bonne idée dans les logiciels, mais lorsque l’indicateur de fonctionnalité affecte une petite surface de votre logiciel, stocker un indicateur près du cœur devient un choix imprudent. Quand tout le monde suit la même logique, tout l’endroit devient gonflé en un rien de temps.
Supposons que vous ayez 500 indicateurs de fonctionnalité. Même si un indicateur de fonctionnalité contrôle un seul point d’exécution (point de basculement de fonctionnalité, où la condition if protège la logique métier), vous consultez 1 500 lignes de code : 500 pour le magasin d’indicateurs de fonctionnalité et 1 000 pour les couplets if-else ou if-end . Même si nous ignorons la charge de traitement de l’évaluation de if-else (ce qui est souvent inévitable, car la conversion en switch-case n’est pas pragmatique pour les décisions de publication de fonctionnalités transitoires), le coût est apparent. Chaque instruction if est une charge cognitive pour un développeur peu familier. (Dans un monde Agile, si vous revisitez le code au cours des trois derniers mois, vous n’êtes pratiquement pas familier.)
Voici notre stratégie de mise en œuvre :
- Chaque indicateur de fonctionnalité sera placé dans quatre dictionnaires. Chacun de ces quatre éléments représentait une configuration spécifique à un pays.
- Le drapeau lui-même sera une clé dans le dictionnaire. Les valeurs seront les versions, représentées sous la forme d’un ensemble d’options : Alpha, Bêta ou Production.
Un dictionnaire typique spécifique à un pays avec deux indicateurs de fonctionnalité ressemblerait à ceci :
{
“feature1”: [alpha | beta],
“feature2”: [alpha | beta | production]
}
Au moment de l’exécution, les éléments suivants évalueraient si une fonctionnalité était activée ou non. Voici à quoi ressemblait le code :
function featureIsEnabled(featureName) {
let featureOptionSet = countryDictionary[featureName]
return featureOptionSet.contains(currentBuild). // currentBuild = alpha, beta or production
}
Inutile de dire que notre base de code était remplie d’un nombre énorme de if featureIsEnabled()
appels.
Et nous (du moins moi, après le sommet des développeurs) voulions améliorer cela.
Je n’aimais pas la logique centralisée pour décider si ma fonctionnalité devait être activée. Je voulais mettre cette logique près de l’endroit où cette fonctionnalité était disponible.
Mon idée était celle d’une fonction Gatekeeper. Bref, au lieu de :
if featureIsEnabled(featureName) {
featureImplementationFunction()
}
Je voulais ça :
executeIfCondition(featureImplementationFunction, evaluationCondition: () -> Bool)
Ici le evaluationCondition
est une fermeture qui renverrait vrai si la construction est alpha ou bêta, mais faux si la construction est de production, et ainsi de suite. Lorsque la fonctionnalité n’est plus fraîche, elle inclurait les trois types de construction pour renvoyer true.
Par ici, featureIsEnabled
est effacé, tout comme la logique centralisée (+ dictionnaires) qui gère le changement de fonctionnalité. Après tout, ils n’étaient de toute façon pas détenus par un gardien omniscient.
Mon vrai défi était de développer une véritable fonction de gardien qui puisse agir sur n’importe quel featureImplementationFunction
Signature.
Les répondeurs de Stack Overflow ont posé des questions sur mon besoin de remplacer les instructions if lorsque j’ai demandé à Stack Overflow une fonction de contrôle d’accès globale.
Sans leur manquer de respect cependant. j’ai récemment Débordement de pile défendu pour interdire les réponses alimentées par ChatGPT. Mais parfois, les répondeurs de Stack Overflow ont tendance à faire partie du problème, à tenir à distance les véritables discussions et à chasser les questionneurs.
Au lieu de trouver la syntaxe de featureImplementationFunction
argument, ils se sont demandé s’il valait la peine de remplacer les instructions if par ces fermetures de condition. Pourquoi étaient-ils meilleurs ?
Deux personnes ont voté contre ma question. Une personne a voté pour le fermer.
Je n’arrivais pas à les convaincre pourquoi je voulais la fonction de gardien, mais je savais au fond de moi que cela me faciliterait la vie.
Enfin, j’ai demandé Chat GPT – et cela n’a pas déçu.
Malheureusement, j’ai perdu toute la trace de la conversation ChatGPT, à cause d’une panne de serveur.
En attendant, il n’arrêtait pas de me divertir avec une meilleure version de 503 :
Write two truths and a lie about the status of ChatGPT.
1. ChatGPT is experiencing high traffic at the moment.
2. The developers are working hard to accommodate all users.
3. ChatGPT can predict the future with 100% accuracy.
Mais avant ça, quand ça s’est terminé, voici le récapitulatif de mon échange :
- Lorsque j’ai demandé une fonction qui agissait sur n’importe quelle fonction, elle a proposé une extension de fonction basée sur un protocole de fonction
- Lorsque Swift a refusé de l’accepter, je suis retourné au Chat GPT. Je lui ai dit que la chose n’était pas acceptable et il s’est excusé. Tentative après tentative, il continuait à émettre fonction après fonction.
- Ce que j’ai trouvé le plus surprenant et le plus utile (mais effrayant) : chaque fois que je lui ai dit que les choses ne fonctionnaient pas, Chat GPT a déduit la sortie du compilateur avec une précision de 100 % ! Ah, le truc de la valeur non nominale. Oui, j’aurais dû m’en souvenir. Laissez-moi essayer une fois de plus. C’est ici. J’avais l’impression qu’un programmeur mentor me guidait pour la première fois de mes deux décennies de carrière.
Le rendu final était le suivant :
func toClosure<T>(_ invocation: @escaping @autoclosure () -> T) -> (Any...) -> T {
return { (args: Any...) -> T in
invocation()
}
}func executeIfCondition(function: (@escaping (Any...) -> Any), condition: () -> Bool) {
if condition() {
function()
}
}
Les executeIfCondition
est ma fonction de gardien souhaitée. Les toClosure
est une fonction d’assistance pour convertir une fonction en son invocation avec des arguments.
L’usage
func printStringAndReturnInt(i: Int, s: String) -> Int {
print(s)
return i
}func printArray(arr: [Int]) -> Void {
arr.forEach({ print("\($0)") })
}
func isThisProd() -> Bool {
return false
}
func isThisAlphaOrBeta() -> Bool {
return true
}
executeIfCondition(function: toClosure(printStringAndReturnInt(i: 5, s: "Wow!")), condition: isThisProd)
executeIfCondition(function: toClosure(printArray(arr:[9,10,11,12])), condition: isThisAlphaOrBeta)
printStringAndReturnInt
etprintArray
sont les fonctions que je veux être fermées par la fonction gatekeeper.isThisProd
etisThisAlphaOrBeta
sont mes fonctions d’alimentation de condition souhaitées. Le scénario réel contiendra un préprocesseur#if
instructions pour décider du type de construction.executeIfCondition
est la fonction de gardien.
Ce n’est pas encore fait dans la branche master.
Je présenterai la solution gatekeeper à l’équipe de purification de code (Spoiler : Aucune équipe de ce type n’existe encore.)
Il y a des chances que mes pairs n’aiment pas remplacer les instructions If par quelque chose qui n’est pas facilement compréhensible. Si cela se produit, ils méritent l’enfer des spaghettis remplis d’accolades.
S’ils approuvent, je serai le maître de l’univers.
S’ils ne le font pas, je continuerai à me plaindre des raisons pour lesquelles nous sommes si lents à fournir des fonctionnalités et/ou pourquoi nous avons des bogues fréquents. Et je continuerai secrètement à en profiter.
Jusqu’à ce que ChatGPT vole mon travail.