mercredi 5 avril 2023

Some economics of refactoring

Refactoring in a separate project is often not the best of ideas 👎 . It might well be the most common form, but it's a terrible idea from an investment point of view. 


Let's look at the investment profile of various forms of refactoring. First refactoring in a separate project; We refactor for weeks or months to make the code somewhat clean again. Of course the business won't see the fruit of the refactoring until it has made us save time developing new features, after the refactoring is finished. 


Compared with no refactoring, the break even comes after many months or more.



Another common type of refactoring is End-of-story refactoring. The value of this refactoring is again reaped when we touch the code again, a few weeks or months in the future. 



While some basic cleaning is called for - encoding "end-of-story" knowledge into the design - any attempt to make the code "extendable" or modular is risky business. It's not KISS/YAGNI and ROI is uncertain. Break even comes after weeks or months.


Now let's look at Preparatory Refactoring, as suggested in 3P ( Protect-Prepare-Produce) https://lnkd.in/eiewkYBe



The return of investment comes directly after the refactoring. It's an investment opportunity we cannot miss! We're almost certain to at least break even, and in the shortest period of time


3P (Protect-Prepare-Produce) acknowledges this and gives us a way of optimizing the ROI while working on stories. Let's see more of 3P and preparatory refactoring, our code will be better off! and perhaps most importantly business people will start to understand and appreciate refactoring 😘 


dimanche 2 avril 2023

Quick and Dirty tests, FTW

If we consider solely the refactoring phase: Then what do we care about in tests. What quality do the tests need to be useful to us?

Imagine we could change the tests later, what do we actually care about during the hours/days we're performing some larger refactoring

It turns out we can do quite a few compromises, a lot of the vital qualities of a tests don't matter over a few days, running on a laptop with no other persons reading the tests


Now why would one want to write unmaintainable tests only to replace them later? Well for one it's a lot faster, secondly writing great tests for legacy code is often close to impossible. Given that we'll modify/replace anyway, the question is "What compromises can we make?"

This type of quick-and-dirty tests is precisely the kind of tests were likely to do in a 3P (Protect-Prepare-Produce) approach. The above post will also somewhat lay out when/how/what to do with those quick and dirty tests later.

What about you, are you doing these compromises. Any other compromises?


lundi 7 novembre 2022

Breaking out of legacy with 3P

Protect, Prepare, Produce - 3P. A recipe to break out of legacy

If you prefer you can read this in French 

Photo by Ugne Vasyliute on Unsplash

Maybe you've already had experiences like

  • A refactoring project over several months was been cancelled
  • Tests that don't really help, perhaps even slowing you down
  • Refactorings that don't turn out that good 
  • Code that continuously degrades, despite the stories dedicated to refactoring and testing that you do regularly.

If this is the case you would probably be interested in knowing why these efforts are not bringing the expected benefit and why the 3Ps would help you.

Let's first consider the classic approaches of either doing refactoring and testing in dedicated stories, or doing most of the refactoring and testing at the end of the story. These approaches seem attractive at first glance, but they have a big ROI problem. Indeed, they lengthen the time between investment (refactoring) and return on investment (a new feature, facilitated by said refactoring). 

Precisely, when we refactor in a separate project or story, or even at the end of a story: who reaps the value, and when

It's the next person who touches the code, hopefully, sometime later. And that's provided that the refactoring actually turns out to facilitate the future work that we don't know beforehand... Basically the ROI will be for someone else, maybe. And this in a potentially distant future. Not ideal, but we can notice that the ROI is closer if we do it at the end of the story than if we do it in a separate project.

Same question for tests. Who reaps the value of tests written after the fact? And when will that be?

The answer is essentially the same. It's the next person who touches the code, hopefully, sometime in the future. Provided that the tests are still relevant with regard to the new need, which we didn't know about when we wrote the tests.

In short, we can see that there is a time that can be more or less long between the investment and the benefit. The shorter the delay, the better. But that's not all. In my experience, we don't always do the ideal refactoring and we don't always do the ideal tests. One of the reasons is that we don't know the future very well. Could we minimise these "mistakes"?

So we should find a way to get feedback on the quality of our tests and our refactoring as quickly as possible, and to minimise the uncertainty about the future. If we did this, we'd have refactoring and tests  that would bring much more for the same amount of time invested. Given that our investments pays off better, we're likely to increase the investments, possibly by a lot!

3P

This is where the idea of the 3Ps comes in, where we not only bring the investment closer to the benefit, but also we are the foremost beneficiaries of the tests just wrote and the refactoring we just did! This has the added benefit that any ill conceived tests and refactoring are immediately brought to the attention of just the right person to fix the issue.  

The idea is simple, for each story break down the work into 

  1. Protect
  2. Prepare
  3. Produce

Protect

Setting up the missing tests

First we have to protect the changes with tests. To do this, we need to identify the code that needs to be touched in order to add any missing tests (for the upcoming refactoring). We don't worry about covering anything but the code that will be changed, that would be a distraction

The code that will be affected must be perfectly covered with tests. Any less and we wouldn't dare to thoroughly refactor in the next phase. However, we can completely ignore the maintainability of the tests, we're only concerned with protecting the refactoring phase. So this phase is way shorter than what we might expect.

We need tests that will not break during refactoring, so low-level tests are not always the best. Typically, duplicating and adapting some high level tests, even end-to-end tests, works quite well. Quite often the code is not testable at all, but with high level tests anything is testable.

