Avec la quantité de ressources gratuites, de livres, de cours en ligne et d'ateliers de codage disponibles aujourd'hui, tout le monde peut apprendre à coder. Cependant, il existe toujours un fossé qualitatif entre le codage et le génie logiciel. Faut-il qu'il y en ait un ?
J'ai écrit mon premier "Hello world" il y a plus de vingt ans - c'est la réponse que je donne si quelqu'un me demande depuis combien de temps je suis codeur. Ces dix dernières années, j'ai eu une carrière qui m'a permis de toucher à tout. code presque tous les jours - c'est la réponse que je donne si on me demande depuis combien de temps je suis un codeur professionnel.
Depuis combien de temps suis-je un ingénieur logiciel? Je dirais environ cinq ans. Attendez, ces chiffres ne semblent pas concorder ! Qu'est-ce qui a changé ? Qui considérerais-je comme un ingénieur logiciel, et qui comme un "simple" codeur ?
La définition d'un ingénieur logiciel
Le codage est relativement facile. Il ne s'agit plus de mnémoniques d'assemblage sur des systèmes ridiculement contraints. Et si vous utilisez quelque chose d'aussi expressif et puissant que Ruby, c'est encore plus facile.
Il vous suffit de prendre un ticket, de trouver l'endroit où vous devez insérer votre code, de déterminer la logique que vous devez y mettre, et boum - c'est fait. Si vous êtes un peu plus avancé, vous vous assurez que votre code est joli. Logiquement divisé en méthodes. A des spécifications décentes qui ne testent pas uniquement le chemin le plus heureux. C'est ce que fait un bon codeur.
Un ingénieur logiciel ne pense plus en termes de méthodes et de classes, du moins pas en premier lieu. D'après mon expérience, un ingénieur logiciel pense en termes de flux. Il voit d'abord et avant tout le fleuve tonitruant de données et d'interactions qui déferle sur le système. Il réfléchit à ce qu'il doit faire pour détourner ou modifier ce flux. Le joli code, les méthodes logiques et les bonnes spécifications viennent presque après coup.
Ce sont des tortues sur toute la ligne
Les gens pensent généralement d'une certaine manière à la plupart des interactions avec la réalité. Faute d'un meilleur terme, appelons cela la perspective "descendante". Si mon cerveau travaille à me préparer une tasse de thé, il comprendra d'abord les étapes générales : aller à la cuisine, faire chauffer la bouilloire, préparer la tasse, verser l'eau, retourner au bureau.
Il ne cherchera pas d'abord à savoir quelle tasse utiliser en premier, alors que je suis assis à mon bureau et que je suis déconnecté ; cette déconnexion se fera plus tard, lorsque je serai devant l'armoire. Il ne tiendra pas compte du fait que nous pourrions être à court de thé (ou du moins, à court d'eau). bon ). Elle est vaste, réactive et sujette aux erreurs. En somme, très humain dans la nature.
Lorsque l'ingénieur logiciel envisage de modifier le flux de données quelque peu ahurissant, il le fait naturellement de la même manière. Examinons cet exemple de récit d'utilisateur :
Un client commande un gadget. Lors de la détermination du prix de la commande, les éléments suivants doivent être pris en compte :
- Prix de base du widget dans la localité de l'utilisateur
- Forme du widget (modificateur de prix)
- S'il s'agit d'une commande urgente (modificateur de prix)
- Si la livraison de la commande a lieu un jour férié dans la région de l'utilisateur (modificateur de prix)
Tout cela peut sembler artificiel (et ça l'est évidemment), mais ce n'est pas très éloigné de certaines histoires d'utilisateurs que j'ai eu le plaisir d'écraser récemment.
Examinons maintenant le processus de réflexion qu'un ingénieur logiciel pourrait mettre en œuvre pour résoudre ce problème :
"Nous devons connaître le nom de l'utilisateur et sa commande. Ensuite, nous commençons à calculer le total. Nous commencerons à zéro. Nous appliquerons ensuite le modificateur de forme du widget. Ensuite, nous appliquons les frais d'urgence. Ensuite, on vérifie si c'est un jour férié, et boum, c'est fait avant le déjeuner !"
Ah, le plaisir que peut procurer une simple histoire d'utilisateur. Mais l'ingénieur logiciel n'est qu'un être humain, pas une machine parfaite à plusieurs fils, et la recette ci-dessus n'est qu'une vue d'ensemble. L'ingénieur continue donc à réfléchir plus en profondeur :
"Le modificateur de forme du widget est... oh, c'est super dépendant du widget, n'est-ce pas. Et ils peuvent être différents selon les lieux, même si ce n'est pas maintenant, mais à l'avenir". ils pensent, brûlés auparavant par l'évolution des besoins de l'entreprise, "et les frais d'urgence peuvent l'être aussi. Et les jours fériés sont aussi très spécifiques à la région, augh, et les fuseaux horaires seront impliqués ! J'ai eu un article ici sur la gestion des heures dans différents fuseaux horaires dans Rails ici... ooh, je me demande si l'heure de la commande est stockée avec le fuseau horaire dans la base de données ! Je ferais mieux de vérifier le schéma".
Très bien, ingénieur logiciel. Arrêtez. Vous êtes censé vous préparer une tasse de thé, mais vous restez à l'écart, devant l'armoire, à vous demander si la tasse fleurie peut s'appliquer à votre problème de thé.
La préparation de la tasse parfaite widget
Mais c'est facilement ce qui peut se produire lorsque vous essayez de faire quelque chose d'aussi peu naturel pour le cerveau humain que de penser à plusieurs niveaux de détail simultanément.
Après avoir fouillé brièvement dans son vaste arsenal de liens concernant la gestion des fuseaux horaires, notre ingénieur se ressaisit et commence à décomposer le tout en code réel. S'il avait essayé l'approche naïve, cela aurait pu ressembler à quelque chose comme ça :
def calculate_price(user, order)
commande.prix = 0
order.price = WidgetPrices.find_by(widget_type : order.widget.type).price
order.price = WidgetShapes.find_by(widget_shape : order.widget.shape).modifier
...
fin
Et ainsi de suite, de cette manière délicieusement procédurale, pour être ensuite lourdement interrompus lors de la première révision du code. Car si l'on y réfléchit bien, il est tout à fait normal de penser de cette manière : les grandes lignes d'abord, et les détails bien plus tard. Vous ne pensiez même pas que vous n'étiez plus dans le coup au début, n'est-ce pas ?
Notre ingénieur, cependant, est bien formé et n'est pas étranger à l'objet de service, alors voici ce qui se passe à la place :
classe BaseOrderService
def self.call(user, order)
new(user, order).call
fin
def initialize(user, order)
@user = user
@order = order
end
def call
puts "[WARN] Implement non-default call for #{self.class.name} !"
utilisateur, ordre
fin
fin
class WidgetPriceService < BaseOrderService ; end
classe ShapePriceModifier < BaseOrderService ; end
classe RushPriceModifier < BaseOrderService ; fin
classe HolidayDeliveryPriceModifier < BaseOrderService ; fin
class OrderPriceCalculator < BaseOrderService
def call
user, order = WidgetPriceService.call(user, order)
utilisateur, commande = ShapePriceModifier.call(utilisateur, commande)
utilisateur, commande = RushPriceModifier.call(utilisateur, commande)
utilisateur, commande = HolidayDeliveryPriceModifier.call(utilisateur, commande)
utilisateur, commande
fin
end
```
C'est bien ! Maintenant, nous pouvons utiliser un bon TDD, écrire un scénario de test et étoffer les classes jusqu'à ce que toutes les pièces se mettent en place. Et ce sera magnifique.
En outre, il est totalement impossible de raisonner à ce sujet.
L'ennemi est l'État
Certes, il s'agit d'objets bien séparés avec des responsabilités uniques. Mais le problème est là : il s'agit toujours d'objets. Le modèle de l'objet de service avec son "prétendre de force que cet objet est une fonction" est vraiment une béquille. Rien n'empêche quiconque d'appeler HolidayDeliveryPriceModifier.new(user, order).something_else_entirely
. Rien n'empêche d'ajouter un état interne à ces objets.
Sans oublier utilisateur
et commande
sont également des objets, et il est aussi facile de les modifier que de faire un petit tour sur le site. order.save
quelque part dans ces objets fonctionnels par ailleurs "purs", en modifiant la source sous-jacente de l'état de la vérité, c'est-à-dire d'une base de données. Dans cet exemple, ce n'est pas très grave, mais cela peut se retourner contre vous si le système devient de plus en plus complexe et s'étend à d'autres parties, souvent asynchrones.
L'ingénieur a eu la bonne idée. Et il a utilisé un moyen très naturel d'exprimer cette idée. Mais savoir comment exprimer cette idée - d'une manière belle et facile à raisonner - a été presque empêché par le paradigme OOP sous-jacent. Et si quelqu'un qui n'a pas encore franchi le pas de l'expression de ses pensées sous forme de détournements du flux de données tente de modifier moins habilement le code sous-jacent, de mauvaises choses se produiront.
Devenir fonctionnellement pur
Si seulement il existait un paradigme dans lequel exprimer ses idées en termes de flux de données n'était pas seulement facile, mais nécessaire. Si le raisonnement pouvait être rendu simple, sans possibilité d'introduire des effets secondaires indésirables. Si les données pouvaient être immuables, tout comme la tasse fleurie dans laquelle vous faites votre thé.
Oui, je plaisante bien sûr. Ce paradigme existe, et il s'appelle la programmation fonctionnelle.
Voyons comment l'exemple ci-dessus pourrait se présenter dans un de mes favoris, Elixir.
defmodule WidgetPrices do
def priceorder([user, order]) do
[utilisateur, commande]
|> widgetprice
|> shapepricemodifier
|> modificateur de prix d'urgence
|> modificateur de prix pour les vacances
fin
defp widgetprice([user, order]) do
%{widget : widget} = commande
price = WidgetRepo.getbase_price(widget)
[utilisateur, %{commande | prix : prix }]]
end
defp shapepricemodifier([user, order]) do
%{widget : widget, price : currentprice} = commande
modifier = WidgetRepo.getshapeprice(widget)
[utilisateur, %{commande | prix : prix courant * modificateur} ]
fin
defp rushpricemodifier([user, order]) do
%{rush : rush, price : currentprice} = order
if rush do
[utilisateur, %{commande | prix : prix courant * 1.75} ]
else
[utilisateur, %{commande | prix : prix courant} ]
end
fin
defp holidaypricemodifier([user, order]) do
%{date : date, price : prix actuel} = commande
modifier = HolidayRepo.getholidaymodifier(user, date)
[utilisateur, %{commande | prix : prix courant * modificateur}]]
end
end
```
Vous remarquerez peut-être qu'il s'agit d'un exemple complet de la manière dont l'histoire de l'utilisateur peut être réalisée. C'est parce que c'est moins compliqué qu'en Ruby. Nous utilisons quelques fonctionnalités clés propres à Elixir (mais généralement disponibles dans les langages fonctionnels) :
Fonctions pures. Nous ne modifions pas réellement les arrivées commande
Nous créons simplement de nouvelles copies, de nouvelles itérations de l'état initial. Nous ne sautons pas non plus sur le côté pour changer quoi que ce soit. Et même si nous le voulions, commande
n'est qu'une carte "muette", nous ne pouvons pas appeler order.save
à aucun moment, car il ne sait tout simplement pas ce que c'est.
Correspondance de motifs. De manière assez similaire à la déstructuration de ES6, cela nous permet d'arracher des prix
et widget
de la commande et de la transmettre, au lieu de forcer nos copains à faire de même. WidgetRepo
et HolidayRepo
de savoir comment traiter un dossier complet commande
.
Opérateur de tuyauterie. Vu dans ordre_de_prix
Il nous permet de faire passer des données à travers des fonctions dans une sorte de "pipeline" - un concept immédiatement familier à quiconque a déjà exécuté un programme de formation à l'utilisation de l'Internet. ps aux | grep postgres
pour vérifier si l'engin fonctionnait encore.
C'est ainsi que vous pensez
Les effets secondaires ne font pas vraiment partie de notre processus de réflexion. Après avoir versé de l'eau dans votre tasse, vous ne vous inquiétez généralement pas qu'une erreur dans la bouilloire puisse la faire surchauffer et exploser - du moins pas au point de fouiller dans ses composants internes pour vérifier si quelqu'un n'y a pas laissé de l'eau par inadvertance. explose_après_versement
est activé.
Le passage du statut de codeur à celui d'ingénieur logiciel, c'est-à-dire le passage de la préoccupation pour les objets et les états à celle pour les flux de données, peut dans certains cas prendre des années. C'est en tout cas ce qu'a fait votre serviteur, qui a été élevé à la POO. Avec les langages fonctionnels, vous commencez à penser aux flux la première nuit.
Nous avons fait ingénierie logicielle La programmation n'a pas besoin d'être compliquée pour nous-mêmes et pour tous les nouveaux arrivants dans le domaine. La programmation n'a pas besoin d'être difficile et de faire travailler les méninges. Elle peut être facile et naturelle.
Ne compliquons pas les choses et optons déjà pour la fonctionnalité. Car c'est ainsi que nous pensons.
Lire aussi :