Ask HN: What books had the greatest effect on how you structure your code?

332 points by rufugee ↗ HN
It'd be interesting to know what books have had the greatest effect on how you structure your code and projects?

161 comments

[ 2.9 ms ] story [ 234 ms ] thread
Alexandrescu's Modern C++ design taught me how to template properly.

Also, Scott Meyers's books were very helpful.

The design of the D standard library has also been very influential on my code (Mainly convincing me of the benefits of ranges over iterator)

Code Complete, Steve McConnell. http://www.stevemcconnell.com/cc.htm

One of the best books on programming style and function, backed up with actual research for the recommendations.

I second this. I read this book in my teens after I had taught myself programming and just been hacking away for a little while. It had never occurred to me to even think about the issues this book was discussing. It really made me think about and appreciate design quality in software.
Same opinion. I always recommend it to my programming students.

Also, The Pragmatic Programmer is good, for topics both about programming and beyond programming per se.

Ditto for both books, though I prefer The Effective Engineer to The Pragmatic Programmer. I found that quite a bit of the tactical advice in TPP is already standard practice (e.g. "always use source control"). Comparatively, TEE has more of an emphasis on project planning and team/company health, and I found that more useful for my work.
A lot if the advice in these books has become widely accepted, so if you might not find that much new in them if you've already worked at a tech company for a bit.
True, but it's likely some junior folks will be reading this thread, so it was worth mentioning, IMO.
(comment deleted)
As a programmer in training who has both Clean Code and Code Complete in a queue on the edge of my desk, I'm following this thread to decide which goes first, or if they both get sold in mint condition.
Slightly kidding ofc. I have already skimmed sections of each.
Code complete is a classic.

Clean code will teach you how writing 3 line functions is a best practice. It's an overhyped, overrated book which is more damaging than helpful and one of the very few I couldn't stand to read until the end due to the authors' dogmatic views.

Care to elaborate what bits of Clean Code are damaging?

I've read it and I thought it has some good ideas. It's the #3 most discussed book on StackOverflow. However, I've been downvoted on HN for recommending something the book talks about (comments are usually misused)

Maybe I'm missing something more experienced developers know.

That's interesting, I had the complete opposite reaction to clean code. As a person that recommends this book, with full heart, to every developer, I'd like to hear any details you could spare re: why you believe it's damaging? Maybe you can provide some examples of well-written codebases and why you believe they're such? And maybe you could comment on why you feel Clean Code's principles harm readability with some examples?

I find it painful working with codebases that don't use most of the principles re:

- Write code like good stories: Code should read like a story, methodically descending the call graph method by method; each working at one level of abstraction.

For example, why is string manipulation littered all over this function that's supposed to be dealing with consolidating reports? Ugh. I like when one function stays at one level of abstraction

- Factor out conditions into descriptive methods: I shouldn't have to sit and read through 5 logical operators, some arithmetic, and method calls in 1 'if' conditional, to understand why we're doing all this.

Once again, this goes back to having your code read like a story. Factor it out into a descriptive function, so that I can understand at a glance and deep dive if necessary.

- Comments are a failure: This applies 99.99% of the time. It's almost comical when someone leaves behind a comment that could be eliminated by factoring something out into a descriptive method call instead.