Prepare

Refactor in depth to make the code fit for the new feature. Address testability

We make sure that the new functionality can be inserted in the code in an elegant way. 

We also make the code easily testable. Maybe we wrote end-to-end tests in the protect phase. This is the time to refactor to move those tests to a level where they're fast, predictable and easy to understand. Basically improve maintainability of the tests without sacrificing much of confidence and refactorability.

Produce

Code the new functionality in TDD

Since the code is clean and testable, this phase can be done using TDD, regardless of the code at the start.

Conclusion

With the 3Ps we write the tests, to do ourselves the refactoring behind. We minimize the time between refactoring and introducing the new feature. We know the functionality before we start. We can work in TDD to benefit from the new tests ourselves. How could the ROI be better? And if it is, wouldn't we do more of it, a lot more?

End notes

I'm not saying to avoid refactoring at the end of the story, just that if that's all we're doing, we're missing a lot.  

I'm aware don't go into much detail about how the 3Ps work in practice. I'll try to make it clearer in a subsequent post.

Translated (mostly) with www.DeepL.com/Translator. Originally posted in French



mercredi 2 novembre 2022

3P pour sortir du Legacy

Protéger, Préparer, Produire - 3P. Une machine pour sortir du legacy.

Photo by Ugne Vasyliute on Unsplash
Photo by Ugne Vasyliute on Unsplash

Peut-être que avez-vous déjà vécu des expériences du style

  • Un projet de refactoring sur plusieurs mois a été annulé
  • Des tests qui n'aident pas vraiment le dev, voire le ralentit
  • Des refactorings qui finalement se montrent pas terribles 
  • Le code qui se dégrade continuellement, malgré des stories dédiées au refactoring et aux tests que vous faites régulièrement.

Si c'est bien le cas vous seriez probablement intéressés par savoir pourquoi ces efforts n'apportent pas le bénéfice escompté et pourquoi les 3P vous aiderait.

Considérons d'abord les approches classiques de faire du refactoring ou tests dans des stories dédiés, ou le simple fait de faire la majorité du refactoring et les tests en fin de story. Certes ces approches semblent attrayantes à première vue,  mais elles ont un gros problème de ROI. En effet, elles rallongent le temps entre investissement (le refactoring) et retour sur investissement (une nouvelle fonctionnalité, facilité par ledit refactoring). 

Justement, quand on fait du refactoring dans un projet ou story à part, ou même en fin de story : qui récolte la valeur, et ce sera quand

C'est la prochaine personne qui touche le code, d'ici qq temps, avec un peu de chance. Et ce à condition que le refactoring facilitera bien le futur travail qu'on ne connaît pas... En gros le ROI sera pour quelqu'un d'autre, peut-être. Et ce dans un futur potentiellement lointain. Pas idéal, mais on peut remarquer que le ROI est plus proche si on le fait en fin de story que si on le fait dans un projet à part.

Même question pour les tests. Qui récolte la valeur des tests écrits après coup? Et ce sera quand?

La réponse est essentiellement la même. C'est la prochaine personne qui touche le code, d'ici qq temps, avec un peu de chance. A condition que les tests seront encore pertinents face à la nouvelle demande, qu'on ne connaissait pas lors de l'écriture des tests.

Bref on voit bien qu'il y un temps qui peut être plus ou moins long entre l'investissement et la récolte du bénéfice. Plus ce temps est court plus on va y trouver un intérêt. Mais ce n'est pas tout. Dans mon expérience, on ne fait pas toujours le refactoring idéal et pas toujours les tests idéaux. Entre autres parce qu'on connaît pas bien le futur.

On aurait donc intérêt à trouver une façon de récolter le plus rapidement le feedback sur la qualité de nos tests et notre refactoring, ainsi que de minimiser l'incertitude par rapport au futur. Si jamais on le faisait, on aurait alors des actions de refactoring et de tests qui apporteraient nettement plus pour un même temps investi. Dans ces conditions on aura tendance à en faire plus, voire beaucoup plus.

3P

C'est là que se trouve l'intérêt des les 3P, où nous rapprochons non seulement l'investissement du bénéfice, mais en plus c'est nous même qui sommes les premiers bénéficiaires des tests que nous venons d'écrire et du refactoring que nous venons d'opérer!  

L'idée est simple, pour chaque story décomposer le travail en 

  1. Protéger
  2. Préparer
  3. Produire

Protéger

Mise en place des tests manquants

D'abord il faut protéger les modifications avec des tests. Pour cela il faut identifier le code qu'on doit toucher pour compléter avec des tests manquants. Attention il s'agit uniquement des tests pour le code qui sera touché, tester plus serait une distraction

Le code qui sera touché doit être parfaitement couvert avec des tests bétons. Sans quoi on n'osera pas faire de refactoring digne de ce nom.

On fait plutôt des tests haut niveau dans cette phase afin qu'elles ne cassent pas lors du refactoring. Pour gagner beaucoup de temps, on ignore complètement l'aspect maintenabilité dans cette phase (voir la suite)

Préparer

Refactoring en profondeur pour rendre plus facile la nouvelle fonctionnalité

On fait en sorte que la nouvelle fonctionnalité puisse s'insérer de façon élégante dans le code. 

