Anti-Software, or Fighting Common Wisdom
and Industry Standards
Common wisdom is rarely wise, merely common
-- common wisdom --
class="text">BBefore embarking on our ambitious project of making software better, obviously one of the questions we had to ask ourselves was: "what is wrong with current software?" Our careful analysis had revealed that many existing software development principles and practices needed to be discarded and/or revised up to the point to becoming their exact opposites. So much of what had hitherto been accepted programming wisdom needed to be thrown away, that we have decided to analyze each and every existing practice, "industry standard" and "rule of thumb" and decide on case by case basis whether or not if it fits our goals. In the process of doing this, we have found that at least a half of existing "industry standards" and "common wisdom" is deeply flawed (at least for our purposes) and needs to discarded. Therefore, at some point we came up with a notion that what we are writing is not just software, but that in a sense it is "anti-software" because it goes against so much "common wisdom" and so many "industry standards". Below, we will show a few of the most prominent examples of such "industry standards" and "industry best practices" which we have thrown away or replaced with their opposites:
Common management wisdom: All team members can easily be replaced by making a call to a recruitment agency and identifying a candidate with an appropriate skillset ("Cogs in the Machine" approach).
While this is a direct result of a perfectly valid concern from the management
("What would happen if X were to leave the company?"), the logic behind this approach is
deeply flawed and leads to disastrous results. The problem is that
if trying to apply this logic (borrowed from assembly line management,
where it is broadly successful) to creative areas (and programming
is as creative as any other engineering job), simply does not work.
There are two distinct approaches to the team building:
the first is to treat every team member as a replaceable unit, which
has an immediate effect of killing all creativity on the spot. The second
is to treat every team member
as an individual with his/her own strengths and weaknesses.
The first approach has been extensively trialled, and (to the best of our knowledge)
has always had disastrous results when applied to creative processes. Instead, we advocate the second approach which entails treating every team member
as an individual ("star"), and making every team a "star team". This does not
necessarily mean that every team member has to be a world-class developer or
architect, but it does mean that the individual strengths and weaknesses of every developer have to be taken
into account and utilized to make team stronger and end-result better. Obviously,
this makes the management of such a team much more difficult (thereby requiring the manager
to be a real "star"
), but we do not see any way around it. There is also a
"hybrid" approach which argues that a few senior developers
be treated as "stars", alongside a number of easily replaceable developers.
However,from our experience this approach is not as successful as the "star teams" vision. Thus we have chosen to
replace the original common wisdom of: "All team members can easily be replaced by
making a call to a recruitment agency and identifying a candidate with an appropriate skillset" with the exact
opposite: "All teams comprise of individual stars".
To address valid concern of "What would happen if X were to leave the company?"
we note that while exact and immediate replacement is not possible within "star
team" model, it is still possible, allowing for a degree of effort and knowledge-sharing,
for one "star" to take over the work previously done by another "star".
Common management wisdom: CMM is applicable to innovative software development
In fact, with all our experience we haven't ever seen an example of a successful innovative software development team or organization with CMM (Capability Maturity Model) levels higher than level 2. We are fully aware that this sounds like a Number One Heresy for the management, but fortunately our investors trust us more than common wisdom theories, and we are going to prove in practice that it is we who are right here. The problem with CMM/CMMI and other similar approaches is that they are process-centered rather than people-centered. From a CMM point of view, a "good" organization is the one which makes its operation independent from which individual is working at which position, essentially allowing any person to be fitted into the existing process (once again, making him/her just a "Cog in the Machine"). Whilst this approach works perfectly for mass production scenarios, it leads to fatal underutilization of creative minds in the case of innovative development. To make things worse, when creative people cannot utilize their potential, they often move elsewhere. We hope that nobody in their right mind would say that a process of inventing something (for instance the chemical formula for Shuttle Solid Rocket Booster fuel) could be made repeatable, and we are sure, in the same way, that repeatability does not apply to invention of things such as the Google page ranking algorithm. This does not mean that repeatability is a bad thing as such: there are many areas when creativity ought to be avoided; for example, if a McDonalds worker were to start inventing ways to save on electricity consumption, or if airplane pilot were to start being too creative about the plane's route, this would create a number of difficulties. However, innovative software development is a creative process and should be managed appropriately, and in a way which is very different from managing fast food workers or airplane technicians or pilots (with all due respect to all of them). Therefore, we insist on replacing the assumption that "CMM is applicable to innovative software development" with the exact opposite: "CMM has nothing to do with innovative software development". The same goes for standards from ISO9000 series. This does not mean though that we deny any organization and management; obviously, certain processes, policies and guidelines do exist and we are working a lot on them and their improvement. The key is that we recognize the inherently highly creative nature of our project, which in turn requires that approach be people-centered, so if an individual doesn't fit into the process, we will try to change the process to fit the person (as opposed to traditional approach of trying to change the person to fit into the process).
Common management wisdom: When hiring, we need a developer with such- and-such a skillset and at least X years of experience
This is a common management wisdom, which is the direct product of the two other management "industry practices" described above. We replace it with its opposite: "When hiring, we need developer who is smart and enthusiastic about such-and-such a field". That's right, being smart enough to do the job and enthusiastic about doing it really count in our software development model; it does not matter how much experience somebody has, in a specific field, since software development is a field when experience often becomes stale long before it is obtained. Once again, this means that our hiring manager needs to be a shrewd enough judge of character to assess the intelligence of each applicant: but once again, we feel this is the only way forward.
Common programming wisdom: Use 3rd-party components/ modules/ frameworks as much as possible
We think that this is one of the worst problems with modern software development, and has significantly contributed to the current, pitiful state of modern software. The problem with the above statement it is extremely simple: the closer to business level the task is, the more difficult is to find a component which will do its job properly, efficiently, and (most importantly) will accommodate future requests from business side. It is more or less straightforward to find 3rd-party support for very well-defined tasks like operating system sockets for TCP communications, though even sockets are not ideal in some more interactive situations. But when the task is to make the end-user happy, and the precise nature of the task is redefined every other day (which is normal for business), using 3rd-party components very quickly leads to difficulties surrounding attempts to make the component work in an unusual/unspecified way, often throwing it away and looking for another one (which usually only helps for a few months before the same stalemate is reached), or replying to a business request with the words: "sorry, we cannot do it because such-and-such component doesn't support the feature you need". The last scenario is the worst possible one, because if it happens often and/or in critical areas, this can easily lead to loss of business and eventually to company bankruptcy. In addition, with 3rd-party components there are always problems of versioning and compatibility, which make things worse. As a result of all these problems, we have replaced the common wisdom of: "Use 3rd-party components/modules/frameworks as much as possible" with its exact opposite "Use 3rd-party components/modules/frameworks as little as possible".
Common C++ wisdom: Use standard libraries like STL, std:: and boost:: as much as possible
While this is merely a specific case of the more generic conventional wisdom about 3rd-party components above, we think it requires special clarification and justification. While we agree that in some (usually relatively small) projects these libraries can and ought to be used, in general we feel that every decent project of medium size and larger should create its own way of expressing thoughts, and, unfortunately, standard libraries usually represent the wrong level of abstraction. We feel that standard libraries are far too specific to be used as primitives for the "project's own way of expressing thoughts", and are also much too specific to allow efficient application implementation in most performance-critical cases. Essentially, they introduce a level of abstraction which is too close to the existing, OS-dependent one, becoming mostly wrappers around OS functions, but losing performance and sometimes functionality because of the need to be cross-platform (because of the infamous "lowest common denominator" problem). Our approach is to create high-level project-specific primitives (to express "thoughts about this very project"), which have the same API across all platforms, and are then implemented for every platform in the most efficient way. This provides better abstraction primitives for higher-level code, and also provides better performance because of better flexibility for the implementors of these abstraction primitives. In other words, we are not creating one generic library "once and for all", but we are trying to create very specific primitives for the specific project we are creating, sometimes receiving huge gains over traditional algorithms. For example, one of our own (rather specific for the project) synchronization primitives provides savings in amount of kernel resources used up to four orders of magnitude (both on Windows and Linux) compared to traditional approaches (including boost::); and this is not because we are smarter than boost:: authors; it is we're trying to solve much more specific tasks then they are. In addition, we cannot help ourselves but to mention extremely ugly std:: libraries such as iostream or locale, which should have never existed in the first place (despite iostreams being a rather obvious personal favorite of Bjarne Stroustrup, whom we harbour great respect for). Our longest-standing favorite from std:: libraries was STL, but eventually, as we have found how different the generated code can be depending on the compiler, and how inefficient STL is in certain specific cases (such as creating a set of a million 32-bit integers on a 64-bit platform which uses about 36M RAM, with almost no data locality), we have decided to replace it with our own derivative from STL (named NSTL, which stands for "Non-Standard Template Library").
Common programming wisdom: Code reuse must be done at all costs
Out of all misconcepts and misunderstandings we have encountered, "code reuse" is probably the most prominent one. Here, we do not really oppose the principle of reusing code as such, but merely the way this is usually interpreted. Nobody objects to the concept of reusing the code which ideally fits the purpose, but the problem we see is that code is too often reused when it does not fit the picture at all. The "code reuse" issue can be divided into two parts: one is "3rd-party code reuse", which is addressed above; another is "own code reuse". In principle, reusing one's own code is a positive thing, but only if this is done properly. Improper code reuse can easily lead to spaghetti code, making the code completely unmaintainable. Proper code reuse requires maintaining clean APIs through the reuse, and discourages techniques such as putting everything in the same function with a dozen 'mode' parameters (or even worse, globals), and a bunch of 'ifs' within it. We would even go as far as stating that in some rare cases, properly commented direct copy-paste is better than improper code reuse; it iscode readability and maintainability which is paramount, not code reuse as such. Therefore, we replace "Code reuse must be done at all costs" with "Code readability must be maintained at all costs, even at the cost of code reuse".
Common programming wisdom: Multithreading is a Good Thing
This is one of the most dangerous concepts in programming, and is probably responsible for more crashes than any other single programming misconcept or mispractice. Multithreading is a Bad Thing. If you want your program to work without crashes, you need to avoid multithreading at all costs. If you can avoid multithreading by switching to non-blocking IO, or by any other similar means, do it! And if it is not possible to avoid (and the only good reason for this is that you need to perform some CPU simultaneously on 2 or more cores), it is absolutely necessary to confine all multithreading into a very-well defined set of functions/classes which never change (ok, we'll settle for "almost never change"), because finding bugs in a multithreaded application is worse than just a nightmare, it is a never-ending perpetual nightmare, and to make things worse, some multithreaded bugs are never found by debugging, but can be found only by code analysis (one of us has in fact written an article with such an example back in late 90's).
Common programming wisdom: All modern programs must be object-oriented.
The object-oriented development model is in fact not as important as it is presented by its die-hard fans and professional object methodologists. It is simply yet another tool in a developer's toolbox, rather than a silver bullet of software development (the history of software development has already seen lots of silver bullets, none of which appear to have been effective). Moreover, going too far in making everything object-oriented quickly distances the developer from the real task s/he needs to solve, working towards more and more complicated abstractions which depart farther and farther from the original task: a recipe for disaster. It was probably this artifical abstraction gap which triggered the (in)famous newsgroup post C++ is a horrible language by Linus Torvalds. On the other hand, while we do agree that an object-oriented approach is too frequently overused, we still feel that using C++ (in a limited way, without abstraction causing the task itself to be obscured) provides more benefits than it causes drawbacks. On the related subject of design patterns - while we acknowledge that these are useful to create some common terminology, we must emphasize that they are useful only as long as there are no attempts to fit everything into existing patterns; real world is far too sophisticated to express itself with a limited vocabulary of predefined patterns, so new project-specific patterns will emerge all the time. As a result of all the above, we have replaced the original statement: "All modern programs must be object-oriented" with our own "Both OOD/OOP and design patterns should be used with caution".
Common programming wisdom: Be conservative in what you do; be liberal in what you accept from others.
"Be conservative in what you do; be liberal in what you accept from others" is
a "Robustness principle", which allegedly originates from RFC761.
While it is indeed important for interacting via existing protocols
like TCP (BTW RFC761 is about TCP), whenever you create any API or protocol of your own,
it is better to restrict tolerance of your implementation to errors and, more
importantly, to confusing scenarios as much as possible. This obviously does not mean
that a program should crash on invalid packet or call, but when using your own
protocols or APIs, it is perfectly ok and, moreover, is highly desirable
to prevent usage scenarios which you didn't plan at all, from being used (probably
"abused" is a better word in this case). Otherwise
one can easily find oneself in a position where somebody exploits something
which you consider to be a bug and would like to fix, but cannot do it because
too much code already depends on this unplanned behaviour, so
you're stuck with it for life to support. How much
easier it would be if you were to ensure that from the very
beginning nobody calls your functions in unplanned way. Therefore,
we have replaced original "Be conservative in what you do; be liberal in what you accept from others"
with a very different "Whenever you create your own API or protocol,
be conservative in what you do, and be even more conservative in what
you accept from the others".
Common programming wisdom: Assembler is faster than C, C is faster than C++
While theoretically it is correct that you can express any C program in assembler, and (not exactly strictly) any C++ program in C, which implies that assembler program can be made at least not slower than C, and any C program can be made at least not slower than C++, in practice this is highly dependent upon the specific scenario. The vast majority of modern processors are out of order ones, and a compiler can often optimize it better than even a good assembler programmer (unless he's ready to spend months per 1000 lines of code). The C and C++ situation is quite similar - while an object-oriented approach (especially as taught by Booch et al.) has nothing to do with performance, proper abstractions help a lot with developing high-performance code, and C++ provides much better ways to express abstractions. The three most obvious examples of such performance-related C++ abstractions are inline functions, consts and templates (yes, we know that modern C has already borrowed two first of them from C++). Aditionally, there are destructors which allow large amounts of time to be saved on the debugging of leaks and deadlocks, and private data/functions which help to enforce clean interfaces (while improving readability), saving even more time on dealing with messy code in the long run. Saving time on trivial things frees time to think about algorithms (and improving algorithms is the most important source of optimizations by far). Therefore, we have replaced the original claim: "Assembler is faster than C, C is faster than C++" with our own "Program speed depends on algorithms much more than on the programming language these algorithms are implemented with".
Common programming wisdom: Use RPC-based architectures, either existing like CORBA/COM/DCOM or homegrown like Mozilla XPCOM
Wikipedia says that component-based programming was invented by IBM with their SOM, or by Microsoft with OLE/COM, both in 1990's. But our view is that essentially they do not differ much from original RPC (remote procedure calls, RFC707) proposed back in 1976. Overall, the whole idea of RPC looks neat on paper, but there is one objection against it, and this is that it doesn't work. For communications on the same computer, it is simply not necessary (function calls and class methods will do nicely without incurring overhead), and for communications over the network they encourage programming style, which does not work, especially over WAN. An endless stream of QueryInterface calls and set/get properties will incur numerous round-trips, which will prevent any application from working reasonably over WAN. Granted, it is possible to bypass it, but eventually any such bypassing technique will lead to the use of COM/CORBA/ whatever-else as just a trivial message-passing engine, so why bother dealing with them in the first place? In the light of this, we have replaced the original: "Use RPC-based architectures" with the exact opposite "Do not use RPC-based architectures"" for intra-process communications we generally prefer function/method calls, for inter-process communications - messages (which, incidentally, can be made unblockable, which in turn goes inline with avoiding multithreading at all costs).
Common programming wisdom: Use DLLs/.so's
Common programming wisdom encourages the use of DLLs/.so's. Our experience has shown that in most cases this reduces end-user reliability (mostly because there is always a chance that some [censored] developer of some other installed software has replaced system DLL with their own "improved" version), which makes programming less obvious, and reduces maintainability, without much benefit. Therefore, we have replaced common wisdom "Use DLLs/.so's" with the exact opposite "Do not use DLLs/.so's". There are two exceptions for this rule, however: the first is when using the operating system DLLs/.so's (like kernel32.dll or glibc.so), the second exception is providing clean API for 3rd-party plug-ins.
This list of common wisdoms we have thrown away is by no means exhaustive; we have compiled a much longer list of guidelines for our developers to follow. On the other hand, we are also prepared to admit that not all common programming wisdoms should be dispensed with, and we do use some of them (especially agile programming principles and concepts such as merciless refactoring or continuous design).