There's a lot of other great stuff in Clean Code that I'm forgetting off the top of my head. (It's been awhile since I last looked over it.)

I've come across very few code bases that are a literal pleasure to read. https://github.com/jekyll/jekyll is one of the cleanest codebases that comes to mind immediately. And on the other side of the coin, https://github.com/kubernetes/kubernetes is one of the dirtiest.

If anyone wants more details/expansion, let me know and I'll reloop. (On my phone right now.)

(Not parent)

Having read both a long time ago, I also feel like Code Complete is the one to recommend: it covers more ground, is pragmatic, and more importantly most of its recommendations are based on studies, i.e. they come from empirical evidences. Clean Code compares poorly to this, in that it's just one guy's opinion.

That doesn't necessarily mean Clean Code is a bad book, but since our work relies primarily on logic, I'd rather have arguments presented to me logically.

Since both books cover pretty much the same ground, it makes more sense to recommend Code Complete.

The problem with Clean Code is, just as the post you replied on said, it's very dogmatic. A problem I often see people who quote Clean Code do is that they take its principles to the extreme.

An example is tons of tiny methods of 1-3 lines each. Sure splitting up methods is good practice but it's painful to read code where you continuously have to "go to definition". There is an article written about this by John Carmack here http://number-none.com/blow/john_carmack_on_inlined_code.htm...

Another example is the "comment are a failure" mindset. Sure I don't need comments that say this code is doing an addition between two numbers, but a piece of code doing some complex business logic definitely benefits from a comment explaining what the hell it's doing. No amount of function-splitting and function-naming can replace such comments. Yet all too often people who read Clean Code treat comments as some sort of abominations that has to be avoided at all cost.

A dogmatic book does not make a dogmatic person. And if that's a "problem", then that's rather a problem with the person reading the book.

Please provide examples of such codebases you're talking about.

One justification of modularity is to hide details. Thus avoiding the need to "go to definition". If the tests pass and the name is descriptive, then you should understand the method without needing the details.

For example, Jekyll's Site#process [1] provides a high-level overview of how the site is built. It reads like simple, plain, easy-to-understand English. Now, I've chosen to dive into Site#write [2]; it tells me that for each site file, we're going to write it out to its destination, as long as it's supposed to be regenerated. Awesome, that's easy to understand.

Say I want to write another method that operates on all the site files. I don't need to know how Jekyll finds all the site files. Heck, they could be in specific directories... or it could pull configuration over the network... or they could be provided through command-line arguments. Who cares! I just use Site#each_site_file because it's an implementation detail.

And yes, Jekyll provides little 1-3 line helper methods like Site#incremetal? [3] all over the place to codify conditionals. These are extremely helpful.

On the other side of the coin, do you know what this conditional is for [4] in Kubernetes? I can't for the life of me understand its purpose without being forced to look into the details. It'd be much easier to read if it was extracted out into its own descriptive method such as activeMultiNodeInterface maybe? I don't know because I literally don't know the intention of that conditional. The original developer could've made their intention far more clear to subsequent developers had they extracted it out.

I find your citation of Carmack underwhelming for two reasons: (1) he's writing to a very specific target audience -- game developers, and (2) he admits himself, "The whole point of modularity is to hide details, while I am advocating increased awareness of details." Carmack is in no way supporting the idea that "it's painful to read code where you continuously have to 'go to definition'". In fact, quite the opposite, he's advocating a very specific recommendation to a very specific type of developer working on a very specific type of project. Simple as that.

You'd do best to provide some examples and empirical data supporting your assertions.

[1]: https://github.com/jekyll/jekyll/blob/master/lib/jekyll/site...

[2]: https://github.com/jekyll/jekyll/blob/master/lib/jekyll/site...

[3]: https://github.com/jekyll/jekyll/blob/master/lib/jekyll/site...

[4]: https://github.com/kubernetes/kubernetes/blob/master/pkg/kub...

Thinking Forth; Software Tools; SICP; Abstraction and Specification in Program Development; Essentials of Programming Languages; Paradigms of AI Programming; I'd like to list something about OO too, but no book I know really measures up to learning from other programmers. But there's Mark S. Miller's thesis Robust Composition.

(in roughly chronological order. As you can see from the chronology, it took me a long time to start to like OOP.)

What I wouldn't give for 5 more books of equal quality to PAIP...
Came here to mention PAIP as well. A lot of Peter Norvig's stuff is good. He makes it all look so beautiful and simple.
Here I was thinking, "I was never really influenced by books. I just read and wrote a lot of code." But you've reminded me that it's just wrong. Thinking Forth. Nearly 35 years later it still probably dominates my approach.

Edit: Just realised that it is available under a CC license: http://thinking-forth.sourceforge.net/

>Thinking Forth. Nearly 35 years later it still probably dominates my approach.

That's interesting. Can you elaborate on why you think it dominates your approach? I'm interested because I had read a good amount of Starting Forth by Leo Brodie some years ago, and liked the language, and had played around with it a bit (only got access to a Forth system for a short time, so could not go deeply into it, on the practical side). But I remember liking the language. I don't think I have read Thinking Forth.

For me the biggest influences underemphasized in other sources are to be alert for relentless simplification, for 'mechanical sympathy', for ways changing your problem could make it much simpler to solve -- and that there's an agility in using a simpler programming system like a Forth which you grok and can change as you need to, which can sometimes outweigh the leverage of a big one that comes with lots of stuff already done for you. These are useful points of view even with Forth being almost never the most practical tool now. Its day was already passing when I learned it in the 80s.

Also, though I don't think this was quite in the book, it helped lead me to the idea of refactoring and its synergy with automated testing almost a decade before the idea started spreading.

Yes, this is pretty much the same as me. The focus of Thinking Forth (as much as I remember it -- I should re-read it now that I can download it) is about creating small words that you can compose. The idea was that you could interactively tests the words as you were writing the code. FORTH is a bit like Smalltalk in that the environment is always loaded, so you're always jumping in and out of the editor to try things.

Later when I learned about refactoring and TDD, it felt really natural. Until now I hadn't made the connection, but it was exactly the same thing I was doing when I was writing FORTH code.

I also program everything bottom up, rather than top down. I'm pretty sure this is from the influence Thinking Forth had on me as a programmer. I'll make a mental sketch of how things will fit together overall, pick a spot, drill down and then start building the pieces I'm going to need. I refactor as I go. Finally I plug everything together. This also leads me to do subsystem decomposition very, very late in the process. Only after I've built the pieces do I start thinking about where they should ultimately end up. This makes refactoring a lot easier: first when you are building the pieces because you don't have to jump through hoops to get at things, and second when you are assembling because the pieces will naturally fall into places based on what data they interact with. If you have problems understanding where a piece should go, then it's a good indication that you have more refactoring to do.

Some people hate the way I do evolutionary design :-) It works for me, though.

