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.
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.
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 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/
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.
One underrated book that I found very helpful: "A Philosophy of Software Design" by John Ousterhout.
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.
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.
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”.
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.
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 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