Class should have only one job.
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.
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.
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.
Classes should be open for extension
and closed to modification
.
Modification
= changing the code of an existing class.Extension
= adding new functionality.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.
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
.
save()
method defined in the InvoicePersistence
class.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).
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
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 programWhen we use inheritance we assume that the child class inherits everything that the superclass has.
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); } }
There are multiple ways of solving the same problem.
Interface segregation means to keep the interface as small as possible.
Different versions of solution to the same problem should still have a common interface.
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.
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.