Good points. Another influence I think it had: an attitude like "what one fool can do, another can". While this doesn't directly affect how you organize programs, it can arm you against cargo-culting what you read elsewhere. You do have to watch out for culting up Forth!
Thanks, abecedarius and mikekchar, for the replies.
On LISP, by Paul Graham. LISP was the second language I learned in college, but only after 6 years programming Java and C# that I came back and really learned LISP. It was when I realized that I was doing everything wrong. For example design patterns exists because OO has serious problems that we don't find in a functional programming language and you only see this when you understand both paradigms.
Design patterns exist because common solutions to problems exist, not because OO has serious problems. I understand both paradigms and Lisp is hardly free of design patterns. Every time you pass a lambda to a higher order function you're using a strategy pattern. If you only see design patterns as problems with OO, you're missing the point of design patterns. All languages have design patterns. Common design pattern in Lisp, with-X macros to deal with scoped resource cleanup.
Are Design Patterns really so out of fashion with the youth of today that nobody here mentions Design Patterns as their answer?

Because it is mine, for better or worse. It took me from understanding OO to understanding how to build large systems with OO, writing maintainable code and using proper encapsulation. A much deeper work than Code Complete (though the latter is worth reading too).

I have used functional languages as well (ML, Lisp; not Haskell yet though I think I get what monads are; plus I write a lot of Python, C++, JS in functional style) but I have yet to use them for any project as big. I hope I get the chance to one day. But for now Design Patterns gets my vote.

> Are Design Patterns really so out of fashion with the youth of today that nobody here mentions Design Patterns as their answer?

Apparently so; sad.

(comment deleted)
I don't think design patterns went out of fashion. I think the opposite happened -- they gained such mindshare that they became a solution looking for a problem for many people. That is, they forgot that design patterns are meant to solve problems, and that you should also only solve problems that actually exist. Instead we started getting things that resembled "Hello World Enterprise Edition" [0], which has been dubbed "lasagna code" -- lots of layers with a little bit of filling in each one.

[0] https://gist.github.com/lolzballs/2152bc0f31ee0286b722

The person I replied to, sideshowb, explicitly asked about both design patterns and Design Patterns. Regardless, that book was what elevated design patterns into common knowledge within the object-oriented world. Their fate is tied together; the book and the patterns described within are practically synonymous.
I'm nearing 50 so I'm hardly a youth.

http://www.perlmonks.org/?node_id=133399 does a very good job of explaining why Design Patterns is a book to be careful with. By contrast Code Complete won't steer people wrong.

See https://steve-yegge.blogspot.com/2006/03/execution-in-kingdo... for a cautionary tale about how a pursuit of OO purity can result in great verbosity, hiding intent behind a barrage of patterns. Resulting in code that I, for one, would rather not work with.

Agree on both those fronts, if you're not careful. Still dp influenced my coding more than any other book to date, which was the original question.
As to that, Design Patterns did not influence my coding style very much. By contrast Code Complete was the first book that I read about how to structure code, and I've gained more from it on multiple rereads. I consider it a much better book.

But then again I'm hardly enthusiastic about OO. See http://www.perlmonks.org/?node_id=318257 for more on my perspective.

