April 12, 2023
A class should have only one reason to change.
public class PostService {
private DataSource database = new MySQLDataSource();
...
}
public class PostService {
private DataSource database = new MongoDBDataSource();
...
}
The above examples violate the SRP because the PostService
class is responsible not only for operations related to posts (creation, update, deletion) but also for managing the database creation logic. If the database changes from MySQL to MongoDB, the class needs modification. In Spring, this can be refactored as follows:
public class PostService {
@Autowired
private DataSource database;
// Business logic for logging and saving
...
}
By refactoring like this, using the @Autowired
annotation to inject the DataSource
interface via field injection (constructor injection is preferred but omitted here for simplicity), we remove the responsibility of database creation from this class, focusing only on post-related logic.
Software entities should be open for extension, but closed for modification.
public class PostService {
private DataSource database = new MySQLDataSource();
...
}
This example, as seen earlier, violates both SRP and OCP. It is closed for extension because it only supports MySQL, and any change to MongoDB would require modifying this class.
public class PostService {
@Autowired
private DataSource database;
// Business logic for logging and saving
...
}
Thus, refactoring as shown above ensures compliance with OCP by allowing the PostService
class to be extended to support different databases without modifying its code.
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program.
Consider the example of rectangle and square:
class Rectangle {
int width;
int height;
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public int getArea() {
return width * width;
}
}
Substituting a rectangle with a square changes the behavior of the getArea()
method, violating LSP. To resolve this, introduce an abstract Shape
class:
public abstract class Shape {
public abstract int getArea();
}
public class Square extends Shape {
private int width;
@Override
public int getArea() {
return width * width;
}
}
public class Rectangle extends Shape {
private int width;
private int height;
@Override
public int getArea() {
return width * height;
}
}
By abstracting to Shape
, both Rectangle
and Square
can be substituted interchangeably without altering expected behavior.
Clients should not be forced to depend on interfaces they do not use.
The Interface Segregation Principle states that if a client does not use certain methods in an interface, it should be split into multiple smaller interfaces.
public interface SmartDevice
{
public abstract void print();
public abstract void fax();
public abstract void scan();
}
public class AllInOnePrinter implements SmartDevice
{
@Override
public void print()
{
// Printing code.
}
@Override
public void fax()
{
// Beep booop biiiiip.
}
@Override
public void scan()
{
// Scanning code.
}
}
In this example, AllInOnePrinter
implements SmartDevice
, but implements unnecessary methods (fax()
and scan()
) for its functionality. Refactor by segregating interfaces:
public interface Print {
void print();
}
public interface Fax {
void fax();
}
public interface Scan {
void scan();
}
public class Printer implements Print
{
@Override
public void print()
{
//Yes I can print.
}
}
public class AllInOnePrinter implements Print, Fax, Scan
{
@Override
public void print()
{
// Printing code.
}
@Override
public void fax()
{
// Beep booop biiiiip.
}
@Override
public void scan()
{
// Scanning code.
}
}
By splitting the interfaces, unnecessary dependencies are avoided, adhering to ISP.
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Summarizing DIP in one sentence: Do not depend on concrete implementations; depend on abstractions.
class SamsungPay {
String payment() {
return "samsung";
}
}
public class PayService {
private SamsungPay pay;
public void setPay(final SamsungPay pay) {
this.pay = pay;
}
public String payment() {
return pay.payment();
}
}
The PayService
high-level module depends directly on SamsungPay
, violating DIP. To adhere to DIP, abstract Pay
as follows:
public interface Pay {
String payment();
}
class SamsungPay implements Pay {
@Override
public String payment() {
return "samsung";
}
}
public class PayService {
private Pay pay;
public void setPay(final Pay pay) {
this.pay = pay;
}
public String payment() {
return pay.payment();
}
}
By abstracting SamsungPay
into Pay
, PayService
is no longer affected by changes in SamsungPay
, demonstrating adherence to DIP.