L'Agilitateur - Mot-clé - anti-ifCréation de logiciels : de l'agilité à l'artisanat2021-10-09T15:11:31+02:00urn:md5:7668924f626a6543fa389f1b4e47e529DotclearSteve Wozniak is not boringurn:md5:675960135fc9e68c4c9d3948d022b4df2016-09-10T21:45:00+01:002016-09-10T21:45:00+01:00Olivier AzeauIn Englishanti-ifcsharpgolanggotorefactoring<p>I saw <a href="https://twitter.com/francesc/status/774438145433534466">this tweet</a> a few hours ago and found the piece of code quite interesting: a small algorithm with searches, logic, external interaction and a nice comment explaining the whole thing. I could have written this kind of code ten years ago. Today, my problem is that my eyes start bleeding when I bump into a mix of loop and conditions, especially when the mix cannot be easily tested.<br />
Let's try to write it differently.</p> <p>First, let's have a look at the code.
<a href="https://agilitateur.azeau.com/public/agilitateur/boringWozniakGoLang.jpeg" title="boringWozniakGoLang.jpeg"><img src="https://agilitateur.azeau.com/public/agilitateur/.boringWozniakGoLang_m.jpg" alt="boringWozniakGoLang.jpeg" style="display:table; margin:0 auto;" title="boringWozniakGoLang.jpeg, sept. 2016" /></a></p>
<p>We hope to write it in a different way but we first need a safety net to avoid changes in the code behaviour. We usually have test cases for that.<br />
Since this code uses a random number generator, we cannot really test it as is. A classic technique is to decouple the random number generator so that we can inject a fake one (called a 'stub') during the tests.</p>
<p>Since I don't know much about the Go language, I will first convert the code to a language I'm more familiar with (here C#). The two codes are almost identical.</p>
<pre class="brush: csharp;">
// GetRandomName generates a random name from the list of adjectives and surnames in this package
// formatted as "adjective_surname". For example 'focused_turing'. If retry is non-zero, a random
// integer between 0 and 10 will be added to the end of the name, e.g `focused_turing3`
public static string GetRandomName(int retry)
{
var rnd = new Random();
begin:
var name = string.Format ("{0}_{1}", left[rnd.Next(left.Length)], right[rnd.Next(right.Length)]);
if( name == "boring_wozniak" )/* Steve Wozniak is not boring */
{
goto begin;
}
if( retry > 0 )
{
name = string.Format ("{0}{1}", name, rnd.Next(10));
}
return name;
}
</pre>
<p>Now I can decouple the random number generation</p>
<pre class="brush: csharp;">
public static string GetRandomName(int retry)
{
var rnd = new Random();
return GetRandomName(retry, max => rnd.Next(max));
}
public static string GetRandomName(int retry, Func<int,int> rnd)
{
begin:
var name = string.Format ("{0}_{1}", left[rnd(left.Length)], right[rnd(right.Length)]);
if( name == "boring_wozniak" )/* Steve Wozniak is not boring */
{
goto begin;
}
if( retry > 0 )
{
name = string.Format ("{0}{1}", name, rnd(10));
}
return name;
}
</pre>
<p>At this point, we can write a bunch of tests. These tests will serve 2 purposes:</p>
<ul>
<li>provide a safety net for refactoring</li>
<li>document the program behaviour</li>
</ul>
<p>The 2nd purpose allows us to remove part of the comments: the examples ('focused_turing', 'focused_turing3') and the whys ("Steve Wozniak is not boring").<br />
They are better written as plain code because the code cannot lie.<br /></p>
<p>The full set of tests is <a href="https://github.com/Oaz/BoringWozniak/blob/master/Tests/Test.cs">here on github</a>.<br />
The tests are better than comments because when the code becomes wrong, for example, if remove the "boring_wozniak" condition, a good test tells me why the code is not correct:
<img src="https://agilitateur.azeau.com/public/agilitateur/SteveWozniakIsNotBoring.png" alt="SteveWozniakIsNotBoring.png" style="display:table; margin:0 auto;" title="SteveWozniakIsNotBoring.png, sept. 2016" /></p>
<p>Now we can try to remove the mix of loop and conditions.<br />
I have no problem with the use of goto but a mix of "goto" and "if" leads to unnecessary complicated code. Here, the goto serves a single purpose: loop over the names generation.<br />
Here is an alternate implementation where the loop is decoupled from the conditions:</p>
<pre class="brush: csharp;">
private static IEnumerable<string> RandomNames(Func<int,int> rnd, Func<string,string,string> format)
{
begin:
yield return format(adjectives[rnd(adjectives.Length)], surnames[rnd(surnames.Length)]);
goto begin;
}
</pre>
<p>Being given an enumeration of random names, the main logic becomes easier to write: we want the first name which is not "boring_wozniak" and we want to concatenate it with an optional suffix:</p>
<pre class="brush: csharp;">
public static string GetRandomName(int retry, Func<int,int> rnd)
{
var baseName = RandomNames (rnd, (adjective, surname) => string.Format ("{0}_{1}", adjective, surname))
.First (name => name != "boring_wozniak");
var optionalSuffix = (retry > 0) ? rnd (10).ToString () : string.Empty;
return baseName + optionalSuffix;
}
</pre>
<p>When the code is written this way, I no longer feel the need for a comment explaining what is going on. The logic is now written with immutable variables. We no longer have to scratch our head about this "name" variable whose value could change many times.<br />
The conditions are no longer if blocks and, as a consequence, have a narrow focus. One of them just just taking the first item verifying some predicate in an enumeration.
The other one is now a ternary conditional operator where we just choose between two values without any side effect.</p>
<p>Is the final code much better, as I tend to think, or am I just splitting hairs?
What do you think?</p>