That second link is great btw. I've always felt that aspect of java was odd, and I do like modern fables :-)
Yegge critiquing OO for verbosity, that's a hoot; talk about the pot calling the kettle black.
But only a person as verbose as Yegge can really on about Java at sufficient length to do the topic justice.
You mean misrepresent the topic and play off bad programming as if OO were the problem; you can write shitty code in any language in any paradigm. Java was verbose because it was Java and it lacked many features other language had to remove that verbosity; it wasn't verbose because it was OO. Yegge was spewing shit for traffic at the time, nothing more, his critiques were baseless.
> build large systems

DP are oft misused, but I think the main idea is a pattern language - a way to communicate to other developers what the architecture is. This is especially important on larger, more complex projects with more developers and developer turnover. Understanding the architecture of a project is crucial for grokking it, but usually not well documented. Even just "DPs are naming conventions" is helpful in this regard.

I really disagree with the idea that most DPs are particularly clever or helpful - they are just the way you'd end up solving those kinds of problems yourself. If you look at your own pre-DP code, you'll see design patterns. What they give is a standardized terminology ("pattern language").

Sometimes an architecture seems more complex than necessary, but makes sense as you better understand the whole problem. e.g. how curl handles http. e.g. youtube-dl. (for me, when I reverse-engineered mini-versions of them, then studied their source)

tl;dr "DPs are docs"

I've heard a lot of people saying they were already using these patterns before they read the gang of four book, it just put a label on those patterns for them.
I think there are two reasons Design Patterns have disappeared:

