I found "A philosophy of software design" to be a well intended but somewhat frustrating book to read.
It seemingly develops a theory of software architecture that is getting at some reasonable stuff, but does so without any reference _at all_ to the already rich theories for describing and modeling things.
I find software design highly related to scientific theory development and modeling, and related to mathematical theories like model theory, which give precise accounts of what it means to describe something.
Just taking the notion of "complexity". Reducing that to _just_ cognitive load seems to be a very poor analysis, when simple/complex ought to deal with the "size" of a structure, not how easy it is to understand.
The result of this poor theoretical grounding is that what the author of A Philosophy of Software Design presents feels very ad-hoc to me, and I feel like the summary presented in this article similarly feels ad-hoc.
> Just taking the notion of "complexity". Reducing that to _just_ cognitive load seems to be a very poor analysis, when simple/complex ought to deal with the "size" of a structure, not how easy it is to understand.
Preface: I'm likely nitpicking here; the use of "_just_" is enough for me to mostly agree with your take.
Isn't the idea that the bulk of complexity IS in the understanding of how a system works, both how it should work and how it does work? We could take the Quake Fast Inverse Square Root code, which is simple in "size" but quite complex on how it actually achieves its outcome. I'd argue it requires comments, tests, and/or clarifications to make sense of what its actually doing.
How do we measure that complexity? No idea :) But I like to believe that's why the book takes a philosophical approach to the discussion.
I agree the arguments in the book largely "make sense" to me but I found myself finding it a little hand-wavey on it actually proving its points without concrete examples. I don't recall there being any metrics or measurements on improvement either, making it a philosophical discussion to me and not a scientific exercise.
I mean, we can definitively talk about simplicity/complexity in a fairly easy way when it comes to mathematical structures or data structures in my opinion.
For instance, a binary tree that contains just a root node is clearly simpler than a binary tree with three nodes, if we take "simple" to mean "with less parts" and complex to mean "with more parts". Similarly, a "molecule" is more complex than an "atom".
This is a useful definition, I think, because when we write computer programs they always written in some programming language, with a syntax that yields some kind of abstract tree, so ultimately we'll always have _some_ kind of graph-like nature to the computer program, both syntactically and semantically, and surely graphs also permit the same kind of complexity metrics.
I'm not saying measuring the number of nodes is _the_ way of getting at complexity, I'm just pointing out that there's no real difficulty in defining it.
Complexity means more stuff, and we simply take it as a premise that we can only fit so much stuff in our head at the same time.
I think my issue with this generalization is assuming the code itself is where complexity is measured and applied.
For example, the Quake Fast Inverse Square Root[1] takes into account nuances in how floating point numbers can be manipulated. The individual operations/actions the code takes (type casts, bit shifts, etc.) are simple enough, but understanding how it all comes together is where the complexity lies, vs just looking at the graph of operations that makes up the code.
Tools like Rubocop for Ruby take an approach like you mention, measuring cyclomatic and branch complexity in your code to determine a mathematical measurement of the complexity of that code. Determining how useful this is, is another conversation I think. I usually find enforcing rules around that code complexity measurement against your code to be subjective.
Going back to the article, the visualization of with vs without abstractions can cover aggregating the mathematical representation of the code and how to tackle complexity. Abstractions lets you take a group of nodes and consider them as a single node, allowing you to build super-graphs covering the underlying structure of each part of the program.
> both syntactically and semantically
I do want to cover semantic program complexity at some point as a deeper discussion. I find that side to me to be quite interesting. How to measure it too.
I haven't read it myself but I probably will because I have a lot of hope for this topic (there must be a better way to do this!)
I worry that it doesn't much matter if it's perfect or mediocre, though, because there's a huge contingent of project managers who mock _any_ efforts to improve code and refuse to even acknowledge that there's any point to doing so - and they're still the ones running the asylum.
Project managers shouldn't be running engineering. They are there to keep the trains running on time, not to design the track, trains and stations.
The generally accepted roles are Product decides what we need to build, Design decides how it should work from user perspective, Engineering decides how to build it at a reasonable upfront and maintenance cost. This involves a fair amount of influence, because Engineering is better equipped to describe the cost tradeoffs than any other function. Of course this comes with the responsibility of understanding the big picture and where the business wants to go. IMHO you should not be speaking to project management about code quality, you should maintain ground level quality as you go, for bigger refactoring/cleanup this needs to be presented to Product leadership (not project managers) in terms of shoring up essential product complexity so it's easier for customers to use, less support, and simpler foundation for the next wave of features. Never talk about code with non-technical stakeholders.
I disagree fundamentally with the modern division of labor. I’ve been around long enough to understand that it doesn’t actually have to work like this.
I don’t think you can be an expert in generic “Product” just like I don’t think you can be a generic management expert.
And I don’t think you can decide what to build or how it should work from a user perspective without taking into account how it’s built. In many ways I think how it’s built tends to inform what it should do more than the other way around.
However, Product alone is never the cause of bad software in my experience. It’s always product plus an engineer who refuses to push back on the initial proposal.
In most cases when product and design comes to you with a feature, and all the solutions you can come up are going to add tech debt or take forever, you should step back and talk to Product about the problem they are actually trying to solve.
If you go back to product with “I can build this very similar feature that will get you 90% of the way there, but will take 1/2 as long and not create maintenance problems down the line”, they will almost always be happy with that.
The real problems are caused when an engineer says immediately “yep I can build that in 2 weeks” and starts trying to force their solution through by telling everyone that product insisted on this specific feature in this specific timeline that unfortunately can only be done in the way they’ve designed. And then they tell product that they have a solution but are being blocked.
I'm very mathematically inclined, so I would probably want a "proper" treatment of this subject to include both formal logic, set theory, type theory and model theory, but they're also subjects I'm still familiarizing myself with.
My basic pitch is that, to a large degree writing sensible computer programs is about modeling some real life activity that the computer program will be part of, and describing things accurately has been done in other fields than programming for many hundreds if not thousands of years, so there's a deep well to draw from.
Despite my appetite for a dry and mathematical treatment of writing computer programs, I still think the book is good for what it is. I think I would go easier on the book if it were not for the title, because philosophy is precisely one of those subjects that tend to favor being very precise about things, something I distinctly think the book lacks. What the book is, however, is an excellent _sketch_ on what we'd want out of program design. I definitely agree about the author's notion of "deep modules" being desirable.
I've written code for a couple of decades. The diagrams in this post are absolutely great. If you're just starting out, try to remember what they say and you'll do really well.
The actual hard question is probably making even 10% of such wisdom and good intentions survive when the program is bombarded by contributor patches, or people taking Jira tickets. TFA talks about it in the context of strategy and tactics.
Organizationally enforcing strategy would be the issue. And also that the people most interested in making rules for others in an organization may not be the ones best qualified to program. And automatic tools (linters) by necessity focus on very surface level, local stuff.
That's how you get the argument for the small teams productivity camp.
It would be cool to see a linter, or a new language, that makes good architecture easy and bad architecture hard.
Like making state machines easier than channels. (Rust is sort-of good at state machines compared to C++ but it has one huge issue because of the ownership model, which makes good SMs a little clumsy)
Or making it slightly inconvenient to do I/O buried in the middle of business logic.
The complexity in our team's code bases have only gotten worse with AI-integrated agents. Maybe it's the prompts we're using, but it's an ironic twist that these tools that promise so much productivity today ends up dumping more tech debt into our code.
It's funny reading the "key contributors to dependency-complexity" -- Duplication, Exceptions, Inheritance, Temporal Decomposition -- because those qualities seem like the standard for AI-generated code.
> but it's an ironic twist that these tools that promise so much productivity today ends up dumping more tech debt into our code.
Because long-term productivity was never about the generated lines of code. You can increase your system features through expansion, or by a combination of expansion and contraction.
Generating new code without spending time to follow through with the contraction step, or alternatively contracting first as a way of enabling the new expansion, will always make the code more complex and harder to sustain and continue to improve wrt the feature set.
Good on him for designing software in the large on the regular and on the daily. I saw him give a talk once in the round. Without him I would be in a bad way.
Nice article! Simple gets complex very fast when creating systems for business problems. For anyone interested in tools check some tips in a free cc-by book at https://nocomplexity.com/simplifyit/
I found "A philosophy of software design" to be a well intended but somewhat frustrating book to read.
It seemingly develops a theory of software architecture that is getting at some reasonable stuff, but does so without any reference _at all_ to the already rich theories for describing and modeling things.
I find software design highly related to scientific theory development and modeling, and related to mathematical theories like model theory, which give precise accounts of what it means to describe something.
Just taking the notion of "complexity". Reducing that to _just_ cognitive load seems to be a very poor analysis, when simple/complex ought to deal with the "size" of a structure, not how easy it is to understand.
The result of this poor theoretical grounding is that what the author of A Philosophy of Software Design presents feels very ad-hoc to me, and I feel like the summary presented in this article similarly feels ad-hoc.
> Just taking the notion of "complexity". Reducing that to _just_ cognitive load seems to be a very poor analysis, when simple/complex ought to deal with the "size" of a structure, not how easy it is to understand.
Preface: I'm likely nitpicking here; the use of "_just_" is enough for me to mostly agree with your take.
Isn't the idea that the bulk of complexity IS in the understanding of how a system works, both how it should work and how it does work? We could take the Quake Fast Inverse Square Root code, which is simple in "size" but quite complex on how it actually achieves its outcome. I'd argue it requires comments, tests, and/or clarifications to make sense of what its actually doing.
How do we measure that complexity? No idea :) But I like to believe that's why the book takes a philosophical approach to the discussion.
I agree the arguments in the book largely "make sense" to me but I found myself finding it a little hand-wavey on it actually proving its points without concrete examples. I don't recall there being any metrics or measurements on improvement either, making it a philosophical discussion to me and not a scientific exercise.
I mean, we can definitively talk about simplicity/complexity in a fairly easy way when it comes to mathematical structures or data structures in my opinion.
For instance, a binary tree that contains just a root node is clearly simpler than a binary tree with three nodes, if we take "simple" to mean "with less parts" and complex to mean "with more parts". Similarly, a "molecule" is more complex than an "atom".
This is a useful definition, I think, because when we write computer programs they always written in some programming language, with a syntax that yields some kind of abstract tree, so ultimately we'll always have _some_ kind of graph-like nature to the computer program, both syntactically and semantically, and surely graphs also permit the same kind of complexity metrics.
I'm not saying measuring the number of nodes is _the_ way of getting at complexity, I'm just pointing out that there's no real difficulty in defining it.
Complexity means more stuff, and we simply take it as a premise that we can only fit so much stuff in our head at the same time.
I think my issue with this generalization is assuming the code itself is where complexity is measured and applied.
For example, the Quake Fast Inverse Square Root[1] takes into account nuances in how floating point numbers can be manipulated. The individual operations/actions the code takes (type casts, bit shifts, etc.) are simple enough, but understanding how it all comes together is where the complexity lies, vs just looking at the graph of operations that makes up the code.
Tools like Rubocop for Ruby take an approach like you mention, measuring cyclomatic and branch complexity in your code to determine a mathematical measurement of the complexity of that code. Determining how useful this is, is another conversation I think. I usually find enforcing rules around that code complexity measurement against your code to be subjective.
Going back to the article, the visualization of with vs without abstractions can cover aggregating the mathematical representation of the code and how to tackle complexity. Abstractions lets you take a group of nodes and consider them as a single node, allowing you to build super-graphs covering the underlying structure of each part of the program.
> both syntactically and semantically
I do want to cover semantic program complexity at some point as a deeper discussion. I find that side to me to be quite interesting. How to measure it too.
[1]: https://en.wikipedia.org/wiki/Fast_inverse_square_root
I haven't read it myself but I probably will because I have a lot of hope for this topic (there must be a better way to do this!)
I worry that it doesn't much matter if it's perfect or mediocre, though, because there's a huge contingent of project managers who mock _any_ efforts to improve code and refuse to even acknowledge that there's any point to doing so - and they're still the ones running the asylum.
Project managers shouldn't be running engineering. They are there to keep the trains running on time, not to design the track, trains and stations.
The generally accepted roles are Product decides what we need to build, Design decides how it should work from user perspective, Engineering decides how to build it at a reasonable upfront and maintenance cost. This involves a fair amount of influence, because Engineering is better equipped to describe the cost tradeoffs than any other function. Of course this comes with the responsibility of understanding the big picture and where the business wants to go. IMHO you should not be speaking to project management about code quality, you should maintain ground level quality as you go, for bigger refactoring/cleanup this needs to be presented to Product leadership (not project managers) in terms of shoring up essential product complexity so it's easier for customers to use, less support, and simpler foundation for the next wave of features. Never talk about code with non-technical stakeholders.
I disagree fundamentally with the modern division of labor. I’ve been around long enough to understand that it doesn’t actually have to work like this.
I don’t think you can be an expert in generic “Product” just like I don’t think you can be a generic management expert.
And I don’t think you can decide what to build or how it should work from a user perspective without taking into account how it’s built. In many ways I think how it’s built tends to inform what it should do more than the other way around.
However, Product alone is never the cause of bad software in my experience. It’s always product plus an engineer who refuses to push back on the initial proposal.
In most cases when product and design comes to you with a feature, and all the solutions you can come up are going to add tech debt or take forever, you should step back and talk to Product about the problem they are actually trying to solve.
If you go back to product with “I can build this very similar feature that will get you 90% of the way there, but will take 1/2 as long and not create maintenance problems down the line”, they will almost always be happy with that.
The real problems are caused when an engineer says immediately “yep I can build that in 2 weeks” and starts trying to force their solution through by telling everyone that product insisted on this specific feature in this specific timeline that unfortunately can only be done in the way they’ve designed. And then they tell product that they have a solution but are being blocked.
I’ve seen this pattern over and over.
I should've noted that, although I found it frustrating, I think it's a good read for most programmers. There are many excellent ideas in the book.
That's very interesting. Can you recommended any resources for learning more about this?
Also, have you considered writing on this subject yourself? I get the feeling that your perspective here would be valuable to others.
I'm very mathematically inclined, so I would probably want a "proper" treatment of this subject to include both formal logic, set theory, type theory and model theory, but they're also subjects I'm still familiarizing myself with.
My basic pitch is that, to a large degree writing sensible computer programs is about modeling some real life activity that the computer program will be part of, and describing things accurately has been done in other fields than programming for many hundreds if not thousands of years, so there's a deep well to draw from.
Despite my appetite for a dry and mathematical treatment of writing computer programs, I still think the book is good for what it is. I think I would go easier on the book if it were not for the title, because philosophy is precisely one of those subjects that tend to favor being very precise about things, something I distinctly think the book lacks. What the book is, however, is an excellent _sketch_ on what we'd want out of program design. I definitely agree about the author's notion of "deep modules" being desirable.
See e.g A Field Guide to Complex Systems , https://www.bm-support.org/problem-solving-methods/
I would also be interested in any book recommendations you have.
I've written code for a couple of decades. The diagrams in this post are absolutely great. If you're just starting out, try to remember what they say and you'll do really well.
The actual hard question is probably making even 10% of such wisdom and good intentions survive when the program is bombarded by contributor patches, or people taking Jira tickets. TFA talks about it in the context of strategy and tactics.
Organizationally enforcing strategy would be the issue. And also that the people most interested in making rules for others in an organization may not be the ones best qualified to program. And automatic tools (linters) by necessity focus on very surface level, local stuff.
That's how you get the argument for the small teams productivity camp.
It would be cool to see a linter, or a new language, that makes good architecture easy and bad architecture hard.
Like making state machines easier than channels. (Rust is sort-of good at state machines compared to C++ but it has one huge issue because of the ownership model, which makes good SMs a little clumsy)
Or making it slightly inconvenient to do I/O buried in the middle of business logic.
The complexity in our team's code bases have only gotten worse with AI-integrated agents. Maybe it's the prompts we're using, but it's an ironic twist that these tools that promise so much productivity today ends up dumping more tech debt into our code.
It's funny reading the "key contributors to dependency-complexity" -- Duplication, Exceptions, Inheritance, Temporal Decomposition -- because those qualities seem like the standard for AI-generated code.
> but it's an ironic twist that these tools that promise so much productivity today ends up dumping more tech debt into our code.
Because long-term productivity was never about the generated lines of code. You can increase your system features through expansion, or by a combination of expansion and contraction.
Generating new code without spending time to follow through with the contraction step, or alternatively contracting first as a way of enabling the new expansion, will always make the code more complex and harder to sustain and continue to improve wrt the feature set.
{ "permissions": { "allow": [ "Human(*)" ] } }
?
Good on him for designing software in the large on the regular and on the daily. I saw him give a talk once in the round. Without him I would be in a bad way.
Nice article! Simple gets complex very fast when creating systems for business problems. For anyone interested in tools check some tips in a free cc-by book at https://nocomplexity.com/simplifyit/