what makes a good software engineer?
I came across this question, and it got me thinking. This a result of that thinking.
How would you find out if somebody is a good software engineer?
When answering this question, you must first detail what you are testing for. This leaves you with some questions, the following are some of those questions:
- What is the problem domain?
- What are the external constraints of the system they are working within?
- What are good and bad characteristics / abilities in the context of the problem?
- What tests will bring these characteristics to light?
Answering these questions will allow you to start to define “success”. Importantly these “success” criteria take into account the environment that the engineer is working in.
what are we testing for?
When you define the steps to test a skill or characteristic, you must define “good” within the environment.
A good software engineer writes good code, but what is good code?
You could define good as:
“The only valid measurement of good code is WTFs/min”
The number of times you, and your team look at something and think (or say) WTF. Good code is not surprising, complex, or hard to read.
Instead good code is characterised by the following criteria;
Clearly communicates intent. The structure, naming, and algorithms should all clearly convey the author’s intent. It should be obvious what any given piece of code does, and how it does it. Easy to change code, is not always easy to read code. Clarity is better than clever. Code and software engineering should predominantly optimise for developer efficiency. Good code is “pretty much what you expected” when you first looked at it.
Does one thing well. Code should be simple. Small is often simple, simplicity is understandable. Services, packages, types, methods, and functions should each do one thing. Each of them should expose a minimal interface and avoid muddled intent and ambiguity as to their purpose. The larger the interface, the weaker the abstraction.
Performant. Good code is a close to optimal performance as reasonably achievable, while considering optimising for engineering efficiency over computational speed. Code that does not appear to be performant, or is not performant, tempts pre-optimisations.
Can be read, and enhanced, by an engineer who was not the code’s original author. Code is communication in a software team. It must be possible to easily understand and to change code that was written by other team members. It’s the responsibility to simplify and communicate their mental model to other team members.
Expresses care by the author. The one biggest indicator of bad code (and it follows: a bad engineer), is lack of care. Caring about the trade of software engineering is clearly expressed in the output. Engineers are crafts-people, and crafts-person-ship shows.
Engineers’ who create code that fulfil this brief definition of good, have a kind of “sense” for code that is good or bad. A feeling when something could (or should) be improved. A “gut” or “instinct” that something is right or wrong. This instinct is often the thing that separates good engineers from great ones. Writing good code, and being a great engineer are hard things; but testing for these things is harder.
how do we test?
Testing for good engineers is a hard problem (NP-hard, maybe, as interviewing seems to take polynomial time, interviews and humans are non-deterministic). Using this definition of good code and good engineers, we can start to reduce the complexity of testing for a good software engineer. Engineers should always be judged on their output. Output should be measured with, and within, their teams.
That output can be observed in 3 ways:
- interviewing
- code
- communication
Interviewing
Interviewing is the immediate choice for testing ability, but limited in it’s utility. Interviews are often a good way of understanding a person; what they are like, what their soft-skill characteristics are, etc. Testing soft-skills can help to understand the characteristics of that engineer when put into a team. These characteristics are as important to a good software engineer as hard skills. Experienced interviewers will be able to forge situations in which even the least interview strong software engineers can still excel.
For hard-skills, interviews can only test knowledge. Knowledge is not the only aspect of software engineering.
An analogy stolen from a well-known software engineering book goes something like:
“I could teach you the physics of riding a bike, the mathematical equations to calculate the correct balance, but that does not mean you can execute on riding a bike”
Interviews are predominantly a test of knowledge, but knowledge alone is not enough. This is why you must also test code.
Code
Code is the currency of a software engineer, testing code is testing output.
To test code: invent, or repurpose, a problem that’s closest to your domain and set it as a challenge. Working as close to the problem domain helps to create a true representation of the ability of a software engineer, in the environment where they will work. Testing code is useful only when the test is close to the problem domain you are working in. Testing on abstract or academic problems does not inform you on the engineers ability with, and within, their team and problem domain.
It should be possible for all engineers to succeed. Success, or high quality output, should not be based on the number of hours invested. If your challenge is a test of hours invested, then you have selected a poor test.
Any code challenge should still take into account the individual: their ability, circumstances, and experience. Perhaps students have more time than single parents, this would give the students an unfair advantage vs. the parents. This creates an unfair and tilted playing field.
Using our definition of good code, we can make clear and reasoned decisions on the quality of any given code. When testing code, the most important aspect is: to assert if that engineer has the care and instinct that leads to high quality output.
Communication
Communication is a vital part of software engineering, no complex or large problem was ever solved in isolation, and as software engineers we do not work in isolation. Working in a team means that the ability to be able to clearly, and concisely, communicate an idea is important.
You must devise a test for communication that is not biased to a single method or format. The following are some formats to consider:
- verbal
- as code
- written
You must devise a test where the quality of the answer, or the deliverable, can be evaluated even if the method of delivery is different.
Final Note - Potential
Potential is an engineer’s currently unrealised ability.
A person’s potential to perform is important, as good software engineers can evolve and grow. Throughout this discussion we have focused on ability that a good software engineer can display, but there’s some room for potential here. This is particularly the case for the most junior software engineers.
Performance is the act of tapping into potential, the realised part of the currently unrealised ability. There’s an argument to be made that performance, in this case one of the definitions of a good software engineer, is a by-product of potential. Performance is also notoriously hard to evaluate.
Potential can be realised into performance, given the right environment, and the right amount of time. It is important to consider an engineers potential when considering if they are good. After all, software engineering is a people problem. It is easier to grow an existing team, than hire and forge a new team from new engineers.
wrap up
Good engineers can communicate clearly, have high potential (both realised and unrealised), and produce high quality code. Good engineers care about their craft and deliver the best within the constraints of any given system. Testing for a good engineer involves creating a definition of good and applying that in a consistent and non-bias way to any given engineer.
Learning if someone is a good engineer is much the same as being a good software engineer, by following these steps:
- Break down a problem into it’s component parts.
- Create a definition of success.
- Create deterministic tests to assert against the definition of success.
- Apply the tests consistently across all inputs, and for all engineers.