As software developer or engineers, our job is not to write code. In fact, we should be writing as little code as we can. Every line of code we write is a liability: a potential security hole, a new bug, a tight coupling that makes future modifications difficult or impossible. It may surprise you to realize that writing code isn’t even what your job is about.

Introduction

As a software engineer (I’m not going into the debate about whether anything we do with software can truly be considered “engineering”, but bear with me), with very few exceptions, we are problem solvers. We need to hear or read a description of what is needed and figure out how to make it happen in code. Except instead of jumping straight into our editor and banging out code that we think does what is asked for, we often should take it further and ask questions. Many times, we are given a task and expected to blindly follow and do what it says – the proverbial, and (now) somewhat derogatory “code monkey” tasks. “Move the button down 4 pixels, change the asterisk to a different shade of green, make these 100 PDF reports output as a single report with 100 pages”. Each one of these requests has a different motivation, and the people requesting them may well have good intentions. But let’s take a further look.

Request for Changes

A request to move a button around on the page is simple, right? Just* change the top CSS property or any of numerous possible ways to move things around. But why? Perhaps where it is, it doesn’t align with other buttons on the same line and makes things look unprofessional. So what about changing the framework to make it easier to ensure everything is aligned properly? Suddenly the solution may not be as simple, but it could be more robust and actually save time in the long run. Or it could be that the button just needs to move down 4 pixels because it is afraid of heights.

* Just is a very dangerous word. It implies simplicity and a completely thorough understanding of the problem. I recommend calling out usages of “just” whenever you hear them. “Oh, it’s ‘just’ a simple change”, “It’s ‘just’ some CSS”, “It’s just a drawn reciprocating dingle arm to reduce sinusoidal deplanaration”, etc. Until the change has been completed and tested and marked as done, we really don’t know what it “just” was. Is it possible that it’s simple? Sure, absolutely. But the people making the change are nearly always in a better position to make the call about what it “just” is, and even then, it may not be that great a position. Avoid using the word “just” in anything dealing with software development. It’s just a bad idea.

Changing an asterisk to a different shade of green sounds like a simple change. Unless it’s defined exactly what that shade is, why it should be that new shade, and the people asking for it know they will be happy with it, the simple request for changing the shade of green may turn into hours or days of back and forth with the developer. Don’t laugh, I’ve seen it happen. My former coworker ended up spending nearly two days going back and forth with the business, changing the size and color shade of a single character on the site. Afterwards, he decorated his cube with a large green asterisk drawn with a white-board marker on paper and stapled to the wall. It was a testament to inefficiency. If you were told, as the owner of the business that hundreds or thousands of dollars had been spent to get an asterisk to the right size, and shade of green, it would be understandable if you were upset.

And yet, this sort of thing happens all the time.

PDF Combinatorics

In a more recent example, the team was asked to allow the system to build a single large PDF report out of what started as many individual small reports for a large number of medical caregivers. As it stood, the system was capable of generating a calendar “report” of each caregiver’s assignments for the month. The request was to allow a user of our software to select a bunch of these caregivers and generate all of these individual calendars for use by the coordinators.

We were and still are, using a reporting software that allows us to build reports. One of the features is that reports can contain sub reports, potentially ad infinitum. Our developer managed to take in the list of caregivers and combine the individual reports into one, but one particular issue was causing him fits. In certain places in the report and at the end, a blank page was inserted into the final PDF. We gathered several developers and tried all sorts of attempts to get rid of these superfluous blank pages, but to no avail. After a few days, we concluded that it was not going to allow us to remove these blank pages. We reported this to the customer.

Their response was rather surprising. It didn’t matter to them because they were using this to print out each of the individual calendars and then scan and email them out to the individual caregivers. What?! Finally, we had the real problem that needed a solution – the electronic delivery of the calendars to the caregivers. We could have simply iterated through the selected caregivers, generated each report in turn and emailed the result to the caregiver. Instead, we were given a potential “solution” but not given enough insight into the problem to determine a better solution.

I know that Agile software development practices are typically either loved or hated, but regardless, the standard story format does have a lot of value. “As a {role or user} I want {feature or change } so I can {benefit}.” There’s a lot of potential value in a sentence with that format. If the request above was spelled out this way, then it would have been the responsibility of the developers to ask questions that would have led to the right solution.