On rend également le code facilement testable. Peut-être qu'on a écrit des tests end-to-end dans la phase protéger. C'est le moment de refactorer pour rendre possible de déplacer ces tests à un niveau où ils seront plus rapides et où il sera plus facile de maintenir les tests.

Produire

Coder la nouvelle fonctionnalité en TDD

Puisque le code est propre et testable, cette phase peut se faire en TDD, quel que soit le code au départ.

Conclusion

Avec les 3P nous écrivons les tests, pour faire nous même le refactoring derrière. Nous pouvons travailler en TDD pour bénéficier nous même des nouveaux tests. Nous minimisons le temps entre refactoring et introduction de la nouvelle fonctionnalité. Nous connaissons la fonctionnalité avant de commencer. Comment le ROI pourrait-il être plus idéal?

Et toi lecteur, tu fais comment?

Notes de fin

Je ne dis pas qu'il faut éviter le refactoring en fin de story, simplement c'est pas la part principale.  

Je ne donne pas beaucoup de détails sur comment s'articule les 3P concrètement. Je vais tâcher à rendre ça plus claire dans un autre post.



jeudi 6 octobre 2022

Surprising Facts about the Dev Practices that drive Business Performance

I'm currently re-reading the book Accelerate, the science behind devops where a list of technical practices are found to contribute to continuous delivery, which in turn contribute to organisational performance - meaning roughly that they increase revenue, market share etc.

This post is meant both as a reminder to myself and as a way to condense the essence of the findings. I particularly put all the findings that were surprising either to the authors or to me.

Photo by Isaac Wendland on Unsplash

Practices

The following technical practices were found to be significant in predicting high Software Delivery Performance that in turn predicts Organisational Performance.


Version control

All teams have version control for the code. However having additionally

  1. system configuration

  2. build and deploy scripts 
under source control is correlated with high performance


Test automation

Having automated tests are correlated with high performance. but only if they're reliable (not flaky). Otherwise having automated test is not correlated to high performance.

Having tests primarily written and maintained by a QA team or an outsourced party is not correlated to high performance. 

I.e better have tests written and maintained by developers


Trunk based development

Teams that did well practiced Trunk Based Development of some sort. The best performing teams had fewer than three active branches at any time and branches were merged at least daily.

Using feature branching or git-flow was negatively correlated to performance. However the authors acknowledge that if you merge your branches at least daily the difference may be negligible. They also acknowledge that git-flow is a good fit for open source community projects.


Architecture

High perf is possible with all kinds of systems, provided that the systems, and the teams, are loosely coupled. That is high performing teams were found in legacy systems as well as green field projects.

Highly correlated with high performance are the ability to 

  1. Do most of the testing without integration with other platforms.
  2. Deploy without synchronisation with other teams

They also found the surprising fact that for high performing teams the number of deploys per developer increased with more devs. The inverse was true for poor performers. This stresses the importance of loosely coupled architecture.


Choice of tools

Being able to choose the best tools for the job contributes to performance and hence is found to outweigh the value from rationalisation by mandating tools within an organisation.

They mention a few exceptions to this rule.

They suggest Architecture as a service, where system teams work as service providers to other teams, as a good way to strike a balance. The feature teams ultimately retain free choice of what is best for them.


Infosec

Teams that integrate infosec in the design and development, as opposed by auditing after the fact, spend 50% less time fixing security problems. I.e infosec teams should be involved in design and assist in demos regularly. 

Info sec teams, just as architectes, can provide tools that are easy-to-consume, pre-approved libraries, packages, toolchains to make it easy for teams to develop secure applications.


Conclusion

There are a lot more in the book than a list of technical practices, it's well worth a read. I hope you found this condensed resume either useful in itself or an inspiration to read the book :)

dimanche 21 novembre 2021

TDD avec tests intégrés

Ce que nous attendons des tests dans une CI est qu'ils soient parfaitement reproductibles, extrêmement rapides, faciles à tourner n'importe où.

Ce que nous attendons des tests lorsque nous développons du nouveau code en TDD est que les tests soient représentatifs de la réalité, que la différence avec la production soit faible, que l'on puisse démarrer en TDD au plus vite (pour écrire le minimum de tests après-coup) et qu'on puisse appliquer le TDD sur une partie importante du code.

Ce sont des besoins très différents, qui de plus nous importe à des phases séparées! Pourtant nous nous efforçons de répondre aux deux besoins avec les mêmes tests en même temps. Mais qui a eu une idée pareille? 

Que se passerait-il si nous commencions les stories avec des tests intégrés et si on isolait uniquement "au besoin"? Si à la fin de la story on s'assurait d'avoir les tests maintenables et adaptés à tourner dans une CI personne ne saurait qu'on fait des tests sales entre temps. Peut-être qu'on en tirerait une grande valeur?

L'approche classique

Typiquement pour répondre aux besoins de maintenabilité des tests nous nous isolons des dépendances externes tels que apis externes, bases de données etc. Ce qui implique que nous introduisons une couche d'isolation de ces dépendances, en simulant ces dépendances avec des mocks ou des simulateurs fait main. Du côté framework qui nous appelle, on va plutôt ignorer le framework dans nos tests en appelant directement notre code.

