Ask HN: How did you learn good program design?

44 points by newsoul ↗ HN
Was it a specific language that caused the shift in mindset? Or was it a book, a project or a course at university?

24 comments

[ 3.1 ms ] story [ 60.0 ms ] thread
Writing a toy compiler, the entire pipeline end-to-end, for a language no matter how small. I can't think of another exercise that covers so many good issues to solve, and good practices for solving them.

The two I like to recommend are:

- PLAI: https://cs.brown.edu/courses/cs173/2012/book/

- Essentials of Compilation: https://wphomes.soic.indiana.edu/jsiek/

For general software design, this blog is quite golden: https://www.tedinski.com/archive/

PLAI seems to be a upper level course. What kind of programming/CS background did you have before that? Did PLAI need knowing the language it uses beforehand?
> PLAI seems to be a upper level course

I am a big believer in "Google what you don't know"; it was mostly self-contained from what I recall.

> What kind of programming/CS background did you have before that

I am self-taught, I had like couple of months of JS web-dev under me.

> Did PLAI need knowing the language it uses beforehand

No. That book and the author tries to teach in language agnostic / programming generalist type of way. His other book is good too, aimed for absolute beginners: https://dcic-world.org/2022-01-25/index.html

Crafting Interpreters [1] is also really good, it's often recommended on here when compilers are mentioned. It does spoon-feed you the code (Java in the first part, C in the second), but it's descriptive enough to follow along in a different language/paradigm if that's what you'd rather do.

[1] https://craftinginterpreters.com/

A game engine will also get you there, even for a small 2D game that loads sprites.
Mostly practice. I'd build toy versions of tools like Redis, Docker, Git etc. from scratch. I now help others do the same with CodeCrafters.io.

One underrated book that I found very helpful: "A Philosophy of Software Design" by John Ousterhout.

>"A Philosophy of Software Design" by John Ousterhout

Indeed, very interesting little book! He's done a talk[0] about it if you want a taste before buying.

[0] https://youtu.be/bmSAYlu0NcY

"The Inmates are Running the Asylum" by Alan Cooper.
Via lots and lots of bad designs. Oh and then more bad designs.

Slowly, you start to realize what didn't work, so you try something different. Read about a new pattern, try it. Realize why it doesn't work in specific cases.

Slowly, you start to find things that do work. And you start to reuse them. And sooner or later, your code becomes more robust. You start seeing less spurious bugs. Your programs seem cleaner and faster.

You start to notice themes and patterns, some of which are a bit complex to even verbalize - almost like a muscle memory. You start seeing these themes across many types of systems.

Revisiting programs later on, you realize old mistakes you made, which further solidifies the "feeling" for what's good and bad design in which cases.

There's no real shortcut here. It's just a matter of experience. The only thing aside from simply writing more code is to read more code (whatever form this might take).

I highly suggest getting involved in OSS.

Well said. To add on to this, I recommend keeping a journal about your design thoughts and experiences. By putting your thoughts into words, you can further solidify the "feeling" of good/bad design into something that you can explain to coworkers.
Study Mother Nature for all her glory and its design.

While trout-fishing (eons ago), I devised fastpath for network packet based on geological trend of meandering brooks and their lovely S-shaped curved when I notice “these bends do eventually connect”.

"Head first design patterns"

I also read: - Beautiful code - Code complete - Design patterns - Refactoring - Object oriented analysis and design and many others, but honestly, the first one was a quick and easy read and gave me 80% of it

The only way to learn this is to study complete systems done by others (books, existing systems that you are working on etc.) As Bismarck said; “Only a fool learns from his own mistakes. The wise man learns from the mistakes of others.” The same is applicable on the positive side too. So read/study as much as you can and memorize/learn key design techniques. C/C++ is particularly rich in this regard since there is so much (of varying degrees of complexity) out there in these languages.

Two concrete examples;

1) When i started out with Computers in early 1990's, i read Al Williams' Turbo C.: Memory Resident Utilities, Screen Input/Output and Programming Techniques This book taught me how to layer abstractions and build systems one layer at a time. For example, the book defines a layer over the BIOS api, layer on top of that to design a windowing system, layer on top of that to design a window manager and use that to write a memory-resident text editor. Lots of different things coming together harmoniously to build a complete system.