“As a scheduling coordinator, I want to be able to build combined caregiver schedules so I can print, scan and email them to the caregivers without contracting arthritis from too many mouse clicks.” In this case, the majority of the valuable information in the request is in the “benefit” or supposed benefit. If the goal is to deliver the report to each caregiver, then printing and scanning are not necessary. The amount of effort could have been reduced to selecting a list of caregivers and clicking a button. The system could then have generated and delivered those calendars. Enhancing the solution even further, depending on the selections, most or potentially all, of the work could have been automated. If the caregivers are associated to the coordinator and they all need their calendars on a specific schedule, the selection could be made via a database query and the triggering of all of this could be done via a cron job. The scheduling coordinator now has one less thing to worry about and we’ve delivered a real solution to the real problem instead of a imagined solution to an unexplained and misunderstood concern.

Other misunderstood requirements have led to such erroneous features as load-bearing notes, exceedingly complex data structures that allowed employees, copy machines, meeting rooms and MRI machines to be treated in exactly the same way. By taking the time to understand exactly what is needed and why, we get a better solution, and we’ll often get it cheaper and faster as well. Speaking of which…

The Iron Triangle of Software Development

Many of you may be familiar with the so-called “iron triangle” of software development. On each of the sides of this triangle are “Good”, “Fast” and “Cheap”, or in other versions “Quality”, “Speed” and “Cost”. In any given software project, you’re only able to pick up to two of them. If we pick “Good” and “Fast”, we’re going to need an excellent team which will not be cheap. If we pick “Good” and “Cheap”, we can get something like open source software: people volunteering what time they can, mostly for free. The resulting software can be very good, but since it’s all on volunteer time, we cannot dictate how long before it is done. If the choice is “Fast” and “Cheap”, perhaps we’ve got a skilled team on a tight deadline or a team of motivated beginners. Quality will likely suffer.

When we don’t take the time to determine what is really needed and figure out the problem we want to solve, there’s a good chance that we will only be picking one (or fewer) of our two possibilities from the iron triangle. There’s no need for that.

Why Do We Build Software?

We build software for a number of reasons: to scratch an itch, to learn, to solve a problem, to reduce errors, to automate, to make money, or just for fun. Each of these reasons leads to a different answer about how much effort we need to expend to answer the questions about what to build and why.

If you’re building an MVP (minimum viable product) or a prototype, fast and cheap may be the best choices. Once it is determined that the product is viable, then time and/or money may be more readily available. The MVP can be scrapped and a better, higher quality replacement can be stood up in its place.

If we’re building an application that we expect to live for a while, then it makes sense to put more time and effort into gathering correct requirements, solving the problems, and architecting maintainable solutions.

If we’re creating code for fun, then it’s up to us to determine how much time and effort we want to spend. At the time I’m writing this, the Advent of Code is running. Each day presents a new challenge which can be solved with code. According to the leader board, many of these problems could be completed within a few minutes by a small set of elite developers. If you know your language of choice well, and potentially know an algorithm or already have a library with an implementation, the problems can be solved relatively quickly. If you’re not familiar with some technologies, it may take a bit longer.

In one of the problems, you’re given a list of a group of people along with a happiness change score for each pair of people. For instance, if Bob sits next to Sue, his happiness may increase by 50 points, but hers drops by 10. The problem wants you to determine the optimal seating of people around a table so that the maximum increase in happiness is achieved. The solution I chose borrowed some permutation code from a previous day and generated all possible permutations of the dinner guest order. Since this was for fun, I decided to stop there. I was able to get the correct answer. However, what I wrote was inefficient.

In a circular seating arrangement, many of the permutations are redundant. If everyone gets up and moves a seat to the left, their happiness doesn’t change because they are still sitting next to the same people. If you flip the ordering from clockwise to counter-clockwise, each of the guests are still next to the same people and their happiness is unchanged. However, my solution didn’t account for this. This means that with an 8 person table, my code was checking 40,320 possibilities. If it accounted for rotations, only 5040 possibilities would have remained. Since I was running this code once, and then likely never again, the runtime being 8x over an optimized solution wasn’t that big of a deal (though it did bother me a bit). If I had code that did this as a service for customers, or for larger tables, the amount of time needed to update the algorithm to only generate what was needed could mean on a popular service, I may only need 1/8th of the servers and resources. In that case, it makes sense to optimize.