Imaginons la situation où nous devons écrire une fonction lambda ou cloudfunction pour modifier une valeur persisté en bdd. Nous devons décrire la fonction, ses paramètres, les ressources auxquelles elle a droit, faire de la validation des droits utilisateur, extraire des données de la requête, récupérer une valeur en bdd, récupérer une autre valeur dans une api externe, sauver une nouvelle valeur en bdd et qq part dans tout ça faire un petit calcul. A quel moment faisons nous du TDD avec tests unitaires? 

Probablement assez tardivement, une fois que nous avons réussi à mettre en place tout le reste et ce uniquement sur la partie calcul - le moins risqué de tout. On comprend bien que l'apport du TDD dans l'ensemble de la story semble dérisoire! Certes au bout d'un moment la complexité grandit et les tests unitaires changent tout, mais le problème est qu'il faut attendre un certain temps et pendant ce temps les tests et le TDD semblent apporter peu, donc ne sont pas faits.

La bonne volonté de faire des tests de bonne qualité, notamment en termes d'isolation, nous contraint de ne les utiliser que pour une petite partie du problème, après avoir presque terminé la tâche. Pas étonnant qu'on commence pas avec les tests!

La bonne question à se poser serait plutôt; Comment puis-je faire pour que les tests me donnent un feedback de qualité le plus tôt possible? Quand je dis "feedback de qualité" je pense à rapide et proche de la réalité sur le code en construction.

Par exemple, il est tout à fait possible de lever temporairement les contraintes de besoins tel que la répétabilité, la rapidité et tout autre aspect de la maintenabilité. Des tests qui prennent 1 seconde à tourner, voire plus, ne poseront pas de problème pendant qq heures. Des tests qui ne tournent pas sur la machine de mes collègues ne poseront pas de problème tant que je ne les mets pas dans le pipeline. Même des tests que je suis le seul à comprendre (avec mon pair) suffisent amplement tant que personne d'autre ne travaille dessus. Je pourrais alors faire du TDD avec des tests très haut niveau en totale intégration avec toute dépendance externe....

TDD en totale intégration avec toute dépendance

Cette approche est le mieux illustré par le workflow d'une story lorsqu'il n'existe aucun code existant. Mon workflow dans ce cas est de commencer avec un test très haut niveau, au niveau de l'api, sans rien mocker, ni même des prestataires externes. En pur TDD je vais faire passer ce test en faisant du code sale, dans cet exemple en faisant tout le code dans mon contrôleur. Mon code ne sera pas très gros, car il n'aura pas de branches, pas de validation, pas de configurabilité, ce sera pour plus tard grâce à d'autres tests (justement TDD), ainsi j'aurai fait passer le test assez rapidement. Je passe ensuite à un deuxième test aussi haut niveau, je le fais passer rapidement, et ainsi de suite.

Au bout d'un moment, typiquement au bout de 3-5 tests la logique commence à se cristalliser. Le comportement d'un service aura été découvert, les interactions avec la BDD stabilisés. Il y aura aussi de la logique pure de transformation de données et validation. C'est un bon moment pour concevoir une interface mockable pour s'isoler de la dépendance externe, concevoir l'interface d'une classe repository, une classe de service et peut-être des value objects. Ces extractions rendent possible l'isolation des dépendances et je peux ainsi continuer avec d'une part des tests haut niveau mais cette fois-ci sans dépendances, soit des tests haut niveau de l'hexagone, et d'autre part des tests bas niveau de mes adapteurs (le code qui invoke des services externes) si j'ai besoin de les explorer davantage.

C'est à ce moment du workflow que les tests commencent à devenir maintenables. C'est donc qu'à partir d'un certain moment que je fais mes choix de design interne et ça c'est clé! Certes on peut faire ces choix au début, mais cela implique de prendre des décisions avec moins d'informations, donc plus souvent sous-optimales. Si on doit revenir sur un choix de design cela coûtera plus cher si des tests sont déjà écrits contre ces choix! Cerise sur le gâteau, ces tests ne pourront pas protéger le refactoring. Mon choix par défaut est donc de commencer avec des tests intégrés afin de retarder la prise de décisions, avoir le maximum de feedback proche de la réalité et assurer la refactorabiltié de l'ensemble le cas échéant. 

Mais là où je trouve l'isolation "d'office" néfaste c'est pour les débutants en TDD, soit presque la totalité des devs… Les inciter à résoudre les problèmes de maintenabilité des tests avant même de commencer à les écrire leur rend la tâche plus dure. Le tout pour un bénéfice discutable, ou inexistant, voire même négatif quand cela, comme souvent, résulte dans des tests pas écrits du tout! Pour apprendre à travailler avec des tests il faut déjà en faire. 

Conclusion

Ce n'est pas parce qu'on veut des tests maintenables qu'on dois s'infliger cette obligation dès le 1er test.

En particulier il peut y avoir un certain nombre d'avantages à faire autrement :

  • Dans la phase TDD (construction) les tests totalement intégrés peuvent nous apporter plus que des tests isolés.
    • En terme de fidélité avec la réalité
    • En terme de couverture
  • Il est plus facile de commencer en TDD avec des tests intégrés.
  • Plus les tests sont haut niveau, plus ils permettent des refactorings

Pour être clair je ne transige pas sur les qualités des tests à la fin de la story. Simplement j'utilise souvent des tests intégrés au début et pendant une story. Je pense aussi que c'est plus facile d'en tirer un bénéfice dans la phase de construction, ce qui inciterait plus de gens à essayer le TDD sur du vrai code.

