What makes robust software so special and why developers (and businesses) need it.
An essential part in the development of an OXID eShop solution for a medium-sized company is the integration of the shop with its surrounding systems. This includes enterprise resource planning systems, product information management systems, CRM systems and external search servers. The latter are often a necessity in order to realize an efficient search service.
If the search service is an “on-premise solution”, it can be seen as part of the shop; yet, it still represents an integration task that must be implemented correctly, robustly and with high performance. With a small code example from such an integration task, I would like to discuss the differences between good and bad practices.
Good code exists beyond the buzz
From the point of view of almost 40 years of practice in software development, certain good and bad practices are recurring themes in the daily business of a software developer. Instead of eliminating bad practices, new hypes and buzzwords are often brought to the table. It is expected that they provide a quasi ‘automatic solution’ to software problems. Software is often commissioned work, which is supposed to improve the so-called “bottom line” of a commercial enterprise and to supply the customers with robust and operable solutions. I would therefore like to plead not to spend too much time searching for the Holy Grail in the form of the current hype, but to develop one’s own skills in writing robust software.
A typical anti-pattern…
I would like to illustrate this with a pretty short and example stemming from a real project, which shows a persistently pursued anti-pattern:
Software for creating a search index shows strange behavior in terms of handling multiple languages. German text ends up in the English language index, although the indexing logic apparently sets the language correctly. An essential part of the code is the class Indexer, which looks like this in reduced form in pseudocode:
class Indexer { private int langId = 0; private string langIsoCode = zero; public Indexer() { } public setLanguageId(int id) { this.langId = id; } public int getLanguageId() { return this.langId; } public setLanguageIsoCode(String isoCode) { this.langIsoCode = isoCode; } public String getLanguageIsoCode() { retrurn this.langIsoCode; } public index(String key) { String view = Database.getLanguageViewFor(this.langId); String value = Database.readFromView(view, key); this.sendToIndex(key, value); } private sendToIndex(key, value) { // not included here } }
This pretty class does a few more things (maybe not a clear “Separation of Concerns”?), but it is used as follows:
String keyToIndex = ‘myKey’; Indexer indexer = new Indexer(); foreach (Languages: String langIsoCode) { indexer.setLanguageIsoCode(langIsoCode); indexer.index(keyToIndex); }
This looks reasonable at first glance, especially when it is embedded in a larger code base.
The problem is, however, that Indexer has two member variables langId and langIsoCode, which should be constrained (the ID must always match the ISO code). However, this constraint does not exist in the code, but at best in the developer’s head, and is not enforced by the construction of this class.
… and the problems that arise from it
The problems already start in the construction phase, which allows the creation of a disfunctional object. The public setters and getters allow any external changes that may violate the connection between ID and ISO code. These problems persist throughout the lifecycle of the object: The user has to be meticulously careful to always set both arguments to match beforehand. So this class also has a big usability problem.
It would be best to create Indexer as a non-mutatable class, which takes only either the ID or the ISO code of the language as constructor parameters and thus can only be set correctly and can no longer be put into an incorrect state by calling a mutating method.
If the class really has to be mutable (which can often be avoided), then there should be no mutation that creates an invalid state. Imagine a setter for either ID and ISO code or a setter that takes both as parameters and can recognize an incorrect combination of the two parameters.
Cross test: Does the pattern score with flexibility?
But how often has a design like the above been defended by a programmer as desirable because it is “flexible”? After all, in many books (which perhaps should never have been printed) exactly such a style is shown. Since I can no longer count the number of bugs that can be traced back to this anti-pattern, I call it “challenging fate”.
The beloved “flexibility” all too often leads to an instance of such a class being placed in an invalid state and the connection between cause and effect is often not easy to recognize, since both are far apart in the code base. On closer inspection, the described anti-pattern is not a flexibilization of the code – e.g. by interfaces and polymorphism – but the introduction of non-determinisms.
The inversion-of-control or Hollywood principle (“don’t call us, we call you”), on the other hand, contributes much more to the robustness of software. In the OO world this can be found in frameworks or in the form of the “template method” pattern, in the functional world it can be found in Erlang/OTP as so-called Behaviours. The reason why this design technique leads to robust code is simple: Here, reuse is performed in best DRY manner (don’t repeat yourself) and the errors in a framework (or Behaviour) are reduced by every concrete realisation of the framework.
A pleasant side effect is that the behavior of software based on such principles is more predictable and shows no unexpected behavior. This way, the developer no longer challenges fate, but puts himself in control.
Flexibility vs. Robustness
The interesting thing is that robust programming techniques have existed for a long time and that they are still successfully applied in modern software ecosystems. Nevertheless, bad practices are still widespread and therefore difficult to eradicate. It seems that you have to experience yourself that your life as a software developer is not made easier by “flexible” designs, but rather by robust ones. The reason for this is that the main effort is not the quick creation of a “proof of concept”, but the so-called maintenance phase, in which bugs have to be corrected and new requirements implemented in such a way that they do not introduce any new bugs.