Backend & APIs Topic
What are Software Principles?
Software Principles are fundamental guidelines and best practices that help developers create maintainable, scalable, and robust software. They provide the "why" behind many design patterns and architectural decisions.
1. SOLID Principles
The SOLID principles are five design principles that make software designs more understandable, flexible, and maintainable.
1.1 Single Responsibility Principle (SRP)
"A class should have only one reason to change."
Explanation:
Every class should have a single responsibility or purpose. When a class has multiple responsibilities, changes to one responsibility can affect the others.
Bad Example (Violating SRP):
java
public class UserService {
// This class has multiple responsibilities
public void registerUser(String username, String password) {
// Business logic
if (username == null || username.isEmpty()) {
throw new IllegalArgumentException("Username cannot be empty");
}
// Validation logic
if (!isValidPassword(password)) {
throw new IllegalArgumentException("Invalid password");
}
// Database logic
saveUserToDatabase(username, password);
// Email logic
sendWelcomeEmail(username);
// Logging logic
System.out.println("User registered: " + username);
}
private boolean isValidPassword(String password) {
return password != null && password.length() >= 8;
}
private void saveUserToDatabase(String username, String password) {
// Database operations
System.out.println("Saving user to database: " + username);
}
private void sendWelcomeEmail(String username) {
// Email sending logic
System.out.println("Sending welcome email to: " + username);
}
}
Good Example (Following SRP):
java
// Each class has a single responsibility
public class UserValidator {
public boolean isValidUser(String username, String password) {
return isValidUsername(username) && isValidPassword(password);
}
private boolean isValidUsername(String username) {
return username != null && !username.isEmpty();
}
private boolean isValidPassword(String password) {
return password != null && password.length() >= 8;
}
}
public class UserRepository {
public void saveUser(String username, String password) {
// Only database operations
System.out.println("Saving user to database: " + username);
// Actual database code would go here
}
public User findUserByUsername(String username) {
// Database query logic
return null; // simplified
}
}
public class EmailService {
public void sendWelcomeEmail(String username) {
// Only email-related logic
System.out.println("Sending welcome email to: " + username);
}
}
public class Logger {
public void log(String message) {
// Only logging logic
System.out.println("LOG: " + message);
}
}
// Main service that coordinates the single-responsibility classes
public class UserService {
private UserValidator validator;
private UserRepository repository;
private EmailService emailService;
private Logger logger;
public UserService() {
this.validator = new UserValidator();
this.repository = new UserRepository();
this.emailService = new EmailService();
this.logger = new Logger();
}
public void registerUser(String username, String password) {
if (!validator.isValidUser(username, password)) {
throw new IllegalArgumentException("Invalid user data");
}
repository.saveUser(username, password);
emailService.sendWelcomeEmail(username);
logger.log("User registered: " + username);
}
}
1.2 Open/Closed Principle (OCP)
"Software entities should be open for extension but closed for modification."
Explanation:
You should be able to extend a class's behavior without modifying its source code.
Bad Example (Violating OCP):
java
public class AreaCalculator {
// This violates OCP because we need to modify this class for every new shape
public double calculateArea(Object shape) {
if (shape instanceof Rectangle) {
Rectangle rect = (Rectangle) shape;
return rect.getWidth() * rect.getHeight();
} else if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return Math.PI * circle.getRadius() * circle.getRadius();
}
// Adding a new shape requires modifying this method
throw new IllegalArgumentException("Unsupported shape type");
}
}
public class Rectangle {
private double width;
private double height;
// constructors, getters, setters
}
public class Circle {
private double radius;
// constructors, getters, setters
}
Good Example (Following OCP):
java
// Base interface that is closed for modification
public interface Shape {
double calculateArea();
}
// Concrete implementations that are open for extension
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
// getters, setters
}
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
// getters, setters
}
// New shape can be added without modifying existing code
public class Triangle implements Shape {
private double base;
private double height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double calculateArea() {
return 0.5 * base * height;
}
}
// AreaCalculator doesn't need to change when new shapes are added
public class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.calculateArea(); // Delegates to the shape's implementation
}
public double calculateTotalArea(List<Shape> shapes) {
return shapes.stream()
.mapToDouble(Shape::calculateArea)
.sum();
}
}
// Usage
public class GeometryApp {
public static void main(String[] args) {
List<Shape> shapes = Arrays.asList(
new Rectangle(10, 20),
new Circle(5),
new Triangle(10, 15) // New shape without modifying AreaCalculator
);
AreaCalculator calculator = new AreaCalculator();
double totalArea = calculator.calculateTotalArea(shapes);
System.out.println("Total area: " + totalArea);
}
}
1.3 Liskov Substitution Principle (LSP)
"Objects of a superclass should be replaceable with objects of its subclasses without breaking the application."
Explanation:
Subtypes must be completely substitutable for their base types.
Bad Example (Violating LSP):
java
public class Rectangle {
protected int width;
protected int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
public class Square extends Rectangle {
public Square(int size) {
super(size, size);
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width); // Violates LSP - changes height too
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height); // Violates LSP - changes width too
}
}
// This function works with Rectangle but breaks with Square
public class AreaTest {
public void testRectangleArea(Rectangle rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(4);
// For Rectangle: 5 * 4 = 20
// For Square: 4 * 4 = 16 (unexpected!)
assert rectangle.getArea() == 20 : "Area should be 20";
}
public static void main(String[] args) {
AreaTest test = new AreaTest();
test.testRectangleArea(new Rectangle(0, 0)); // Works fine
test.testRectangleArea(new Square(0)); // Fails! Violates LSP
}
}
Good Example (Following LSP):
java
// Base interface/class
public abstract class Shape {
public abstract int getArea();
}
// Separate classes without inheritance relationship
public class Rectangle extends Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
public class Square extends Shape {
private int size;
public Square(int size) {
this.size = size;
}
public void setSize(int size) {
this.size = size;
}
@Override
public int getArea() {
return size * size;
}
}
// Factory for creating shapes
public class ShapeFactory {
public static Shape createRectangle(int width, int height) {
return new Rectangle(width, height);
}
public static Shape createSquare(int size) {
return new Square(size);
}
}
// Usage - both shapes can be used interchangeably through the Shape interface
public class GeometryApp {
public void processShape(Shape shape) {
System.out.println("Area: " + shape.getArea());
// Both Rectangle and Square can be substituted without issues
}
public static void main(String[] args) {
GeometryApp app = new GeometryApp();
Shape rectangle = ShapeFactory.createRectangle(5, 4);
Shape square = ShapeFactory.createSquare(5);
app.processShape(rectangle); // Area: 20
app.processShape(square); // Area: 25 - both work correctly
}
}
1.4 Interface Segregation Principle (ISP)
"Clients should not be forced to depend on interfaces they do not use."
Explanation:
Create specific, focused interfaces instead of general-purpose ones.
Bad Example (Violating ISP):
java
// One giant interface that does too much
public interface Animal {
void eat();
void sleep();
void fly(); // Not all animals can fly!
void swim(); // Not all animals can swim!
void run(); // Not all animals can run!
}
// Classes are forced to implement methods they don't need
public class Dog implements Animal {
@Override
public void eat() { System.out.println("Dog eating"); }
@Override
public void sleep() { System.out.println("Dog sleeping"); }
@Override
public void fly() {
// Dogs can't fly! But we have to implement this method
throw new UnsupportedOperationException("Dogs can't fly!");
}
@Override
public void swim() { System.out.println("Dog swimming"); }
@Override
public void run() { System.out.println("Dog running"); }
}
public class Bird implements Animal {
@Override
public void eat() { System.out.println("Bird eating"); }
@Override
public void sleep() { System.out.println("Bird sleeping"); }
@Override
public void fly() { System.out.println("Bird flying"); }
@Override
public void swim() {
// This bird can't swim! But we have to implement this method
throw new UnsupportedOperationException("This bird can't swim!");
}
@Override
public void run() { System.out.println("Bird running"); }
}
Good Example (Following ISP):
java
// Segregated interfaces - each has a specific purpose
public interface Animal {
void eat();
void sleep();
}
public interface Flyable {
void fly();
}
public interface Swimmable {
void swim();
}
public interface Runnable {
void run();
}
// Classes only implement interfaces they need
public class Dog implements Animal, Swimmable, Runnable {
@Override
public void eat() { System.out.println("Dog eating"); }
@Override
public void sleep() { System.out.println("Dog sleeping"); }
@Override
public void swim() { System.out.println("Dog swimming"); }
@Override
public void run() { System.out.println("Dog running"); }
// No fly() method - that's correct!
}
public class Bird implements Animal, Flyable, Runnable {
@Override
public void eat() { System.out.println("Bird eating"); }
@Override
public void sleep() { System.out.println("Bird sleeping"); }
@Override
public void fly() { System.out.println("Bird flying"); }
@Override
public void run() { System.out.println("Bird running"); }
// No swim() method - that's correct!
}
public class Fish implements Animal, Swimmable {
@Override
public void eat() { System.out.println("Fish eating"); }
@Override
public void sleep() { System.out.println("Fish sleeping"); }
@Override
public void swim() { System.out.println("Fish swimming"); }
// Only implements what it needs
}
// Usage with focused interfaces
public class Zoo {
public void makeAnimalEat(Animal animal) {
animal.eat();
}
public void makeAnimalFly(Flyable flyable) {
flyable.fly();
}
public void makeAnimalSwim(Swimmable swimmable) {
swimmable.swim();
}
}
1.5 Dependency Inversion Principle (DIP)
"Depend on abstractions, not on concretions."
Explanation:
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Bad Example (Violating DIP):
java
// High-level module depending on low-level module
public class WeatherTracker {
// Direct dependency on concrete implementation
private EmailNotifier emailNotifier;
private SMSNotifier smsNotifier;
public WeatherTracker() {
this.emailNotifier = new EmailNotifier();
this.smsNotifier = new SMSNotifier();
}
public void setCurrentConditions(String weather) {
if (weather.equals("rainy")) {
emailNotifier.sendWeatherAlert("It's rainy!");
smsNotifier.sendWeatherAlert("It's rainy!");
}
}
}
// Low-level modules
public class EmailNotifier {
public void sendWeatherAlert(String message) {
System.out.println("Email: " + message);
}
}
public class SMSNotifier {
public void sendWeatherAlert(String message) {
System.out.println("SMS: " + message);
}
}
Good Example (Following DIP):
java
// Abstraction that both high and low levels depend on
public interface Notifier {
void sendWeatherAlert(String message);
}
// High-level module depends on abstraction
public class WeatherTracker {
private List<Notifier> notifiers;
public WeatherTracker(List<Notifier> notifiers) {
this.notifiers = notifiers; // Dependency injection
}
public void setCurrentConditions(String weather) {
if (weather.equals("rainy")) {
String message = "It's rainy!";
for (Notifier notifier : notifiers) {
notifier.sendWeatherAlert(message);
}
}
}
public void addNotifier(Notifier notifier) {
notifiers.add(notifier);
}
}
// Low-level modules depend on abstraction
public class EmailNotifier implements Notifier {
@Override
public void sendWeatherAlert(String message) {
System.out.println("Email: " + message);
}
}
public class SMSNotifier implements Notifier {
@Override
public void sendWeatherAlert(String message) {
System.out.println("SMS: " + message);
}
}
// New notifier can be added without changing WeatherTracker
public class PushNotifier implements Notifier {
@Override
public void sendWeatherAlert(String message) {
System.out.println("Push Notification: " + message);
}
}
// Usage with dependency injection
public class WeatherApp {
public static void main(String[] args) {
List<Notifier> notifiers = Arrays.asList(
new EmailNotifier(),
new SMSNotifier(),
new PushNotifier() // Easy to extend
);
WeatherTracker tracker = new WeatherTracker(notifiers);
tracker.setCurrentConditions("rainy");
// We can also add new notifiers at runtime
tracker.addNotifier(new PushNotifier());
}
}
2. DRY Principle (Don't Repeat Yourself)
"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."
Explanation:
Avoid code duplication by extracting common functionality.
Bad Example (Violating DRY):
java
public class OrderProcessor {
public void processDomesticOrder(Order order) {
// Validation
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
if (order.getCustomer() == null) {
throw new IllegalArgumentException("Order must have a customer");
}
// Business logic
calculateTax(order, "DOMESTIC");
applyDiscounts(order);
// Processing
saveToDatabase(order);
updateInventory(order);
sendConfirmationEmail(order);
}
public void processInternationalOrder(Order order) {
// Duplicated validation
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
if (order.getCustomer() == null) {
throw new IllegalArgumentException("Order must have a customer");
}
// Different business logic but same structure
calculateTax(order, "INTERNATIONAL");
applyDiscounts(order);
handleCustoms(order);
// Duplicated processing
saveToDatabase(order);
updateInventory(order);
sendConfirmationEmail(order);
}
// Duplicated methods across classes
private void saveToDatabase(Order order) {
System.out.println("Saving order to database");
}
private void updateInventory(Order order) {
System.out.println("Updating inventory");
}
private void sendConfirmationEmail(Order order) {
System.out.println("Sending confirmation email");
}
}
Good Example (Following DRY):
java
public class OrderValidator {
// Single source of truth for validation
public void validateOrder(Order order) {
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
if (order.getCustomer() == null) {
throw new IllegalArgumentException("Order must have a customer");
}
}
}
public class OrderRepository {
// Single source of truth for database operations
public void saveOrder(Order order) {
System.out.println("Saving order to database");
// Actual database logic
}
}
public class InventoryService {
// Single source of truth for inventory operations
public void updateInventory(Order order) {
System.out.println("Updating inventory");
// Actual inventory logic
}
}
public class NotificationService {
// Single source of truth for notifications
public void sendConfirmationEmail(Order order) {
System.out.println("Sending confirmation email");
// Actual email logic
}
}
public abstract class OrderProcessor {
// Template method pattern - common structure
protected OrderValidator validator;
protected OrderRepository repository;
protected InventoryService inventoryService;
protected NotificationService notificationService;
public OrderProcessor() {
this.validator = new OrderValidator();
this.repository = new OrderRepository();
this.inventoryService = new InventoryService();
this.notificationService = new NotificationService();
}
// Template method defining the common process
public final void processOrder(Order order) {
validateOrder(order);
calculateTax(order);
applyDiscounts(order);
performCustomOperations(order);
saveOrder(order);
updateInventory(order);
sendNotification(order);
}
// Common operations with default implementation
protected void validateOrder(Order order) {
validator.validateOrder(order);
}
protected void saveOrder(Order order) {
repository.saveOrder(order);
}
protected void updateInventory(Order order) {
inventoryService.updateInventory(order);
}
protected void sendNotification(Order order) {
notificationService.sendConfirmationEmail(order);
}
// Operations that vary - to be implemented by subclasses
protected abstract void calculateTax(Order order);
protected abstract void applyDiscounts(Order order);
protected abstract void performCustomOperations(Order order);
}
// Concrete implementations without duplication
public class DomesticOrderProcessor extends OrderProcessor {
@Override
protected void calculateTax(Order order) {
// Domestic tax calculation
System.out.println("Calculating domestic tax");
}
@Override
protected void applyDiscounts(Order order) {
// Domestic discounts
System.out.println("Applying domestic discounts");
}
@Override
protected void performCustomOperations(Order order) {
// No customs for domestic
}
}
public class InternationalOrderProcessor extends OrderProcessor {
@Override
protected void calculateTax(Order order) {
// International tax calculation
System.out.println("Calculating international tax");
}
@Override
protected void applyDiscounts(Order order) {
// International discounts
System.out.println("Applying international discounts");
}
@Override
protected void performCustomOperations(Order order) {
// Customs handling for international
System.out.println("Handling customs documentation");
}
}
3. KISS Principle (Keep It Simple, Stupid)
"Simplicity should be a key goal in design, and unnecessary complexity should be avoided."
Bad Example (Overly Complex):
java
public class ComplexOrderProcessor {
private Map<String, BiFunction<Order, Context, Result>> strategyMap;
private Supplier<Context> contextFactory;
private Predicate<Order> preConditionValidator;
public ComplexOrderProcessor() {
this.strategyMap = new HashMap<>();
this.contextFactory = () -> new Context();
this.preConditionValidator = this::validatePreConditions;
initializeStrategies();
}
private void initializeStrategies() {
strategyMap.put("DOMESTIC", (order, context) -> {
return executeWithRetry(() -> processDomesticInternal(order, context));
});
// ... more complex mappings
}
private Result executeWithRetry(Supplier<Result> operation) {
// Complex retry logic
return operation.get();
}
public Result processOrder(Order order, String type) {
if (!preConditionValidator.test(order)) {
return Result.failure("Preconditions failed");
}
Context context = contextFactory.get();
BiFunction<Order, Context, Result> strategy = strategyMap.get(type);
if (strategy != null) {
return strategy.apply(order, context);
}
return Result.failure("Unknown order type");
}
}
Good Example (Simple):
java
public class SimpleOrderProcessor {
public boolean processOrder(Order order) {
// Simple, straightforward logic
if (!isValid(order)) {
return false;
}
calculateTotal(order);
applyTax(order);
saveOrder(order);
return true;
}
private boolean isValid(Order order) {
return order != null &&
order.getItems() != null &&
!order.getItems().isEmpty();
}
private void calculateTotal(Order order) {
double total = order.getItems().stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum();
order.setTotal(total);
}
private void applyTax(Order order) {
double tax = order.getTotal() * 0.1; // 10% tax
order.setTax(tax);
order.setTotalWithTax(order.getTotal() + tax);
}
private void saveOrder(Order order) {
// Simple save operation
System.out.println("Saving order: " + order.getId());
}
}
4. YAGNI Principle (You Ain't Gonna Need It)
"Don't implement functionality until you actually need it."
Bad Example (Violating YAGNI):
java
public class UserService {
// Implementing features that might be needed in the future
private UserRepository userRepository;
private EmailService emailService;
private AnalyticsService analyticsService;
private RecommendationService recommendationService;
private SocialMediaService socialMediaService;
public UserService() {
this.userRepository = new UserRepository();
this.emailService = new EmailService();
this.analyticsService = new AnalyticsService(); // Not needed yet
this.recommendationService = new RecommendationService(); // Not needed yet
this.socialMediaService = new SocialMediaService(); // Not needed yet
}
public void registerUser(User user) {
userRepository.save(user);
emailService.sendWelcomeEmail(user);
analyticsService.trackRegistration(user); // Not currently required
recommendationService.generateInitialRecommendations(user); // Not currently required
socialMediaService.postWelcomeMessage(user); // Not currently required
}
// Methods for future use
public void exportUserData() {
// No requirement for this yet
}
public void generateUserReport() {
// No requirement for this yet
}
}
Good Example (Following YAGNI):
java
public class UserService {
// Only what we actually need right now
private UserRepository userRepository;
private EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public void registerUser(User user) {
// Only essential functionality
userRepository.save(user);
emailService.sendWelcomeEmail(user);
}
// Simple, focused API - no speculative features
public User findUserById(String id) {
return userRepository.findById(id);
}
public void updateUser(User user) {
userRepository.update(user);
}
public void deleteUser(String id) {
userRepository.delete(id);
}
}
Summary Table
| Principle | Acronym | Key Idea | Benefit |
|---|---|---|---|
| Single Responsibility | SRP | One class, one purpose | Easier maintenance, testing |
| Open/Closed | OCP | Open for extension, closed for modification | Reduced regression risk |
| Liskov Substitution | LSP | Subtypes must be substitutable | Reliable inheritance |
| Interface Segregation | ISP | Many specific interfaces better than one general | Reduced coupling |
| Dependency Inversion | DIP | Depend on abstractions, not concretions | Flexibility, testability |
| Don't Repeat Yourself | DRY | Eliminate duplication | Consistency, maintainability |
| Keep It Simple | KISS | Avoid unnecessary complexity | Readability, fewer bugs |
| You Ain't Gonna Need It | YAGNI | Don't build what you don't need | Faster delivery, less waste |
These principles work together to create software that is easier to understand, maintain, and extend over time. They form the foundation of good software design and are essential for building professional, enterprise-grade applications.
Comments (0)
No comments yet. Be the first to comment!