To summarize a fairly lengthy answer to all the points you've raised:
- While some of your statements are correct, you are using them to draw faulty conclusions.
- Over all, your question reads as you building up a justification to not have to put in any more than the least amount of development effort that you have to.
- Your reasoning seem justified to you, but from historical experience those exact lines of thinking are commonly the root cause of endemic development issues in teams that have neverending bugs, growing technical debt, and a lack of code quality and oversight.
I've done my best to identify your logical reasoning and point out the fallacies or dangerously misinterpretable statements contained within.
Does this concept have a name?
I'd argue that this is a variation on defensive programming, but instead of defending against user actions and unforeseeable events; you're defending against reasonably foreseeable developer error. But it's the same approach overall.
throw new Exception("Should never happen, you probably forgot to add your new case)
Specifically for .NET, this kind of exception is best described by a NotImplementedException
.
NotImplementedExceptions specifically telegraph to the developer that an implementation has not yet been provided, as is the case for your example.
switch(something) case 1: return good; case 2: return fine; case 3: return okay;
If you want to avoid future developer error, then you have to focus on the readability and self-documenting nature of the code you write. Focusing on that, whenever you're dealing with an int-switch, you're generally better off using a more descriptive enum instead.
While it doesn't quite change how the code works on a technical level (enums are ints under the hood), it significantly improves your code's readability and the ability to remember which specific int values are in use and which are not.
I just tried for way too long to write an abstract base class in a way that somehow forces anyone who derives from it, to implement 2 methods, while having those two methods execute base class code at the end. I don't think its possible.
It is possible:
public abstract class Base { public void MyPublicBaseLogic() { MyProtectedDerivedLogic(); MyPrivateBaseLogic(); } protected abstract void MyProtectedDerivedLogic(); private void MyPrivateBaseLogic() { Console.WriteLine("This is FORCED base logic"); } } public class Derived : Base { protected override void MyProtectedDerivedLogic() { Console.WriteLine("This is derived logic"); } } public class OtherDerived : Base { protected override void MyProtectedDerivedLogic() { Console.WriteLine("This is other derived logic"); } }
If you make a new instance of Derived
or OtherDerived
, you will see that it only has one public method: MyPublicBaseLogic()
. When you call this method, it will first execute the derived class' custom logic (defined in the specific derived class), but it will then finish with some forced base logic from the base class (exemplified by the "forced base logic" writeline in this example) which is the same logic since it's defined in Base
.
Anyone who derives from a class should know why and be perfectly able to figure this out by himself. Right?
This statement starts correctly and ends wrongly.
There's a difference between "I shouldn't expect this basket to break" and "I should put all of my eggs in this basket". The former is correct, but the latter is the faulty conclusion that a lot of people end up drawing from it.
"Anyone who derives from a class should know why"
Yes, it's reasonable to assume that developers know what they're doing when they're doing it.
"Anyone should be perfectly able to figure this out by himself"
This is a bridge too far. No, you cannot guarantee that developers haven't made an oversight when doing something.
Just because you expect developers to be informed does not mean that you should avoid informing them anyway. To prove my point, what you're saying is the same as saying:
I assume that anyone who drives on this road should be familiar with the traffic situation on this road. Therefore, we don't need to put up any traffic signs. People will already know, right?
I hope you see the flawed reasoning here. You're assuming everyone knows what you know, which is a lack of foresight.
Do not use this reasoning to justify not writing documentation or logging. I'm putting that in bold because finding reasons to not have to write documentation or implement clear logging is often the root cause of a team's future technical debt and downward spiral, and the argument you're making here is precisely the response I get whenever I'm brought in to tackle a development team's problems and question why there's no documentation or logging.
Your question is specifically on how to pre-emptively prevent future errors. But rather than assume you can perfectly prevent them, also assume that errors can still be made (no matter how well you defend against them) and therefore you should clearly document and describe these errors as well.
I don't really need to provide a perfectly abstracted way to do caching in any repository that requires no knowledge, no code adjustments and works automagically every time. I doubt I can.
I mean, if you conclusively know that you're not able to do it, then don't do it. That's just common sense.
However, you not knowing or being able to do something is not a justification for it not needing to be done. "I can't do it, and therefore it's not necessary" is a nonsensical conclusion.
And looking at it pragmatically, as soon as I introduce one cached repository into our code base, even the most junior developer can figure out how to write another one.
Factoring in your previous statement, what you're saying here is effectively "I don't need to abstract, others can just copy/paste/adjust my concrete implementation".
That is the absolute antithesis of good development practices. The promotion of reusability and not having to reinvent the wheel are quitessential developer skills. I would seriously question the value of any developer who actively argues against doing so.
I have done so in the past. When being brought in to fix a development team's persistent technical debt issues, I usually identified one or more developers who actively preached bad practice (either knowingly or unknowingly), and the way to fix the team's issues was to re-educate those developers, or remove them from their position as the lead developer or a trusted source of quality assurance.
How can I better figure out where to stop?
What is correct, however, is that you don't always need to abstract. You're right that there is a line to draw here, where abstraction is no longer necessary and would no longer add value to the codebase.
My general rule of thumb here is to count like a caveman: one, two, many. When you hit "many", i.e. 3 or more, it has become time to abstract.
I'm anticipating comments that are going to say "but even 2 occurrences should be abstracted". And those comments are correct. However, they forget to account for human error and overzealous pattern-matching.
Looking for abstractions is essentially a pattern-matching skill. It's all about spotting a reusable pattern. But just because you spot a pattern doesn't mean that it's a correct pattern to spot. You can overapply this and start seeing patterns that aren't there.
A simple example here is having a Person
and Company
class which both have a string Name
property. That's a pattern, but does it really need to be abstracted?
No. Just because these two entities both have a name doesn't mean that their name-related logic is going to be shared or reusable.
This is why I suggest only looking for abstractions starting from the third occurrence. It's easy to misinterpret two similar things as a pattern, but when a third instance occurs, it's much more likely that there is an actual reusable abstraction happening here. Still not a guarantee, but the odds are much better.
That being said, common sense still applies. Even though you're only implementing the first (and only) cached repository, if it's very reasonable to either expect (based on common sense) or know (based on future development phases) that multiple repositories are going to be cached in the future, then it makes sense to already create this abstraction from the get go, if you can.
My rule of thumb only exists to suggest that you evaluate abstractions when three similar occurrences exist. But that doesn't mean that you're not allowed to already evaluate this earlier.