SOLID Principles of OOP
SOLID Principles of OOP
Section 3, Lecture 42
Single Responsibility Principle
Object-Oriented Terminology
In object-oriented programming (Java, among other languages, follows this
paradigm), you will often hear terms such as robustness, cohesion, coupling
etc. Cohesion is a way to measure how much the code segments within one
module (methods of a class, classes inside a package…) belong together. The
higher the cohesion – the better, since high cohesion implies easier
maintenance and debugging, greater code functionality and reusability. The
term cohesion is sometimes contrasted with the concept of coupling, and often,
loose coupling of modules is related to high cohesion.
Another widely used term is robustness, which could be defined as the ability
of a computer system or algorithm to handle mistakes and malfunctions (which
could be caused by various factors such as programmer’s mistake or incorrectly
formatted user input). A robust system is one that can handle these unwanted
situations elegantly. There are various ways for a software engineer to achieve
robustness, such as testing the code for different kinds of inputs, but generally,
in order to achieve robustness (and high cohesion), programmers follow a
certain set of rules and principles for better organization of object-oriented
programs. One such principle is the single responsibility principle.
The single responsibility principle is founded on one of the basic, general ideas
of object-oriented programming – the so-called divide and conquer principle –
solving a problem by solving its multiple sub-problems. This approach prevents
the creation of “God objects” – objects that “know too much or do too much“.
The classes you write, should not be a swiss army knife. They should do one
thing, and to that one thing well.
(Bad) Example
Let’s consider this classic example in Java – “objects that can print themselves”.
. class Text {
. String text;
. String author;
. int length;
. String getText() { ... }
. void setText(String s) { ... }
. String getAuthor() { ... }
. void setAuthor(String s) { ... }
. int getLength() { ... }
. void setLength(int k) { ... }
. /*methods that change the text*/
. void allLettersToUpperCase() { ... }
. void findSubTextAndDelete(String s) { ... }
. /*method for formatting output*/
. void printText() { ... }
. }
At first glance, this class might look correctly written. However, it contradicts
the single responsibility principle, in that it has multiple reasons to change: we
have tho methods which change the text itself, and one which prints the text for
the user. If any of these methods is called, the class will change. This is also not
good because it mixes the logic of the class with the presentation.
Better Example
One way of fixing this is writing another class whose only concern is to print text.
This way, we will separate the functional and the “cosmetic” parts of the class.
. class Text {
. String text;
. String author;
. int length;
. String getText() { ... }
. void setText(String s) { ... }
. String getAuthor() { ... }
. void setAuthor(String s) { ... }
. int getLength() { ... }
. void setLength(int k) { ... }
. /*methods that change the text*/
. void allLettersToUpperCase() { ... }
. void findSubTextAndDelete(String s) { ... }
. }
. class Printer {
. Text text;
. Printer(Text t) {
. this.text = t;
. }
. void printText() { ... }
. }
Summary
In the second example we have divided the responsibilities of editing text and
printing text between two classes. You can notice that, if an error occurred, the
debugging would be easier, since it wouldn’t be that difficult to recognize where
the mistake is. Also, there is less risk of accidentally introducing software bugs,
since you’re modifying a smaller portion of code.
Even though it’s not that noticeable in this example (since it is small), this kind
of approach allows you to see the “bigger picture” and not lose yourself in the
code; it makes programs easier to upgrade and expand, without the classes
being too extensive, and the code becoming confusing.
The Open Closed Principle represents the “O” of the five SOLID software
engineering principles to write well-designed code that are more readable,
maintainable, and easier to upgrade and modify. Bertrand Meyer coined the
term Open Closed Principle, which first appeared in his book Object-Oriented
Software Construction, release in 1988. This was about eight years before the
initial release of Java.
1. “Open for extension “: This means that the behavior of a software module,
say a class can be extended to make it behave in new and different ways. It
is important to note here that the term “extended ” is not limited to
inheritance using the Java extend keyword. As mentioned earlier, Java did
not exist at that time. What it means here is that a module should provide
extension points to alter its behavior. One way is to make use
of polymorphism to invoke extended behaviors of an object at run time.
2. “Closed for modification “: This means that the source code of such a module
remains unchanged.
It might initially appear that the phrases are conflicting- How can we change the
behavior of a module without making changes to it? The answer in Java is
abstraction. You can create abstractions (Java interfaces and abstract classes)
that are fixed and yet represent an unbounded group of possible behaviors
through concrete subclasses.
Before we write code which follows the Open Closed Principle, let’s look at the
consequences of violating the Open Closed principle.
HealthInsuranceSurveyor.java
. package guru.springframework.blog.openclosedprinciple;
. public class HealthInsuranceSurveyor{
. public boolean isValidClaim(){
. System.out.println("HealthInsuranceSurveyor: Validating health insurance claim...");
. /*Logic to validate health insurance claims*/
. return true;
. }
. }
ClaimApprovalManager.java
. package guru.springframework.blog.openclosedprinciple;
. public class ClaimApprovalManager {
. public void processHealthClaim (HealthInsuranceSurveyor surveyor)
. {
. if(surveyor.isValidClaim()){
. System.out.println("ClaimApprovalManager: Valid claim. Currently processing claim for appr
oval....");
. }
. }
. }
Both the HealthInsuranceSurveyor and ClaimApprovalManager classes work
fine and the design for the insurance system appears perfect until a new
requirement to process vehicle insurance claims arises. We now need to include
a new VehicleInsuranceSurveyor class, and this should not create any problems.
But, what we also need is to modify the ClaimApprovalManager class to process
vehicle insurance claims. This is how the modified ClaimApprovalManager will
be:
Modified ClaimApprovalManager.java
. package guru.springframework.blog.openclosedprinciple;
. public class ClaimApprovalManager {
. public void processHealthClaim (HealthInsuranceSurveyor surveyor)
. {
. if(surveyor.isValidClaim()){
. System.out.println("ClaimApprovalManager: Valid claim. Currently processing claim for appr
oval....");
. }
. }
. public void processVehicleClaim (VehicleInsuranceSurveyor surveyor)
. {
. if(surveyor.isValidClaim()){
. System.out.println("ClaimApprovalManager: Valid claim. Currently processing claim for appr
oval....");
. }
. }
. }
In the example above, we modified the ClaimApprovalManager class by adding
a new processVehicleClaim( ) method to incorporate a new functionality (claim
approval of vehicle insurance).
InsuranceSurveyor.java
. package guru.springframework.blog.openclosedprinciple;
. public abstract class InsuranceSurveyor {
. public abstract boolean isValidClaim();
. }
Next, we will write the specific classes for each type of claim validation.
HealthInsuranceSurveyor.java
. package guru.springframework.blog.openclosedprinciple;
. public class HealthInsuranceSurveyor extends InsuranceSurveyor{
. public boolean isValidClaim(){
. System.out.println("HealthInsuranceSurveyor: Validating health insurance claim...");
. /*Logic to validate health insurance claims*/
. return true;
. }
. }
VehicleInsuranceSurveyor.java
. package guru.springframework.blog.openclosedprinciple;
. public class VehicleInsuranceSurveyor extends InsuranceSurveyor{
. public boolean isValidClaim(){
. System.out.println("VehicleInsuranceSurveyor: Validating vehicle insurance claim...");
. /*Logic to validate vehicle insurance claims*/
. return true;
. }
. }
In the examples above, we wrote the HealthInsuranceSurveyor and
VehicleInsuranceSurveyor classes that extend the abstract
InsuranceSurveyor class. Both classes provide different implementations of the
isValidClaim( ) method. We will now write the ClaimApprovalManager class to
follow the Open/Closed Principle.
ClaimApprovalManager.java
. package guru.springframework.blog.openclosedprinciple;
. public class ClaimApprovalManager {
. public void processClaim(InsuranceSurveyor surveyor){
. if(surveyor.isValidClaim()){
. System.out.println("ClaimApprovalManager: Valid claim. Currently processing claim for appr
oval....");
. }
. }
. }
In the example above, we wrote a processClaim( ) method to accept a
InsuranceSurveyor type instead of specifying a concrete type. In this way, any
further addition of InsuranceSurveyor implementations will not affect the
ClaimApprovalManager class. Our insurance system is now open to support
more types of insurance claims, and closed for any modifications whenever a
new claim type is added. To test our example, let’s write this unit test.
ClaimApprovalManagerTest.java
. package guru.springframework.blog.openclosedprinciple;
. import org.junit.Test;
. import static org.junit.Assert.*;
. public class ClaimApprovalManagerTest {
. @Test
. public void testProcessClaim() throws Exception {
. HealthInsuranceSurveyor healthInsuranceSurveyor=new HealthInsuranceSurveyor();
. ClaimApprovalManager claim1=new ClaimApprovalManager();
. claim1.processClaim(healthInsuranceSurveyor);
. VehicleInsuranceSurveyor vehicleInsuranceSurveyor=new VehicleInsuranceSurveyor();
. ClaimApprovalManager claim2=new ClaimApprovalManager();
. claim2.processClaim(vehicleInsuranceSurveyor);
. }
. }
The output is:
. . ____ _ __ _ _
. /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
. ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
. \\/ ___)| |_)| | | | | || (_| | ) ) ) )
. ' |____| .__|_| |_|_| |_\__, | / / / /
. =========|_|==============|___/=/_/_/_/
. :: Spring Boot :: (v1.2.3.RELEASE)
. Running guru.springframework.blog.openclosedprinciple.ClaimApprovalManagerTest
. HealthInsuranceSurveyor: Validating health insurance claim...
. ClaimApprovalManager: Valid claim. Currently processing claim for approval....
. VehicleInsuranceSurveyor: Validating vehicle insurance claim...
. ClaimApprovalManager: Valid claim. Currently processing claim for approval....
. Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec - in guru.springframework.bl
og.openclosedprinciple.ClaimApprovalManagerTest
Summary
Most of the times real closure of a software entity is practically not possible
because there is always a chance that a change will violate the closure. For
example, in our insurance example a change in the business rule to process a
specific type of claim will require modifying the ClaimApprovalManager class. So,
during enterprise application development, even if you might not always
manage to write code that satisfies the Open Closed Principle in every aspect,
taking the steps towards it will be beneficial as the application evolves.
Another example would be a class called Woman with a child class called
Mother. A mother is still a woman, with the addition of having a child. This
brings us to the next term we should explain, which is called polymorphism:
objects can behave in one way in a certain situation, and in another way in some
other situation. In object-oriented programming, this is
called context-dependent behavior. To use the last example: a mother, when
taking a walk with her child or attending a school parent’s meeting, will behave
as a mother. But when she is out with her friends, at work or simply doing
errands, she will behave as a woman. (As you can see, this difference is not that
strict.)
This brings us to the original theme of the article – the Liskov Substitution
Principle.
. class TrasportationDevice
. {
. String name;
. String getName() { ... }
. void setName(String n) { ... }
. double speed;
. double getSpeed() { ... }
. void setSpeed(double d) { ... }
.
. Engine engine;
. Engine getEngine() { ... }
. void setEngine(Engine e) { ... }
. void startEngine() { ... }
. }
These are the kinds of problems that violation of Liskov Substitution Principle
leads to, and they can most usually be recognized by a method that does
nothing, or even can’t be implemented.
. class TrasportationDevice
. {
. String name;
. String getName() { ... }
. void setName(String n) { ... }
.
. double speed;
. double getSpeed() { ... }
. void setSpeed(double d) { ... }
. }
Now we can extend TransportationDevice for non-motorized devices.
Conclusion
Object Oriented languages such as Java are very powerful and offer you as a
developer a tremendous amount of flexibility. You can misuse or abuse any
language. In the Polymorphism post I explained the ‘Is-A’ test. If you’re writing
objects which extend classes, but fails the ‘Is-A’ test, you’re likely violating the
Liskov Substitution Principle.
What the Interface Segregation Principle says is that your interface should not
be bloated with methods that implementing classes don’t require. For such
interfaces, also called “fat interfaces”, implementing classes are unnecessarily
forced to provide implementations (dummy/empty) even for those methods
that they don’t need. In addition, the implementing classes are subject to
change when the interface changes. An addition of a method or change to a
method signature requires modifying all the implementation classes even if
some of them don’t use the method.
Toy.java
. public interface Toy {
. void setPrice(double price);
. void setColor(String color);
. void move();
. void fly();
. }
A class that represents a toy plane can implement the Toy interface and provide
implementations of all the interface methods. But, imagine a class that
represents a toy house. This is how the ToyHouse class will look.
ToyHouse.java
. public class ToyHouse implements Toy {
. double price;
. String color;
. @Override
. public void setPrice(double price) {
. this.price = price;
. }
. @Override
. public void setColor(String color) {
. this.color=color;
. }
. @Override
. public void move(){}
. @Override
. public void fly(){}
. }
As you can see in the code, ToyHouse needs to provide implementation of the
move() and fly() methods, even though it does not require them. This is a
violation of the Interface Segregation Principle. Such violations affect code
readability and confuse programmers. Imagine that you are writing the
ToyHouse class and the intellisense feature of your IDE pops up the fly() method
for auto complete. Not exactly the behavior you want for a toy house, is it?
The solution is- Segregate the Toy interface into multiple role interfaces each
for a specific behavior. Let’s segregate the Toy interface, so that our application
now have three interfaces:Toy, Movable, and Flyable.
Toy.java
. package guru.springframework.blog.interfacesegregationprinciple;
. public interface Toy {
. void setPrice(double price);
. void setColor(String color);
. }
Movable.java
. package guru.springframework.blog.interfacesegregationprinciple;
. public interface Movable {
. void move();
. }
Flyable.java
. package guru.springframework.blog.interfacesegregationprinciple;
. public interface Flyable {
. void fly();
. }
In the examples above, we first wrote the Toy interface with the setPrice() and
setColor() methods. As all toys will have a price and color, all Toy
implementation classes can implement this interface. Then, we wrote the
Movable and Flyable interfaces to represent moving and flying behaviors in toys.
Let’s write the implementation classes.
ToyHouse.java
. package guru.springframework.blog.interfacesegregationprinciple;
. public class ToyHouse implements Toy {
. double price;
. String color;
. @Override
. public void setPrice(double price) {
. this.price = price;
. }
. @Override
. public void setColor(String color) {
. this.color=color;
. }
. @Override
. public String toString(){
. return "ToyHouse: Toy house- Price: "+price+" Color: "+color;
. }
. }
ToyCar.java
. package guru.springframework.blog.interfacesegregationprinciple;
. public class ToyCar implements Toy, Movable {
. double price;
. String color;
. @Override
. public void setPrice(double price) {
. this.price = price;
. }
. @Override
. public void setColor(String color) {
. this.color=color;
. }
. @Override
. public void move(){
. System.out.println("ToyCar: Start moving car.");
. }
. @Override
. public String toString(){
. return "ToyCar: Moveable Toy car- Price: "+price+" Color: "+color;
. }
. }
ToyPlane.java
. package guru.springframework.blog.interfacesegregationprinciple;
. public class ToyPlane implements Toy, Movable, Flyable {
. double price;
. String color;
. @Override
. public void setPrice(double price) {
. this.price = price;
. }
. @Override
. public void setColor(String color) {
. this.color=color;
. }
. @Override
. public void move(){
. System.out.println("ToyPlane: Start moving plane.");
. }
. @Override
. public void fly(){
. System.out.println("ToyPlane: Start flying plane.");
. }
. @Override
. public String toString(){
. return "ToyPlane: Moveable and flyable toy plane- Price: "+price+" Color: "+color;
. }
. }
As you can see, the implementation classes now implement only those
interfaces they are interested in. Our classes do not have unnecessary code
clutters, are more readable, and lesser prone to modifications due to changes in
interface methods.
ToyBuilder.java
. package guru.springframework.blog.interfacesegregationprinciple;
. public class ToyBuilder {
. public static ToyHouse buildToyHouse(){
. ToyHouse toyHouse=new ToyHouse();
. toyHouse.setPrice(15.00);
. toyHouse.setColor("green");
. return toyHouse;
. }
. public static ToyCar buildToyCar(){
. ToyCar toyCar=new ToyCar();
. toyCar.setPrice(25.00);
. toyCar.setColor("red");
. toyCar.move();
. return toyCar;
. }
. public static ToyPlane buildToyPlane(){
. ToyPlane toyPlane=new ToyPlane();
. toyPlane.setPrice(125.00);
. toyPlane.setColor("white");
. toyPlane.move();
. toyPlane.fly();
. return toyPlane;
. }
. }
In the code example above, we wrote the ToyBuilder class with three static
methods to create objects of the ToyHouse, ToyCar, and ToyPlane classes.
Finally, let’s write this unit test to test our example.
ToyBuilderTest.java
. package guru.springframework.blog.interfacesegregationprinciple;
. import org.junit.Test;
. public class ToyBuilderTest {
. @Test
. public void testBuildToyHouse() throws Exception {
. ToyHouse toyHouse=ToyBuilder.buildToyHouse();
. System.out.println(toyHouse);
. }
. @Test
. public void testBuildToyCar() throws Exception {
. ToyCar toyCar=ToyBuilder.buildToyCar();;
. System.out.println(toyCar);
. }
. @Test
. public void testBuildToyPlane() throws Exception {
. ToyPlane toyPlane=ToyBuilder.buildToyPlane();
. System.out.println(toyPlane);
. }
. }
The output is:
. . ____ _ __ _ _
. /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
. ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
. \\/ ___)| |_)| | | | | || (_| | ) ) ) )
. ' |____| .__|_| |_|_| |_\__, | / / / /
. =========|_|==============|___/=/_/_/_/
. :: Spring Boot :: (v1.2.3.RELEASE)
. Running guru.springframework.blog.interfacesegregationprinciple.ToyBuilderTest
. ToyHouse: Toy house- Price: 15.0 Color: green
. ToyPlane: Start moving plane.
. ToyPlane: Start flying plane.
. ToyPlane: Moveable and flyable toy plane- Price: 125.0 Color: white
. ToyCar: Start moving car.
. ToyCar: Moveable Toy car- Price: 25.0 Color: red
. Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec - in guru.springframework.bl
og.interfacesegregationprinciple.ToyBuilderTest
As the size and scope of the application you’re building grows, you are going to
need pluggable components. Even when just for unit testing your classes, the
Interface Segregation Principle has a role. If you’re testing a class which you’ve
written for dependency injection, as I’ve written before, it is ideal that you write
to an interface. By designing your classes to use dependency injection against
an interface, any class implementing the specified interface can be injected into
your class. In testing your classes, you may wish to inject a mock object to fulfill
the needs of your unit test. But when the class you wrote is running in
production, the Spring Framework would inject the real full featured
implementation of the interface into your class.
The Interface Segregation Principle and Dependency Injection are two very
powerful concepts to master when developing enterprise class applications
using the Spring Framework.
“A. High-level modules should not depend on low-level modules. Both should
depend on abstractions.
B. Abstractions should not depend on details. Details should depend on
abstractions.”
LightBulb.java
ElectricPowerSwitch.java
Switch.java
. package guru.springframework.blog.dependencyinversionprinciple.highlevel;
. public interface Switch {
. boolean isOn();
. void press();
. }
We wrote an interface for switches with the isOn() and press() methods. This
interface will give us the flexibility to plug in other types of switches, say a
remote control switch later on, if required. Next, we will write the abstraction in
the form of an interface, which we will call Switchable.
Switchable.java
. package guru.springframework.blog.dependencyinversionprinciple.highlevel;
. public interface Switchable {
. void turnOn();
. void turnOff();
. }
In the example above, we wrote the Switchable interface with the turnOn() and
turnoff() methods. From now on, any switchable devices in the application can
implement this interface and provide their own functionality. Our
ElectricPowerSwitch class will also depend on this interface, as shown below:
ElectricPowerSwitch.java
. package guru.springframework.blog.dependencyinversionprinciple.highlevel;
. public class ElectricPowerSwitch implements Switch {
. public Switchable client;
. public boolean on;
. public ElectricPowerSwitch(Switchable client) {
. this.client = client;
. this.on = false;
. }
. public boolean isOn() {
. return this.on;
. }
. public void press(){
. boolean checkOn = isOn();
. if (checkOn) {
. client.turnOff();
. this.on = false;
. } else {
. client.turnOn();
. this.on = true;
. }
. }
. }
In the ElectricPowerSwitch class we implemented the Switch interface and
referred the Switchable interface instead of any concrete class in a field. We
then called the turnOn() and turnoff() methods on the interface, which at run
time will get invoked on the object passed to the constructor. Now, we can add
low-level switchable classes without worrying about modifying the
ElectricPowerSwitch class. We will add two such classes: LightBulb and Fan.
LightBulb.java
. package guru.springframework.blog.dependencyinversionprinciple.lowlevel;
. import guru.springframework.blog.dependencyinversionprinciple.highlevel.Switchable;
. public class LightBulb implements Switchable {
. @Override
. public void turnOn() {
. System.out.println("LightBulb: Bulb turned on...");
. }
. @Override
. public void turnOff() {
. System.out.println("LightBulb: Bulb turned off...");
. }
. }
Fan.java
. package guru.springframework.blog.dependencyinversionprinciple.lowlevel;
. import guru.springframework.blog.dependencyinversionprinciple.highlevel.Switchable;
. public class Fan implements Switchable {
. @Override
. public void turnOn() {
. System.out.println("Fan: Fan turned on...");
. }
. @Override
. public void turnOff() {
. System.out.println("Fan: Fan turned off...");
. }
. }
In both the LightBulb and Fan classes that we wrote, we implemented the
Switchable interface to provide their own functionality for turning on and off.
While writing the classes, if you have missed how we arranged them in
packages, notice that we kept the Switchable interface in a different package
from the low-level electric device classes. Although, this did not make any
difference from coding perspective, except for an import statement, by doing so
we have made our intentions clear- We want the low-level classes to depend
(inversely) on our abstraction. This will also help us if we later decide to release
the high-level package as a public API that other applications can use for their
devices. To test our example, let’s write this unit test.
ElectricPowerSwitchTest.java
. package guru.springframework.blog.dependencyinversionprinciple.highlevel;
. import guru.springframework.blog.dependencyinversionprinciple.lowlevel.Fan;
. import guru.springframework.blog.dependencyinversionprinciple.lowlevel.LightBulb;
. import org.junit.Test;
. public class ElectricPowerSwitchTest {
. @Test
. public void testPress() throws Exception {
. Switchable switchableBulb=new LightBulb();
. Switch bulbPowerSwitch=new ElectricPowerSwitch(switchableBulb);
. bulbPowerSwitch.press();
. bulbPowerSwitch.press();
. Switchable switchableFan=new Fan();
. Switch fanPowerSwitch=new ElectricPowerSwitch(switchableFan);
. fanPowerSwitch.press();
. fanPowerSwitch.press();
. }
. }
The output is:
. . ____ _ __ _ _
. /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
. ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
. \\/ ___)| |_)| | | | | || (_| | ) ) ) )
. ' |____| .__|_| |_|_| |_\__, | / / / /
. =========|_|==============|___/=/_/_/_/
. :: Spring Boot :: (v1.2.3.RELEASE)
. Running guru.springframework.blog.dependencyinversionprinciple.highlevel.ElectricPowerSwitchTest
. LightBulb: Bulb turned on...
. LightBulb: Bulb turned off...
. Fan: Fan turned on...
. Fan: Fan turned off...
. Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.016 sec - in guru.springframework.bl
og.dependencyinversionprinciple.highlevel.ElectricPowerSwitchTest