Surtout, n'hésite pas à essayer.

Notes de fin

En pratique je fais des variantes dans le workflow ci-dessus, plus classiques comme approche. Par exemple, pour intégrer un service externe on peut très bien directement explorer l'api avec des tests bas niveau. Un autre exemple c'est lorsque le code d'intégration avec une dépendance externe existe déjà j'utilise directement son simulateur, au lieu d'intégrer avec le service externe. 

Encore un autre exemple est lorsque la story ne concerne pas les dépendances alors évidemment on utilise pas de test intégré.


lundi 4 octobre 2021

Configure XDebug for code coverage in PhpStorm


A frequent problem when running TDD or legacy testing workshops is to get code coverage for PHP to work within PhpStorm. In fact XDebug is needed and it can be a bit tricky to install on some configurations. This post shows how to make it work any time, by running it within a docker container.

PhpStorm has the ability to run the php executable, for testing or for debugging using a docker container. While that's easy and well documented we also need a docker image with xdebug installed and we also need to configure the code coverage to NOT try to code cover the whole "vendor" directory which would be slow to say the least. So let's dive into how it's done

Configure a php cli to run in docker

Open settings and click on the "three dots"






Click "+"





Choose "From docker, Vagrant, ..."




Enter the full image name. 





At this stage your tests should run, the debugger should work. You might have to configure the "Test Framework" in the settings, and in particular the autoloader to use. For instance 





Note for PHP > 7.3 : PhpStorm produced images up until PHP7.3. But if you need 7.4 or above you'll have to make your own. For instance here's the whole Dockerfile you need for 7.4

FROM php:7.4
RUN pecl install xdebug-3.1.0
RUN docker-php-ext-enable xdebug

Configure the code coverage to exclude vendor

It boils down to use the option of "whitelist" or the more recent "coverage" to tell phpunit which source code to instrument for code coverage.

While it should work to pass options to phpunit to ignore vendor, it seems that PhpStorm makes this impossible. I've never been able to configure this without an xml file. On the other hand with the xml file it's easy.




 



Add a section "filter" or "coverage" with your source code directory.












Now you're ready to run the tests with code coverage




mardi 13 avril 2021

On premature optimisation



Dev: this code is more optimal, i.e. better

Senior dev: most performance problems appear where we don't expect them, let's wait and measure

Senior dev: Besides most code optimizes one thing, it can be CPU-time, but also readability (i.e. developer time), but rarely more one thing. If we have to choose, which is the best optimisation in this case?


Dev: we need to optimise before it is too late

Senior dev: most perf problems I've had to deal with, were very simple to solve. Just limit the number of round-trips to the db, if not possible parallelize, if not possible maintain a read-model. Let's wait and measure.


Dev: but some problems require a completely different paradigm

Senior dev: True, and most often the business never scales that much. Let's wait (building a working system), validating that we actually have that problem! And read up on the CALM theorem, CRDT's*, CQRS etc


PS: Both devs are me. Just separated by two decades of experience 

* CRDT is a basically a data-structure that allows parallel writes

mardi 5 janvier 2021

Hexagonal architecture vs Functional core / Imperative shell

There's been some comparison about theese two architectural styles recently. To me they address totally very different concerns and are by no means mutually exclusive. Let's review that visually.

Let's take the example of some user calling a rest-service with some request for modification. A typical application will have the workflow of 

  • Validation 
  • Load state from db 
  • Compute new state
  • Save state to db

Hexagonal architecture

I assume you have basic knowledge about this architecture. If not you might want to have a look at this. In this paradigm we'd distribute the steps of the given use case in the following way.

Inside the "hexagon" everything except what talks to the outside world


