软件设计原则
软件设计原则:代码示例与应用场景清单
软件设计原则是指导开发者构建高内聚、低耦合、易维护、可扩展软件系统的核心思想。遵循这些原则能有效提升代码质量,降低后续迭代成本。本文将详细介绍七大经典软件设计原则,每个原则均配套核心思想解析、代码示例(以 Java 语言实现)及典型应用场景,帮助开发者快速理解与实践。
一、单一职责原则(Single Responsibility Principle, SRP)
核心思想
一个类只负责一个功能领域内的职责,即 “一个类只有一个引起变化的原因”。若一个类承担过多职责,职责间的耦合会导致修改一个职责时影响其他职责,增加代码维护风险。
代码示例(反例与正例)
反例:职责混叠的UserService类
// 同时负责用户数据管理和日志记录,违反单一职责原则
public class UserService {
// 职责1:用户数据管理
public void saveUser(User user) {
// 保存用户到数据库的逻辑
System.out.println("保存用户:" + user.getName());
}
// 职责2:日志记录(不属于用户数据管理职责)
public void logOperation(String operation) {
// 写入日志到文件的逻辑
System.out.println("日志记录:" + operation);
}
}
正例:职责拆分后的两个类
// 职责1:用户数据管理(仅负责用户相关业务)
public class UserService {
public void saveUser(User user) {
System.out.println("保存用户:" + user.getName());
}
}
// 职责2:日志记录(仅负责日志相关业务)
public class LogService {
public void logOperation(String operation) {
System.out.println("日志记录:" + operation);
}
}
// 使用方式
public class Client {
public static void main(String\[] args) {
UserService userService = new UserService();
LogService logService = new LogService();
User user = new User("张三");
userService.saveUser(user);
logService.logOperation("保存用户【张三】");
}
}
应用场景
-
业务模块拆分:如电商系统中,
OrderService(订单管理)、PaymentService(支付处理)、InventoryService(库存管理)需拆分,避免一个类同时处理订单、支付、库存逻辑。 -
工具类设计:如
StringUtil(字符串处理)、DateUtil(日期处理),每个工具类仅专注一类工具方法。 -
数据传输对象(DTO):如
UserQueryDTO(用户查询参数)、UserResultDTO(用户查询结果),避免一个 DTO 同时承担查询和结果返回职责。
二、开放 - 封闭原则(Open-Closed Principle, OCP)
核心思想
软件实体(类、模块、函数)应对扩展开放(新增功能时无需修改原有代码),对修改封闭(不改变现有代码的逻辑)。核心实现手段是 “抽象定义 + 具体扩展”,通过接口或抽象类约束行为,新增功能时通过实现类扩展。
代码示例
需求:实现商品折扣计算,支持普通折扣、会员折扣、节日折扣
步骤 1:定义抽象折扣接口(约束行为)
// 抽象折扣接口(对修改封闭:接口定义后无需修改)
public interface DiscountStrategy {
// 计算折扣后价格
double calculateDiscount(double originalPrice);
}
步骤 2:实现具体折扣类(对扩展开放:新增折扣只需加实现类)
// 普通折扣(无折扣)
public class NormalDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double originalPrice) {
return originalPrice; // 无折扣,返回原价
}
}
// 会员折扣(9折)
public class MemberDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double originalPrice) {
return originalPrice \* 0.9;
}
}
// 节日折扣(8折)
public class FestivalDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double originalPrice) {
return originalPrice \* 0.8;
}
}
步骤 3:使用折扣策略(无需修改现有代码,直接扩展)
// 商品服务类(依赖抽象接口,无需修改)
public class ProductService {
private DiscountStrategy discountStrategy;
// 构造器注入折扣策略
public ProductService(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
// 计算最终价格
public double getFinalPrice(double originalPrice) {
return discountStrategy.calculateDiscount(originalPrice);
}
}
// 客户端调用
public class Client {
public static void main(String\[] args) {
double originalPrice = 100.0;
// 普通用户(无折扣)
ProductService normalService = new ProductService(new NormalDiscount());
System.out.println("普通用户最终价:" + normalService.getFinalPrice(originalPrice)); // 100.0
// 会员用户(9折)
ProductService memberService = new ProductService(new MemberDiscount());
System.out.println("会员用户最终价:" + memberService.getFinalPrice(originalPrice)); // 90.0
// 节日活动(8折,新增扩展无需修改原有代码)
ProductService festivalService = new ProductService(new FestivalDiscount());
System.out.println("节日活动最终价:" + festivalService.getFinalPrice(originalPrice)); // 80.0
}
}
应用场景
-
策略模式场景:如支付方式(支付宝、微信支付、银联支付)、日志输出方式(控制台日志、文件日志、数据库日志),新增方式只需加实现类。
-
框架插件扩展:如 Spring 的
BeanPostProcessor(Bean 后置处理器),新增处理器只需实现接口并注册,无需修改 Spring 核心代码。 -
业务规则变化频繁的场景:如电商促销规则(满减、折扣、赠品),通过扩展规则类应对变化。
三、里氏替换原则(Liskov Substitution Principle, LSP)
核心思想
子类对象必须能替换掉所有父类对象,且替换后程序逻辑不发生改变。即 “子类是父类的加强版,而非破坏版”,子类需遵守父类的行为契约(如方法参数、返回值、异常类型、业务逻辑约束)。
代码示例(反例与正例)
反例:违反父类行为契约的子类
// 父类:矩形(长和宽可独立修改)
public class Rectangle {
private double width;
private double height;
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
public double getArea() {
return width * height; // 面积=长×宽
}
}
// 子类:正方形(长和宽必须相等,违反父类“长和宽可独立修改”的契约)
public class Square extends Rectangle {
// 重写setWidth:修改宽时强制同步修改长
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setHeight(width); // 破坏父类独立修改宽的逻辑
}
// 重写setHeight:修改长时强制同步修改宽
@Override
public void setHeight(double height) {
super.setHeight(height);
super.setWidth(height); // 破坏父类独立修改长的逻辑
}
}
// 客户端调用:替换后逻辑异常
public class Client {
public static void main(String[] args) {
// 父类场景:设置长=2,宽=3,面积应为6
Rectangle rectangle = new Rectangle();
rectangle.setWidth(2);
rectangle.setHeight(3);
System.out.println("矩形面积:" + rectangle.getArea()); // 正确:6
// 子类替换父类:期望设置长=2,宽=3,但实际宽被同步改为2,面积=4(逻辑异常)
Rectangle square = new Square();
square.setWidth(2);
square.setHeight(3);
System.out.println("正方形面积:" + square.getArea()); // 错误:4(违反LSP)
}
}
正例:重构为符合 LSP 的设计
// 步骤1:定义抽象父类(约束“四边形”的通用行为,不强制长和宽的修改逻辑)
public abstract class Quadrilateral {
public abstract double getWidth();
public abstract double getHeight();
public abstract double getArea();
}
// 步骤2:实现矩形子类(遵守自身行为契约)
public class Rectangle extends Quadrilateral {
private double width;
private double height;
@Override
public double getWidth() {
return width;
}
@Override
public double getHeight() {
return height;
}
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
@Override
public double getArea() {
return width * height;
}
}
// 步骤3:实现正方形子类(遵守自身行为契约,不继承Rectangle)
public class Square extends Quadrilateral {
private double side; // 正方形只需“边长”属性
@Override
public double getWidth() {
return side; // 宽=边长
}
@Override
public double getHeight() {
return side; // 长=边长
}
public void setSide(double side) {
this.side = side;
}
@Override
public double getArea() {
return side * side;
}
}
// 客户端调用:替换后逻辑正常
public class Client {
public static void main(String\[] args) {
Quadrilateral rectangle = new Rectangle();
((Rectangle) rectangle).setWidth(2);
((Rectangle) rectangle).setHeight(3);
System.out.println("矩形面积:" + rectangle.getArea()); // 正确:6
Quadrilateral square = new Square();
((Square) square).setSide(2);
System.out.println("正方形面积:" + square.getArea()); // 正确:4(符合LSP)
}
}
应用场景
-
继承关系设计:如
Animal(父类)、Dog(子类)、Cat(子类),子类需遵守父类 “吃、睡” 的行为契约,不可重写为 “不吃、不睡”。 -
接口实现:如
List接口(父类),ArrayList、LinkedList(子类),调用add()方法时均需保证 “添加元素到集合” 的核心逻辑,不可修改为 “删除元素”。 -
框架适配:如 Spring 的
Resource接口,ClassPathResource、FileSystemResource(子类),均需实现 “获取资源流” 的契约,确保替换后资源获取逻辑正常。
四、依赖倒置原则(Dependency Inversion Principle, DIP)
核心思想
-
高层模块(业务逻辑层)不依赖低层模块(工具 / 数据层),两者都依赖抽象(接口或抽象类);
-
抽象不依赖具体实现,具体实现依赖抽象。
核心目的是解耦高层与低层模块,使低层模块的变化不影响高层模块。
代码示例(反例与正例)
反例:高层依赖低层具体实现
// 低层模块:MySQL数据库操作(具体实现)
public class MysqlDao {
public void saveUser(String userName) {
System.out.println("MySQL保存用户:" + userName);
}
}
// 高层模块:用户业务逻辑(直接依赖MysqlDao具体类,违反DIP)
public class UserService {
private MysqlDao mysqlDao; // 依赖具体实现
public UserService() {
this.mysqlDao = new MysqlDao();
}
public void registerUser(String userName) {
mysqlDao.saveUser(userName); // 若更换数据库(如Oracle),需修改UserService代码
}
}
正例:高层与低层均依赖抽象
// 步骤1:定义抽象接口(高层与低层的依赖核心)
public interface UserDao {
void saveUser(String userName);
}
// 步骤2:低层模块实现抽象(具体实现依赖抽象)
public class MysqlDao implements UserDao {
@Override
public void saveUser(String userName) {
System.out.println("MySQL保存用户:" + userName);
}
}
public class OracleDao implements UserDao { // 新增Oracle实现,无需修改高层
@Override
public void saveUser(String userName) {
System.out.println("Oracle保存用户:" + userName);
}
}
// 步骤3:高层模块依赖抽象(通过构造器注入,不依赖具体实现)
public class UserService {
private UserDao userDao; // 依赖抽象接口
// 构造器注入:高层不关心低层是MySQL还是Oracle
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public void registerUser(String userName) {
userDao.saveUser(userName); // 低层变化(如换Oracle),高层代码无需修改
}
}
// 客户端调用:灵活切换低层实现
public class Client {
public static void main(String\[] args) {
// 用MySQL保存
UserService mysqlService = new UserService(new MysqlDao());
mysqlService.registerUser("张三"); // 输出:MySQL保存用户:张三
// 用Oracle保存(无需修改UserService)
UserService oracleService = new UserService(new OracleDao());
oracleService.registerUser("李四"); // 输出:Oracle保存用户:李四
}
}
应用场景
-
数据库切换:如从 MySQL 迁移到 Oracle,只需新增
OracleDao实现,高层UserService无需修改。 -
第三方服务集成:如支付服务从 “支付宝” 切换到 “微信支付”,定义
PaymentService接口,实现AlipayService、WechatPayService,高层业务逻辑依赖接口即可。 -
框架依赖注入:如 Spring 的 IOC 容器,通过依赖注入(DI)将低层实现注入高层模块,高层仅依赖抽象,符合 DIP。
五、接口隔离原则(Interface Segregation Principle, ISP)
核心思想
客户端不应依赖其不需要的接口,即 “一个接口只包含客户端需要的方法,避免臃肿的‘万能接口’”。核心是将大接口拆分为多个小而专的接口,降低接口间的耦合。
代码示例(反例与正例)
反例:臃肿的 “万能接口”
// 臃肿接口:包含用户管理、订单管理、支付管理的所有方法(违反ISP)
public interface AllInOneService {
// 用户管理方法
void saveUser();
void queryUser();
// 订单管理方法
void createOrder();
void cancelOrder();
// 支付管理方法
void doPayment();
void refund();
}
// 客户端1:仅需用户管理,但必须实现所有方法(冗余)
public class UserClient implements AllInOneService {
@Override
public void saveUser() { /\* 实现 \*/ }
@Override
public void queryUser() { /\* 实现 \*/ }
// 无需的方法,被迫空实现(冗余且易出错)
@Override
public void createOrder() {}
@Override
public void cancelOrder() {}
@Override
public void doPayment() {}
@Override
public void refund() {}
}
// 客户端2:仅需订单管理,同样被迫实现冗余方法
public class OrderClient implements AllInOneService {
@Override public void createOrder() { /\* 实现 \*/ }
@Override public void cancelOrder() { /\* 实现 \*/ }
// 无需的方法,被迫空实现
@Override public void saveUser() {}
@Override public void queryUser() {}
@Override public void doPayment() {}
@Override public void refund() {}
}
正例:拆分后的小而专接口
// 步骤1:拆分接口为3个小接口(每个接口专注一类职责)
public interface UserService {
void saveUser();
void queryUser();
}
public interface OrderService {
void createOrder();
void cancelOrder();
}
public interface PaymentService {
void doPayment();
void refund();
}
// 步骤2:客户端仅实现需要的接口(无冗余)
public class UserClient implements UserService {
@Override
public void saveUser() {
System.out.println("保存用户");
}
@Override
public void queryUser() {
System.out.println("查询用户");
}
// 无需实现其他接口方法,符合ISP
}
public class OrderClient implements OrderService {
@Override
public void createOrder() {
System.out.println("创建订单");
}
@Override
public void cancelOrder() {
System.out.println("取消订单");
}
// 无需实现其他接口方法,符合ISP
}
// 步骤3:若客户端需多个接口,可多实现(灵活组合)
public class OrderPaymentClient implements OrderService, PaymentService {
@Override public void createOrder() { /\* 实现 \*/ }
@Override public void cancelOrder() { /\* 实现 \*/ }
@Override public void doPayment() { /\* 实现 \*/ }
@Override public void refund() { /\* 实现 \*/ }
}
应用场景
-
多客户端接口设计:如后台管理系统中,
AdminService(管理员接口)、UserService(普通用户接口),避免普通用户接口包含管理员的 “删除用户” 方法。 -
SDK 设计:如支付 SDK,拆分
PaymentQueryService(支付查询)、PaymentRefundService(支付退款),客户端按需引入,避免依赖冗余方法。 -
微服务接口设计:如用户微服务对外提供
UserQueryAPI(查询接口)、UserModifyAPI(修改接口),前端查询页面仅调用UserQueryAPI,不依赖修改接口。
六、迪米特法则(Law of Demeter, LoD)
核心思想
一个对象应该对其他对象保持最少的了解,即 “只与直接朋友通信,不与陌生人通信”。
-
直接朋友:当前对象本身、方法参数、方法返回值、成员变量、局部变量;
-
陌生人:非直接朋友的对象。
核心目的是降低对象间的耦合,减少不必要的依赖传递。
代码示例(反例与正例)
反例:依赖陌生人对象
// 陌生人1:地址类
public class Address {
private String city;
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
}
// 直接朋友:用户类(包含Address成员变量)
public class User {
private Address address;
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
// 客户端:直接调用陌生人Address的方法(违反迪米特法则)
public class Client {
public static void main(String\[] args) {
User user = new User();
Address address = new Address();
address.setCity("北京");
user.setAddress(address);
// 客户端直接访问user的朋友(Address)的方法,Address是客户端的陌生人
String city = user.getAddress().getCity();
System.out.println("用户所在城市:" + city);
}
}
正例:通过直接朋友间接通信
// 陌生人1:地址类(不变)
public class Address {
private String city;
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
}
// 直接朋友:用户类(新增获取城市的方法,封装Address的访问)
public class User {
private Address address;
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
// 新增方法:客户端通过User获取城市,无需直接访问Address
public String getCity() {
return address.getCity();
}
}
// 客户端:仅与直接朋友User通信(符合迪米特法则)
public class Client {
public static void main(String\[] args) {
User user = new User();
Address address = new Address();
address.setCity("北京");
user.setAddress(address);
// 客户端仅调用User的方法,不直接访问Address(陌生人)
String city = user.getCity();
System.out.println("用户所在城市:" + city);
}
}
应用场景
-
封装中间层:如电商系统中,
OrderService(订单服务)需要获取用户地址时,通过UserService(用户服务)的getUserAddress(Long userId)方法获取,而非直接创建User和Address对象。 -
工具类封装:如
RedisUtil封装 Redis 的连接、读写操作,业务层仅调用RedisUtil.set()、RedisUtil.get(),无需了解 Redis 客户端的底层 API(如Jedis的方法)。 -
微服务通信:如订单微服务需要用户信息时,调用用户微服务的 API(直接朋友),而非直接访问用户微服务的数据库(陌生人)。
七、合成复用原则(Composite Reuse Principle, CRP)
核心思想
优先使用合成 / 聚合(Has-A)的方式实现代码复用,而非继承(Is-A)。
-
合成:整体与部分生命周期一致(如 “汽车 - 发动机”,汽车销毁时发动机也销毁);
-
聚合:整体与部分生命周期独立(如 “班级 - 学生”,班级解散后学生仍存在);
-
继承的问题:子类会继承父类的所有属性和方法,导致耦合过高,父类修改会影响所有子类。
代码示例(反例与正例)
反例:通过继承复用代码
// 父类:日志打印功能
public class LogPrinter {
public void printLog(String message) {
System.out.println("日志:" + message);
}
}
// 子类:用户服务通过继承复用日志功能(违反CRP,耦合过高)
public class UserService extends LogPrinter {
public void saveUser(String userName) {
// 复用父类的日志方法
super.printLog("保存用户:" + userName);
// 保存用户逻辑
}
}
// 问题:若需更换日志实现(如打印到文件),需修改LogPrinter父类,所有子类都会受影响
正例:通过合成复用代码
// 步骤1:定义日志接口(抽象日志行为)
public interface LogService {
void printLog(String message);
}
// 步骤2:实现具体日志类(控制台日志、文件日志)
public class ConsoleLogService implements LogService {
@Override
public void printLog(String message) {
System.out.println("控制台日志:" + message);
}
}
public class FileLogService implements LogService {
@Override
public void printLog(String message) {
System.out.println("文件日志:" + message); // 假设写入文件
}
}
// 步骤3:用户服务通过合成(Has-A)复用日志功能(符合CRP)
public class UserService {
private LogService logService; // 合成:UserService包含LogService对象
// 构造器注入日志实现(灵活切换)
public UserService(LogService logService) {
this.logService = logService;
}
public void saveUser(String userName) {
// 复用日志功能(通过组合对象调用)
logService.printLog("保存用户:" + userName);
// 保存用户逻辑
}
}
// 客户端调用:灵活切换日志实现,无耦合
public class Client {
public static void main(String\[] args) {
// 复用控制台日志
UserService consoleUserService = new UserService(new ConsoleLogService());
consoleUserService.saveUser("张三"); // 输出:控制台日志:保存用户:张三
// 复用文件日志(无需修改UserService)
UserService fileUserService = new UserService(new FileLogService());
fileUserService.saveUser("李四"); // 输出:文件日志:保存用户:李四
}
}
应用场景
-
跨模块功能复用:如日志、缓存、加密等通用功能,通过合成注入到业务类中,而非继承。
-
多实现切换场景:如数据存储(MySQL、Redis、MongoDB),业务类通过合成
StorageService接口,灵活切换存储实现。 -
避免继承滥用:如 “鸟会飞”,但 “鸵鸟是鸟但不会飞”,若用继承
Bird类,鸵鸟需重写fly()为空实现;用合成则可给鸵鸟注入 “不会飞” 的FlyService实现,更合理。
八、软件设计原则总结与实践建议
1. 原则间的关联
-
OCP 是核心目标:其他原则(SRP、LSP、DIP、ISP、LoD、CRP)都是为了实现 “开放 - 封闭”;
-
DIP 是实现 OCP 的关键:通过抽象解耦高层与低层,使扩展成为可能;
-
LSP 是 OCP 的基础:若子类违反父类契约,OCP 的扩展会导致逻辑异常;
-
SRP、ISP、LoD、CRP 是降低耦合的手段:通过拆分职责、隔离接口、减少依赖、优先合成,降低模块间耦合,支撑 OCP 实现。
2. 实践建议
-
不追求 “过度设计”:小型项目或需求稳定的模块,可适当简化原则(如单类职责可适度合并);
-
优先解决当前问题:原则是指导而非约束,需结合业务场景灵活调整(如简单工具类可忽略 DIP);
-
迭代优化:初期代码可能不完美,后续迭代中逐步重构,向设计原则靠拢;
-
结合设计模式:设计原则是思想,设计模式是具体实现(如 OCP + 策略模式、DIP + 依赖注入),两者结合效果更佳。

浙公网安备 33010602011771号