Mastering Advanced Software Design Principles

Elevate Your Code with Abstraction, Cohesion, and SOLID Principles

Auriga Aristo
Indonesian Developer
8 min readJun 1, 2024

--

Photo by Oskar Yildiz on Unsplash

Welcome back to our journey through the world of software design principles! In the first part, we laid the groundwork by discussing the basics of clean coding practices, including naming conventions and fundamental principles like DRY, KISS, and YAGNI. Now that you’ve mastered these concepts, it’s time to elevate your coding skills to the next level.

Check out the first part here:

This second installment will delve into more advanced topics: Abstraction, Cohesion, and the SOLID principles. These principles are theoretical and pivotal in crafting robust, scalable, and maintainable software. By mastering these concepts, you’ll improve your programming skills and ability to design and develop software that stands the test of time, enhancing your professional value.

Get ready to explore how these advanced principles can transform your approach to software design, making your applications more efficient and your codebase cleaner. Let’s dive in!

Deep Dive into Abstraction

Abstraction is a fundamental concept in software engineering and an essential skill in a developer’s toolkit. It involves hiding the complex reality while exposing only the necessary parts of an object or a system. Abstraction helps manage complexity by allowing programmers to focus on interactions at a higher level without understanding all the details of lower-level operations.

Why abstraction?

Abstraction handles complexity by generalizing common properties among various entities and focusing on what is essential. Abstraction is often achieved using abstract classes and interfaces.

Why is it Important?

Abstraction allows you to:

  • Reduce Complexity: By abstracting away details, developers can manage larger systems and understand what a component does without knowing how it does it.
  • Increase Reusability: Abstracting standard methods and properties in a superclass or interface allows different subclasses to inherit or implement them, promoting reuse.
  • Improve Maintainability: Changes in abstracted code can be made in one place and affect all dependent classes, making the system easier to maintain.
abstract class Employee {
private String name;
private String id;

public Employee(String name, String id) {
this.name = name;
this.id = id;
}

// Abstract method
public abstract double calculateBonus();

public String getDetails() {
return "ID: " + id + ", Name: " + name;
}
}

class FullTimeEmployee extends Employee {
private double salary;

public FullTimeInstructor(String name, String id, double salary) {
super(name, id);
this.salary = salary;
}

@Override
public double calculateBonus() {
return salary * 0.1;
}
}

class PartTimeEmployee extends Employee {
private double hourlyRate;
private int hoursWorked;

public PartTimeEmployee(String name, String id, double hourlyRate, int hoursWorked) {
super(name, id);
this.hourlyRate = hourlyRate;
this.hoursWorked = hoursWorked;
}

@Override
public double calculateBonus() {
return hoursWorked * hourlyRate * 0.05;
}
}

In this example, the Employee class is abstract and provides a structure and common methods for all employees, like getDetails(). In contrast, the method calculateBonus() is abstract because its implementation varies depending on the type of employee.

Using abstraction, the Employee class encapsulates common features and requirements for all employees while allowing specific employee types to provide detailed behavior. This makes the overall codebase more flexible and more accessible to extend.

Understanding Cohesion

Cohesion refers to the degree to which elements inside a module or a class belong together. A highly cohesive module or class performs a single task or group of related functions in software development, making it more understandable and less complex. Higher cohesion within a class module is generally preferable as it promotes encapsulation, making the software easier to maintain and extend.

What is cohesion?

Cohesion measures the strength of the relationship between pieces of functionality within a single module. A well-cohesive class focuses on a single or closely related task, which means all its services are aligned toward a common functionality or goal.

Benefits of High Cohesion

  • Enhanced Maintainability: Classes with high cohesion are easier to understand and manage because their functionalities are tightly related.
  • Reduced Complexity: High cohesion often results in smaller, focused classes that are less complex and easier to debug.
  • Increased Reusability: Components designed to perform a specific task well can be more easily reused in different parts of an application or even in other projects.
// Non-Cohesive Class Example
class UserManager {
void addUser(User user) {}
void deleteUser(int userId) {}
void logUserActivity(User user, String activity) {}
void sendUserNotification(User user, String message) {}
double calculateUserStatistics(List<User> users) { return 0.0; }
}

This UserManager class performs too many tasks: managing users, logging activities, sending notifications, and calculating statistics. It could be more cohesive because these functionalities can be logically separated into different classes.

// Cohesive Class Example
class UserManagement {
void addUser(User user) {}
void deleteUser(int userId) {}
}

class UserActivityLogger {
void logUserActivity(User user, String activity) {}
}

class UserNotifier {
void sendUserNotification(User user, String message) {}
}

class UserStatistics {
double calculateUserStatistics(List<User> users) { return 0.0; }
}

In the refactored version, each class has a specific responsibility and performs a related set of functions, increasing cohesion. This separation makes the system easier to manage, extend, and debug.

By striving for high cohesion in your classes and modules, you enhance the clarity and quality of your code. It’s a simple yet effective principle that leads to better software design.

The SOLID Principles

SOLID is an acronym for five design principles intended to make software designs more understandable, flexible, and maintainable.

Single Responsibility Principle (SRP)

A class should have one and only one reason to change, meaning it should have one job.

// Violates SRP
class UserSettings {
void changeUsername(User user, String newUsername) {}
void saveUserSettings(User user) {}
}

// Adheres to SRP
class User {
void changeUsername(String newUsername) {
// Change username
}
}

