Hello everyone. To write code that not only runs correctly but is also easy to fix, easy to develop further and helps the team work effectively, it is essential to understand and apply software design principles . These are important guidelines to help you organize your code in a reasonable way, avoiding creating complex software that is difficult to manage. Let's learn these principles!
Key and Useful Design Principles
These are sets of principles or tips that have been recognized and applied by many good programmers to improve software quality.
For example:
// Bad: Employee class is doing too much
class Employee {
public void calculatePay() { /* ... */ }
public void saveToDatabase() { /* ... */ }
public void generateReport() { /* ... */ }
}
// Good: Each class has a single responsibility
class EmployeePayCalculator {
public void calculatePay(EmployeeData employeeData) { /* ... */ }
}
class EmployeeRepository {
public void save(EmployeeData employeeData) { /* ... */ }
}
class EmployeeReportGenerator {
public void generateReport(EmployeeData employeeData) { /* ... */ }
}
class EmployeeData { /* ... */ } // Represents employee information
For example, let's say you have a payment system.
// Instead of modifying PaymentProcessor class for every new payment method:
// if (type == "creditcard") { ... } else if (type == "paypal") { ... }
// Better design following OCP:
interface PaymentStrategy {
void processPayment(double amount);
}
class CreditCardPaymentStrategy implements PaymentStrategy {
public void processPayment(double amount) { /* process credit card payment */ }
}
class MomoPaymentStrategy implements PaymentStrategy { // Extend by adding a new class
public void processPayment(double amount) { /* process Momo payment */ }
}
class PaymentProcessor {
public void executePayment(PaymentStrategy strategy, double amount) {
// PaymentProcessor does not need to be modified for new strategies
strategy.processPayment(amount);
}
}
For example:
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
// Is Square really a Rectangle in all behaviors?
class Square extends Rectangle {
@Override
public void setWidth(int side) {
super.setWidth(side);
super.setHeight(side); // Side effect: changing width also changes height
}
@Override
public void setHeight(int side) {
super.setHeight(side);
super.setWidth(side); // Side effect: changing height also changes width
}
}
// Client code that might break
class AreaCalculator {
public void printArea(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
// Expected area for a rectangle is 5*4=20
// If r is a Square:
// r.setWidth(5) makes width=5, height=5.
// r.setHeight(4) makes height=4, width=4.
// Area will be 4*4=16. This violates LSP as behavior changes.
System.out.println("Calculated Area: " + r.getArea());
}
}
This shows that Square above may not be a good replacement for Rectangle in this principle if the client expects independent behavior of setWidth and setHeight.
For example:
// "Fat" interface - not good
interface GeneralWorker {
void work();
void eat();
void sleep();
}
class HumanEmployee implements GeneralWorker { /* implements all three */ }
class RobotEmployee implements GeneralWorker {
public void work() { /* ... robots work */ }
public void eat() { /* Robots don't eat! Forced to implement or throw exception */ }
public void sleep() { /* Robots don't sleep biologically! */ }
}
// Better: Segregated interfaces
interface Workable { void work(); }
interface Eatable { void eat(); }
interface Sleepable { void sleep(); }
class HumanStaff implements Workable, Eatable, Sleepable { /* ... */ }
class IndustrialRobot implements Workable { /* Robots only need to work */ }
For example:
// Bad: Direct dependency on a concrete low-level module
class LightBulb { // Low-level module (concrete implementation)
public void turnOn() { System.out.println("LightBulb on"); }
public void turnOff() { System.out.println("LightBulb off"); }
}
class ElectricSwitch { // High-level module
private LightBulb bulb; // Directly depends on LightBulb
public ElectricSwitch() { this.bulb = new LightBulb(); }
public void press() {
// Logic tied to LightBulb's specific methods
if (/* bulb is off */ true) { // Simplified condition
bulb.turnOn();
} else {
bulb.turnOff();
}
}
}
// Good: Depending on abstractions (interfaces)
interface Switchable { // Abstraction
void activate();
void deactivate();
}
class ConcreteLightBulb implements Switchable { // Detail depends on abstraction
public void activate() { System.out.println("ConcreteLightBulb on"); }
public void deactivate() { System.out.println("ConcreteLightBulb off"); }
}
class ConcreteFan implements Switchable { // Another detail
public void activate() { System.out.println("ConcreteFan spinning"); }
public void deactivate() { System.out.println("ConcreteFan stopped"); }
}
class UniversalSwitch { // High-level module depends on abstraction
private Switchable device;
private boolean isOn = false;
public UniversalSwitch(Switchable device) { // Dependency is injected
this.device = device;
}
public void operate() {
if (isOn) {
device.deactivate();
isOn = false;
} else {
device.activate();
isOn = true;
}
}
}
Conclusion
Understanding and applying design principles such as SOLID, DRY, KISS will help you write better code, create more robust, flexible and maintainable software systems. Start practicing them in your projects, even small ones, and you will see clear progress. Good luck!