In Defense of Ignoring All Rules
A defense of violating established processes in Wikipedia and software engineering
[…] sometimes improving Wikipedia requires making exceptions. Be bold, but not reckless, in updating articles.
In the link above we see the text of
Wikipedia: Ignore all rules or
If a rule prevents you from improving or maintaining Wikipedia, ignore it.
While intentionally vague, the idea that principles apply in most-but-not-all situations is a core tenet of why newcomers to Wikipedia can have a decent time and see their changes preserved. Chances are that frequent readers of Wikipedia are going to understand the tone to strike, the passive voice, and other implicit essentials to making a good edit. So why create a set of rules that can't be violated? Of course, going too far into the weeds can result in ideological differences that can't be reconciled with the hivemind, but more basic changes of the sort that newcomers would make are unlikely to bring about such opposition.
In my view, this translates quite well into the modern view of software development in teams: for consistency, matters of design (systems design, API design, etc.) require specific standards set within the group (consensus) or above it (directive), but the development (implementation details, coding style, etc.) tend to be less heavily policed. This is good; it is important to keep developers on-task building the right stuff, but bad to nitpick matters of opinion unless they are consequential to the final product.1
I have worked on solo projects where I adhered to certain principles, and been a member of teams where I deemed it inappropriate to do so. Consider the following statement I could make as to why:
When I determine whether to adhere to a principle,2 I naturally do so in the course of attempting to make the most efficient choices for the project at hand.
Surely an engineer with different priors could responsibly make the opposite choices as me and utter the same sentence, yes? Well, probably, but let's go through some examples with increasing amounts of context.
Zen and the Art of Maintaining Someone Else's Code
In the beginning, Engineer Arthur created an Application. This had never been done before, and no one else knew how to do it, so Arthur built an application that made sense to him. When someone asks him how something works, he can coherently explain it, and then the other person understands. Great! When something goes wrong, Arthur can find the bug and fix it. Now the program works again. Also great!
Arthur makes a lot of money from this Application and retires. Now his son, Bob, is put in charge of the Application. Bob thinks differently than Arthur and doesn't have the intuition his father did. When someone asks him how something works, he has trouble explaining it. He has to study the codebase. This is fine, but the codebase doesn't follow any standards—it's tailored to how one specific person thinks—so it takes forever to interpret. When something goes wrong, Bob doesn't have a strong enough mental map to find it quickly. In this way Bob is frustrated by his project but can ultimately deal with it. Bob spends his career like this and retires, leaving his twins, Charlie and Daniel, to maintain the Application in his stead. Before Bob retires, though, he writes some rudimentary documentation so the problems he faced won't repeat themselves.
Charlie and Daniel kick off their understanding of the Application by reading Bob's documentation. It's pretty good, and the twins can maintain the project reasonably well with it. They gain an understanding faster and feel more confident, so they even have time to implement some new features! Great! Charlie and Daniel independently decide to build integrations with Service 1 and Service 2, which are pretty similar. They work on their solutions and add them to the application. They both work, they don't introduce any new bugs, and everyone is happy. Stellar! Eventually, Charlie and Daniel move on with their careers and turn over the reins to Eric.
A while later, newly-minted principal engineer Eric needs to update the connections to Services 1 and 2. He reads the code for each—it's terrible! Redundant! The two are completely different, but they could've shared so many common functions! Not without some frustration, Eric determines that Service 2's integration is much better and completely rewrites Service 1's integration to be analogous to it. Then Eric updates the documentation with some guidelines for setting up an eventual Service 3. In this way Eric has created some guidelines for a standard implementation of a new service. Satisfied, Eric is comfortable moving on to a new project, so he steps down and lets Finn take over for him.
Finn hires an associate engineer, Grant. Years later, the time has come to implement Service 3. Services 1 and 2 were so complex; comparatively, Service 3 is very simple, so Grant can heavily simplify the implementation. The finished product is simple, elegant and easy to follow. Grant eagerly submits the code to his manager Finn for review. Finn takes an hour to read Grant's work, and then sends it back. Why? Grant didn't follow the guidelines! Finn suggests a number of changes to the Application. These increase its complexity substantially, but now it follows the guidelines—and it's more analogous to the other Service implementations.
This frustrates them both:
To Finn, Grant didn't follow the guidelines. The guidelines are clearly laid out, and they help with reusability and maintenance—if Service 3 changes in some way, only a small portion of the code needs to be changed, which would be easy for a new engineer coming along (like Hugh, who might lead this project one day). The decision to follow the guidelines is academically correct—they're good guidelines—and also correct by inertia—it's the way things have been done up to now, in his view, and there's nothing wrong with that.
To Grant, the guidelines didn't need to be followed! The guidelines turn something simple into something complex. Sure, it's more analogous to the implementations of Services 1 and 2, but if a new engineer like Hugh came along and looked at Service 3 first, they'd be so confused! Why the bloat? Why the pieces that don't seem to do anything? It would be better, even if the guidelines are good and valid and nice, to write something simple enough to be quickly understood by a newcomer. The codebase isn't as prepared for Service 3 eventually changing in the future, but even a newcomer can understand and maintain something simple.
Who's right? Vote now on your phones.
Notice a few things about this story. First, after five generations, Grant is making a decision much like Arthur, but on a different scale: Arthur made an entire application without standards; Grant is performing a smaller, more replicable task without standards. Arthur was an innovator, but iteratively his mindset would fall flat in a modern software development process. Grant is attempting to be selective in how he deviates from standards—this keeps him in line with
Second, both Finn and Grant are valid in their frustration. In the real world, this would probably play out with Grant attempting to convince Finn that his decision to deviate from principle was correct, and the chips would then fall where they may. This is fine, as long as both avenues forward are considered. It may be that Grant is incorrect in his view that future codebase maintenance would be easier with a simpler implementation versus a more standardized one. It's a difficult call, and the conflict between simplicity and standardization tends to crop up a lot in API design: some endpoints are so simple that building an entire infrastructure around them seems pointless, i.e. “this one just fetches an exact object from a database, so why does it need all this bloat?“ But others in the same application are performing more intricate operations and need that bloat to stay coherent.
The “bloat” here typically relates to layers of abstraction: a simple API endpoint tied to a specific service may be able to expect a certain type of return object, manipulate it somehow and then return it. A more complex one might want to introduce some translation, one or more DTOs, etc. so that one day, if the tied service changes, it is easy to understand the exact place in the code that needs to change—everything else is standardized. More complexity + more external services = more abstract layering. This is a problem of interface segregation.
So how do you handle a project with mixed levels of complexity but a desire for standardized design? This question has come about several times in my career. What's my answer? Put simply, I believe that Grant is correct, i.e. ignoring the principles is the right idea 90% of the time. Noting that only half of these reasons relate to actual code, I explain:
In an industry with so much turnover / dynamism, onboarding new engineers efficiently is critical. This emphasis on transferability skews upward the benefit of making decisions designed to keep code coherent and readable by new people. It is also a hedge against Brooks's law and increases the bus factor.
Engineers like complex problem-solving, but taking a long time to understand or build something simple is frustrating and bad for morale. This is a related but distinct point to the first one above; software engineering is a uniquely human discipline where keeping your employees happy is very important (because churn is so expensive for businesses and it's nice to not want to quit your job, etc.) so if someone wants to deviate from principles, and has a good reason, it's potentially worth hearing them out.
If abstraction reduces the amount of changes you need to make to the codebase when an external service changes or is switched out, for example, then it is justifiable. However, the future benefit that may be realized if that service does change must be weighed against the amount of added time and complexity it takes to wrap those external service calls in abstractions in the first place. If the service is controlled in-house or extremely unlikely to change, or if the abstraction would cause a great increase in complexity, it may not be worth it—developers may take more time putting it together and wrangling it in peacetime before the service actually changes and it's time to go to war.
The same approach applies to the Liskov substitution principle (LSP), to cite another common example: in general, constructors for subclasses ought not set or adjust anything that would cause differing behavior, relative to its superclass, within methods or properties the classes share. However, sometimes, this is just the easiest way to do something!
Say you have an extremely general superclass
InventoryItemand an extremely specific subclass
ShippingOnlyItem. The constructor for
InStorePickupCapable = trueif not specified, and that for
InStorePickupCapable = false. In most (all?) languages, the subclass constructor is executed last, rendering the subclass instance non-substitutable with its superclass and violating the LSP.3 Yet this is clearly a reasonable property to set.4
Thus I urge software teams to consider appropriate policies for violations of established principles. Give your devs the freedom to break the rules and you will find yourself building a better Wikipedia!
In other words, it's fine to criticize an implementation if a variant runs in linear time versus exponential time, but if two competing methods have the same time and space complexity, and the decision makes no consequence to the final product, it may simply irritate the developer. Ideally this would come out of feedback from (or meta-discussions surrounding) code review.
Brief clarification: here, “adhere to a principle” should be read as “follow the guidelines associated with a principle,“ thus the converse “ignore the guidelines associated with a principle.”
In this example I would expect a responsible implementation to either check or explicitly set a property like
InStorePickupCapable anyway, but the LSP doesn't care about anything but class definitions and these would be in violation. Of course, the solution is not to explicitly set
InStorePickupCapable = true in the superclass; this property should only be set in each final subclass, so these classes probably should be refactored to be sibling classes with an abstract parent, etc.
Assuming, of course, that you can't do what the prior footnote suggests and refactor the classes altogether. Suppose it would be a highly laborious change to do the suggested refactor and make these classes siblings. In that case, it seems reasonable to at least consider carrying out this LSP violation.