Tom Butler's programming blog

Hazard 2: But I've always done it this way

This is part 2 of my guide to writing better code

As developers we hear this from clients all the time. Here's a couple of real things I was asked to do by clients over the years:

We have a booking form PDF that people download and complete interactively before emailing it to us. Can you write some code to extract the data from the PDF into a spreadsheet?

Obviously I suggested recreating the form on the website. The client said "But we've always done it this way".

We need to you send an email to our mailing list asking them to sign up for special offers. When they land on the page, they should enter their email address to sign up.

You are emailing people... to ask them their email address?

Any developer who's got more than a few years of experience has a dozen similar stories. We get it all the time and we're quick to pick up on the issues when it's someone else who is stuck in their ways.

Most developers are just as guilty of getting stuck in their ways as the clients they work for. You learn about a new tool or technique but immediately brush it off because it seems hard to implement. Someone will say "global variables are bad" or "singletons are bad", "don't use public variables" or "inheritance is bad" and because you are so used to using them, you can't imagine writing code without them.

Trying to stop using a tool you are so reliant on is hard because it requires rethinking everything you've been doing and refining for the last 5 years. It also requires a slice of humble pie to admit that your code could have been better.

Warning signs

You have become stuck in this hazard if you're learning about a new programming practice/approach and you:

  • Dismiss it because it's drastically different to what you're used to
  • Consider it wrong, confusing or too much work because it requires a radical change to the way you approach writing code
  • Think about how much work it will be to implement the practice in all your existing codebase
  • Don't want to admit the hundreds of thousands of lines of code you've written in the past could be much better

New Approaches

You probably already avoid global variables and singletons. Did you always? Well, I guarantee if you keep reading you'll have that reaction when I say don't use service locators, don't use inheritance, avoid static methods and all objects should be immutable if you don't already do this.

Changing the way you code often requires a radical rethink about your approach. It can be very difficult to do and adopting a new approach is the most challenging aspect of moving through the milestones and becoming a better programmer.

As developers we get so familiar with our way of working that anything that goes against that makes us recoil and think "that can't be right". Good developers are the ones that are able to see past that immediate reaction and look into it for themselves.

The first time I came across arguments against inheritance was Allen Hollub's article Why extends is evil. This was back in about 2008. At the time I could sort of see the arguments but found trying to apply it to my own code impossible. Using inheritance was so ingrained in my programming technique that I just couldn't comprehend a world without it.

Over time, I forced my self to avoid inheritance and only then, having tried programming without it, and working out how to avoid it, did I properly understand what Allen (and others) were saying. It took a lot of mental effort to work out how to design something without inheritance because I was so accustomed to using it. Once I managed it a few times, the concept clicked and I realised how unnecessary it ever was. A year earlier I never would have considered it even being possible to solve some problems without it. These days, I never use it in projects.

Even as an experienced programmer, even if you can see the advantages of a different approach (e.g. composition over inheritance), actually implementing that in your project the first time is difficult. It requires re-training yourself to think about the code in a completely different way. You're probably familiar with the feeling from when you went from procedural to OOP code.

Similarly, when I learned about immutability I struggled. A lot. I still struggle to write code that is technically 100% immutable.

There are two types of developer. Those who listen to what others are saying and those who immediately brush aside any ideas that go against what they already know. Some of the brushers will eventually learn the lesson on their own, some will double down and fight a losing battle to the bitter end.

As an example, a recent discussion on annotations over at reddit. Anyone who was arguing against annotations with examples and reasoning immediately got voted down. Few of the downvoters commented to explain why. Those downvoters are almost certainly in the mindset of my code uses annotations and it works fine. You must be wrong. That's not to say everyone was like that, a few users did argue their case, and helped me refine my own thoughts on annotations during the process, thanks guys!

Don't be a Tony Marston

People get so attached to their existing codebase or the way their favorite library works and they will do anything they can to justify its implementation, because they feel comfortable with the code and put up with it because they feel safe in their codebase. Any critique of it would be better if... is treated as an attack and they immediately become defensive (Go talk to Doctrine fanboys about annotations and see what happens).

A perfect example is a developer called Tony Marston. If you've searched around PHP topics you may have come across his website. Yes, his site unironically uses Comic Sans. And frames. In 2018. Frames were already long established as bad practice in 2001 when his site launched which tells you a lot about Tony's attitude to best practices even then.

I don't mean to single out Tony here but he epitomises the but I've always done it that way attitude and he's quoted me on his site enough times that I don't think he'll mind me returning the favour. He's stuck just before Milestone 1 and refuses to progress to it. He's a perfect example of what not to do.

To give you a quick insight into Tony's mindset, on his website he claims to have a "minimalist approach to OOP" and to "keep it simple" yet all he's done is built a single framework (Download it here) which he uses as an example of perfect code. It relies on a 9000 line god class with 150 methods and 70 properties. He maintains this class follows the Single Responsibility Principle even after being repeatedly shown otherwise.

In this long winded discussion, Tony Marston could not think in terms of individual concepts but only how what was being said related to his existing code, and he'd do anything to prove that his existing framework was essentially perfect and followed the best programming practices.

Instead of listening to what others were saying he either redefined common terms so they applied to his existing code or when redefining is impossible he'd claim the practice his code wasn't already following was evil (except in the situations his code was using the practice, of course).

Look through any of his articles. Everything he says for or against any given practice is immediately related back to code he has already written.

His argument against Dependency Injection is a list of reasons it's hard to implement in his beloved framework. Doing that would require rewriting/rethinking my 15 year old code so it must be wrong. He is incapable comparing concepts such as singleton vs dependency injection on their own merit without relating it back to his existing code and thinking about how much work it would be to change it.

