The 7 Worst Atrocities of Cleverness

We know we shouldn't be clever, but what is clever? There are seven particularly bad forms of cleverness, from very small to very large.

Bitwise Logic and Ternary Statements


This is the lowest-level cleverness. We write a one-liner that uses bitwise logic or a ternary statement. They start out innocently. We have an assignment based on a simple condition. A full-blown if-else block feels like overkill. Then that turns into this:

int i = 16 + (!(a & 4 == 0 ? thing : other).getValue().equals(b & 1 == 0 ? first : last) ^
    (b & 2 != 0 ? w[0][1] : w[0][0])) ? counter - 1 : counter << 1;
That's not clever. That's a train wreck.

Singletons and Lying About Dependencies

Design patterns are general solutions to common problems, and are often useful. But Singleton is one "pattern" (actually an anti-pattern) that can be destructive to a codebase. Singletons are easy to write and easy to use, but they are just another form of global state. Programmers who don't understand this will use them everywhere for everything, tightly coupling their code to this global state. Singletons hide large dependencies in seemingly innocuous calls. For example:
public String getURLFor(String name) {
    Database db = App.getDatabase();
    ResultSet rs = db.query("SELECT url FROM Urls WHERE name = ?", name);
    return rs.getRow(0).getString("url");
}
From the outside, this looks like a straightforward method, but the method interface is deceptive. A database Singleton is conjured from the global state. An expensive database query maps the name to a URL. There is no way to test this easily, or to substitute that dependency.

Subverting the Type System

We make a conscious choice to work in statically-typed languages like Java or C#. We accept that the code will be more verbose and more complex. We do it for the benefit of having stronger compile-time assurances that our code is correct. Sometimes we feel writing out the type of every variable, parameter, and return type is a bit onerous. We will even feel that forcing one part of the code to know every type is restricting. We could instead pass around Objects and cast everywhere. This cleverness, of course, loses compile-time assurances the language writers worked to preserve. We eventually need to know the types we are dealing with. So we embed implicit knowledge of type across the code base in a completely haphazard manner as casts and instanceof operations. We must always be on guard in case we receive an object of an unexpected type.
public Object getValue(Object key) {
    if (key instanceof String) {
        return stringKeyedMap.get(key);
    } else if (key instanceof Long) {
        return longKeyedMap.get(key);
    } else if (key instanceof Date) {
        return dateKeyedMap.get(key);
    } else {
        return null;
    }
}

// Meanwhile, elsewhere in the code
Object key = thingy.getKey();
String value = ((String)mappings.getValue(key)).trim();
Why is that a String? How do we know? It simply is what it is. Any change could break everything, and we'd only find out at runtime. This seems like a clever way around types at first, but it turns out to be a source of bugs and errors.

Code as Configuration

Configuration, a set of values in a file or database table, are a way to adjust the behavior of a system outside of the code. Configuration as key-value pairs or a slightly richer hierarchy of values is simple to use in code. It can be edited by end users without too much effort. There's no logic in that kind of configuration. There are no conditionals. As soon as the idea of a conditional is introduced, it goes from being configuration to being code.

Once configuration contains conditionals, it is no longer simply a manifest of data. It has embedded logic, and it won't be long until loops and even more powerful idioms tag along. The configuration may evolve to take advantage of an existing language, like Javascript, but modifying it to its own special needs. Or it may evolve into its own separate beast of a language. As it grows, more of the program flow control will live outside of the code itself.

Code as configuration promises greater flexibility and end-user pliability. Instead of paying real developers to make these complex changes, we can hire "configurators" to write these rules instead. These rules grow to a complexity so great that they require a real developer to write and maintain.

Code belongs in the code base. There it can be tracked, controlled, searched, managed, integrated, and maintained. Tools can be used to gain insight and understanding of code, but not of external configuration. Because the configuration language is often non-standard, developers cannot reach for any of the tools that they use on code. Clever developers will move large chunks of business logic into "configuration" to make the application more flexible, but lose visibility into that logic.

Everything, Absolutely Everything in the Database

Databases are powerful stores for many different types of data. We can store groups of data into tables, and relate data in different tables to each other through foreign key relationships. Databases can perform well under stress with tens of gigabytes inside them. This power is enticing to the clever developer. If the database can store so much arbitrary structured data, how much can we put in there?

The cleverest developers can find ways to put just about everything inside of a database. Soon, tables holding application data are rubbing elbows with tables holding environment configuration, SQL queries, scheduled calls, and even chunks of code. As more of this non-data finds its way into the database, the application becomes more difficult to manage. Constant calls to the database slow the system down. To see some parts of the application require making slow queries against a remote database instead of fast grepping through local files. Visibility into that code decreases. Version control is hampered since swaths of the code can no longer be tracked as local files. Upgrades become hair-raising adventures of ensuring that the database was not corrupted.

While once again the clever developer promises flexibility, the code instead becomes less flexible.

Solve Every Problem

Line-of-business apps are boring. Who wants to learn all about how to sell car parts or how to do double-entry bookkeeping? Not the clever developer. In their mind, there are bigger problems to solve, and they can write the software to solve every problem!

The problem with solving every problem is that they won't solve any problem well. Instead, we end up with frameworks and engines and workflows that are poor abstractions for any particular use. Writing a system to encompass an entire problem set is a massive undertaking. It is also a useless undertaking. An application focused on business needs is smaller, faster, and easier to maintain.

We're Different

Every developer and business wants to believe they are a special snowflake. Everyone should have something special about them, but too many feel like everything is special about them. Their code, their organizational structure, their process, and their communications are needlessly unique. They reinvent a lot of wheels in code. They create byzantine processes to ensure quality, but that actually just slow things down and make things worse. Even when adopting something from the outside world, they tweak it and and bend it and contort it until all of the benefits are lost.

The uniqueness weighs on every aspect. Talent is harder to acquire because they cannot be familiar with the systems. Changes are harder to make because a unique process has a limited pool of experience to improve with. Communications are harder because of vocabulary that doesn't quite mean what everyone else would think it means. Progress is hard because the group is organized and run by fiat.

Not all "tried and true" methods are good for every organization, but they generally have years of real-world testing behind them. Many of them have the support of academic study as well. By leveraging this accumulated understanding, a group can be properly organized to meet its needs without having to reinvent the wheel blindfolded.

Bonus! The Compound Sin

Each of these atrocities of cleverness is bad enough on its own, but their awfulness multiplies when they are used together. Imagine a system developed with all unique components to solve a general problem like "workflow." Much of the "configuration" that is actually code is stored in the database. When those bits of code are pulled out, they are "type independent," pretty much doing everything with Objects and casting so that we can pull any type we want out of the database. We store some chunks in cached singletons so they can be used anywhere without needing to pass them around or go to the database again. And because we are so clever, we throw in a lot of ternary statements to keep down the lines of code.

This program is an unmaintainable mess. It doesn't do anything. It likely doesn't even run without a significant amount of configuration already in the database. It is an oddity, admired for its cleverness but not for its usefulness.

Comments

Popular posts from this blog

The Timeline of Errors

Assuming the Bugs

Assuming Debugging