1. Being able to use functional programming concepts in OO languages made a lot of design patterns irrelevant (Python, Ruby, C# 3.5, C++11, Java 8). I feel Crockford's The Good Parts really introduced these concepts to the OO crowd with javascript acting as the bridging language for the concepts.

2. Many design patterns were terrible ideas in practice. Factory patterns were a disaster, as were many of the others. Good in theory, utterly useless in practice. There was a blog here recently about TDD that argued TDD is useless because you need to be a great architect to do TDD correctly, and most programmers aren't great architects. Design patterns had the same problem, it was very easy to get them wrong and create an utter mess. Ultimately it's more maintainable to have a complicated monolith method than some crazy pattern no-one understands or dare touch.

> Design patterns exist because common solutions to problems exist, not because OO has serious problems

Implementation-recipe design patterns only need to exist (or, rather, only need to be part of the active, day-to-day awareness of more than a small number of programmers per language) when the target language does not support implementing the common solutions as reusable code modules because doing so would require a kind of abstraction the language does not support.

This has nothing to do with the OOP paradigm particularly; there's no reason OOP languages can't support powerful abstractions that reduce the need for that type of design patterns; the GoF book just emerged when the most popular industrial languages were both OOP and lacking in those facilities.

Can you provide an example of how a language like C++ or Java would require a "design pattern" when another language wouldn't?
In early Java versions, functions were not objects, and therefore could not be passed as parameters. So you needed patterns such as Command or Observer. In Javascript, for example, you wouldn't need them. More recent versions of Java, with the introduction of lambdas for example, are also reducing the need for these patterns.
I'm not the person you asked, and not a patterns expert, but will try to answer, using the Template Method Pattern (a GoF book pattern) [1] as an example:

- In Java [2] or C++, to implement that pattern, you may have to define an abstract class, or a class with some abstract and some concrete methods, and subclass it, and in the subclass, redefine some of the abstract methods (of the superclass) concretely. (Maybe interfaces can be used here instead of abstract classes/methods - in Java. Haven't used Java for a while, so not sure right now.)

- In Python, you can just do:

def transform(input, output):

    # Code here to read data from input, transform it, and 
    # write the transformed data to output.
Now let's say the code in the transform function does input.read() and output.write() (with maybe some way to indicate when the end of input is reached, like the null string is used in Python). In other words, methods like those of Python file objects are used for reading and writing. Given all this, you can now pass objects of any types as arguments for the input and output parameters, as long as they implement a read and a write method respectively, and the code will just work.

This has been described as "smooth seamless polymorphism" in Python. It's also known as duck typing.

https://en.wikipedia.org/wiki/Duck_typing

[1] The Template Method Pattern is one of the ways in which frameworks (as opposed to libraries) are implemented. The abstract methods of the framework are implemented concretely by your code that the framework then calls, using the Hollywood principle - Don't call me, I'll call you, a.k.a. Inversion of Control (IoC).

https://en.wikipedia.org/wiki/Template_method_pattern

https://en.wikipedia.org/wiki/Inversion_of_control

[2] Note that what I wrote is based on knowledge of older versions of Java - newer versions may have features that make the pattern simpler to implement, maybe without abstract classes/methods.

The Visitor pattern mostly addresses the shortcomings of the type system (i.e no "match" statement).

The Command pattern, in its most basic form, could also be considered as implementing 1st class functions by wrapping them into objects.

So if you're using a functional langage, there's a good chance you're wondering why consider both of the above as "design patterns", as you're seing them as basic things, and the compiler implements them for you.

Functional languages have a different set of patterns.
> The Visitor pattern mostly addresses the shortcomings of the type system (i.e no "match" statement).

The visitor pattern addresses doing multiple dispatch in a single dispatch language.

> The Command pattern, in its most basic form, could also be considered as implementing 1st class functions by wrapping them into objects.

BTW, that's not really true; first class functions are not a replacement for commands. Commands are about turning behavior into state so it can be stored and executed, potentially replayed for example, at a later time. Commands aren't just functions, even languages that have first class functions still use the OO command pattern when doing things that benefit from command patterns.

> Commands are about turning behavior into state so it can be stored and executed, potentially replayed for example, at a later time.

First-class functions plus partial application do that trivially. (First-class functions provide the ability to store a generic command, partial application let's you get a specific instance with local state that can be “replayed” against a different external state object.)

Again, you're missing the point of the command pattern. It is not replaceable by a first class function. I'm well aware of what first class functions are, what they can and can't do, and yes in the most trivial cases at run-time only where only a single action is required of the command, they can be used instead of a command pattern.

Many times commands come with multiple actions, to allow execution and rollback, require being able to be serialized to disk to database and replayed later, or are used to better organize and plug in extension commands to large code bases. None of these cases are amenable to first class functions.

The command pattern isn't just a hack around not having first class functions, we still use command even when first class functions are available. You aren't going to extend Photoshop with a new filter using a first class function; you're going to write a plugin which is a command pattern for filter execution.

Stop trying to tell me what first class functions can do, I'm fully aware, I use them every day and have for nearly 20 years. You aren't talking to some kid who never heard of functional programming.

> > The Command pattern, in its most basic form, could also be considered as implementing 1st class functions by wrapping them into objects.

> BTW, that's not really true; first class functions are not a replacement for commands.

Please note that you're extrapolating from my post: this isn't what I said.

We still need this pattern: there's no way you're going to implement "undo" using only first-class functions, and I'm well aware of that.

In some specific situations, though, we don't need it anymore, like when connecting a GUI "save" button to a "document.save" function.

https://drewverlee.github.io/ i cover some (most) of the Design Patterns from the GOF book though the lens of Clojure. I stopped largely because i concluded the same thing Peter Norvig did many years ago, most DP in that book are work arounds for language limitations.
> Implementation-recipe design patterns only need to exist (or, rather, only need to be part of the active, day-to-day awareness of more than a small number of programmers per language) when the target language does not support implementing the common solutions as reusable code modules because doing so would require a kind of abstraction the language does not support.

False. Lisp has macro's and can abstract anything syntactically, yet still has commonly recurring design patterns such as with-X macros. You misunderstand what a design pattern is if you think new language features can remove the need for them.

Here's some old school thought leadership on dynamic languages and design patterns using actual data. Specifically Peter Norvig looked at all the examples in books like Design Patterns and found that:

16 of 23 patterns are either invisible or simpler, due to [...]

http://norvig.com/design-patterns/design-patterns.pdf

He's talking about the original design patterns book which is a book of OO patterns, it's hardly surprising that many OO patterns are either invisible or simpler in a functional language, functional languages have different design patterns. And yes, I know who Norvig is and I've read that article before.
The parent comment is a variation of the well-known claim from http://wiki.c2.com/?DesignPatternsInDynamicProgramming which says that in a more powerful language, a variety of design patterns go away and become invisible. And Lisp in particular renders many common patterns irrelevant.
The irony is that the languages which are the targets of design patterns already encode considerable design patterns from a previous generation of language research.

Those languages are higher-level languages. A considerable number of design patterns disappears in them already.

In Java or C++, you have functions with parameters and local variables.

In assembly language, there is a "design pattern" of pushing words onto a stack to generate arguments, and moving the stack pointer to reserve a frame for local variables, and then unwinding these actions when the function returns. Also, saving certain registers and restoring them may be part of the pattern. This pattern can be supported with macros and pseudo-ops to varying degrees.

In these higher level language, this pattern disappears. The concept of parameters and local variables doesn't disappear, of course. Those things are just declared in a simple way.

The assembly language pattern for calling conventions is in fact a back-formation from higher level languages. It won't even occur to assembly language programmers until they think of their programs at a higher level, and translate from that to machine code.

Or: let's look at C++ vs C. In C++, there are declarative mechanisms which generate code that can be understood in terms of C. It captures coding patterns, such as ensuring that a structure is properly initialized when instantiated, and cleaned up on every exit path from a function.

And yet Lisp, the language that can abstract anything, still has design patterns because there's no such thing as a language without patterns that are useful to solve common problems.
Design Patterns is about typing out boilerplate code by hand to solve a problem, because the language doesn't have the expressivity to encode the same solution directly.

When it does have the expressivity, the underlying rendering of the pattern hasn't gone away; just the manual boiler-plate for producing it.

For instance, if you're working in assembly language, then you might sometimes benefit from a "while loop design pattern" to keep your code clean. The while loop design pattern calls for some test code before a block which jumps past the block when the test is negative, and an unconditional backward branch at the end of the block back to that test expression.

The while loop doesn't go away when you use a higher level language. You just indicate that you would like that pattern, and the compiler spits it out for you, invisibly.

> Every time you pass a lambda to a higher order function you're using a strategy pattern.

Every time you pass a "naked" lambda somewhere, you're potentially missing the opportunity to have a macro there do that for you.

> Every time you pass a "naked" lambda somewhere, you're potentially missing the opportunity to have a macro there do that for you.

True but equally sad that you must use Lisp to hide ugly Lisp because naked lambda's are so ugly. Yet the point remains, whether hidden behind a macro or not, design patterns still exist in Lisp and every other language and always will.

Lambdas aren't "ugly"; they are sometimes just a mechanism that is not directly relevant to the problem domain: the "how" part of the solution, rather than "what".

The point isn't to hide the lambdas, but to hide the "how", which might or might not use lambdas.

The "how" could instead open-code the procedure that would have otherwise called the lambdas; then the material just becomes embedded forms in the inline code.

Lets not kid ourselves, hiding lambda's is one of the primary uses of macros in Lisp (obviously not the only). Yes, you're hiding the "how", we don't disagree there, but my point was that such syntactic abstraction is only necessary in Lisp because it's so ugly to directly use lambda. Compare to Smalltalk which uses naked lambda's everywhere because they're nice looking as is and don't need to be hidden away by special forms in order to feel idiomatic.
> Lets not kid ourselves, hiding lambda's is one of the primary uses of macros in Lisp (obviously not the only). Yes, you're hiding the "how", we don't disagree there, but my point was that such syntactic abstraction is only necessary in Lisp because it's so ugly to directly use lambda. Compare to Smalltalk which uses naked lambda's everywhere because they're nice looking as is and don't need to be hidden away by special forms in order to feel idiomatic.

Hiding lambdas with macros isn't really that common in my experience. The "how" being hidden is much more often (ultimately) IF, LET, BLOCK, and GO. Of course, higher order functions could often be used instead, but they're generally not.

That's true. I think the GP's point was probably that some GoF patterns, specifically, just exist to work around language deficiencies. My go-to example for that is the visitor pattern; something that's trivial in Haskell or Lisp is ridiculously heavyweight in C++ or Java.
That is true, some of "those" particular patterns work around language deficiencies, however OP misunderstand and think that means all patterns do that, they don't.
OO doesn't have serious problems. Crap implementation of OO bolted onto Algol derivatives has problems.
That was also the impression I got on this issue; however nowadays I'm usually less frustrated with what the respective languages provide and more with what people use them for.

C++ since 11, C# since ~5 and Java 9 for example have actually introduced very useful tools¹ that would allow people to write much more readable code - yet there's always someone who insists that anything beyond the C++98-style is somehow evil and inherently unreliable.

¹Yes, they're by no means perfect, but they introduce are a lot of low-hanging fruit when it comes to improving code quality - if one decides to use them...

Refactoring. If I could shove that book down every coder's throat in the world I would. Or, if I could even just get them to use extract method like ... half the times they should I'd consider that to be a major historical achievement.
Refactoring: Improving the Design of Existing Code

After reading I began to think about programming as an algebraic transformation from one system to another, in doing so radically reduced the amount of errors I made.

I got this when it first came out, I was kind of doing it in a "gut feeling" kind of way, but this made me think of it more as a discipline, and solidified the idea of micro improvements and let me drop the idea of trying to get it exactly right the first time.
Of lesser-known books, Avdi Grimm's "Confident Ruby": http://www.confidentruby.com/

Learning Ruby itself was a huge influence to me; hadn't considered that a language should be designed to make programmers "happy", as Matz said. "Confident Ruby" was one of several books that had this human-happiness focus. "Confident" is broken down into patterns, many of which can be found in books like Sandi Metz's POODR, but as a semi-experienced programmer, Grimm's way of writing really appealed to me.

Even the title of the book itself was revelatory to me. The idea that the functions and methods and conventions we create should be rooted in a "confident" mentality (such as the old adage of being promiscuous in what a function accepts, and strict in what it returns) really improved the way I designed code. Not just in terms of technical proficiency, but with less cognitive burden, which ultimately leads to the elegant simplicity we desire in our work.

Ruby makes programmers happy while they're writing their sexy new code base. It makes them cry when it's time to refactor it.
I don't know why you're being voted down.. even Avdi Grimm supports that:

http://www.virtuouscode.com/2015/08/11/what-its-like-to-come...

I didn't downvote but I suspect whoever did saw the GP comment as bashing Ruby and being off-topic, even though some of my comment talked about Ruby's philosophy as a whole (though I felt it was important to include the context in which Grimm's philosophy is grounded).

FWIW, I almost never use Ruby today, having switched to Python for both teaching and development purposes. That doesn't mean that learning Ruby didn't influence me. For starters, it taught me the dangers of giving programmers too much syntactic freedom :).

I would not equate refactoring an active project with returning to a dormant project.
Avdi is a phenomenal teacher in my opinion and doesn't get the airtime he deserves. If you use ruby in any capacity, I highly recommend subscribing to his Ruby Tapas.
Smalltalk Best Practice Patterns by Kent Beck. Refactoring by Martin Fowler. Design Patterns by the Gang of Four. Domain Driven Design by Eric Evans.
Clojure Applied by Ben Vandergrift and Alex Miller. That and Rich Hickey's Greatest Hits (https://changelog.com/posts/rich-hickeys-greatest-hits).
I haven't read that many coding textbooks not specific to a language but the Joy of Clojure probably slapped me with the most ideas I'd never seen before. Not sure it made me a better developer in the short run, but I'd like to think that it will in the long run.
Fountainhead
The Little Lisper by Dan Friedman. It is probably 30 years out of publication but I pulled it off of my shelf the other day, reread it, and the next day I noticed that my code had improved. It is a quick read, nothing earth shattering, but you will be amazed by what you have forgotten.
It surprised me just how huge the impact was of reading about Flow-based Programming years ago (J Paul Morrison's book, though, his site is probably a sufficient substitute http://www.jpaulmorrison.com/fbp/)

Of course, in recent times, it has become all that much more relevant to me as I began working in data science/engineering space. Even though it's not specifically about code structure for a particular language, it addresses a common flaw in most programming approaches that seem to treat all functionality as a servant of the current context which is strange and silly and not how anything works in physical information processing so why do it in code?

A somewhat common pattern that maps well to FBP is "Railway Oriented Programming." Though FBP in full takes this well beyond simply shooting errors along in sideband to the happy path.

Hands-down, Practical Object-Oriented Design in Ruby (http://www.poodr.com/) - some of it I don't agree with but it's all wonderfully put-together: clear and concise, with wonderful examples. Much more about OO design than Ruby, non-Rubyists will get 98% of the value out of it that Rubyists would.
I'm glad to see someone posted this book. I think it's far undervalued. Definitely a must read. The Ruby written could be completely replaced by pseudo code, and it wouldn't change the lessons taught.
And there is another book by Sandi, released this year. https://www.sandimetz.com/99bottles/

Her books and her talks had the biggest influence on me regarding code design. At least influenced to think a lot more about it.

Kent Beck'a Smalltalk book. I've never used Smalltalk but it transfers to any dynamic OOP language. (It also used to be $25, yikes what happened?)
SICP made the biggest difference; it really changed the way I approach mutability and state management in everything I write (To name just this).