We have separated the blue parts from the yellow because those are the parts that talk to the outside world. The boundaries that we create by separating the repository from the rest are useful for testability (and a few other things that I won't go into in this post). We can test the whole logic by simply mocking the repository. So here the emphasis is on creating boundaries from the outside world. All the rest is considered inside the hexagon.

Functional core / imperative shell

In this paradigm we focus on what is pure and impure. A pure function is a function that needs nothing else than values as inputs and that does nothing else than returning a result. No side effects, no loading data from the file-system etc. A pure function always returns the same values given the same inputs. The properties of a pure function make them very nice to work with. For one thing they can be tested without any mocks, they don't surprise us, results can be cached(!) and so on. Because they have these nice properties we want to separate them from the rest, and possibly maximize the scope of them.

Let's look at the same application from the point of view of what is functional core and what is the imperative shell. 
The adapters and all the coordination logic is in the imperative shell. This shell delegates as much as possible to the functional core through calls to pure functions.


There is no change! The only difference is how we classify things. Admittedly this is because I already separated the pure parts from the rest. Note that the diagram is simplified in the sense that the validate and compute stages do not actually call the next stages. Both are pure functions that just return some value. 

Although in this case the architecture is both hexagonal and functional core / imperative shell, it is seldom the case. Often what we do in traditional applications is a lot of validation and computational logic in the middle of our service classes, just next to where we load and save state. Even worse we load state and save state in several steps with computational logic intermingled so there is very little to extract. 

Functional islands

What is called functional core is actually more like functional islands. It only makes sense if we can make them big enough so that at-least it makes sense to test them in isolation. That is, we tend to mould the architecture to increase the size of the functional islands. To get the most out of functional core we will tend to load everything beforehand and save everything afterwards, limiting the round-trips to the DB while risking loading data that is not used. One of the architectures that gives us the greatest opportunity for functional islands is EventSourcing, a though I detail here.

Personally I tend to extract pure functions a lot. It's easy to edge towards this in legacy apps and I find it a better trade-off for new apps because it makes testing a breeze, tests are blazing fast, they are easier to read without mocks and it is easier to write them in a manner that minimizes test maintenance. I tend not to do it when the islands are too small.

For more on this paradigm, here's a good collection of links

Conclusion

The two patterns are not mutually exclusive, in fact a lot of the goodness that both strive for is common, like testability and separation of concerns. As a result we get changeability.

Comment éviter un piège avec les systèmes "EventSourced"


Dans ce type de système apparaît un problématique pour laquelle nous sommes mal armées car elle n'existe pas dans des systèmes traditionnels. Certes il n'est pas compliqué de l'éviter si on le connaît, mais il est possible de le découvrir tard et ainsi de payer cher la correction.

TLDR; l'état perceptible d'un système est un cache. Quand on vide ce cache pour le re-calculer, il va être calculé avec la dernière version du code et donc le résultat pourrait être différent. Pour s'en prémunir il y a une invariante à respecter

Quelques bases pour comprendre

Pour comprendre la problématique, revoyons d'abord les bases de l'approche EventSourcing. Visuellement, ca va être simple. Si jamais mon résumé en 10 lignes ne suffit pas, il a y a ceci.

Quand une commande arrive par le biais d'un utilisateur ou d'une autre application, elle se fait transformer en un event (ou plusieurs) qui est persisté. Ces évènements sont utilisés pour calculer l'état (state) actuel du système. La fonction decide transforme la commande en event et la fonction evolve transforme un event en état qui est incrémenté avec chaque nouvel évènement. Certes les deux fonctions ont aussi besoin  de l'état actuel pour ce calcul, mais j'ai omis ce détail dans le dessin. 





On pourrait faire sans l'état mais pour des raisons de performance on fait un (ou plusieurs) états. A n'importe quel moment cet état peut être réinitialisé, c'est pas grave on sait tout recalculer à partir des évents. 

C'est ce recalcul qui présente un piège de temporalité.

Le piège

De temps en temps on livre de nouvelles versions de l'application. En particulier la fonction evolve, comme ceci



Ici les deux premières itérations d'état ont été calculées avec la première version d'evolve et les deux derniers avec evolve'. Dans le cas  d'un recalcul c'est différent car tous les évènements sont processés avec evolve'.  Si la dernière version de evolve traite un évent différemment alors il y a une différence, on n'a plus s4 comme état final mais s4'.



Mais quel est le problème? C'est pas ce qu'on veut? Eh ben pas toujours. Cela pourrait être un bug. Imaginons qu'on calcule la taxe sur des évènements d'achat et qu'à un moment donné la taxe a changé. On veut que ce calcul donne le même résultat qu'il soit fait maintenant ou au fil de l'eau. L'utilisateur ne doit pas être impacté par quand on a calculé l'état. Imaginons l'utilisateur qui a travaillé dur pour attendre un niveau sur StackOverflow. Il serait bien content s'il perd des points suite à une livraison.

L'invariant à respecter

Une nouvelle version de la fonction evolve doit toujours donner le même résultat pour un événement donné.

Si on choisit de le respecter, les états, les caches du système, sont toujours cohérents et n'ont pas besoin d'être recalculés. Par contre dans le cas d'un bug dans la fonction evolve on aurait un état incorrect, alors il est probable qu'on souhaite recalculer l'état pour avoir un résultat différent justement. Dans les deux cas c'est un choix intentionnel, le pire serait de ne pas avoir les idées claires sur ce mécanisme et de se retrouver surpris devant des changements d'état d'apparence aléatoires.

Mais comment fait-on pour résoudre ce dilemme de ne pas pouvoir changer le comportement actuel, tout en introduisant de nouvelles fonctionnalités ou des changements de règles métier? 

Les solutions

La seule axe de solution, à ma connaissance, se trouve dans la fonction decide qui décide quel événement créer suite à la commande. Elle peut changer, une même commande qui arrive maintenant n'est pas obligé de produire le même événement. Pourquoi donc? Simplement parce que la fonction decide s'exécute toujours au moment de l'arrivée de la commande, jamais de façon rétroactive. Cette fonction décide comment interpréter la commande en créant un événement, qui est persisté. A partir de là je connais trois approches, créer un nouveau type d'évènement, créer un nouvelle version ou se baser sur la date.

Un nouveau type d'événement

Si on change decide pour émettre une nouvelle version de l'événement alors evolve peut le traiter en cas "special". Puisque ce évènement n'existait pas avant alors on a pas cassé l'invariant. Par exemple pour un achat à 5% de TVA au lieu de 20%, au lieu d'émettre Purchased on peut émettre PurchasedVAT5-5. Toute la nouvelle logique se trouve dans le traitement de ce nouvel événement. On a pas touché à l'ancien car pour un évènement Purchase on applique toujours l'ancien taux.

Evolve reste ouvert à l'extension de nouveaux types, mais fermé à la modification sur les types existantes

Une nouvelle version d'un event existant

Si on a plus besoin de l'événement d'avant. Genre il n'existe plus de TVA à 20% alors on peut utiliser l'ancienne type. Le fait d'annoter l'événement avec une version +1 permet d'appliquer un traitement différent et donc préserver l'invariant. 

Se baser sur la date

Une autre possibilité est de se baser sur la date. L'invariant nous interdit de changer l'algo pour un même évènement, mais finalement les évènements récentes ont une date différente. Donc il suffit de préserver le fonctionnement pour les évènements antérieures à une certaine date.

Utiliser une date semble intéressant car c'est légèrement plus facile et c'est peut-être justifié dans certains cas. L'inconvénient est que dans cette approche le code n'exprime pas bien le concept métier qui a changé. La règle métier sera enfouie dans un simple if quelque part. Alors qu'en introduisant un nouveau type cela devient plus visible dans la structure du code, même dans la base de données. Avec un nouveau type, le code communique mieux le métier.

Conclusion

Pour un changement de fonctionnalité, ajouter un nouvel type/variante d'évènement en changeant decide. Étendre la fonction evolve pour le traiter, sans changer l'existant.

Une façon de résumer est de dire que 

Tant qu'une nouvelle version de evolve est une extension de la version précédente alors on n'a pas besoin de recalculer les caches du système 

accessoirement on est aussi certain de ne pas surprendre l'utilisateur - il verra toujours un état cohérent avec le passée.

Bref, c'est un piège qui peut causer bien des maux de tête, mais il est facile de s'en prémunir, dès lors que l'on sait s'en méfier.

Merci à Jérémie Chassaing de m'avoir expliqué le problème, et à Arnaud Bailly pour son aide. 

lundi 14 décembre 2020

Coder et tester "mieux" avec CQRS et EventSourcing

Le titre fait le plein de buzzwords, mais c'est pas pour cela que je l'ai choisi. Non j'ai la conviction et l'expérience que l'architecture basé sur CQRS - le principe de séparation de commande et de query et le EventSourcing - le fait de calculer l'état de l'application basé sur les évènements passés, facilite les tests automatiques tout comme il facilite l'expression du domaine métier dans le code - le DDD. Je vais essayer d'expliquer pourquoi de manière visuelle. Mais tout d'abord une petite explication sur ce que je veux dire par "mieux" dans le titre de ce billet. Ici mieux veut simplement dire "plus métier", c'est pas toujours "mieux" mais "mieux" fait mieux dans un titre ;) 

Bon revenons aux choses sérieuses, en commençant par un diagramme basique de CQRS implémenté de façon EventSourcing



CQRS et EventSourcing

Lorsqu'une commande arrive, elle se fait valider par le CommandHandler qui la transforme en 0 à n évènements. Par exemple une demande de réservation hotel, peut se solder par une réservation ou non. Les évènements, s'il y en a, se font ajouter à la log des events et servent aussi à entretenir des "projections". Les projections servent avant-tout à répondre rapidement à une question (query), car avec ces projections pré-calculées pas besoin de parser l'ensemble des évènements pour chaque question. A noter tout de même qu'on peut faire sans projection pour les cas qui ne nécessitent que peu de performance.

Code orienté métier 

Mais qu'est-ce qu'il y ait de si fantastique pour les tests et en quoi cela permet de mieux exprimer le domaine? Pour y répondre regardons de plus près les 3 éléments principaux :

  1. CommandHandler
  2. ProjectionHandler
  3. QueryHandler

Le CommandHandler consiste à recevoir la commande, une intention utilisateur, la valider par rapport aux évènements passés et la traduire en quelques évènements du domaine. Bon déjà l'entrée et la sortie sont purement des données du domaine, compréhensibles par n'importe quelle personne du domaine métier.

Le ProjectionHandler reçoit des évènements et par rapport à l'état actuel de la projection calcule un nouveau état de projection. Les projections sont souvent des états pré-machés pour l'UI ou pour une autre application, donc proche du besoin métier. 

Finalement le QueryHandler répond à une requête GET de façon spécialisé (non CRUD) soit en passant une projection directement soit en parsant des évènements métier. Parfois un peu des deux. 

On voit bien que la logique est très tourné métier et que les interactions avec les bases de données sont faibles. Donc le code est à la fois tourné métier mais aussi peu couplé à la BDD.

Fonctions pures

Mais ce n'est pas tout. Ce que moi je fais énormément dans le code un peu partout, quelque soit l'architecture en place, est d'isoler des parties de pure logique. En gros avoir des fonctions qui ne font que prendre des valeurs en entrée et qui retournent des valeurs en sortie, rien d'autre. Dans le cas ci-dessus ça donnerait quelque chose comme (la logique pure en jaune)






Tel des satellites la logique est extraite et il ne reste que le travail de coordonneer appel à la BDD et appel à la logique pure. Presque l'ensemble de la logique métier se trouve alors dans des fonctions pures, toutes bien orientée métier. 

Les tests

Mais alors en quoi cela aide pour les tests? Ben c'est le doux rêve de tout écrivain de test d'avoir des fonctions pures. Pas de dépendances hard-codés, pas de dépendances à injecter, pas de mocks à initialiser. En gros cela donne des tests faciles à écrire et surtout faciles à lire. De plus ces tests ne sont pas trop sensibles à des refactorings, car plus le code est orienté métier et haut niveau plus son API est stable.

Attention ici je ne décris que les avantages de cette architecture, il y a bien-sur des inconvénients, mais ce sera le sujet d'un autre billet. Peut-être :)







dimanche 6 décembre 2020

Faut-il commenter son code?

Parfois on croise des développeurs qui disent qu'il faut toujours commenter son code. Toutes les fonctions. Parfois on croise des personnes qui ne veulent pas de commentaire du tout. Nul part! 

D'autres personnes ont un avis moins tranché, elles ont déjà vécu des moment où elles auraient pu payer très fort pour avoir un peu de doc, tout comme des fois où la doc il ne fallait surtout pas s'y fier! 

Prenons un peu de hauteur pour voir plus clair. La documentation a ou peut avoir une valeur, mais elle a aussi un coût.

La valeur

Les commentaires sont comme toute documentation, elles peuvent apporter de la valeur, parfois beaucoup. Je pense notamment à des documentations d'api publique de style swagger ou la documentation sur les libraries open source. Je pense aussi à des bouts de code bien tordus pour une raison, là une bonne documentation n'a pas de prix.

Il y a des contre exemples (plus nombreux) comme par exemple ce code qui n'apporte rien de plus que la signature même de la fonction

   /**
    * @param persons list of persons
    * @return number max age of group
    */
   function maxAgeOfGroup(persons: Person[]): number
       ... 
   }
    

