前言
關于設計原則SOLID具體指的是什么,怎么理解這些設計原則,我覺得有必要記錄一筆,畢竟這個設計原則確實經常在關鍵技術文檔中提及,在編程思想中提及,在日常的開發中使用,但是對我來說,似乎知道但又不那么明確,我希望自己對設計原則的思想有一個更加準確和全面的理解,也想明確如果沒有這個設計原則會如何?此設計原則的亮點和優勢是什么?我在日常開發中怎么使用到這些設計原則的?
本文就是基于以上問題的總結歸納,方便自己日后復盤。
說明:匯總風格和內容借助AI工具
一、什么是SOLID?
SOLID是面向對象編程和軟件設計的五個基本原則的首字母縮寫,這些原則幫助我們編寫更易于維護、擴展和理解的代碼。
- S - 單一職責原則 (Single Responsibility Principle)
- O - 開閉原則 (Open/Closed Principle)
- L - 里氏替換原則 (Liskov Substitution Principle)
- I - 接口隔離原則 (Interface Segregation Principle)
- D - 依賴倒置原則 (Dependency Inversion Principle)
1. 單一職責原則(SRP)
- 核心:一個類應該只有一個引起它變化的原因(即只有一個職責)。
- 關鍵點:
- 方法層面:一個方法只做一件事(如
saveStudent()
不應同時包含驗證和存儲邏輯)。 - 類層面:
Student
類管理學生屬性,若需日志記錄,應拆分出StudentLogger
類。
- 方法層面:一個方法只做一件事(如
- 優勢:降低復雜度、提高可維護性,修改一個功能時不會意外影響其他功能。
- 現實類比:就像餐廳里廚師負責烹飪,服務員負責上菜,收銀員負責結賬,各司其職,而不是一個人做所有事情。
日常開發中的問題:忽視SRP會導致"上帝類"(God Class),修改一處可能影響多處功能,測試困難,代碼難以復用。
- 反例:
class Student {void saveToDatabase() { /* 數據庫操作 */ }void generateReport() { /* 生成PDF */ } // 違反SRP }
2. 開閉原則(OCP)
- 核心:通過擴展(繼承/組合)添加新功能,而非修改已有代碼。
- 關鍵點:
- 多態是手段之一,但OCP更強調抽象(接口/抽象類)的設計。
- 示例:支付系統支持新支付方式時,應實現
Payment
接口,而非修改原有代碼。
interface Payment { void pay(); } class CreditCard implements Payment { /* 無需修改現有類 */ }
- 優勢:減少回歸測試風險,提高系統可擴展性。
- 現實類比:USB接口設計 - 你可以插入各種設備(擴展開放),而不需要修改電腦的USB接口本身(修改關閉)。
日常開發中的問題:忽視OCP會導致每次需求變更都要修改核心類,增加回歸測試負擔,引入新bug的風險高。
- 反例:
class Shape {private String type;public double calculateArea() {if (type.equals("circle")) {// 計算圓形面積} else if (type.equals("rectangle")) {// 計算矩形面積}// 每添加一個新形狀都要修改這個方法}
}
3. 里氏替換原則(LSP)
- 核心:子類必須能夠替換父類而不破壞程序邏輯(行為一致性)。
- 關鍵點:
- 子類可擴展父類功能,但不能改變父類的契約(如輸入/輸出約束)。
- 優勢:保證繼承體系的健壯性,避免運行時意外錯誤。
- 現實類比:正方形是長方形的特例,但如果長方形有設置不同長寬的方法,正方形繼承長方形就會有問題,因為正方形長寬必須相同。
日常開發中的問題:忽視LSP會導致在使用多態時出現意外行為,子類無法真正替代父類,增加了代碼的脆弱性。
- 反例:
父類Bird
有fly()
方法,子類Penguin
重寫為空方法——違反LSP。
class Bird {public void fly() {System.out.println("Flying");}
}class Ostrich extends Bird {@Overridepublic void fly() {throw new UnsupportedOperationException("鴕鳥不會飛!");}
}public class Main {public static void makeBirdFly(Bird bird) {bird.fly(); // 對于鴕鳥,這會拋出異常}
}
4. 接口隔離原則(ISP)
- 核心:客戶端不應被迫依賴它不需要的接口方法。
- 關鍵點:
- 將龐大接口拆分為更小、更具體的接口(如
Printer
和Scanner
分開,而非合并為MultiFunctionDevice
)。 - 示例:
interface Printable { void print(); } interface Scannable { void scan(); } class SimplePrinter implements Printable { ... } // 無需實現scan()
- 將龐大接口拆分為更小、更具體的接口(如
- 優勢:減少接口污染,降低依賴耦合。
- 現實類比:多功能工具 vs 專用工具 ,你不會用瑞士軍刀上的剪刀功能來剪頭發(雖然可以,但不合適)。
日常開發中的問題:忽視ISP會導致"胖接口",實現類被迫提供空實現或拋出異常,接口變得難以理解和維護。
- 反例:
interface Worker {void work();void eat();void sleep();
}class HumanWorker implements Worker {// 實現所有方法
}class RobotWorker implements Worker {public void work() {// 機器人可以工作}public void eat() {throw new UnsupportedOperationException("機器人不需要吃飯");}public void sleep() {throw new UnsupportedOperationException("機器人不需要睡覺");}
}
5. 依賴倒置原則(DIP)
- 核心:
高層模塊不應直接依賴低層模塊,二者都應依賴抽象(接口或抽象類)。
抽象不應依賴細節(具體實現),細節應依賴抽象。 - 關鍵點:
“反轉”傳統的依賴關系方向,使得軟件的設計更加靈活、可復用,并且更容易應對變化。 - 現實類比:電源插座提供標準接口(抽象),各種電器(具體實現)只要符合接口標準就能使用,插座不需要知道具體是什么電器。
日常開發中的問題:忽視DIP會導致高層模塊與低層模塊緊耦合,難以替換實現,單元測試困難(因為難以mock依賴)。
- 反例:
class LightBulb {public void turnOn() {// 開燈}public void turnOff() {// 關燈}
}class Switch {private LightBulb bulb;public Switch(LightBulb bulb) {this.bulb = bulb;}public void operate() {// 直接依賴具體實現bulb.turnOn();}
}
二、SpringBoot+MyBatis后臺系統中的SOLID原則實踐
1. 單一職責原則(SRP)在SpringBoot中的體現
反面案例(違反SRP):
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;// 用戶CRUDpublic User getUserById(Long id) { /*...*/ }public void saveUser(User user) { /*...*/ }// 密碼加密public String encryptPassword(String raw) { /*...*/ }// 權限檢查public boolean checkPermission(User user) { /*...*/ }// 日志記錄public void writeLog(User user, String action) { /*...*/ }
}
問題:這個Service類做了太多事情,違反了SRP。如果密碼加密算法或日志記錄方式需要修改,都要改這個類。
正面案例(遵循SRP):
// 用戶CRUD服務
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate PermissionChecker permissionChecker;@Autowiredprivate UserActionLogger actionLogger;public User getUserById(Long id) { /*...*/ }public void saveUser(User user) { user.setPassword(passwordEncoder.encode(user.getPassword()));userMapper.insert(user);actionLogger.log(user, "CREATE");}
}// 密碼加密組件
@Component
public class BCryptPasswordEncoder implements PasswordEncoder {public String encode(String raw) { /* 使用BCrypt加密 */ }
}// 權限檢查組件
@Component
public class PermissionChecker {public boolean check(User user) { /*...*/ }
}// 日志記錄組件
@Component
public class UserActionLogger {public void log(User user, String action) { /*...*/ }
}
SpringBoot中的體現:
- Controller只處理HTTP請求和響應
- Service只處理業務邏輯
- Mapper只負責數據庫操作
- 各種Util/Helper類各司其職
2. 開閉原則(OCP)在MyBatis中的體現
場景:我們需要支持多種數據庫查詢方式(ID查詢、姓名查詢、條件組合查詢)
反面案例(違反OCP):
@Mapper
public interface UserMapper {@Select("SELECT * FROM user WHERE ${condition}") List<User> findByCondition(String condition); // 危險!SQL注入風險// 每新增一種查詢方式都要添加新方法
}
正面案例(遵循OCP):
使用MyBatis-Plus,它的Wrapper設計就符合OCP:
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;// 使用條件構造器,不需要修改原有代碼就能擴展新查詢方式public List<User> findUsers(String name, Integer age) {QueryWrapper<User> wrapper = new QueryWrapper<>();if (name != null) {wrapper.like("name", name);}if (age != null) {wrapper.eq("age", age);}return userMapper.selectList(wrapper);}
}
MP的設計:
- 通過Wrapper可以靈活組合查詢條件
- 新增查詢條件不需要修改Mapper接口
- 符合"對擴展開放,對修改關閉"
3. 里氏替換原則(LSP)在權限系統中的應用
場景:我們有普通用戶和管理員用戶
反面案例(違反LSP):
class User {public void deletePost(Post post) {// 基礎權限檢查}
}class Admin extends User {@Overridepublic void deletePost(Post post) {throw new UnsupportedOperationException("管理員應該用adminDeletePost方法");}public void adminDeletePost(Post post) {// 跳過權限檢查}
}
問題:Admin無法替換User,因為重寫的方法拋出了異常。
正面案例(遵循LSP):
interface PostDeleter {void deletePost(Post post);
}class UserPostDeleter implements PostDeleter {public void deletePost(Post post) {// 基礎權限檢查}
}class AdminPostDeleter implements PostDeleter {public void deletePost(Post post) {// 管理員有特殊處理,但不拋出異常}
}// 使用時
@Autowired
private Map<String, PostDeleter> deleterMap; // Spring會自動注入所有實現public void deletePost(Post post, String userType) {PostDeleter deleter = deleterMap.get(userType + "PostDeleter");deleter.deletePost(post); // 無論什么用戶類型都能安全調用
}
4. 接口隔離原則(ISP)在Service層設計中的應用
場景:用戶操作有讀操作和寫操作,有些客戶端只需要讀功能
反面案例(違反ISP):
public interface UserService {User getById(Long id);List<User> findAll();void save(User user);void delete(Long id);void resetPassword(Long id);// 很多方法...
}// 報表系統只需要讀功能,但被迫實現所有方法
正面案例(遵循ISP):
// 拆分接口
public interface UserReadService {User getById(Long id);List<User> findAll();
}public interface UserWriteService {void save(User user);void delete(Long id);void resetPassword(Long id);
}@Service
public class UserServiceImpl implements UserReadService, UserWriteService {// 實現所有方法
}// 報表系統只需要注入UserReadService
@Autowired
private UserReadService userReadService;
5. 依賴倒置原則(DIP)在SpringBoot中的體現
場景:用戶數據存儲可能使用MySQL或Redis
反面案例(違反DIP):
@Service
public class UserService {// 直接依賴具體實現private UserMySQLRepository userRepository = new UserMySQLRepository();// 如果改用Redis需要修改代碼
}
正面案例(遵循DIP):
// 定義抽象接口
public interface UserRepository {User findById(Long id);void save(User user);
}// MySQL實現
@Repository
public class UserMySQLRepository implements UserRepository {// 實現方法
}// Redis實現
@Repository
public class UserRedisRepository implements UserRepository {// 實現方法
}@Service
public class UserService {@Autowiredprivate UserRepository userRepository; // 依賴抽象// 可以通過@Qualifier或Profile決定注入哪個實現
}
SpringBoot天生支持DIP:
- 通過@Autowired注入接口
- 具體實現由Spring容器管理
- 輕松替換實現而不修改業務代碼
三、實際應用建議
(1)實際應用
- Spring框架:依賴注入(DI)是DIP的典型實現。
- Java集合框架:
List
接口(抽象)與ArrayList
/LinkedList
(實現)遵循DIP和OCP。 - 日志庫:SLF4J是抽象,Logback/Log4j是具體實現,符合DIP。
(2)實際編程中的選擇
- 寫業務代碼時:優先用 SRP 和 DIP(拆分職責+依賴接口)。
- 設計架構時:重點考慮 OCP 和 ISP(方便擴展+接口精簡)。
- review代碼時:檢查 LSP(子類是否破壞父類行為)。
后記
SOLID不是教條,而是幫助寫出更健壯代碼的工具。在SpringBoot項目中,很多設計已經遵循了這些原則,我們只需要有意識地應用它們。
參考鏈接
SOLID,面向對象設計五大基本原則