Most engineers I talk to on a regular basis have said at one time or another…
Look at this spaghetti code! How did such a simple system function turn into this 500-line disaster of engineering?
It’s because as intriguing and unique an experience as seeing one’s idea simply execute is, it’s difficult for many people to come up with a vision beyond their initial invention. For other people, it’s difficult to carve down a broad concept into a more limited scope of implementation, to escape over-engineering.
Software is, in and of itself, a fluid medium while it is being developed. Business requirements will always change and blocks of code will always need to be pruned to make way for a new workflow or adaptation to an external library.
Knowing all this, how can any engineer possibly understand how to model a system when they can see the path of imminent destruction ahead brought on by simply “the unexpected”?
I choose to follow these guidelines to best prepare myself for the inevitable changes ahead in my software:
Understand the Business
Make sure you understand how information flows from system to system in the existing business’ software or servers. This will help you avoid missteps later when coming up with specifications for your own component, but it will also help you understand places where there could be exceptions that someone who is just casually explaining the data flow might not catch. Say you are writing software for a shipping company. A worker that is intimately familiar with the workflow explains that the boxes are filled from shelves, then validated, then sealed, stamped, and released. However, a condition that may have not been mentioned is a fringe condition where an item marked as hazardous is removed from a box with only that item in it. Does the box then get shipped anyways without checking again to make sure it has items in it? Hopefully an empty box doesn’t get sent off the line to a customer! Understanding not just the core workflow but how and why it is executed in that way will help catch error conditions before they happen, as well as provide valuable insight for how to better improve associated business processes in the future.
Don’t Pigeonhole Broadly-Used Code
It can be easy to think in a very closed space when developing a new component for an existing system. However, writing code that is more usable for other components will cut down on code churn later on when workflows outside of that component change and suddenly the needs have pivoted slightly. I once came into a project after another engineer had developed an imaging process that would overlay a transformation matrix sequentially against an entire image; the matrix was static and written in many places in the code. However, the needs had changed for this operation shortly after I started, and I had to tear apart the classes to rewrite them in a way the business could make better use of by taking in the transformation matrix as part of the constructor.
Don’t Over-Engineer Easily Reproduced Code
If this seems contrary to #2, then good! You’re paying sufficient attention. As much as pigeonholing code is a problem, creating too many degrees of freedom is just as significant of a problem. Simple code is sometimes written in several layers of abstraction, making following a path during debugging very difficult for anyone even somewhat unfamiliar with the execution path, and usually would have been better served if a few lines were duplicated. Simplicity is, to some people, the “Holy Grail” of software engineering, and sometimes it’s best to just take the “magic” out of code in favor of old fashioned brute force. Just strike a balance between #2 and #3 and you’ll be a software savant in no time.
Test With Runtime Data, If Possible
Unit and integration tests are excellent ways to keep bugs out of production code, but only if used appropriately. One mistake I’ve seen frequently is that the developer generates their own data set for a test. This is a huge problem because it ignores any degrees of freedom introduced by other systems.Does the test check for a UTF-8 character set? How about Unicode terminators or BOM markers?
Does the test intentionally displace the input data to see if slightly malformed data will still run?
Does the test check to see if the input data uses 3 or 4 spaces instead of a tab? Or vice versa?
All of the above are simple conditions to miss when your nose is in the data itself, and without some real data from the business, can be big problems.
These are just a handful of ideas on how people can improve their ability to be more agile while developing software in a realistic business scenario. While not all of them will apply to every engineer, hopefully at least one struck a chord.