While he might understand what he is talking about, based on a corpus of experience, people who learn about this "principle" as they learn to program do not typically understand it and tend to draw only the wrong conclusions from it. What makes for coupling, and how to avoid it, you simply learn with a lot of practice, and there are not many clearcut guidelines you can formulate, it all varies depending on the problem. At best you can read SICP and see how they decouple everything by doing lots of data-driving and dynamic dispatch and things of this sort.
This is too vague to be a "principle", and only causes confusion. It is about as precise, and as useful, as the "write good code" principle.
A lot of people seem to chafe at these principles because they convey truths about a intangible reality they may not even be able to perceive. They only see more work for little payoff. They don't have the body of knowledge and experience (nor the intuition) to know when when to abstract more. We're also in a post-enterprise era, where any sort of deliberate design is often mocked by way of strawman AbstractComposerFactorySingleton references. Rhetoric masquerades as honest intellectual debate for self-promotional purposes.
I consider myself fortunate to have written enough C++ to learn coupling and cohesion. When you screw these up in C++, you pay for it, over and over, through increased compile times and link times.
The principles apply to any language. I wrote a lot of C++ and today I write a lot of Python and the principles of building solid maintainable systems hold the same for both.
I think there are other factor at work here:
- The size of the project. I think most code written today is in the context of tiny systems, e.g. a bit of Javascript that goes with a single web page or some trivial backend code within an existing framework.
- The life cycle of the project. Most software projects have a short life time. This is for various reasons, some of them technical, some of them business related.
- You pay for bad design much farther down the road so it's harder for people to see the causation.
- The investment of individual engineers. If you move from one startup to the other every year you may not care that much about principles that will affect maintainability. You should but a lot of people don't. By the time coupling matters you'll already be hacking somewhere else. Being disconnected from the results of your work creates a difficulty in understanding design principles.
- Not Invented Here. People generally aren't open to receiving lessons from other people's experience. Learning often has to happen through individual experience.
I don't think there are many successful, large, long term, software projects where everything is a jumbled mess with no design intent and everything coupled with everything.
They don't chafe because they convey truths about a intangible reality they may not even be able to perceive. They chafe because nothing is actually being conveyed. Its a bunch of words removed from any actual meaning - from the concrete problems that caused the person to arrive to the truth being "conveyed". They're like the conclusion part of an essay with all the rest left out. They tell nothing to people that don't already know, and nothing new to people that do.
Its no wonder people either tend to discard them or to apply them incorrectly.
I remember first hearing about Uncle Bob's principles and the GoF patterns in a lunch meeting a company in town (Rogue Wave Software) held for anyone interested. When I heard those guys talking about object oriented design and system architecture in general I could just listen in awe. This was before the AbstractFactoryFactory antipattern (Java was just starting) and I thought if I could just gain the level of understanding the RW guys had I'd be golden.
15 years later I've written enough software to understand the principles but no amount of study got me there, just lots of practice and fixing my own mistakes.
Yeah, the actual context is rarely ever delivered. And that matters a lot. Sometimes I wonder how well it would be received, though. Not to mention many books on software engineering tend to be written in a more abstract style than most blog posts. I'm not sure of the precedent for this is there; it would certainly help.
The GoF book does a decent job balancing context with theory, IMO.
I liked the GOF book. But what I'd really love to read about are the descriptions and (architectural) histories of complete, real software systems, especially about the design decisions and the reasons behind those decisions.
I think such information will be received with great interest - it would be like receiving a shot of condensed experience in readable form. Not exactly the same with actual experience of course, but way better than just abstract principles that leave me unconvinced.
Have you seen the Architecture of Open Source Applications[1]? I think it fits very well with what you just described. It explains not only the what, but also the why, with real examples of successful applications.
Yup. Which is why I believe that if you really want to teach someone software design principles, you have to be prepared show them an actual, real software system; the real problem you encountered; the reason why you picked a particular solution; and the results of that choice (both pros and cons). All the details.
Given that data, its easy for a developer to extrapolate (more general) principles that may apply for their own concrete situations and problems, if any.
If you do the extrapolation of the principle yourself but hide the data that caused you to arrive to that principle, nobody will learn anything.
I have no idea why software architects do this. I guess because they're so used to abstracting all the details away in software, they start thinking it also applies to teaching/writing. It does not.
I think it's like this: Six months to two years after starting a moderately-sized code base, you start to see why these things are important. You see that they're actually saying something fairly (though not totally) concrete. The more experience you have bumping into the issues, the more concrete the advice becomes.
But I can't give that to you in something that you can read in an hour, or even a day. I might be able to give it to you in something you could read in a month (of 8-hour days reading). It's really hard to show in a one-page example.
An essay of 10-100 pages with references to the corresponding code is completely acceptable. The problem is, nobody actually does this while they're learning about it (or rather, they have other more important things to do, like the actual refactor) so there are little or no records of it.
GoF almost managed to do it, except they removed the real system context. One can almost learn something from that book :)
This is nice and drives home something particularly important to me. In Parnas' quote he suggests modularizing such that "things that change together stay together". I think this is highly sensible. Another way of saying it is that your APIs should be fixed.
But often this gets conflated in OO because objects are the hearth of modularization (via encapsulation) along with state and interaction and any number of other things.
---
As a comparison point, you might examine ML modules. They look a bit like this
module counter(X)
count : X -> Int
incr : X -> X
and they specify nothing more than the fact that some unknown type X satisfies the interface `(count, incr)`. We can then create a concrete implementation of such a counter
incCounter : counter
incCounter = structure(Int)
count n = n
incr n = n + 1
The incCounter internally uses `Int` to represent `X`, but externally it's completely impossible to tell. This means that modules define exactly two things: encapsulation and interface.
---
So why does this fall down in OO? Because objects lend themselves to being thought of as entities which move through time and space in a stateful fashion. This means you're also likely to encapsulate differences of entity without regard for how they might change together or apart.
Returning to Parnas' quote: it's a bad idea to decompose into modules based on a flowchart. Flowcharts allow you to emphasize the entities of your system, but they are not demonstrating the boundaries of change.
So you can probably get better OO design by being clear when you're using objects as entities (and thus perhaps you do not need encapsulation at all!) and when you're using them as modules. Once this distinction is made it can be clear when objects will derive from flowcharts and when objects will derive from selections of choices made by people.
I personally find it easier to think of it as Conway's Law:
"organizations which design systems ... are constrained to produce designs which are copies of the communication structures of these organizations" - M. Conway
However, I also hate this mental model of software engineering because I have often found it easier to refactor the organization. Maybe because I'm a prima donna and only like working at startups.
I think it's better to require each portion of code has a narrow interface so you can reason in your mind easily about what that code segment does and should do into the future. A function or a class must promise what it should deliver given some inputs and not violate that expectation. If you ever had to reason about code using invariants, you'll grok this.
My team and I have been working on a problem over the last week or so that screams modularity problems. Each time we think we have it solved, someone says "Wait, there's this report only 2 of us have ever heard about that doesn't work because X, Y, Z." The discussion has lasted a couple weeks and is starting to push deployment times back.
Essentially, we have a distributed set of `devices` which interact with `customers`. Each customer has a `session` with the device. During the session, the `customer` may make various types of `payments` (coin or credit card) for various types of `fees`. Additionally, the customer may receive one or more `tickets`. The data model is getting pretty big with:
* Devices
* Sessions
* Line Items
* Allocations
* Payments
* Adjustments
* Violations
* Fees
For accounting purposes, we need to be able to map our payments to the fees and violations they are paying for. Customers might make a single payment to cover multiple violations and those violations may be across multiple sessions.
The number of times I've heard "this new solution fixes X but breaks π" is frustrating, but I don't see how this could be separated out. Perhaps others have insights that would simplify all of this, but it seems to me that the essential fact of our system is that the payment/line item/allocation system is responsible for many tasks/reports. I read articles like this and pine for:
A) A project where true modularity is achievable.
B) The skills to make my current project truly modular.
I'd say code a raw draft to gain a better understanding of the problem space, then talk about it again. If you're close enought to something workable refactor iteratively and let the concepts emerge.
"The Single Responsibility Principle (SRP) states that each software module should have one and only one reason to change. This sounds good, and seems to align with Parnas' formulation. However it begs the question: What defines a reason to change?" (emphasis added)
Ok I realize that languages evolve and all; and I see how the nearly universal appeal of using the phrase "begging the question" in this way will ensure it will soon make its way into the dictionary; but I think people should at least know the original meaning of the phrase [1] and that, in some pedantic or predominantly academic circles today, it is considered as incorrect usage. That is all.
I don't think you're right here. In this case, the fact that "a reason to change" is central to the definition and itself undefined means that the definition is begging the question in the original sense.
This is too vague to be a "principle", and only causes confusion. It is about as precise, and as useful, as the "write good code" principle.