Leveling up as a Software Engineer
I’m a pondering freak, especially on things that are so large that my mind needs to nibble one piece at a time. In the things that I like to ponder on, software engineering, being my career, has alway been among the top ones.
From my university programming intro course taken almost 7 years back, I learned that software engineering (SE) is all about controlling complexity, by different means of abstraction. I learned abstraction by function, data, process, etc. “Complexity control” is a rather philosophical understanding to approach the topic. I also believe it should be equally applicable to any other engineering discipline too. What I would like to discuss here, is a slightly more SE specific interpretation of SE itself, based on different stages that I myself, as a software engineer, went through. Even though I can’t even be regarded as experienced in the field, hopefully my thoughts below can help new comers to have a better idea how to work for the better.
The first stage is universal in learning anything — getting to know the basic constructs (primitives). Should a “software engineer” in this stage be hired by a company to work on a project of substantial scale, the project is mostly doomed to be abhorred by any successor. Note that I put quotes around “software engineer” — it’s due to the fact that at this early stage, he/she usually are not doing engineering work at all, but merely programming, if it sounds better than “coding”. All too often, a programmer would now put together anything that makes the feature work. This usually implies a lot of repetitions and scattered boilerplate.
Below are two images I copied from https://androbit.net/articles/973/vicces_de_igaz_oreilly_konyvboritok.html. I don’t understand the language used in the website nor did I find anything that looks like copyright notice; so I just address the source here first. If this violates any will of the author I’ll immediately take them out.
This brings to the transition out of the explorative learning stage. Once a friend in other engineering department told me that he doesn’t think programmers are doing anything special: “I learned C in my very first university semester and did it fine.” he said, “Tell me the syntax, I can code that thing out for you too.” This is actually legit if code quality is out of concern. But as pointed out earlier, working on a project in long term, any serious maintainer would think about how to reduce the pain in doing chores — thus emerges the principle of Don’t Repeat Yourself, aka DRY. And it’s at this stage, software engineering sheds light into the gloomy internals of a “usable” product, which could already be as messy as entangled and deteriorated spaghetti soup.
Therefore rational software engineers (now that they are) would apply tricks and techniques in solving the issue of repetition. And that’s why seniors ahead of us passed down their good works to inherit, and the most notable being Design Patters. To many software engineers, the patterns used to enlighten the pathway to cleaner code bases. But nowadays, even though our problems are arguably still similar to those that our seniors confronted, as the programming languages evolve, we have different tools to tackle them. This trend is obviously observed from the programming language popularity in GitHub. The applicability of classical patterns in new languages are thus more and more challenged. So this brings us to the question, if repetition awareness is the start of software engineering, what is THE THING that defines its quality? In other words, what’s the more concrete attribute that makes an engineered work good?
My best answer to this question is: the interface semantics. And to talk about interfaces and its quality, we need to set abstraction layers. Any big project is built on top of a set of smaller utilities — be it in any form of abstraction: class or function, async or sync. We may call them supporting components here. With that in place, good semantic means:
- The final project is largely based on its direct supporting component, but rarely the components yet another level below (sometimes known as “don’t cross the boundary”);
- The supporting components have most of its exposed functionalities orthogonal to each other, and named precisely (sometimes known as “do one thing and do it well”).
The first point is stated from the final project itself, but in fact implies the completeness in functionality that the supporting components provide. So simply put, a piece of engineered work is good, if it relies on semantics that are limited in size, and precise in boundary, or put it another way, its supporting components are well written, and the project uses them well.
The example given is obviously much simpler than most software engineers’ daily tasks. But the idea should be the same. All too often, it’s the simple thing that slips out of our minds — Being a mere 3-year-full-time-experience software engineer, I myself am also learning, and making a whole lot of mistakes too. That’s why I summarize all the rules the principles the checklists the practices that need to consider when writing code, into a word of “semantics”, which is easy to remember, yet still applicable and practical.