A frequently used term in Object-Oriented design is "SOLID" which is a mnemonic acronym that covers five principles of OO design. These principles do not stand alone; there is interplay among them. And they are not laws, merely principles, like "an apple a day keeps the doctor away." But they serve to guide us in the right direction as we design and build our systems.
There are no real "shortcuts" here. As Euclid said to Ptolemy, who wanted to know the easy way to learn geometry, "There is no royal road to geometry." So, too, there is no royal road to good object-oriented design. But that said, read on for some of the summary and background information that will make your software designs more coherent and easier to build, test, extend and reuse.
Do I Really Have to Learn This?
What happens when these principles are ignored? Some call it software "rot." I prefer to think of it in terms of fragility
. Software that is of fragile
design is difficult to extend or modify. Even simple changes can induce seemingly unrelated entropy elsewhere in the system (Einstein called this, "spooky action at a distance
"). Eventually this phenomenon comes to the attention of the business managers, and they become resistant to change. Developers become frustrated as micromanagement ensues. Ultimately the system must be scrapped and re-invented.
Software that is viscous
can be changed, but only slowly and at great cost. I recently looked at an application that had 479 instances of long-ago deprecated calls to mysql_query()
. Imagine the remediation workload! The owner, in complete denial, said he would handle the problem by doing nothing and keeping the current software as long as he could. Perhaps he will keep it until his competitors have overtaken him and relieved him of his clients!
Requirements change and designs must change to accommodate the requirements. If our designs and implementations cannot change in response to the requirements we do our missions a disservice. This is true whether we are building a small online store or the web network of the US Army. The principles of OO design allow our software to make room for change.
The S in SOLID
The S stands for Single Responsibility Principle
("SRP" for short).
The goal of the SRP is to isolate program functionality into small units of code, each with a single and clear purpose. The reason for this goal is that it helps us make our programming highly modular and testable. If a class only does one thing, the unit tests can be both simple and highly accurate -- either the thing is done or not. It also helps us minimize the effects of program changes, because we would only change the software with functionality that directly related to our changed requirements. We can thereby avoid "spooky action at a distance."
SRP is formally described by saying that an object should never have more than one responsibility, and that there should never be more than one reason for an object to change
. Corollaries to these rules include, "just because you can doesn't mean you should." Related concepts include cohesion and coupling.
Cohesion tells us how strongly-related and focused are the responsibilities of a module (acknowledging that while we want a single responsibility, we will build our collection of objects into modules and systems of multiple functions and responsibilities). High cohesion is good; our code focuses on only the important things.
Coupling tells us the degree to which each program module relies on the behavior or internal structure of other modules. Low coupling is good
; our code is written in such a way that we can swap objects or modules with minimal risk that a swap will disrupt the system. In all things there is a separation of concerns.
The O in SOLID.
The O stands for Open / Closed Principle
("OCP" for short).
The goal of the OCP is to allow us to introduce new behaviors into our system without having to change existing behaviors. Each change may introduce bugs and requires re-testing, therefore we want to avoid changes that affect the current working parts of our system. Writing new classes is less likely to introduce intractable problems when compared to changing existing classes.
The OCP tells us that our code should be open for extension, but closed for modification. Code is open to extension if new behavior can be added in the future via extension and inheritance. Code is closed to modification if new behavior can be added without changing any existing code.
How can we change behavior without changing code? We can rely on abstractions and class extension. We can couple our code to an interface
instead of to a concrete class. In procedural code, some level of the OCP can be achieved by using parameters that guide different behaviors in a piece of software. The PHP function library
is full of functions that change behaviors when you change parameters. Do you think these functions adopt the OCP but violate the SRP? I do!
To put it another way, once you have written and tested and deployed a class, you're done with it and you should never have to go back to change the class. If you need more functionality you can extend the class. See how this plays with the SRP? If your class is misdesigned and does many things, you might find yourself having to go back into the code to make modifications. Every such modification requires a new round of testing and risks introducing unwanted side effects ("bugs").
When we think about programming with the OCP in mind, we often think about writing our programs as if they are intended not only for ourselves, but also for others to use
. We separate our functionality at the obvious points of interface. Perhaps more than any other principle, this states the goal of OO design.
The L in SOLID.
The L stands for the Liskov Substitution Principle
("LSP" for short).
The goal of the LSP is to allow any module that uses a parent class to also use any derived child class that extends the parent class. Programming written this way will play well with the OCP and will generally make sense to any programmer who comes to look at the code. Specifically, child classes must not remove base class behavior or violate base class principles. If the child class replaces a method of the parent class, the method must still work correctly for all objects created from either the child or parent class.
The LSP states that subtypes must be substitutable for their base types. Note that this is not to say that each child "is-a" version of the parent. It says that each child "is substitutable for" or "behaves like" the parent. To be effective in substitution, the interfaces between objects should be small and concise.
when you find that you have two classes that share a lot of behavior but are not LSP-interchangeable. Maybe you can think of a third class that both can derive from.
A closely related principle is "Design by Contract
," a process that uses preconditions and postconditions to assert the change in state that will be induced by invoking a piece of software. This is made somewhat more difficult in PHP because of its limited type-hinting
The I in SOLID.
The I stands for the Interface Segregation Principle
("ISP" for short).
The goal of ISP is to keep our code down to the minimum required to provide the required functionality. "Fat" interfaces are a disadvantage because the implementing classes contain too much code and may be slow to load or difficult to test.
Clients should not be forced to depend on information or methods they do not use. A classic example is the reliance on a large configuration settings class, when all a web page really needs is the Title and the name of the Author. The web page is coupled to unnecessary information, reducing speed, flexibility, and maintainability. ISP code smells include unimplemented interface methods. They often violate the LSP, too. A good way forward when you encounter ISP smells might be to create a new, smaller interface. Since PHP has extendable interfaces
, we have a good tool available for designs that need interface segregation.
The D in SOLID.
The D stands for the Dependency Inversion Principle
("DIP" for short).
At the time it was coined, "dependency inversion" was so named because hard-coupled dependencies were the norm in programming, and the principle inverted that norm. Today, I would call it the "dependency independence principle." If OCP states the goal of OO design, then DIP teaches us how to achieve that goal. Formally stated, "High-level modules should not depend on low-level modules; both should depend on abstractions. And abstractions should not depend on details; details should depend on abstractions." The goal here is to produce software that is loosely coupled and not brittle or rigid.
The classic analogy of depending on an abstracted interface is the notion of a lamp and an electrical supply in your home. You could solder the lamp wires to the electrical wires and the lamp would work, but there is a better way through the interface that is provided by the electrical outlet. The lamp makers agree with the home builders on the design of the interface. Lamps come with plugs, homes come with sockets. The resulting implementation is flexible; any lamp can work in any room of the house.
To see this in software, consider a User class that runs a Database query; the User and Database are tightly coupled. A change in the Database will necessitate a change in the User. These separate responsibilities can be decoupled and made independent with an interface. Properly designed, the User class does not need to know where the user information came from (it could come from any correct version of the acceptable data source
), it only needs to know that its data source conforms to the interface.
Classes should declare their need for dependencies
. A class that contains the keyword "new" inside the constructor has created an internal dependency on a low-level module. These classes become difficult or impossible to test without violating the OCP. In contrast, a class that uses dependency injection can be tested with mock objects. Though beyond the scope of this article, you might want to learn more about Inversion of Control Containers
In the decades since software development began, we have come upon several inflection points that mark giant leaps forward in the state of the art. Early compilers
moved our work beyond "machine language" and thereby engendered one of these paradigm shifts. The development of object-oriented programming and the discernment of the SOLID principles are among today's most important advances in the field.
References and Further Reading
Please give us your feedback!
If you found this article helpful, please click the "thumb's up" button below. Doing so lets the E-E community know what is valuable for E-E members and helps provide direction for future articles. If you have questions or comments, please add them. Thanks!