"But my code doesn't work like that!"

I have read the principles of MVC and built software which follows those principles, just as I read the principles of OOP and built software which followed those principles. The fact that my implementation is different from your implementation is totally irrelevant - it works therefore it cannot be wrong

...

I do not have a separate Controller

...

In my implementation

In his article How to write testable code the author identifies three distinct categories of object:

...

My framework contains the following objects:

...

Note that the above examples do not show the volume of code that would be required to deal with multiple dependencies, and in my application it is not unknown for an object to have 10 dependencies. Also, what about the situation where each of those dependent objects has dependents of its own?

...

I do not use mock objects when building my application, and I do not see the sense in using mock objects when testing. If I am going to deliver a real object to my customer then I want to test that real object and not a reasonable facsimile. .

Don't get stuck in your ways

Tony, like a lot of novice developers, is unable to separate a conceptual discussion surrounding the merits of different practices from specific implementations in his workflow and codebase.

When he reads an article that tells him to avoid singletons he sees it as a personal attack on his code because he's emotionally and financially invested in it. He immediately gets defensive and refuses to discuss the pattern on its own terms.

He's fixated on the thought But making that change would require rewriting thousands of lines of code, updating all my documentation and deploying the change across all my projects! and weighing that up against the benefits of avoiding singletons.

In the last example he uses circular reasoning stemming from the fact his codebase breaks most best practices. He is genuinely arguing against unit testing here. Why? Because his code doesn't support it.

His code can't be unit tested, therefore he can't see the merit in unit testing and makes excuses for not unit testing. It's not difficult to see the problem with this line of thinking and how it prevents you improving at all.

Because he doesn't need (read: can't use) Dependency Injection for testing, it is evil (except in the one place he's used it before by accident, of course) and singleons are better. It's well established that they aren't[1][2][3] and even junior developers can understand the issues they cause once they've been explained.

Everything he writes on his website is a defensive justification of his existing code.

Tony cannot discuss practices on their own merit, only how they relate to code he wrote fifteen years ago.

If he finds a best practice that he hasn't already implemented in his framework (which is most of them), he'll write a post bashing it because rewriting his monstrous spaghetti code framework to follow that best practice it is near impossible without a complete rewrite.

I'm using Tony as an example here because he's a perfect example of what not to do. Tony has been coding for 25 years, apparently. Yet his understanding of the concepts is less than a lot of my second year students because he is stuck in this hazard and takes any argument for avoiding a bad practice as a personal attack on his spaghetti code and feels the need to defensively justify it.

It's good to be proud of what you've built but never get defensive about your existing code, doing so will stop you making improvements to it!

Don't even think about your existing code

It's easy to see where Tonys come from. We've all been there, we realise what we've been doing for years can be improved upon and it feels pretty bad. I've been there myself, it's not fun knowing what you were doing wasn't as good as it could have been. And the first stage is often a mix of confusion and denial.

If you've been using singletons for years and someone tells you should replace singletons with dependency injection your gut reaction is to argue against this newfangled approach for three reasons:

  1. The Concorde fallacy. You don't want to admit what you've been doing for the last half decade could be improved, often drastically.
  2. You are subconsciously weighing up the cost of removing this bad practice from (or introducing a new practice to) your existing code.
  3. You do not know how to code in that way yet. When you come across Inversion of Control for the first time, it requires a completely different way of approaching the problem. You have to learn the new approach and it's difficult to comprehend how to apply it across a large codebase if you've been using singletons for years. This perceived additional complexity is seen as a negative against the new concept. Even if you can see the benefits of an approach, applying it to real code and breaking old habits is always difficult.

Rather than comparing benefits of the suggested approach to the benefits of the approach you're currently using, you are factoring in the time it would take to change your code and the mental overhead of learning to use it. It often requires a completely different way of looking at the problem which requires a lot of thinking. Chances are, changing your current codebase would be a large amount of work.

When you factor in your existing code, you're not comparing the pros/cons of the singleton pattern to the pros/cons of dependency injection you're comparing the singleton pattern to learning about dependency injection, rewriting your existing code to replace the singletons and dependency injection

Because you're factoring in the cost of redesigning fundamental parts of code you've already written, the new approach seems like a lot of work for little to no gain. You will lean towards arguing against it because the pros don't outweigh the cons.

My advice is: When considering new approaches, never factor in what the code you write currently looks like. This also goes for code you're using but didn't write (how your favorite framework/library works). If someone tells you a practice you've been using should be replaced with an alternative, don't immediately try to go back and change all your existing code!

Instead, implement it in any code you write in future. This will give you a clean slate to work from and allow you to view the practice from a neutral perspective. Once you're sold and fully understand the practice, then consider implementing it in the next version of your older code.

Over the years I've seen a lot of Tonys. Colleagues, students, people posting online. For the most part it's seasoned developers who get stuck in their ways and won't admit that new practices emerge and sometimes they're better.

The best developers are the ones who are critical of their own code. The ones that look at their old code and say "If I was starting that today, I'd do it differently". Anyone who doesn't do that is saying they haven't learned anything since the code was written.

As a personal example, I wish I'd made all of my projects immutable from the start. It's harder to change (and force an API change on users) after the fact.

Conclusion

Take a look at this post by Yegor Bugayenko in which he says "Here is how I would design jcabi-http, if I could do it now" and compare his reflective attitude and ability to discuss practices conceptually to Tony Marston's defensive attitude that brings everything back to justifying the way his existing code works.

You don't have to change your existing code to admit that it could be improved!