> When reading software design advice, always imagine the examples given are 10x longer.
How about: if you're peddling software design advice, take some time to make (or find) a realistic example so I don't have to imagine so hard. Even if it's in a toy-problem type of scenario the code can be realistic. Working Effectively With Legacy Code has spoiled me by setting such a higher standard than most peddlers, since it actually presents real code (sometimes 3 pages of it at once) in real languages (Java and C++) using styles and highlighting problems I actually see in legacy codebases with those languages, and how to address them.
Somewhere around 1989 I swore that the next author of an OOP book that used animals as class examples was going to get an angry personal visit from me.
"Suppose you have an Animal class. We subclass to a Cat, and add a Meow method..." If it wasn't animals, it was cars: "we'll subclass Car to create a Ford class, add a Horn property..."
Because Customer/Vendor/Invoice was too commonplace? Between Customer classes and Animal classes, I'll give you a hint as to which I've created more instances of.
Humming birds, I tell ya thank goodness for multiple inheritance! I was able to inherit from both birds and bees. It saved me a ton of work. There was the time I used the flying mixin on sharks though, that was such a mess to cleanup.
Classes are a neat way of hiding implementation detail behind a mini-api that has reasonable code-hygiene benefits and works well in a team setting. None of these things have anything in common with meowing cats.
There was a classic on HN a while ago [1] illustrating how blindly guessing an OOP hierarchy for a problem isn't going to help. Throw that at a learner without thinking too hard, and they are going to question what the benefits of OOP even are - it doesn't always simplify a problem.
[1] https://www.quora.com/Is-abstraction-overrated-in-programmin...
- Common operations between classes, operating on common data, but requiring an external API (so composition is a pain because you would have to proxy those actions to the member.)
- Restricting/specifying the types of objects you can store in a container if you are programming in a language/codebase that cares about that (incl. the C++ "definitely has the vtable I want".)
And maybe that's it? I guess all the taxonomy talk might be useful in the first hour of learning about inheritance, but after that I think the analogy should give way to more concrete "what are the code and data doing?" angle.
It’s like if someone took the cascading idea of CSS and decided “this works so well for UI styling, let’s build a language paradigm out of it and convince people they need to express every problem in terms of it.”
I think it's a classic case of teaching people the answer before the question.
Animal examples explain what types and subtypes are, but the thing that warrants explaining is why and when it's useful to separate things into types and subtypes, and animal examples are terrible for that. If someone asks "should Cat and Dog inherit from Quadruped? Mammal? Pet?" there's no useful answer.
I don't know which I hate more. Animals and cars, or this. And don't get me started on portfolios and stocks.
Some of us learned programming having absolutely zero clue what an invoice is. Hell, I remember being sad that all this fun knowledge is introduced with these weird, boring examples from bankers' world, as if written for PHBs.
The point is, I guess, no examples are ideal. That said, animal examples probably deserve a special place in hell, as they mess up not just with your understanding of OOP, but also biology.
To put it in another anecdote. I dealt with a SOAP api for a successful company that was purposefully designed to have no versioning. This meant that every decision that went into the API had to live forever and be backwards compatible with every decision that had ever come before.
This leads to the form of architecture I find to be the least useful. One where the architect tries to anticipate and solve all future problems. In my experience, this never leads to software that can grow or evolve. It's fine if you are solving a known problem and can set known boundaries around which your solution will never be applied. I, personally, have never encountered problems like that in the field.
The rationale was that the abstraction was necessary to make things easier to replace if they weren't needed but it was a false assertion on two levels (and it almost always is):
- that kind of replacement is unlikely to happen in the short/mid-term, and if it does it won't be in a way you anticipated.
- the simple version could be deleted and rewritten in less time than it would take to fix your highly abstracted/decoupled/meticulously architected integration
Of course, simple isn't easy and this approach to abstraction (where you take classes/methods longer than x lines and extract them into more classes and methods) is very easy to achieve... at a great cost.
> I hate code, and I want as little of it as possible in our product.
I know how I'll be spending a few hours in the next few days. Thank you for reminding me of his talk!
I don't know what particular mistakes could come about in this case, but more code == more errors in general. Wrapping stuff into mini abstractions is not always an improvement.
As a rule of thumb, if some code pattern repeats 5 or more times, an abstraction wrapper is probably justified. Between 2 and 4 is a situational judgement, but lean toward skipping the wrapper. KISS.
you can't have bugs in code you never have to write!
Design patterns are a great thing to understand but they can be dangerous when used incorrectly. I've had to deal with overly design patterned code and it can be a nightmare. You go searching and searching for the "sharp tip of the spear" where you can actually get something done and then you find out that you need some sort of special object. Then you find out that the object doesn't have a constructor it has to come from a factory method. And you can't just call that factory method with parameters, oh no, you need to pass in some special property bag object, and so on.
Don't worry about trying to show off, worry about making your life easier. The ideal situation is that if you want to do some new thing X that is similar to but slightly different from other stuff your system does then you will not only have a good idea of how to do that with the existing primitives, utility methods, abstractions, etc. but it will also be a fairly straightforward task to add that functionality.
Ask yourself these questions about your code base:
- how easy is it to read the code and figure what it does?
- how easy is it to figure out how to use it?
- how confident can you be that the code is correct just by looking at it?
- how difficult is it to maintain and add new functionality to?
- how easy is it to test?
Let those guide your designs.
From the author's page:
> I work at MIT trying to make program transformation and synthesis tools easier to build
So, the author works in an academic environment. He doesn't have to deal with real-world code, budgets, bugs, teammates, etc. Please take his coding advice with a grain of salt.
I think engineering advices from non-engineers should just be discarded...
I've been on the wrong end of those abstractions and it can (and often does) end up resulting in a lot of pain for everyone involved.
https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstracti... is pretty relevant.
I constantly re-writing and re-organization code as the problem changes but I feel like that's the exception rather than the norm. We need to teach that change is good and a normal part of the process.
I think this is worse than changing code that has been field tested but admit I am biased because I work on a large project where we are beginning to run into this problem.
The power of automated testing is really to ensure that nothing changes, which is great when doing bug fixes, but not so great for actually evolving software.
Ultimately it just becomes easier to add new code than it is to ever change a design that is already in place.
I was talking about a regression test for a fix for a bug found in production. For a typical backend, write such tests against the publicly exposed API.
If you end up breaking tests as you refactor it means that you have broken backwards compatability for others who use the API...
To put it another way, if you never break your tests you can only ever fix bugs or add more code -- you can never remove code or redesign.
We've got a weird situation where the best (best potential) devs are not financially incentivized to reach that potential, the money is in being a locust, showing up, devouring all local resources and then moving on to the next green field.
And only by practice, and making the mistake enough, did I reached the same conclusion as you.
Although there is a balance, as sometime you know your future requirements, and making an abstraction right away may save you some time. It's hard to teach experience and the ability to evaluate something vaguely.
And a call that any decent set of tools would surely have highlighted as redundant at that.
I'm all for being clear about the design of a program, but I don't think the example here is convincing, and I agree with the parent that over-engineering can itself damaging.
Simplicity is key to happiness and productivity.
When things are immutable design early.
Consider an api. You can build a basic api without a version parameter when you release. When you need to change the api and keep backwards compability you introduce a version parameter. You are forever stuck with the first version being the default.
More important for an API is building in a way to signal users that the version they're using will be or has been discontinued, and a way for the users to test that.
Please, please, please absolutely disregard this advice. More errors, pain and suffering come from early abstraction and poor understanding then not enough abstraction.
Build simple, extend as needed. It's a 'frggin stats page where a developer forgot to remove a call. It could of been written much cleaner initially, but breaking it into a bunch of classes at the initial stage is fruitless abstraction.