Re: your comment about "And of course I have seen adverse effects from it (eg. your linked article complains of innumerable, non-sensical interfaces) but that is mostly due to inexperienced developers that don't get it"
While this is true, there is a deeper implication here. Assume programming talent is normally distributed. Now, ask yourself above what percentile do you have to be in that distribution to truly grasp the how/why of SOLID and to be able to wield it to solve problems. Now, ask yourself what percentile do you have to be below where you just go crazy with creating non-sensical interfaces and thousands of awfully named classes with single responsibilities such as "CustomerCommandMapEmbelisherConverter"?
The real problem is that code bases tend to be horrible (also normally distributed!) because a lot of talent doesn't meet the bar and can't actually produce programs that aren't rubbish. In any org you'll find the quality of the code base is somewhere on a normal distribution. And you will find all the engineers somewhere on a normal distribution. You'll have a couple of brilliant people, a couple of horrible people, a lot of average people.
The only time you truly see exceptional code bases that everyone stops and goes "wow, this is nice!" are the rare times the stars aligned.
1. Single-responsibility Principle. Objects should have only 1 responsibility
Objects by default often have two responsibilities. Changing it's own state and Holding it's own State.
2. Open-closed Principle. Objects or entities should be open for extension, but closed for modification.
The very concept of a setter or update method on an object is modification. Primitive methods promoted by OOP immediately violate this principle.
3. Liskov substitution principle. Subtypes can replace parent types.
This principle represents a flaw in OOP typing. In mathematics all types should be replaceable by all other types in the same family, otherwise they are not in the same type family. The fact you have the ability to implement a non-replaceable subtype that the type checker identifies as correct means that OOP or the type checker isn't mathematically sound... or in other words the type system doesn't make logical sense.
4. Interface Segregation Principle. Instead of one big interface have functions depend on smaller interfaces.
I agree with this principle. Though many composable types leads to high complexity. I don't think it's an absolute necessity.
5. Dependency Inversion principle. High level module must not depend on the low level module, but they should depend on abstractions.
This is a horrible, horrible design principle. Avoid runtime dependency injection always. Modules should not depend on other modules or abstractions, instead they should just communicate with one another.
If you are creating a module that manipulates strings. Do not create the module in a way such that it takes an Database interface as a parameter than proceeds to manipulate whatever the database object outputs.
Instead create a string manipulation module that accepts strings as input and outputs strings as well. Have the IO module feed a string into the input of the string manipulation module. Function compositions over dependencies... Do not build dependency chains.
1. Single-Responsibility Principle: Whether or not an object can change it's own state has nothing to do with how many responsibilities it has. Even a pure function that takes a single argument can have multiple responsibilities. To give a silly example a spell-check-and-update-wordcount function/object would violate SRP.
2. Open-Closed Principle is about modification of the code. It means the function/object should do its thing so well, you never have to touch its code. But if you want to modify the behavior of your program you should have a way to insert your new function/object so the new behavior is added.
3. Liskov Substitution Principle: "the type checker isn't mathematically sound" No type checker is mathematically sound. Obviously correct statement, since even math itself cannot be automatically proven. However what LSP basically warns against is to say: "a square is a special type of rectangle". It's not, because if you take this 'rectangle' and multiply its width by 2 and its height by 3, you either end up with a 'not a square', which is unexpected or you don't end up with 2xwidth by 3xheight, which is also unexpected.
4. Interface Segregation Principle: agreed
5. Dependency Inversion Principle: "modules should just communicate with one another" is exactly what DIP warns against. Your monthly-activity-calculator shouldn't 'just' communicate with the user-database module. It should take a user-collection interface and let another part of the program that is responsible (SRP!) for setting up that system provide it. That way this program-setup can decide based on configuration / the environment to pass it a redis-user-collection instead of an oracle-user-database.
2. Why does the open and closed principle only have to apply to code? What if it could apply to everything. You gain benefits when you apply this concept to code... what is stopping the benefits from transferring over to runtime structures. SOLID for OOP is defined in an abstract hand wavy way, for FP many of those guidelines become concrete laws of the universe.
>No type checker is mathematically sound.
3. A type checker proves type correctness. Languages can go further with automated provers like COQ or agda. They are mathematically sound. Your square example just means that types shouldn't be defined that way. It means that the type checker isn't compatible with that method of defining types.
5. I highly disagree. There should only be communication between modules NEVER dependency injection. The monthly activity module should not even accept ANY module, or module interface as a parameter. It should only accept the OUTPUT of that module as a parameter. This makes it so that there are ZERO dependencies.
For example don't create a Car object that takes in an engine interface. Have the engine output joules as energy and have the car take in joules to drive. Function Composition over Dependency Injection. (Also think about how much easier it is to unit test Car without a mock engine)
If you get rid of dependency injection, you get rid of the dependency inversion principle. DIP builds upon a very horrible design principle which makes the entire principle itself horrible.
But the rest of your examples are very far off base.
I somewhat agree with Single Responsibility being perhaps not quite right as "single" isn't always desired, appropriate or possible. But the general philosophy is absolutely on point. It's an instruction to carefully consider whether a component should be responsible for something or not and if not then think about where else that responsibility should lie. It gets pretty gnarly when you see things that just have way too many responsibilities. They become unwieldy. An object being responsible for holding and manipulating its state isn't what I would class as a responsibility. That is below the line. That's thinking far too granularly about what a responsibility is.
Same for open/closed. There is a great picture that represents open/closed of a human body (being the closed system) that you can put different layers of clothes on (open for extension) which I think beautifully captures the essence of the principle. When this is done right it's an absolute blessing. You mostly find it in frameworks that have a life-cycle and at certain points (say before anything happens or after everything has happened) they provide an overridable method with no behavior. That method allows you to insert logic the framework designers didn't think to cater for, but also keeps the framework life-cycle intact.
The example of dependency inversion just doesn't make sense. If you're creating a string manipulation library it should take strings and nothing else. It doesn't need anything else. If you're creating a string manipulation library in the first place you probably should just use the standard library. Maybe that's just a bad example, but I still don't agree with your sentiment with always avoid runtime dependency injection.
Forgetting the string manipulation example - I'm curious what you have in mind when you say "modules should instead communicate with one another". How does this communication take place? What language are we talking about and what does some code look like? Mostly what comes to mind when I think of that are either newing up an instance of a class or calling a static method, or perhaps making some kind of http/tcp request?
See what I wrote about composition. I also have an example about a Car and engine class later in the thread.
Function Composition > Dependency Injection.
> ...I agree with your comments on Liskov Substitution Principle, that the fact it's even possible is a weakness in OOP type systems.
It's not actually a weakness in the type system. It's the weakness in the language. The language should never allow for such types to be constructed. Basically Inheritance is not compatible with the type checker. You get rid of inheritance, you get rid of this problem.