class UserPersistence {
void saveUser(User user) {
// Save user settings to a database
}
}

By separating the responsibilities into two classes, each class has one reason to change: User changes if the way usernames are managed changes, and UserPersistance changes if the way users are saved changes.

Open/Closed Principles (OCP)

Software entities should be open for extension but closed for modification. This means you should be able to add new functionality without altering existing code.

abstract class Discount {
abstract double apply(double price);
}

class SeasonalDiscount extends Discount {
double apply(double price) {
return price * 0.85; // 15% discount
}
}

class ClearanceDiscount extends Discount {
double apply(double price) {
return price * 0.5; // 50% discount
}
}

In this example, adding a new type of discount doesn’t require changes to existing classes; you extend Discount.

Liskov Substitution Principle (LSP)

Objects of a superclass shall be replaceable with objects of its subclasses without affecting the correctness of the program.

class Bird {
void fly() {}
}

class Duck extends Bird {}

class Ostrich extends Bird {
@Override
void fly() {
throw new UnsupportedOperationException("Ostriches cannot fly");
}
}

This violates LSP because Ostrict cannot be used as a substitute for Bird. A better approach is reorganizing the hierarchy so that flying capability is only in relevant subclasses.

Interface Segregation Principle (ISP)

No client should be forced to depend on methods it does not use.

interface Worker {
void eat();
}

class HumanWorker implements Worker {
public void work() {
// working
}

public void eat() {
// eating during break
}
}

class RobotWorker implements Worker {
public void work() {
// working
}

public void eat() {
// irrelevant
throw new UnsupportedOperationException();
}
}

This violates ISP because RobotWorker is forced to implement eat(). A better design would be to have separate interfaces for work and dietary needs.

Dependency Inversion Principle (DIP)

High-level modules should be independent of low-level modules. Both should depend on abstractions.

interface Storage {
void save(Object data);
}

class FloppyDisk implements Storage {
public void save(Object data) {
// save data to floppy disk
}
}

class FileManager {
private Storage storage;

FileManager(Storage storage) {
this.storage = storage;
}

void saveData(Object data) {
storage.save(data);
}
}

In this example, FileManager does not depend directly on a FloppyDisk but instead on the Storage interface. This makes the code more flexible and more accessible to modify or extend.

Adhering to these SOLID principles allows developers to create more manageable and modular software, which makes it easier to scale, maintain, and extend.

Additional Tips and Tricks

Comment Wisely

Use comments to explain “why” something is done, not “what” is done. The code itself should be clear enough to describe what is happening. Comments should provide context that the code cannot, such as explaining complex algorithms or decisions that might not be immediately obvious.

// Bad: Explains what the code does, which is already clear
// Increment count by one
count++;

// Good: Explains why this decision was made
// Use count increment as a fail-safe against infinite loops
count++;

Consistent Formatting

Adhere to a consistent style and formatting practice within your code. This includes brace styles, line lengths, naming conventions, and file structure. Consistency makes the code easier to read and understand for everyone working on the project.

For example, you use curly braces in Java, even for single-line statements, to enhance readability.

Refactor Regularly

Refactor your code regularly to keep it clean and maintainable. Look for signs of code smells, such as duplicate code, long methods, and large classes, and start simplifying wherever possible.

// Before refactoring
public void processData() {
// 100 lines of code that handle different types of data processing
}

// After refactoring
public void processData() {
processTypeA();
processTypeB();
processTypeC();
}

private void processTypeA() { /*...*/ }
private void processTypeB() { /*...*/ }
private void processTypeC() { /*...*/ }

Optimize Imports

Keep your import statements clean and organized. Remove unused imports and avoid using wildcard imports, as they can make the code less readable and make it more challenging to understand which classes are being used.

// Bad Example
import java.util.*;

// Good Example
import java.util.List;
import java.util.ArrayList;

Use Meaningful Commit Messages

For collaborative projects, use clear, concise, and meaningful commit messages. This helps others understand the purpose of changes quickly and can be especially useful for tracking down when specific changes were made.

// Bad Example
Fixed stuff

// Better example
Refactored UserLogin handling to improve security against SQL injection

Learn from Code Reviews

Actively participate in code reviews and be open to giving and receiving constructive feedback. Code reviews are not just about catching bugs. They’re a learning process to share knowledge and improve coding standards among all team members.

Incorporating these additional tips and tricks into your coding practices can enhance your code’s readability, maintainability, and overall quality. These practices are designed to complement the software design principles covered in the previous parts of this series, providing a comprehensive toolkit for writing exceptional code.

Conclusion

Congratulations on completing Part Two of our series on software design principles! By now, you’ve delved into the complexities of Abortion, grasped the importance of cohesion, and navigated the essential SOLID principles.

These advanced concepts are vital to building robust, scalable, and maintainable software systems. Whether you’re refining an existing project or starting a new one, integrating these principles will undoubtedly elevate the quality of your work and make your code easier to manage and extend.

Remember, the journey to mastering clean code is ongoing. Practice consistently, seek feedback through code reviews, and continue learning and adapting. By applying the foundational and advanced principles discussed in these articles, you’ll be well-equipped to tackle any software development challenges confidently and skillfully.

--

--

Auriga Aristo
Indonesian Developer

4+ years in Backend Developer | PHP, Java/Kotlin, MySQL, Golang | New story every week