8th June 2011
The premise of the article SOLID OO Principles is that in order to be any "good" at OO programming or design you must follow the SOLID principles otherwise it will be highly unlikely that you will create a system which is maintainable and extensible over time. This to me is another glaring example of the Snake Oil pattern, or "OO programming according to the church of <enter your favourite religion here>". These principles are nothing but fake medicine being presented to the gullible as the universal cure-all. In particular the words "highly unlikely" lead me to the following observations:
Some of these principles may have merit in the minds of their authors, but to the rest of us they may be totally worthless. For example, some egotistical zealot invents the rule "Thou shalt not eat cheese on a Monday!" What happens if I ignore this rule? Does the world come to an end? Does the sun stop shining? Do the birds stop singing? Does the grass stop growing? Does my house burn down? Does my dog run away? Does my hair fall out? If I ignore this rule and nothing bad happens, then why am I "wrong"? If I follow this rule and nothing good happens, then why am I "right"?
It is possible to write programs which are maintainable and extensible WITHOUT following these principles, so following them is no guarantee of success, just as not following them is no guarantee of failure. Rather than being "solid" these principles are vague, wishy-washy, airy-fairy, and have very little substance at all. They are open to interpretation and therefore mis-interpretation and over-interpretation. Any competent programmer will be able to see through the smoke with very little effort.
My own development infrastructure is based on the 3 Tier Architecture, which means that it contains the following:
It also contains an implementation of the Model-View-Controller (MVC) design pattern, which means that it contains the following:
Please note that the 3 Tier Architecture and Model-View-Controller (MVC) design pattern are not the same thing.
The results of my approach clearly show the following:
Yet in spite of all this my critics (of which there are many) still insist that my implementation is wrong simply because it breaks the rules (or their interpretation of their chosen rules).
My main criticism of each of the SOLID principles is that, like the whole idea of Object Oriented Programming, it is possible for individuals to create their own interpretation, then try to enforce that as the "only" interpretation worth having. There are too many people out there who are fond of saying "Don't do it like that, do it like this!" where "this" is always different to what the previous person said. Let me examine each of the principles in turn and show you what I mean.
The Single Responsibility Principle (SRP) states that an object should have only a single responsibility, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.
This actually makes sense to me - identify an entity, then define a class which encapsulates all the properties of that entity and all the operations which can be performed on that entity. This implies that you should exclude any properties or operations which do not belong to that entity. In the applications that I write each entity exists as a table in a database, so I have a separate class for each table. Each class has standard methods to perform the standard CRUD operations that can be performed on a database table - Create (insert), Read (select), Update and Delete, so these are inherited from an abstract table class.
That sounds perfectly OK until some wise-ass comes up and says "Your classes all deal with create, read, update and delete, but in my opinion it is the operations, not the entities, which are separate responsibilities and it is therefore the operations which should be split into separate classes". This seems rubbish to me as it totally violates the principle of encapsulation which is "The act of placing data and all the operations that can be performed on that data in the same class". In other words the classes are centered around entities and not operations. My implementation adheres to that fundamental principle, but here is someone telling me that each operation belongs in a separate class! So who is correct?
My own implementation of the Data Access Object allows it to perform any operation on any database table, but some would say that this violates their interpretation of the Single Responsibility Principle as it allows too many operations on too many tables. I believe that my implementation is perfectly valid, so which is wrong - the principle itself, or their interpretation of it?
The Open/Closed Principle (OCP) states that "software entities should be open for extension, but closed for modification". This is actually confusing as there are two different descriptions - the Meyer's Open/Closed Principle and Polymorphic Open/Closed Principle. The idea is that once completed, the implementation of a class can only be modified to correct errors; new or changed features will require that a different class be created. That class could reuse coding from the original class through inheritance.
While this may sound a "good" thing in principle, in practice it can quickly become a real PITA. My main application has over 200 database tables with a different class for each table. These classes are actually subclasses which are derived from a single abstract table class, and this abstract class is quite large as it contains all the standard code to deal with any operation that can be performed on any database table. The subclasses merely contain the specifics for an individual database table. Over the years I have had to modify the abstract table class by changing the code within existing methods or adding new methods. If I followed this principle to the letter I would leave the original abstract class untouched and extend it into another abstract class, but then I would have to go through all my subclass definitions and extend them from the new abstract class so that they had access to the latest changes. I would then end up with a collection of abstract classes which had different behaviours, and each subclass would behave differently depending on which superclass it extended.
It may take time and skill to modify my single abstract table class without causing a problem in any of my 200 subclasses, but it is, in my humble opinion, far better than having to manage a collection of different abstract table classes.
According to Craig Larman in his article Protected Variation: The Importance of Being Closed (PDF) the OCP principle is is essentially equivalent to the Protected Variation (PV) pattern: "Identify points of predicted variation and create a stable interface around them". OCP and PV are two expressions of the same principle - protection against change to the existing code and design at variation and evolution points - with minor differences in emphasis. In the same article he also makes this interesting observation:
We can prioritize our goals and strategies as follows:Low coupling and PV are just one set of mechanisms to achieve the goals of saving time, money, and so forth. Sometimes, the cost of speculative future proofing to achieve these goals outweighs the cost incurred by a simple, highly coupled "brittle" design that is reworked as necessary in response to true change pressures. That is, the cost of engineering protection at evolution points can be higher than reworking a simple design.
- We wish to save time and money, reduce the introduction of new defects, and reduce the pain and suffering inflicted on overworked developers.
- To achieve this, we design to minimize the impact of change.
- To minimize change impact, we design with the goal of low coupling.
- To design for low coupling, we design for PVs.
If the need for flexibility and PV is immediately applicable, then applying PV is justified. However, if you're using PV for speculative future proofing or reuse, then deciding which strategy to use is not as clear-cut. Novice developers tend toward brittle designs, and intermediates tend toward overly fancy and flexible generalized ones (in ways that never get used). Experts choose with insight - perhaps choosing a simple and brittle design whose cost of change is balanced against its likelihood. The journey is analogous to the well-known stanza from the Diamond Sutra:
Before practicing Zen, mountains were mountains and rivers were rivers.
While practicing Zen, mountains are no longer mountains and rivers are no longer rivers.
After realization, mountains are mountains and rivers are rivers again.
Even if I do create a core class and modify it directly instead of creating a new subclass, what problem does it cause (apart from offending someone's sensibilities)? If the effort of following this principle has enormous costs but little (or no) payback, then would it really be worth it?
The Liskov Substitution Principle (LSP) states that "objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program". In geek-speak this is expressed as: "If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program". This is supposed to prevent the problem where a subtype may introduce methods that are not present in the supertype, and the introduction of these methods may allow state changes in the subtype that are not permissible in the supertype.
This may be difficult to comprehend unless you have an example which violates this rule, and such an example can be found in the Circle-ellipse problem. This can be summarised as follows:
One possible solution would be for the subclass to implement the inapplicable method, but to either return a result of FALSE or to throw an exception. Even though this would circumvent the problem in practice, it would still technically be a violation of LSP, so you will always find someone somewhere who will argue against such a practical and pragmatic approach and insist that the software be rewritten so that it conforms to the principle in the "proper" manner.
But how does this rule fit in with my application where I have 200 table classes which all inherit from the same abstract class? Should I be expected to substitute the Product subclass with my Customer subclass and still have the application perform as expected?
The Liskov Substitution Principle is also closely related to the concept of Design by Contract (DbC) as it shares the following rules:
Unfortunately not all programming languages have the ability to support DbC, and PHP is not one of them due to the fact it is not statically typed and not compiled. This is discussed in Programming by contracts in PHP. Certain languages, like Eiffel, have direct support for preconditions and postconditions. You can actually declare them, and have the runtime system verify them for you.
The Interface Segregation Principle (ISP) states that "many client specific interfaces are better than one general purpose interface". Once an interface has become too 'fat' it needs to be split into smaller and more specific interfaces so that any clients of the interface will only know about the methods that pertain to them. In a nutshell, no client should be forced to depend on methods it does not use.
What exactly does 'fat' mean in this context? From the simple description above I would define a general-purpose interface as one which could perform several operations, such as create, read, update or delete, where one of the arguments defined the operation to be performed. The method would then invoke a different piece of code depending on what operation name was supplied at runtime. I totally agree that such a method, which is capable of performing several separate and distinct operations, should be split into a separate method for each operation. That is why my abstract table class does not have a single doSomething($operation, $data) method, but instead has a separate method for each separate operation:
You may think that this is OK, but some bright spark will jump up and say that my getData method is too generic as it can return any number of records using any combination of selection criteria. "You should do what I do" he says, "and have separate and distinct methods for getById, getByName, getByX, getByY, getMultiple, et cetera". Now why would I want to follow his example and create more work without any tangible benefit?
Yet there is another definition of what "fat" means. In his paper entitled The Interface Segregation Principle (PDF) Robert C. Martin defines "fat interfaces" as those which are non-cohesive, or not specific to a single client. In other words, the interfaces of the class can be broken up into groups of member functions. Each group serves a different set of clients. Thus some clients use one group of member functions, and other clients use the other groups. In his example of the ATM User Interface he defines a "client" as a transaction (with its own UI) which performs a distinct operation such as deposit, withdraw and transfer. Each of these transactions uses some of the methods which are available in the ATM object, but not all of them, and it is these "unused" methods which cause a (theoretical) problem. But what is the problem, exactly? According to Robert "This creates the possibility that changes to one of the derivatives of Transaction will force corresponding change to the UI, thereby affecting all the other derivatives of Transaction, and every other class that depends upon the UI interface." I'm sorry but "creates the possibility" is simply not good enough. If I know that this possibility does not exist in my application, especially as I am programming in a dynamic language which has no compilation phase and no DLL files, then why should I expend effort to deal with a situation that simply will not happen?
If I have a class that contains 10 methods, and this class has 10 clients which access only 1 method each and ignore the other 9, this means that I do not have separate interfaces which are specific to a single client but rather a single set of interfaces which can be used, or not used, by any client. This would appear to be a violation of this principle, but so what? What exactly is the problem caused by a client being exposed to interfaces that it does not actually use? If a client does not reference a particular method then is it actually "exposed" to that interface? How does the existence of this non-referenced method affect this particular client? If it does not cause a problem, then why should I implement the solution of creating a separate subclass with a separate interface which is unique to each client? This would not work anyway as it is simply not possible, using inheritance, to create a subclass which has fewer methods than its superclass. I could do this by creating separate classes instead of subclasses (which would break encapsulation by the way), but the effort would be considerable and the payback absolutely nil, so why should I bother?
My interpretation of this principle is that although each class provides separate methods for the Create, Read, Update and Delete (CRUD) operations, I have a collection of controllers which only refer to the operations that they actually use. Thus the Create controller does not refer to the Delete operation just as the Delete controller does not refer to the Create operation. This means that I can change any interface without having to change any controller which does not refer to that interface. By change I also mean recompile as PHP is not a compiled language.
The Dependency Inversion Principle (DIP) states that "the programmer should depend upon abstractions and not depend upon concretions". This is pure gobbledygook to me! Another way of saying this is:
This to me is confusing as it uses the terms "abstract" and "concrete" in ways which contradict how OO languages have been implemented. My understanding is as follows:
The only workable example of this principle which makes any sense to me is the "Copy" program which can be found in Robert C. Martin's The Dependency Inversion Principle (PDF). In this the Copy module is hard-coded to call the readKeyboard module and the writePrinter module. While the readKeyboard and writePrinter modules are both reusable in that they can be used by other modules which need to gain access to the keyboard and the printer, the Copy module is not reusable as it is tied to, or dependent upon, those other two modules. It would not be easy to change the Copy module to read from a different input device or write to a different output device. One method would be to code in a dependency to each new device as it became available, and have some sort of runtime switch which told the Copy module which devices to use, but this would eventually make it bloated and fragile.
The proposed solution is to make the high level Copy module independent of its two low level reader and writer modules. This is done using dependency inversion or dependency injection where the reader and writer modules are instantiated outside the Copy module and then injected into it just before it is told to perform the copy operation. In this way the Copy module does not know which devices it is dealing with with, nor does it care. Provided that they each have the relevant read and write methods then it will work. This means that new devices can be created and used with the Copy module without requiring any code changes or even any recompilations of that module.
While this contrived example shows obvious benefits from using the Dependency Inversion Principle, it would be foolish to assume that similar benefits can be obtained from implementing this principle in every situation where there is a dependency. What if you know that your dependencies will never change, or will never need a different configuration? Why go through the effort of implementing the ability to make changes when those changes will never happen? This particular topic is described more fully in Dependency Injection is Evil.
Like any other design pattern each of these principles has been formulated to offer a solution to a specific problem, and this leads me to make the following observations:
Sometimes, the cost of speculative future proofing outweighs the cost incurred by a simple, highly coupled "brittle" design that is reworked as necessary in response to true change pressures.Is it really worth while to spend a pound to save a penny?
Following these principles with blind obedience and implementing them without question is no guarantee that your software will be perfect. As I said in the introduction different OO "experts" have different opinions as to what is the "right" way and what is the "wrong" way. It is simply not possible to follow one person's opinion without offending someone else. If you don't follow the SOLID principles someone will be offended. Even if you do attempt to follow them someone else will jump up and say "Your implementation is wrong!", or "Your implementation goes too far!" or "Your implementation does not go far enough!" or "Don't do it like that, do it like this!" It is simply not possible to find a solution which satisfies everyone and offends no one, and if you attempt to do so you may end up in the situation described in The Man, The Boy, and The Donkey where the punch line is "if you try to please everyone you may as well kiss your ass goodbye!"
If it is not possible to please everyone then what can you do? The simple answer is to please yourself - ignore everyone else and do what you think is best for your particular circumstances. After all, it is you building the software, not them. You are the one who is going to deploy and maintain it, not them. It is your ass on the line, not theirs.
When I was a junior programmer I had to follow the lead set by my so-called "superiors", but I kept hitting obstacles that their methodologies created. When I proposed solutions to these obstacles I was constantly put down with "You can't do that as it is against the rules!" or sometimes "How dare you have the audacity to question the rules! Don't think about them, just do as you're told!" When I became senior enough to create my own methodology I concentrated on what I needed to do to get the job done with as few of these known obstacles as possible, and in the process I found myself throwing more and more of these silly rules into the waste bin. When others see that I am not following "their" set of rules they instantly accuse me of being "wrong", "impure" and a heretic, but why should I care? I am results-oriented, not rules-oriented, so the fact that I have created software which is powerful, flexible, extensible and maintainable is all the justification that I need. If it works, and if the effort required to keep it working is minimal, then how can it possibly be "wrong"? I have seen projects fail because too much attention was focussed on the rules instead of the results, so if something fails how can it possibly be "right"?
© Tony Marston
8th June 2011
http://www.tonymarston.net
http://www.radicore.org