One traditional division of software knowledge-–into algorithms, data structures, and system design-–leaves out software design: interfaces, object definition, dependency structure, and so on. So, for example, not which database to use but how to use the database. Here are some resources on the subject.
- Jimmy Koppel's essay on the three levels of software. If true, this has deep consequences for how we think about creating, describing, and testing code.
- Here is Adam Gordon Bell interviewing Jimmy Koppel. I loved listening to this, and review it periodically as a way to shake up my thinking and come back focused on more important things.
- A Philosophy of Software Design, John Osterhout. This is best paired with Jimmy Koppel's review. I think Koppel basically has the best of it when they disagree, but even if (if!) there are some basic conceptual mistakes floating around, Osterhout explains too much, too clearly, with too many useful examples, for a serious student to ignore this book.
- Architecture Patterns in Python, Percival and Gregory. The authors describe the creation of a full (not quite production-ready, but much more than a toy example) system, pausing to explain repositories, units of work, and much more. I marvel at how much widely applicable information they fit into a medium-length book.
- Working Effectively with Legacy Code, Feathers. This is my single favorite software engineering book. One big reason it's relevant here--despite its being primarily a book about fixing, rather than designing, code--is because it's full of wonderful information about interfaces. (Also dependencies, responsibilities, and much more.)
- Jonathan Locke's Coding: On Software Design Process is a short, lovely book, roughly half of which is a series of arguments for and about thinking in terms of "micro-architectures." I'm not completely convinced, but I'm glad to have the notion of a micro-architecture in my toolbox. I'd love it if more programmers wrote this sort of novella-length manifesto.
...And an exercise:
Sketch out the dependency graph for some fragment of your code base. Do this at both the "logical level" and the "code level" (as Koppel would put it), and don't conflate the two. Don't dismiss this as an elementary exercise: (i) it's rare to see it actually done (as usual, people rarely "do the reading") and (ii) I've never regretted taking the time to do it. Be careful not to conflate data flow with the direction of a dependency. (Ousterhout warns us against "temporal decomposition," and I've found both the phrase and the warning useful.)