La valeur de la documentation est en effet très variable. Probablement il s'agit d'une courbe suivant la loi de puissance. Si c'est bien le cas, alors il y a un faible nombre de commentaires qui apportent une majorité de la valeur.


La valeur des commentaires est très variable


Le coût

Ce qu'il est facile d'oublier est que toute commentaire vient avec un coût de maintenance. Quand on modifie le code il faut parfois modifier le commentaire. Quand la maintenance n'est pas faite cela se solde potentiellement par un bug, car on s'est fié à un commentaire qui mentait. Plus souvent les développeurs arrêtent tout simplement de le regarder. On arrête les frais. Mais même si on ignore les commentaires et on les laisse là sans les maintenir ils finissent quand même pour demander un coût. Les commentaires prennent de la place sur nos écrans au dépens des lignes des code. Dans un code commenté on voit en effet moins de lignes. Moins de lignes veut parfois dire une vue d'ensemble réduite.

Donc un commentaire qui n'a que peu de valeur, finit par coûter au lieu d'apporter.

Il faut donc écrire des commentaires avec parcimonie, là où on pense qu'elle sera lue, utile et maintenue.

Quand alors?

Je ne sais pas réellement, je ne peux partager que la conclusion que je prend personnellement. Outre la documentation des apis "publiques" qui a une valeur évidente j'essaie de mettre un maximum d'effort à rendre mon code explicite et écrit pour le lecteur. Mais je n'arrive pas toujours bien, alors dans ce cas je documente. 

