软件设计原则

软件设计原则:代码示例与应用场景清单

软件设计原则是指导开发者构建高内聚、低耦合、易维护、可扩展软件系统的核心思想。遵循这些原则能有效提升代码质量,降低后续迭代成本。本文将详细介绍七大经典软件设计原则,每个原则均配套核心思想解析代码示例(以 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("保存用户【张三】");

   }

}

应用场景

  1. 业务模块拆分:如电商系统中,OrderService(订单管理)、PaymentService(支付处理)、InventoryService(库存管理)需拆分,避免一个类同时处理订单、支付、库存逻辑。

  2. 工具类设计:如StringUtil(字符串处理)、DateUtil(日期处理),每个工具类仅专注一类工具方法。

  3. 数据传输对象(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

   }

}

应用场景

  1. 策略模式场景:如支付方式(支付宝、微信支付、银联支付)、日志输出方式(控制台日志、文件日志、数据库日志),新增方式只需加实现类。

  2. 框架插件扩展:如 Spring 的BeanPostProcessor(Bean 后置处理器),新增处理器只需实现接口并注册,无需修改 Spring 核心代码。

  3. 业务规则变化频繁的场景:如电商促销规则(满减、折扣、赠品),通过扩展规则类应对变化。

三、里氏替换原则(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)

   }

}

应用场景

  1. 继承关系设计:如Animal(父类)、Dog(子类)、Cat(子类),子类需遵守父类 “吃、睡” 的行为契约,不可重写为 “不吃、不睡”。

  2. 接口实现:如List接口(父类),ArrayListLinkedList(子类),调用add()方法时均需保证 “添加元素到集合” 的核心逻辑,不可修改为 “删除元素”。

  3. 框架适配:如 Spring 的Resource接口,ClassPathResourceFileSystemResource(子类),均需实现 “获取资源流” 的契约,确保替换后资源获取逻辑正常。

四、依赖倒置原则(Dependency Inversion Principle, DIP)

核心思想

  1. 高层模块(业务逻辑层)不依赖低层模块(工具 / 数据层),两者都依赖抽象(接口或抽象类);

  2. 抽象不依赖具体实现,具体实现依赖抽象。

    核心目的是解耦高层与低层模块,使低层模块的变化不影响高层模块。

代码示例(反例与正例)

反例:高层依赖低层具体实现

// 低层模块: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保存用户:李四

   }

}

应用场景

  1. 数据库切换:如从 MySQL 迁移到 Oracle,只需新增OracleDao实现,高层UserService无需修改。

  2. 第三方服务集成:如支付服务从 “支付宝” 切换到 “微信支付”,定义PaymentService接口,实现AlipayServiceWechatPayService,高层业务逻辑依赖接口即可。

  3. 框架依赖注入:如 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() { /\* 实现 \*/ }

}

应用场景

  1. 多客户端接口设计:如后台管理系统中,AdminService(管理员接口)、UserService(普通用户接口),避免普通用户接口包含管理员的 “删除用户” 方法。

  2. SDK 设计:如支付 SDK,拆分PaymentQueryService(支付查询)、PaymentRefundService(支付退款),客户端按需引入,避免依赖冗余方法。

  3. 微服务接口设计:如用户微服务对外提供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);

   }

}

应用场景

  1. 封装中间层:如电商系统中,OrderService(订单服务)需要获取用户地址时,通过UserService(用户服务)的getUserAddress(Long userId)方法获取,而非直接创建UserAddress对象。

  2. 工具类封装:如RedisUtil封装 Redis 的连接、读写操作,业务层仅调用RedisUtil.set()RedisUtil.get(),无需了解 Redis 客户端的底层 API(如Jedis的方法)。

  3. 微服务通信:如订单微服务需要用户信息时,调用用户微服务的 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("李四"); // 输出:文件日志:保存用户:李四

   }

}

应用场景

  1. 跨模块功能复用:如日志、缓存、加密等通用功能,通过合成注入到业务类中,而非继承。

  2. 多实现切换场景:如数据存储(MySQL、Redis、MongoDB),业务类通过合成StorageService接口,灵活切换存储实现。

  3. 避免继承滥用:如 “鸟会飞”,但 “鸵鸟是鸟但不会飞”,若用继承Bird类,鸵鸟需重写fly()为空实现;用合成则可给鸵鸟注入 “不会飞” 的FlyService实现,更合理。

八、软件设计原则总结与实践建议

1. 原则间的关联

  • OCP 是核心目标:其他原则(SRP、LSP、DIP、ISP、LoD、CRP)都是为了实现 “开放 - 封闭”;

  • DIP 是实现 OCP 的关键:通过抽象解耦高层与低层,使扩展成为可能;

  • LSP 是 OCP 的基础:若子类违反父类契约,OCP 的扩展会导致逻辑异常;

  • SRP、ISP、LoD、CRP 是降低耦合的手段:通过拆分职责、隔离接口、减少依赖、优先合成,降低模块间耦合,支撑 OCP 实现。

2. 实践建议

  1. 不追求 “过度设计”:小型项目或需求稳定的模块,可适当简化原则(如单类职责可适度合并);

  2. 优先解决当前问题:原则是指导而非约束,需结合业务场景灵活调整(如简单工具类可忽略 DIP);

  3. 迭代优化:初期代码可能不完美,后续迭代中逐步重构,向设计原则靠拢;

  4. 结合设计模式:设计原则是思想,设计模式是具体实现(如 OCP + 策略模式、DIP + 依赖注入),两者结合效果更佳。

posted @ 2025-11-22 16:19  圣祖帝皇  阅读(57)  评论(0)    收藏  举报