Fundamentals 26 min read

Software Design Principles: SOLID and Beyond with Java Examples

This article explains the seven core software design principles—Open/Closed, Dependency Inversion, Single Responsibility, Interface Segregation, Liskov Substitution, Law of Demeter, and Composite Reuse—illustrating each with Java code samples, class diagrams, and practical advice for building maintainable systems.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Software Design Principles: SOLID and Beyond with Java Examples

Introduction

We continue the series on architect skills; this is the second article, encouraging readers of any background to improve a little each day and eventually master system design.

Experts' Summary

Robert C. Martin

Low maintainability often stems from four causes: Rigidity (hard to change), Fragility (changes break unrelated parts), Immobility (low reuse), and Viscosity (hard to do the right thing).

Peter Coad

A good system design should be Extendable, Flexible, and Pluggable. Applying object‑oriented design principles and patterns helps refactor without altering existing functionality.

Software Design Seven Principles

Open/Closed Principle

Dependency Inversion Principle

Single Responsibility Principle

Interface Segregation Principle

Liskov Substitution Principle

Law of Demeter

Composite Reuse Principle

The following sections detail each principle with definitions, benefits, and concrete Java examples.

Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification. The classic definition is “software entities should be open for extension, closed for modification.”

Effect

Testing impact: only new extensions need tests.

Improves reusability by keeping modules small.

Enhances maintainability and stability.

Actual Case

We model a course with ICourse , JavaCourse , and a demo class:

public interface ICourse {
String getCourseName();
Integer getCourseId();
BigDecimal getCoursePrice();
}
public class JavaCourse implements ICourse {
@Override public String getCourseName() { return "JAVA课程"; }
@Override public Integer getCourseId() { return 1; }
@Override public BigDecimal getCoursePrice() { return new BigDecimal("599"); }
}
public class OpenCloseDemo {
public static void main(String[] args) {
ICourse course = new JavaCourse();
System.out.println("课程ID=" + course.getCourseId());
System.out.println("课程名称=" + course.getCourseName());
System.out.println("课程价格=" + course.getCoursePrice());
}
}

To add a discount without modifying JavaCourse , we create JavaDiscountCourse that extends JavaCourse and adds a discount method.

public class JavaDiscountCourse extends JavaCourse {
public BigDecimal getDiscountCoursePrice(BigDecimal discount) {
return super.getCoursePrice().multiply(discount);
}
}
public class OpenCloseDemo {
public static void main(String[] args) {
JavaCourse course = new JavaDiscountCourse();
JavaDiscountCourse discountCourse = (JavaDiscountCourse) course;
System.out.println("课程ID=" + course.getCourseId());
System.out.println("课程名称=" + course.getCourseName());
System.out.println("课程价格=" + course.getCoursePrice());
BigDecimal discount = new BigDecimal("0.5");
System.out.println("课程折后价=" + discountCourse.getDiscountCoursePrice(discount));
}
}

Running the demo prints the original price and the discounted price, demonstrating OCP.

Dependency Inversion Principle (DIP)

High‑level modules should not depend on low‑level modules; both should depend on abstractions. This reduces coupling and improves stability.

Case

We start with a concrete Tian class that directly calls studyJavaCourse and studyCCourse . To follow DIP, we introduce an ICourse interface and let Tian depend on it:

public interface ICourse { void study(); }
public class JavaCourse implements ICourse { @Override public void study() { System.out.println("老田在学习java课程"); } }
public class PythonCourse implements ICourse { @Override public void study() { System.out.println("老田在学习Python课程"); } }
public class Tian {
public void study(ICourse course) { course.study(); }
}

Clients can now inject any ICourse implementation via constructor or setter injection, illustrating both constructor and setter injection styles.

Single Responsibility Principle (SRP)

A class should have only one reason to change. The article shows a Course class handling both live and replay logic, then refactors it into LiveCourse and ReplayCourse to separate responsibilities.

public class LiveCourse { public void study() { System.out.println("现场直播,无法修改播放速度"); } }
public class ReplayCourse { public void study() { System.out.println("看录像,可以随便切换播放速度,以及来回播放"); } }

Further splitting of interfaces into ICourseInfo and ICourseManager demonstrates SRP at the interface level.

Interface Segregation Principle (ISP)

Clients should not be forced to depend on methods they do not use. The article starts with a monolithic IAnimal interface and then splits it into IEatAnimal , IFlyAnimal , and ISwimAnimal . Dog implements only the relevant interfaces.

public interface IEatAnimal { void eat(); }
public interface IFlyAnimal { void fly(); }
public interface ISwimAnimal { void swim(); }
public class Dog implements IEatAnimal, ISwimAnimal {
@Override public void eat() { System.out.println("小狗吃东西"); }
@Override public void swim() { System.out.println("小狗在游泳"); }
}

Law of Demeter (LoD)

Objects should only talk to their immediate friends. The example refactors a TeamLeader that previously created a list of Course objects and passed it to Employee . After refactoring, Employee handles the list internally, removing the direct dependency between TeamLeader and Course .

Liskov Substitution Principle (LSP)

Subtypes must be usable wherever their base types are expected without altering program behavior. The article demonstrates a violation using Rectangle and Square where a method that increments height until width ≥ height enters an infinite loop when a Square is passed. The fix is to introduce a common Quadrangle interface and limit the method to work only with true rectangles.

public interface Quadrangle { long getHeight(); long getWidth(); }
public class Rectangle implements Quadrangle { private long height, width; /* getters/setters */ }
public class Square implements Quadrangle { private long length; @Override public long getHeight() { return length; } @Override public long getWidth() { return length; } }

Composite Reuse Principle (CARP)

Prefer composition over inheritance for code reuse. The article shows a ProductDao that depends on an abstract DBConnection . Concrete subclasses MySQLConnection and OracleConnection provide specific implementations, allowing ProductDao to remain unchanged when the database type changes.

public abstract class DBConnection { public abstract String getConnection(); }
public class MySQLConnection extends DBConnection { @Override public String getConnection() { return "MySQL 数据库连接"; } }
public class OracleConnection extends DBConnection { @Override public String getConnection() { return "Oracle 数据库连接"; } }
public class ProductDao { private DBConnection dbConnection; public void setDBConnection(DBConnection dbConnection) { this.dbConnection = dbConnection; } public void addProduct() { System.out.println("使用" + dbConnection.getConnection() + "连接数据库"); } }

Summary

Design principles provide a balanced approach to writing clean, maintainable code; they are not strict rules but guidelines to apply where appropriate, helping developers create elegant architectures.

Reference: Tom's Architect Notes.

Javaarchitecturesoftware designOOPSOLIDprinciples
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.