Hacker News new | ask | show | jobs
by vorpalhex 2950 days ago
> Standard disclaimer: When reading software design advice, always imagine the examples given are 10x longer. Overengineering is bad. But, if you’re not sure whether applying a technique to your code would be overengineering, error on the side of doing it. Abstract early.

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.

15 comments

Fully agree, let's not go back to instinctively applying all of the patterns from GoF. I'd also like to quibble with just this:

> 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.

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.

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.

Oh! You're a software developer at the local zoo too?

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.

I'm no expert at OOP, but never have I seen a student who telegraphs a look of comprehension after seeing an example of classes involving animals. I've seen many who get more confused.

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...

Mm. For me there are only a few reasons to use inheritance:

- 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.

I agree. Inheritence is useful in a very small amount of situations and can be the start of a long abstract chain of bullshit if other devs are allowed to build more funtionality on top...
I’ve only ever seen inheritance make sense in UI toolkits. That’s literally it.

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.”

> never have I seen a student who telegraphs a look of comprehension after seeing an example of classes involving animals

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.

> Because Customer/Vendor/Invoice was too commonplace?

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.

Once I learned about Unix, I felt that IO devices would make a better example of class inheritance. You have a base abstract class. From that, you get a block device (seekable devices with fixed-sized blocks), a character device (say, a serial port, or a keyboard), a variableblock device (like a tape drive or network, with variable sized blocks that may or may not be seekable). And then you can subclass from there. A more or less realistic example.
I've found Holub on Patterns to be a fantastic resource on applying GoF patterns to real world code.
Thought the exact same thing. Abstract early is not good in practice. You can't understand what problem you are solving until, as the last architect I worked with would say, n is 3. When you have three consumers of a feature, then you start to have an understanding of what the needs are.

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.

I agree. Good architecture is something that can naturally emerge. You start with a simple and less abstract solution and gradually evolve when requirements are better understood. Upfront design mostly does not work.
This boils down to code removeability. A few larger classes wit large methods is in general easier to remove/refactor then a large amount of small cohesive classes with an obtuse design concept behind it. Of all the codebases I refactored, I take the "large methods and large classes" hands down. Pulling a few classes here and there is easy, compared to first understanding a clusterf*ck of overengineered abstractions.
I worked with a domain driven setup at one point, which seemed like it was designed to sell JetBrains licenses because it became almost unbearable to try and maintain the codebase with a text editor. You would have to go through a controller, a DI container, a repository, an entity, a factory, a builder, maybe a facade, and an event bus... almost all of which were single-method classes (except for the DI boilerplate which was split between constructors and YAML files) that just called the method of the next dependent class. One line of concrete business logic hidden between half a dozen files full of architecture.

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.

This makes me think of a good thinking talk, with a quote I pull out from time to time:

> I hate code, and I want as little of it as possible in our product.

http://pyvideo.org/pycon-us-2012/stop-writing-classes.html

I was always disappointed I couldn't find more talks by Jack Diederich after watching this one. His approach was so practical compared to so much programming advice out there. I see now that he's got a few more talks linked from here.

I know how I'll be spending a few hours in the next few days. Thank you for reminding me of his talk!

Indeed. I am always surprised/amused by how often someone will go to the ends of the earth to criticise big up-front design and proclaim that you ain't gonna need it, but then routinely set up several layers of indirection and abstractions in the most basic code, frequently for no immediate benefit other than letting their similarly complicated automated test suite run.
As long as we acknowledge that large classes with large methods can also cause problems. Dealt with that before and it is not fun by any means. The worst offenders, though, are the projects that have a LOT of large classes with large methods, plus some crazy abstraction thrown in like a dash of pepper.
code removeability, or, write code that is easy to delete, not easy to extend: https://programmingisterrible.com/post/139222674273/write-co...
I have to agree. There are OTHER errors that can be introduced by having too many wrapper classes/methods. Clutter (code volume) also adds to causes of mistakes.

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.

> but more code == more errors in general.

you can't have bugs in code you never have to write!

To add to this, don't diminish simple and easy abstractions. There's often a tendency in software development to make use of the fanciest, most complicated tool available, rather than the most practical one. It's easy to forget that just wrapping something up in a handful of static functions and a couple data types is still abstraction, and often it's the right level of abstraction.

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.

+1

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.

> Please take his coding advice with a grain of salt.

I think engineering advices from non-engineers should just be discarded...

I saw that as well and had the same reaction.

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.

A recent quote by Evan Czaplicki of Elm fame that I've come to like; "Abstraction is a tool, not a design goal."
Comments along this line are my favourite in our industry. Harry Roberts said, back in 2014, "Modularity, DRY, SRP, etc. is never a goal, it’s a trait. [...] but understand that they’re approaches and not achievements". This advice has stuck with me ever since.

https://twitter.com/csswizardry/status/539726989159301121

Because DRY as an end-goal can become over-abstracted and taken too far, we came up with WET as the counterpart: Whatever is Efficient and Transparent. In the end, we found you want to be somewhere in between.
When I read the article, I was confused about the end result, as I am fully aware of the legacy garbage from overly abstracted code base, and I was very surprised this guy seems fully confident about its approach...
This is great advice but the real problem is that so many developers never go back and change their code. They are always moving forward and never revisit their old solutions.

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.

The problem is of course that you may then be changing code which has been field-tested for a long time.
I agree but also think it's worth acknowledging the trade off you're making. If you never touch a piece of code, then at some point nobody remembers or is able to understand what it's doing.

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.

As long as the process had been that the only way to get a pull request for a bugfix accepted is by proving the bugfix worked with an automated regression test, then you can change the code as much as you want...
Tests lock you down to particular design by way of the interface. If you want to fundamentally redesign something, you're likely going to have to change the tests as well.

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.

Sounds like you are writing tests at the wrong level then.

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...

We seem to be talking at cross-purposes here. I agree that tests enforce backwards compatibility (or compatibility in general) but they also enforce a particular design. Fundamentally redesigning something generally involves breaking the testa and 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.

This assumes that if you rewrite a piece of code then the possible bugs in the new code are similar to the bugs in the old code. And that is in general not true.
This seems to be industry and not developer driven, business thinks it's like building a bridge and the manpower (and money) is only needed at the build stage, not the maintenance stage. After it is built money can only be directed into adding features and fixing bugs, not to improving code. This applies at both the macro (project/product) and micro (class, module) level.

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 the sad thing is, it's the opposite of what they teach you in school.

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.

Thank you for saying it, cannot agree more. Premature derivation of abstractions so often leads to code that is later difficult to modify and understand and leads to a vicious circle of adding more abstraction and complexity.
It's a 'frggin stats page where a developer forgot to remove a call.

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.

I agree. Developers should never build anything more complicated than it has to be at the moment.

Simplicity is key to happiness and productivity.

We shouldn't go too far with this line of thinking either.

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.

Whatever version is in the documentation people read is the default. If it's not compatible to use v2 when expecting results from v1, you can't change the behavior for no version specified anyway.

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.

Versions, timestamps and unique IDs are definitely base design requirements on anything.
Fully agree. KISS