HomeToolsAbout

Object Oriented Programming

Single Responsibility

What is it

Class should have only one job.

  • therefore, have only one reason for change.

Why

Because many different teams can work on the same project and edit the same class for different reasons, conflicting definition/changes could lead to incompatible modules.

Fewer merge conflicts and easier version control by design.

Illustration

Suppose you have a class called Car and another class called Invoice at a car dealership.

class Invoice takes in a Car class and has methods calculateTotal, printInvoice, and saveToDatabase.

printInvoice changes the printing logic.

  • Having printInvoice as a method of Invoice class is considered a violation of single responsibility principle because the Invoice class should only contain methods related to business logic, not printing logic.

The printInvoice method should be extracted out to a separate InvoicePrinter class.

saveToDatabase is another violation.

  • saveToDatabase method is a persistence logic, not a business logic on Invoice class.

saveToDatabase method should be extracted out to a separate InvoicePersistence class.

Open-Closed

What is it

Classes should be open for extension and closed to modification.

  • Modification = changing the code of an existing class.
  • Extension = adding new functionality.

Why

We should be able to add new functionality without touching the existing code for the class.

Whenever we modify the existing code, we are taking the risk of creating potential bugs.

So we should avoid touching the tested and reliable production code if possible.

Illustration

The InvoicePersistence class that was extracted out for persistence logic has a method saveToDatabase.

Let's say we want to add an alternate method of saveToFile.

Adding saveToFile to InvoicePersistence would be the first instinct move.

However, this requires the InvoicePersistence to add a method every time a new persistence method needs to be added.

InvoicePersistence class should've defined a method save() with a common interface for persistence logic.

Then, new classes DatabasePersistence and FilePersistence should implement InvoicePersistence.

  • These two classes should define and override the save() method defined in the InvoicePersistence class.
  • The save() method in each of these classes will have different implementation, but will have common interface signature.

This common interface signature becomes a contract for polymorphism.

public class DatabasePersistence implements InvoicePersistence { @Override public void save(Invoice invoice) { // Save to DB } } public class FilePersistence implements InvoicePersistence { @Override public void save(Invoice invoice) { // Save to file } }

When your app extends to have multiple persistence classes, you can create a PersistenceManager class that manages all persistence classes.

public class PersistenceManager { InvoicePersistence invoicePersistence; BookPersistence bookPersistence; public PersistenceManager ( InvoicePersistence invoicePersistence, BookPersistence bookPersistence ) { this.invoicePersistence = invoicePersistence; this.bookPersistence = bookPersistence; } }

With this interface design, you can pass any class that implements the InvoicePersistence interface to the PersistenceManager class (polymorphism).

Liskov Substitution

Subclasses should be substitutable for their base classes.

An object (such as a class) may be replaced by a sub-object (such as a class that extends the first class) without breaking the program

  • 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

When we use inheritance we assume that the child class inherits everything that the superclass has.

  • The child class extends the behavior but never narrows it down.
class Test { static void getAreaTest(Rectangle r) { int width = r.getWidth(); r.setHeight(10); System.out.println("Expected area of " + (width * 10) + ", got " + r.getArea()); } public static void main(String[] args) { Rectangle rc = new Rectangle(2, 3); getAreaTest(rc); // squares test would fail Rectangle sq = new Square(); sq.setWidth(5); getAreaTest(sq); } }

Interface Segregation

There are multiple ways of solving the same problem.

Interface segregation means to keep the interface as small as possible.

  • e.g. same function signature for location elsewhere (common interface).

Different versions of solution to the same problem should still have a common interface.

  • Swapping out service or component becomes easy.
public interface ParkingLot { void parkCar(); // Decrease empty spot count by 1 void unparkCar(); // Increase empty spots by 1 void getCapacity(); // Returns car capacity double calculateFee(Car car); // Returns the price based on number of hours void doPayment(Car car); } class Car { }

When we want to implement a free parkinglot:

public class FreeParking implements ParkingLot { @Override public void parkCar() { } @Override public void unparkCar() { } @Override public void getCapacity() { } @Override public double calculateFee(Car car) { return 0; } @Override public void doPayment(Car car) { throw new Exception("Parking lot is free"); } }

In above example, ParkingLot interface is trying to do too much.

  • ParkingLot implemented both parking and payment related logic.

We can improve this by separating the logic to following:

ParkingLot < PaidParkingLot < HourlyFeeParkingLot < ConstantFeeParkingLot < FreeParkingLot

Now the model is more flexible, extendable, and the clients do not need to implement any irrelevant logic because we provide only parking-related functionality in the parking lot interface.

Dependency Inversion

Classes should depend upon interfaces or abstract classes instead of concrete classes and functions.

Back to earlier example, PersistenceManager class depends on InvoicePersistence instead of the classes that implement that interface.

AboutContact