{"id":2841,"date":"2015-01-21T13:00:36","date_gmt":"2015-01-21T13:00:36","guid":{"rendered":"http:\/\/writeasync.net\/?p=2841"},"modified":"2015-01-16T02:56:04","modified_gmt":"2015-01-16T02:56:04","slug":"a-loop-is-a-responsibility","status":"publish","type":"post","link":"http:\/\/writeasync.net\/?p=2841","title":{"rendered":"A loop is a responsibility"},"content":{"rendered":"<p>A lot has been written about the <a href=\"http:\/\/en.wikipedia.org\/wiki\/Single_responsibility_principle\">single<\/a> <a href=\"http:\/\/msdn.microsoft.com\/en-us\/magazine\/cc546578.aspx#id0390008\">responsibility<\/a> <a href=\"http:\/\/blog.8thlight.com\/uncle-bob\/2014\/05\/08\/SingleReponsibilityPrinciple.html\">principle<\/a> over the years. Rather than rehash what <a href=\"http:\/\/www.objectmentor.com\/resources\/articles\/srp.pdf\">many<\/a> <a href=\"http:\/\/www.developerfusion.com\/article\/137636\/taking-the-single-responsibility-principle-seriously\/\">others<\/a> have said better, I want to give a common example of how a seemingly innocuous control structure can subtly but surely increase coupling and confusion.<\/p>\n<p>Imagine a system that depends on a collection of &#8220;<a href=\"http:\/\/martinfowler.com\/articles\/microservices.html\">microservices<\/a>.&#8221; These services all have a simple &#8220;ping&#8221; API that can be accessed over HTTP. The response code indicates whether the service is functioning properly (e.g. HTTP 200 means everything healthy, HTTP 503 means the service is degraded, and so on). Now suppose that you need to write a watchdog which can periodically ping each service and raise a warning or error based on the result.<\/p>\n<p>Here are the supporting interfaces:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\npublic interface IPingClient\r\n{\r\n    string Id { get; }\r\n    Task&lt;HttpStatusCode&gt; PingAsync();\r\n}\r\n\r\npublic interface IAlert\r\n{\r\n    Task RaiseWarningAsync(string id, int code);\r\n    Task RaiseErrorAsync(string id, int code);\r\n}\r\n<\/pre>\n<p>And now a na\u00efve first cut at the health check function:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\npublic static async Task CheckHealthAsync(IAlert alert, IPingClient&#x5B;] clients)\r\n{\r\n    foreach (IPingClient client in clients)\r\n    {\r\n        HttpStatusCode status = await client.PingAsync();\r\n        switch (status)\r\n        {\r\n            case HttpStatusCode.OK:\r\n                \/\/ All is good; do nothing.\r\n                break;\r\n            case HttpStatusCode.ServiceUnavailable:\r\n                \/\/ Degraded but still alive.\r\n                await alert.RaiseWarningAsync(client.Id, (int)status);\r\n                break;\r\n            default:\r\n                \/\/ Treat anything else as fatal.\r\n                await alert.RaiseErrorAsync(client.Id, (int)status);\r\n                break;\r\n        }\r\n    }\r\n}\r\n<\/pre>\n<p>It looks simple enough. Let&#8217;s move on to discuss what the tests for this function would look like. There is the degenerate case of an empty client array. We don&#8217;t expect anything to happen (no pings or alerts) and luckily the code &#8220;just works&#8221; in that situation:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\n\/\/ Stub to ensure that an alert is never raised\r\npublic class FailOnAnyAlert : IAlert\r\n{\r\n    public Task RaiseWarningAsync(string id, int code)\r\n    {\r\n        throw new NotSupportedException(&quot;This should not be called.&quot;);\r\n    }\r\n\r\n    public Task RaiseErrorAsync(string id, int code)\r\n    {\r\n        throw new NotSupportedException(&quot;This should not be called.&quot;);\r\n    }\r\n}\r\n\r\n&#x5B;TestMethod]\r\npublic void CheckZeroItemsDoesNothing()\r\n{\r\n    Task task = Watchdog.CheckHealthAsync(new FailOnAnyAlert(), new IPingClient&#x5B;0]);\r\n\r\n    Assert.IsTrue(task.IsCompleted);\r\n    Assert.IsNull(task.Exception);\r\n}\r\n<\/pre>\n<p>Now we need to verify an array of size one, but there are three simple sub-cases: the status was OK (no alert), the status was ServiceUnavailable (warning), and the status was anything else (error):<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\n\/\/ Stub to always return given ID\/status code\r\npublic class PingClientStub : IPingClient\r\n{\r\n    public PingClientStub(string id, HttpStatusCode code)\r\n    \/\/ ...\r\n}\r\n\r\n\/\/ Stub to keep track of warnings\/errors raised\r\npublic class AlertStub : IAlert\r\n{\r\n    \/\/ ...\r\n    public IList&lt;Tuple&lt;string, int&gt;&gt; Warnings { get; private set; }\r\n    public IList&lt;Tuple&lt;string, int&gt;&gt; Errors { get; private set; }\r\n    \/\/ ...\r\n}\r\n\r\n&#x5B;TestMethod]\r\npublic void CheckOneItemOnOKDoesNothing()\r\n{\r\n    Task task = Watchdog.CheckHealthAsync(new FailOnAnyAlert(), new IPingClient&#x5B;] { new PingClientStub(&quot;One&quot;, HttpStatusCode.OK) });\r\n\r\n    Assert.IsTrue(task.IsCompleted);\r\n    Assert.IsNull(task.Exception);\r\n}\r\n\r\n&#x5B;TestMethod]\r\npublic void CheckOneItemOnUnavailableRaisesWarning()\r\n{\r\n    AlertStub alert = new AlertStub();\r\n\r\n    Task task = Watchdog.CheckHealthAsync(alert, new IPingClient&#x5B;] { new PingClientStub(&quot;One&quot;, HttpStatusCode.ServiceUnavailable) });\r\n\r\n    Assert.IsTrue(task.IsCompleted);\r\n    Assert.IsNull(task.Exception);\r\n    Assert.AreEqual(1, alert.Warnings.Count);\r\n    Assert.AreEqual(Tuple.Create(&quot;One&quot;, 503), alert.Warnings&#x5B;0]);\r\n}\r\n\r\n&#x5B;TestMethod]\r\npublic void CheckOneItemOnAnyOtherStatusRaisesError()\r\n{\r\n    AlertStub alert = new AlertStub();\r\n\r\n    Task task = Watchdog.CheckHealthAsync(alert, new IPingClient&#x5B;] { new PingClientStub(&quot;One&quot;, HttpStatusCode.BadRequest) });\r\n\r\n    Assert.IsTrue(task.IsCompleted);\r\n    Assert.IsNull(task.Exception);\r\n    Assert.AreEqual(1, alert.Errors.Count);\r\n    Assert.AreEqual(Tuple.Create(&quot;One&quot;, 400), alert.Errors&#x5B;0]);\r\n}\r\n<\/pre>\n<p>For completeness we would also want to verify an array with more than one item, so let&#8217;s try with two clients; if we want to be exhaustive we can cover again the three sub-cases from the single item case but maybe it&#8217;s okay to just test a different outcome for each item in a single test:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\n&#x5B;TestMethod]\r\npublic void CheckTwoItemsRaisesErrorAndWarningAccordingToEachStatus()\r\n{\r\n    AlertStub alert = new AlertStub();\r\n    IPingClient&#x5B;] clients = new IPingClient&#x5B;]\r\n    {\r\n        new PingClientStub(&quot;One&quot;, HttpStatusCode.BadRequest),\r\n        new PingClientStub(&quot;Two&quot;, HttpStatusCode.ServiceUnavailable)\r\n    };\r\n\r\n    Task task = Watchdog.CheckHealthAsync(alert, clients);\r\n\r\n    Assert.IsTrue(task.IsCompleted);\r\n    Assert.IsNull(task.Exception);\r\n    Assert.AreEqual(1, alert.Errors.Count);\r\n    Assert.AreEqual(Tuple.Create(&quot;One&quot;, 400), alert.Errors&#x5B;0]);\r\n    Assert.AreEqual(1, alert.Warnings.Count);\r\n    Assert.AreEqual(Tuple.Create(&quot;Two&quot;, 503), alert.Warnings&#x5B;0]);\r\n}\r\n<\/pre>\n<p>Whew. We&#8217;re done, right? Unfortunately, as you do a bit more experimentation, you realize that PingClient can actually throw an exception in some cases, e.g. when the network address cannot be resolved. The code as is will fail on first error without raising any sort of alert which is undesirable. That means we need another set of tests to account for what should happen instead to guide the right set of fixes:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\n\/\/ Full code elided for brevity...\r\n&#x5B;TestMethod]\r\npublic void CheckOneItemIfThrowsWebExceptionRaisesError()\r\n{\r\n    \/\/ ... Set up one client to throw WebException\r\n    \/\/ ... Verify that one error was raised with code -1\r\n}\r\n\r\n\/\/ This test would ensure that we don't fail early on exceptions which was the bad side effect\r\n\/\/ of the naive implementation and only needed because we have a loop...\r\n&#x5B;TestMethod]\r\npublic void CheckTwoItemsFirstThrowWebExceptionRaisesErrorSecondReturnUnvailableRaiseWarning()\r\n{\r\n    \/\/ ... Set up two clients, first throw WebException, second return warning status\r\n    \/\/ ... Verify that one error was raised with code -1 and one warning was raised with 503\r\n}\r\n<\/pre>\n<p>Okay, that&#8217;s the last of it&#8230; but wait, you learn that the alerting infrastructure can <em>also<\/em> throw (e.g. when a backlog of alerts has built up)! Now you need more tests to ensure that throwing later on won&#8217;t cause early failure (again, a side effect of using a loop) and that a fallback behavior is invoked instead:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\n&#x5B;TestMethod]\r\npublic void CheckOneItemIfAlertThrowsInvalidOperationRaisesError()\r\n{\r\n    \/\/ ...\r\n}\r\n\r\n\/\/ Another test would ensure that we don't fail early on exceptions, only needed because\r\n\/\/ of the loop...\r\n&#x5B;TestMethod]\r\npublic void CheckTwoItemsFirstIfAlertThrowsDoesNotSkipSecond()\r\n{\r\n    \/\/ ...\r\n}\r\n<\/pre>\n<p>That&#8217;s surely the end, right? Well, the service architect is now complaining that the ping checks are serialized and would actually work better if they were parallelized to some degree. By their estimation, it should be possible to do the pings in batches of up to four at a time. At this point, it should be clear that the loop is a liability and is causing all sorts of required but not-very-relevant-to-a-watchdog tests to emerge.<\/p>\n<p>What went wrong? Well, the biggest issue is that this <code>for<\/code> loop is essentially a <strong>hard-coded workflow sequence and error handling strategy<\/strong>. If we lift the loop <em>outside<\/em> the health check code, we can better focus on the matter at hand, which is the process for converting ping responses to alerts. Then we are free to define the sequencing and error handling <em>orthogonally<\/em> as a separate responsibility. Imagine instead that we did this:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\n\/\/ Generic async workflow sequence and exception aggregation\r\npublic static async Task ForEachAsync&lt;TInput&gt;(\r\n    Func&lt;TInput, Task&gt; doAsync,\r\n    IEnumerable&lt;TInput&gt; inputs)\r\n{\r\n    List&lt;Exception&gt; exceptions = new List&lt;Exception&gt;();\r\n    foreach (TInput input in inputs)\r\n    {\r\n        try\r\n        {\r\n            await doAsync(input);\r\n        }\r\n        catch (Exception e)\r\n        {\r\n            exceptions.Add(e);\r\n        }\r\n    }\r\n\r\n    if (exceptions.Count &gt; 0)\r\n    {\r\n        throw new AggregateException(exceptions);\r\n    }\r\n}\r\n\r\n\/\/ SINGLE ping check\r\npublic static async Task CheckHealthAsync(IAlert alert, IPingClient client)\r\n{\r\n    HttpStatusCode status;\r\n    try\r\n    {\r\n        status = await client.PingAsync();\r\n    }\r\n    catch (WebException)\r\n    {\r\n        status = (HttpStatusCode)(-1);\r\n    }\r\n\r\n    \/\/ ... more code\r\n}\r\n<\/pre>\n<p>Now we are free to write a separate set of simple unit tests for the sequencing\/error handling via <code>ForEachAsync<\/code>, totally decoupled from the logic in <code>CheckHealthAsync<\/code> (which now would only have tests relating to a <em>single<\/em> ping client). And we are free to combine this logic as we see fit in an upper layer, for example:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\npublic static Task DoAllPingsAsync(IAlert alert, IEnumerable&lt;IPingClient&gt; clients)\r\n{\r\n    return ForEachAsync(c =&gt; CheckHealthAsync(alert, c), clients);\r\n}\r\n<\/pre>\n<p>This integration code in <code>DoAllPingsAsync<\/code> is straightforward enough that we shouldn&#8217;t even feel compelled to test it &#8212; all the actual logic is already fully tested. And now to fulfill the architect&#8217;s request, we could do a simple switch like so:<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\n\/\/ Generic *batched* async workflow sequence and exception aggregation\r\npublic static async Task ForEachBatchAsync&lt;TInput&gt;(\r\n    int batchSize,\r\n    Func&lt;TInput, Task&gt; doAsync,\r\n    IEnumerable&lt;TInput&gt; inputs)\r\n{\r\n    \/\/ ... code here\r\n}\r\n\r\npublic static Task DoAllPings(IAlert alert, IEnumerable&lt;IPingClient&gt; clients)\r\n{\r\n    return ForEachBatchAsync(4, c =&gt; CheckHealthAsync(alert, c), clients);\r\n}\r\n<\/pre>\n<p>This would cause zero impact to the ping client tests and would require only a new top-level function and tests for the batched workflow &#8212; no ripple effect, and one fewer reason to change!<\/p>\n<p>Try lifting loops outside of your business logic. You might be surprised at the simplifications that result.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A lot has been written about the single responsibility principle over the years. Rather than rehash what many others have said better, I want to give a common example of how a seemingly innocuous control structure can subtly but surely&hellip; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[21,91],"tags":[],"class_list":["post-2841","post","type-post","status-publish","format-standard","hentry","category-async","category-design"],"_links":{"self":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/2841","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2841"}],"version-history":[{"count":0,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/2841\/revisions"}],"wp:attachment":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2841"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2841"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2841"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}