2) A few years ago when i started to study Embedded Systems, as a Software Guy (no Electronics knowledge) i was at a loss to understand HW gobbledygook until i came across Michael Pont's Patterns for Time-Triggered Embedded Systems (large book freely available at https://www.safetty.net/publications/pttes) It is full of C code showing exactly how to interface with various HW in an Embedded System. That gave me everything i needed to understand how to implement a bare-metal Embedded System and eventually understand the HW (from other books).

> The only way to learn this is to study complete systems done by others. As Bismarck said; “Only a fool learns from his own mistakes. The wise man learns from the mistakes of others.”

So there are at least 2 ways. Saying "only" is a little misleading, no?

I like the book Domain Driven Design by Eric Evans.

Also, pure functional programming and immutability have greatly influenced my design even in languages other than Haskell (my main language is Python).

Eric Evans also stresses the importance of immutability of what he calls Value Objects.

Working with languages that force a lot of immutability (Erlang, Haskell, etc) will change how you think, for sure.
I think the Evan's book about DDD is the worst by far. The one from Vaughn Vernon is better imho (more practical without losing the theoretical aspect).
By iteration. I don't think the problem is solved, we still don't exactly know how to architect our software and the current mainstream of "OO-design" is flawed IMHO. There is no one size fits all architecture for software.

I think size matters, tools matter, technologies matter along with the domain. Our architects are usually too formal, too idealist and we really have difficulty in priorities. We know it requires experience to apply programming languages to problems, this is also true for patterns. In order to apply them, we need experience.

Read a lot of code. Write a lot of code.

I read a lot of books too but most were not especially enlightening.

Probably the most influential thing in how I write software was what I learned in English in high school about structuring essays (I.e. classic rhetoric). A lot of analogies there in my opinion.

I might be late to the show, but here goes:

It's tough to follow other's advice on how to design programs. Each program is different, and applying a paint-by-numbers approach like making everything an object just adds unnecessary bloat to the code. The code should always be crystal-clear, so following design frameworks is not going to be helpful. You are pretty much on your own in terms of design.

A good rule of thumb for designing programs is to prioritize code simplicity. It's fine if it's too simple -- once a piece of code no longer works, iterate and redesign it. Generally, simple code is most flexible. Puting off design decisions until later => more knowledgeable decisions => less technical debt => more flexibility later on.

Be very wary of making abstractions early on. Spaghetti codebases usually result because of bad abstractions. Don't be ashamed of duplicating code many times, as long as it's simpler to read than interfaces and implementations.

0. Working with very talented people exposed me to a lot of ideas and concepts I probably wouldn’t have found. I’m self taught, most of my prior learning was unstructured trial and error.

1. Worked on distributed systems with externally imposed fault tolerance considerations. Designing around these constraints was painful, but forced me to learn how to learn the relevant technical terms and prior art.

2. Functional programming (in Clojure) taught me how to really think about data flow, and design around it. Limiting and isolating state is fundamental to how I work now even in imperative languages.

3. Static types (in TypeScript, which wasn’t my choice but which I love now) taught me how to really think about modeling data upfront, in ways I find more challenging in dynamic languages.

4. Working on several projects either focused on performance or with significant perf challenges has taught (and continues to teach) me how to anticipate where to focus either in initial design or how to approach performance-sensitive refactors.

Edit to add:

5. Reading a lot of code has helped immensely. Often when I find a project I’m interested in, I’ll read its source code in my free time. I’ve learned so much and been quite inspired, even reading through projects I’d never take on myself.

Good design can be performed with any language. However, many language distract you from what good design is all about : thinking deeply about what your particular problem is about. The go programming language, because it is so minimalist, will encourage you to spend time on that, instead of which language feature you should use for representing one part of your data.
Back in the mid-1980s PC world, it would have been hard to avoid at least one of the many competing One True Ways. But the majority of what became my personal ideas of good design came from Phil Burns of Northwestern University, who posted the sources for commercial-grade programs as freeware.

Pib's style was probably evolved from his time teaching programming, and was seriously old-school even then.

A) define the problem set

B) outline a solution set

C) break the solution down into logical, preferably standalone or reuseable, modules

D) write detailed interfaces between every module

E) wrap it all in a main execution loop

This not only broke what might be an overwhelmingly complex program into smaller pieces, but was practically a self-hosting test framework to start with. And being highly modular, it was suited for splitting modules across different programmers.

I've watched many-many styles go by since then, but what I learned from Pib and NWU has always worked well for me, even when programs evolved to where most of my actual "programming" was just calling outside APIs, frameworks, or classes.