Je documente quand j'échoue à rendre le code suffisamment explicite.

D'ailleurs je conseille vivement le livre Living Documentation par Cyrille Martraire pour la clarté qu'elle apporte sur ce terrain.

jeudi 5 novembre 2020

Continuous Delivery : What about a database change?



Yesterday I ran a Continuous Deployment workshop in production, namely Continuous Deployment of a
database change. Sounds advanced? 

Actually it's easy, just counter intuitive...


It can be really scary to roll out this big feature because it includes a database change. But by splitting it into 3 to 5 chunks it's a lot less scary and can be done with sometimes shorter downtime. Here are the chunks:


1st deploy: Add the column/table to the database

2nd deploy: fill the new column with values. Check

3rd deploy: Rerun migration script while app is down. New code writing to the new column (but not reading). 

4th deploy: New code or feature flip, to only use the new column both for reads and writes

5th deploy: remove old column and old code. (Possibly days later)


dimanche 4 octobre 2020

Less error handling noice in GO: CombineErrors


TDLR; A nice trick to greatly reduce error handling in Go code.



Error handling in go can easily get in the way of readability in Go. Big time! Reducing the verbosity of error handling and in particular the amount of if-statements that invade our application code has been occupying my thoughts for a few months now.  
I’m appalled that it’s so difficult to find good tricks on this. The go blog has very little information on this. Let's face it I risk GOing nuts!


Some time ago I realised that some errors don’t have to be handled immediately. Postponing error handling opens up a few possibilities. The first and foremost is CombiningErrors. But let’s first look at the problem. Here’s a bit of typical code for obtaining data from somewhere.



Now that’s a short example but imagine this constructor taking a few more arguments.


In this case we’re producing 3 errors. What if we could just combine those three into a single error, that will be nil if all sub-errors are nil, then we could return only one function. Something like 



This combining function seems like generic code so there’s probably a library out there doing the job. Indeed there is, for instance https://godoc.org/go.uber.org/multierr and https://godoc.org/github.com/hashicorp/go-multierror 


Now there’s just one small improvement to do here, one that I actually learnt from one of the posts at the go blog. There’s no need to check for a combined error with an extra if-statement. We can just return the globalErr, it'll be nil if all sub-errors are nil, and the calling function will test for errors either way.




Much better :)

Now this is a trick that won't apply in every case. Actually when does it apply? Well I'd argue that this can be used whenever there is no damage done by pursuing execution after a first error. Which in my team's code is quite often. Nice :)



Next time let’s look at another trick where we’ll transform a sequence of error generating statements into a list of functions. Let’s see how that helps.