Make it Work, Make it Right, Make it Fast

Once we’ve determine what to build and why, our next task is the make it happen. This means our first pass at the code can be messy, inefficient and, to a certain extent, bad. But we should certainly not stop at this phase. Once it is working, we move to the next step, “make it right”. This means we clean up the code, put things right. In my throwaway code above, I stopped after making it work. I got the answer I needed. However, in our day jobs or hobbies, if we want code that will last, we need ot continue. Making it work involves understanding the problem at a code level – perhaps reaching the point that you know what libraries are needed, how the inputs fit together, even understanding that if you’re dealing with a circular permutation, there are a lot fewer possibilities than a standard permutation.

The make things right phase is where refactoring can happen. You can DRY (don’t repeat yourself) out your code by moving repeated functionality into functions and methods, renaming variables and functions to be more clear about what’s happening, potentially simplifying what’s going on since our understanding of the problem has likely increased. We can reorganize to make it easier to maintain. This can include tasks like removing calls to “new up” objects inside our constructors and providing those objects via dependency injection.

After the code is cleaned up, the final step is “make it fast”. This means that we need to realize there is a trade-off in nearly everything we do. Sometimes we trade run-time for memory. Other times, we might call out to a custom C-compiled extension in order to speed things up, but perhaps at the expense of ease of maintenance.

Of course, the make it right and make it fast steps are much easier and less scary if you’ve written tests in order to make it work. Whether you follow a TDD workflow (which I recommend) or you write the tests afterwards, having those tests in place as you refactor will allow you to be confident that you’ve not traded in your working solution for a better looking, but broken, solution.

Why Being DRY Is Not Always Best

As software developers, we have loads of acronyms to help us remember various principals. There’s SOLID (ironically, each letter in SOLID stands for its own acronym), KISS (Keep it simple, stupid), KICK (keep it complex, knucklehead), DRY (do not repeat yourself), YAGNI (you ain’t gonna need it), GRASP (general responsibility assignment software pattern), DDD (domain driven development), as well as DRY (don’t repeat yourself).

We are often taught that DRY is important. We are told that if two bits of code are the same, they should go in a function. If two bits are nearly the same, put the common bits in a function, abstract out the variable parts to the parameters, lather, rinse, repeat. This means that often in my career I’ve seen designs and software go sideways when what was initially determined to be two different pieces of code doing the same thing, followed by refactoring that to a function, turns out to be two bits of code doing nearly the same thing but for completely different reasons. In that case, the redundant code is arguably less bad than a single complex solution that must account for a plethora of different running conditions.

“Duplication is far cheaper than the wrong abstraction.” - Sandi Metz

In some cases, DRYing out our code can actually be harmful. Just because two bits of code appear to be doing the same thing, doesn’t mean they are. The quote above is by Sandi Metz, an outstanding speaker and software engineer. I encourage you to find her videos and watch. She is a Ruby developer, but the talks I’ve seen apply no matter what language you’re working in. I highly recommend watching any of her talks or reading what she has written.

The point here links back to the beginning. De-duplication of code just for the sake of having a lower score on phpcpd serves no good purpose. It’s better to have identical code in a few places while we strive to understand the real problem and determine the real solution than to DRY out the code to a point where we don’t understand what’s really happening and modifications become costly and potentially impossible. Strive to make your code as simple as you can, but no simpler.

Conclusion

I encourage you to look into the change requests, issues, tickets, stories or whatever you may call them, and determine what they are really asking. It’s our job as software developers to build solutions to real problems, not just to turn one silly request into code after another. We are being paid to think more than we’re being paid to type. This means we need to ask the right questions, dig deeper into understanding what we need to do, and provide real solutions to the real problems our customers have. It’s not about changing the asterisk to another shade of green, it’s about determining why that needs to happen and what problem is being solved.