Novice programmers, people in a rush for a deadline or someone with simply a disturbing lack of interest, will create 'surprising' code. Not just bad code, but bad code disguised as proper quality software. There are, unfortunately, many ways to achieve such code.
Here is a few ways I have encountered that seem to keep popping up every now and then:
- seemingly meaningful but ultimately useless indirection
- reuse code but don't apply any abstractions
- 'generify' beyond any recognition
- use tech tricks for no reason
- translate yes, but always choose false friends
- have detailed javadoc about the precise implementation (that has long left the building)
- verify that things work -not- (verify the implementation)
- use design pattern naming but never actually apply it
- open up your data to endless possibilities
So what can we (programmers) do? Kent Beck has some advice for you, he actually has been advertising this advice for 20 years now. A nice, up to date summary can be found here: https://martinfowler.com/bliki/BeckDesignRules.html
He basically says there are 4 main principles to check:
- Passes the tests
- Reveals intention
- No duplication
- Fewest elements
Let's apply the principles to the ways listed above and see what went wrong.
1. seemingly meaningful but ultimately useless indirection
Indirection can be useful but it introduces 'elements', so it probably isn't the simplest solution. Indirection also hides or even distorts any clear intention.
Use indirection only if you really need the abstraction.
There are many cases where abstraction is useful, but usually: KISS (fewest elements).
2. reuse code but don't apply any abstractions
Let's say that some application has code for handling livestock, money and buildings. These different parts in the code might all do 'relocations': transferring money, moving resources to another building, and moving livestock to another owner. The code could be similar. A programmer in a rush could just reuse the unit that 'relocates' for buildings and apply it to money and livestock.
With this action of applying DRY, at best, all intention of the code is lost. It probably causes many other problematic effects.
The proper action would be to introduce some kind of abstraction that fits the mentioned purposes, or better, choose a better level for the abstraction and just leave some duplication in place, reveal the intention.
3. generify beyond any recognition
The problem is in the intention, and to lesser extent the fewer items. When applying DRY, a common pitfall is to go to far. Yes, a train can be expressed as a collection of movable transport item holders for resource relocation, but the downside is that no human being is able to understand the code anymore.
Making code very specific to the use case, even when reducing the ability to reuse it, is often the preferred choice (reveals intention).
4. use tech tricks for no reason
Programmers love using the latest technology. This includes 'clever' tricks. Just because a language offers advanced features, they are not suitable to every problem. In java for example, inner classes can access fields of the outer/parent class. But if the only reason to do so is to avoid passing variables, then intention is lost. The simpler solution is just a regular class with a constructor taking the variables needed. This results in a clear lifecycle for the object instantiation, avoids any risk of loading cycles, and is just many times easier to read especially by novices (both fewest elements and reveals intention).
5. translate yes, but always choose false friends
When modeling in code, using english has many benefits. Translation can be hard though, especially since naming is actually already one of the 'hard things' to start with. It gets painful when a word is chosen that actually looks like the word translated but means the exact opposite! An example would be: rare (unique, valuable) vs. 'raar' in dutch meaning just strange or weird. Make sure you properly reveal intention.
6. have detailed javadoc about the precise implementation (that has long left the building)
Old documentation is an excellent time waster. The reader can spend hours trying to understand how the code relates to the description, while it actually doesn't. Having implementation details in documentation is not DRY. Documentation is the perfect place to describe intention, let the reader know what he cannot know from the code alone. No duplication and reveal intention.
7. verify that things work -not-
Don't think about meaningful scenarios and having correct results, just verify that the code does 'whatever' it does. Instead, of course you should actually care about what it does, and more importantly: about why a test exist? This is the intention of the test that should ultimately reveal the intention of the code even more. Having tests is only really useful if they verify that the software delivers valuable output, otherwise software is only a liability. So 'passes the tests' is also a problem here.
8. use design pattern naming but never actually apply it
Fancy naming, without any comprehension about the concepts are hiding all intention. It's easy not to do the hard work of naming things properly after what they are, but instead just apply some pattern that sounds familiar. There are not many contexts in which a factory is something valid to have in your model, unless perhaps when dealing with nation wide greenhouse gas emissions for example. The context actually determines what kind of naming is suitable to properly reveal intention.
9. open up your data to endless possibilities
Data has no intrinsic intention. It needs protection against misuse. Immutability can greatly reduce the amount of ways to (mis)use code. Assertions; checks on illegal state are very practical. This is a best practice often called the 'fail early' principle. Having fewest elements helps.