摘要
- 在系統設計的時候,注意域的區分,功能區分、類的區分、方法區分范圍和定義。
- 在系統設計的時候的,需要思考類、方法在什么情況下會涉及到修改,遵循記住:一個類應該只有一個原因被修改! 當不滿足,可能就考慮拆分的問題。
- 學會T泛型使用,因為泛型是通用類型?使用泛型(通用、公共方法,不涉及業務邏輯)、使用具體類型(涉及業務相關使用的具體實現類)。
- 使用對象抽象能力。
1. 什么是低耦合,高內聚
低耦合(Low Coupling)和高內聚(High Cohesion)是軟件設計中的兩個重要原則,它們有助于提高代碼的可維護性、可復用性和擴展性。
1.1. 低耦合(Low Coupling)
耦合指的是模塊或組件之間的依賴程度。低耦合意味著不同模塊之間的依賴性較小,修改一個模塊時不會影響或最小影響其他模塊。
低耦合的特點:
- 接口清晰:模塊之間通過接口進行交互,而不是直接依賴具體實現。
- 減少依賴:一個模塊的變化不會導致多個模塊需要修改。
- 提高可擴展性:可以獨立替換或修改某個模塊,而不會影響整體系統。
如何實現低耦合?
- 使用接口和抽象類,而不是直接依賴具體類。
- 依賴倒置原則(DIP):依賴于抽象(接口),而不是具體實現。
- 單一職責原則(SRP):每個模塊只負責一個明確的功能,減少不必要的依賴。
- 避免全局變量和靜態方法,降低模塊之間的隱藏依賴。
1.2. 高內聚(High Cohesion)
內聚指的是模塊內部各個功能之間的關聯程度。高內聚意味著一個模塊內的功能緊密相關,模塊內部的代碼共同完成一個明確的任務,而不是負責多個不相關的功能。
高內聚的特點:
- 單一職責:一個模塊專注于完成一項任務,而不是承擔多個不同的職責。
- 增強可讀性和可維護性:代碼容易理解和修改。
- 減少代碼重復:相似功能集中在同一個模塊內,而不是散落在不同模塊中。
如何實現高內聚?
- 遵循單一職責原則(SRP),一個模塊只負責一件事。
- 模塊內部方法緊密相關,不包含與主要功能無關的代碼。
- 減少對外暴露的接口,盡量在模塊內部解決問題,避免對外部造成不必要的依賴。
1.3. 低耦合 vs. 高內聚示例
二者相輔相成:
- 高內聚使得模塊內部功能緊密相關,保證模塊內部的一致性。
- 低耦合減少模塊之間的依賴,使得模塊可以獨立修改和維護。
1.3.1. 示例反例(高耦合、低內聚)
public class OrderService {public void processOrder() {// 處理訂單System.out.println("處理訂單");// 發送通知sendEmail();sendSMS();// 記錄日志logOrder();}private void sendEmail() {System.out.println("發送郵件通知");}private void sendSMS() {System.out.println("發送短信通知");}private void logOrder() {System.out.println("記錄訂單日志");}
}
- 訂單處理(核心業務邏輯)和通知(郵件、短信)耦合在一起,修改通知方式需要改
OrderService
。 - 訂單邏輯、日志記錄、通知都混在
OrderService
里,導致內聚度低。
1.3.2. 優化(低耦合、高內聚)
public class OrderService {@Autowiredprivate final NotificationService notificationService;public void processOrder() {System.out.println("處理訂單");notificationService.sendNotification();}
}public class NotificationService {public void sendNotification() {System.out.println("發送郵件通知");System.out.println("發送短信通知");}
}
- 低耦合:
OrderService
依賴NotificationService
接口,而不是直接調用通知方法。 - 高內聚:訂單邏輯在
OrderService
,通知相關的邏輯在NotificationService
,各自只關注自己的職責。
1.4. 低耦合,高內聚總結
原則 | 低耦合 | 高內聚 |
定義 | 模塊之間的依賴性低 | 模塊內部功能緊密相關 |
作用 | 提高系統的靈活性,易于擴展和維護 | 使模塊更易于理解、修改和復用 |
實現方式 | 依賴抽象、接口隔離、減少直接依賴 | 遵循單一職責原則,把相關功能放在一起 |
典型示例 | 使用接口、依賴注入(DI)、事件驅動 | 業務邏輯和工具類分開,方法職責清晰 |
在實際開發中,低耦合和高內聚是軟件設計的重要目標,合理設計可以提高系統的穩定性和可維護性。
2. 什么是單一職責原則(SRP)
定義:一個類(或者模塊、方法)應該只有一個引起它變化的原因,即只負責一個職責。
這個原則的核心思想是高內聚、低耦合,避免一個類承擔過多的職責,從而提高代碼的可讀性、可維護性和可復用性。
2.1. 如果一個類承擔多個職責,就會導致:
- 代碼難以維護:一個職責的修改可能影響另一個不相關的職責。
- 代碼耦合度高:不同職責之間存在隱式依賴,修改一部分可能導致整個類的修改。
- 測試困難:一個類承擔多個職責,測試時可能需要處理不必要的復雜性。
通過遵循 SRP,我們可以:
? 提高代碼可讀性:一個類的功能清晰,易于理解。
? 降低修改成本:只需修改受影響的部分,而不會影響其他功能。
? 提高復用性:模塊職責清晰,可以在不同場景下復用。
2.2. 如何判斷一個類是否違反 SRP?
- 是否有多個原因導致它需要修改?
- 類中的方法是否處理多個不同的邏輯?
- 類的功能是否可以拆分成多個獨立的部分?
- 是否可以將不同的功能分配給不同的類?
如果一個類滿足以上幾個條件,就可能違反了 SRP,需要拆分。
2.3. 代碼示例
public class OrderService {public void processOrder() {System.out.println("處理訂單");}public void sendEmailNotification() {System.out.println("發送郵件通知");}public void saveOrderToDatabase() {System.out.println("訂單數據存入數據庫");}
}
問題分析:
OrderService
既負責訂單處理,又負責通知,還負責數據庫操作,承擔了多個職責。- 如果需要修改通知方式(比如從郵件改成短信),就必須修改
OrderService
,影響了訂單處理的核心邏輯。
循 SRP 的優化:拆分為三個獨立的類,每個類只負責一個職責:
// 訂單處理類
public class OrderService {@Autowiredprivate NotificationService notificationService;@Autowiredprivate OrderRepository orderRepository;public void processOrder() {System.out.println("處理訂單");orderRepository.saveOrder();notificationService.sendNotification();}
}// 訂單數據存儲類
public class OrderRepository {public void saveOrder() {System.out.println("訂單數據存入數據庫");}
}// 通知服務類
public class NotificationService {public void sendNotification() {System.out.println("發送郵件通知");}
}
優化后的好處:
- 職責分離:
OrderService
只負責訂單處理,OrderRepository
負責數據庫存儲,NotificationService
負責通知。 - 修改影響范圍小:如果要修改通知方式,只需修改
NotificationService
,不會影響OrderService
。 - 可測試性更強:每個類都可以單獨測試,避免不相關的代碼影響測試。
2.4. 什么時候該拆分?
并不是所有的類都必須拆分,如果拆分過度,會導致代碼結構過于復雜,影響可讀性。
適合拆分的情況:
- 職責明顯不同:比如訂單處理、日志記錄、支付等功能應該分開。
- 不同職責會頻繁變更:如果兩個功能的變更頻率不同,應該拆分。例如,訂單邏輯可能經常變化,但日志邏輯可能一直穩定。
- 職責之間的依賴很弱:如果兩個功能可以獨立開發、測試和維護,應該拆分。
2.5. SRP 在方法層面的應用
不僅僅是類,方法也應該遵循單一職責原則。
? 違反 SRP 的方法:
public void processOrder() {// 處理訂單System.out.println("處理訂單");// 記錄日志System.out.println("記錄訂單日志");// 發送通知System.out.println("發送郵件通知");
}
? 遵循 SRP 的方法拆分:
public void processOrder() {handleOrder();logOrder();sendNotification();
}private void handleOrder() {System.out.println("處理訂單");
}private void logOrder() {System.out.println("記錄訂單日志");
}private void sendNotification() {System.out.println("發送郵件通知");
}
這樣,每個方法只負責一項具體任務,代碼更清晰、更易維護。
2.6. SRP 與其他設計原則的關系
- 與開閉原則(OCP):SRP 使類職責單一,減少對原有代碼的修改,提高擴展性。
- 與依賴倒置原則(DIP):通過拆分職責,可以讓高層模塊依賴抽象,而不是具體實現。
- 與接口隔離原則(ISP):如果一個接口承擔了多個職責,應該拆分成多個獨立的接口。
2.7. 單一職責原則總結
原則 | 單一職責原則(SRP) |
定義 | 一個類或方法應該只有一個引起它變化的原因,即只負責一個職責。 |
核心思想 | 高內聚、低耦合,避免一個類承擔過多職責,提高代碼的可讀性、可維護性。 |
違反的表現 | 一個類或方法承擔多個不同的功能,需要經常修改多個部分。 |
如何優化 | 拆分為多個職責單一的類或方法,每個類/方法只負責一件事。 |
好處 | 代碼更清晰、可讀性更高、易擴展、易測試、低耦合。 |
記住:一個類應該只有一個原因被修改!
3. 什么是開放-封閉原則?
3.1. 開放-封閉原則定義
定義:軟件實體(類、模塊、函數等)應該 對擴展開放,對修改封閉。
- 對擴展開放(Open for extension):可以通過增加新功能來擴展現有代碼的行為。
- 對修改封閉(Closed for modification):不應該修改已有代碼來實現新需求,避免影響已有功能。
👉 目標:提高代碼的可擴展性和穩定性,避免因修改老代碼導致新 Bug。
3.2. 為什么要遵循 OCP?
? 減少代碼變更:修改老代碼容易引入 Bug,遵循 OCP 可以降低維護成本。
? 提高系統穩定性:不修改現有代碼,避免影響已有功能。
? 增強可擴展性:新需求可以通過新增代碼實現,而不是修改老代碼。
3.3. 示例:如何應用 OCP?
3.3.1. 不遵循 OCP(錯誤示范)
假設我們有一個計算不同形狀面積的方法:
public class AreaCalculator {public double calculateArea(Object shape) {if (shape instanceof Circle) {Circle c = (Circle) shape;return Math.PI * c.getRadius() * c.getRadius();} else if (shape instanceof Rectangle) {Rectangle r = (Rectangle) shape;return r.getWidth() * r.getHeight();}return 0;}
}
問題:
- 每次增加新的形狀(如
Triangle
),都要修改calculateArea()
方法。 - 違反 OCP,因為要修改原來的代碼,風險高,代碼不穩定。
3.3.2. 遵循 OCP(正確示范 - 使用多態)
可以使用 抽象類 + 繼承 讓系統支持擴展,而不修改原有代碼:
// 1. 創建 Shape 抽象類
abstract class Shape {public abstract double calculateArea();
}// 2. 具體形狀實現各自的計算邏輯
class Circle extends Shape {private double radius;public Circle(double radius) { this.radius = radius; }public double getRadius() { return radius; }@Overridepublic double calculateArea() {return Math.PI * radius * radius;}
}class Rectangle extends Shape {private double width, height;public Rectangle(double width, double height) { this.width = width; this.height = height; }@Overridepublic double calculateArea() {return width * height;}
}// 3. 計算面積的方法
public class AreaCalculator {public double calculateArea(Shape shape) {return shape.calculateArea();}
}
好處:新增形狀(如 Triangle)時,不需要修改 AreaCalculator
代碼,只需要新增一個 Triangle
類即可:
class Triangle extends Shape {private double base, height;public Triangle(double base, double height) { this.base = base; this.height = height; }@Overridepublic double calculateArea() {return 0.5 * base * height;}
}
🔹 這樣我們擴展了新功能,但沒有修改 AreaCalculator
,符合 OCP!
3.4. 其他 OCP 實現方式
除了繼承 + 多態,還有:
- 使用接口
interface Payment {void pay(double amount);
}class WeChatPay implements Payment {public void pay(double amount) {System.out.println("使用微信支付:" + amount + " 元");}
}class AliPay implements Payment {public void pay(double amount) {System.out.println("使用支付寶支付:" + amount + " 元");}
}
- 擴展新支付方式(如
ApplePay
),無需修改老代碼,符合 OCP! - 使用策略模式(Strategy Pattern):適用于有多種行為可擴展的情況(比如不同的折扣策略、支付方式)。
3.5. 什么時候使用 OCP?
- 系統需求變更頻繁(避免頻繁修改老代碼導致 Bug)。
- 需要支持多種類型的行為(如不同形狀、不同支付方式)。
- 核心業務邏輯比較穩定,但可能會增加新功能。
4. 泛型原理與示例
是的,泛型(Generics) 是 Java 中的一種特性,允許我們編寫通用的、類型安全的代碼。泛型的主要目的是在編譯時提供類型檢查,避免強制類型轉換帶來的問題,同時提高代碼的復用性。
4.1. 泛型的基本用法
4.1.1. 泛型類
可以在類定義時指定泛型:
public class Box<T> {private T value;public void setValue(T value) {this.value = value;}public T getValue() {return value;}
}
使用時,可以為 T
指定具體類型:
Box<String> stringBox = new Box<>();
stringBox.setValue("Hello");
System.out.println(stringBox.getValue()); // HelloBox<Integer> intBox = new Box<>();
intBox.setValue(123);
System.out.println(intBox.getValue()); // 123
4.1.2. 泛型方法
除了泛型類,還可以定義泛型方法:
public class Util {// 這里泛型表示入參是一個泛型,表示可以傳遞類型數組(可以是String、Integer、其他類型)public static <T> void printArray(T[] array) {for (T item : array) {System.out.print(item + " ");}System.out.println();}
}
使用泛型方法:
String[] words = {"Hello", "World"};
Integer[] numbers = {1, 2, 3};Util.printArray(words); // Hello World
Util.printArray(numbers); // 1 2 3
4.1.3. 泛型接口
可以讓接口使用泛型:
//泛型接口
public interface Storage<T> {void add(T item);T get(int index);
}
實現接口時指定具體類型:
public class StringStorage implements Storage<String> {private List<String> list = new ArrayList<>();public void add(String item) {list.add(item);}public String get(int index) {return list.get(index);}
}
4.1.4. 泛型通配符 ?
當不確定具體類型時,可以使用 ?
作為通配符:
public static void printList(List<?> list) {for (Object item : list) {System.out.println(item);}
}
List<?>
表示可以接收任何類型的 List
:
List<String> strList = Arrays.asList("A", "B", "C");
List<Integer> intList = Arrays.asList(1, 2, 3);printList(strList);
printList(intList);
💡 注意:List<?>
不能添加元素,因為 Java 不能確定它的實際類型,只能讀取。
4.1.5. 限定類型(extends
和 super
)
4.1.5.1. 上界通配符 <? extends T>
如果只需要讀取數據,可以使用 ? extends T
,表示接受 T
及其子類:
public static void readList(List<? extends Number> list) {for (Number num : list) {System.out.println(num);}
}
可傳入 List<Integer>
、List<Double>
:
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);readList(intList);
readList(doubleList);
💡 特點:
- 可以讀取數據(
Number
或其子類)。 - 不能添加數據(除了
null
)。
4.1.5.2. 下界通配符 <? super T>
如果只需要寫入數據,可以使用 ? super T
,表示接受 T
及其父類:
java復制編輯
public static void addNumbers(List<? super Integer> list) {list.add(10);list.add(20);
}
可傳入 List<Integer>
、List<Number>
、List<Object>
:
java復制編輯
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList); // [10, 20]
💡 特點:
- 可以添加
Integer
及其子類數據。 - 讀取時只能當作
Object
處理。
4.2. 泛型的限制
- 泛型不能用于基本數據類型:
List<int> list = new ArrayList<>(); // ? 錯誤
需要使用包裝類型:
List<Integer> list = new ArrayList<>(); // ? 正確
- 不能創建泛型數組:
T[] array = new T[10]; // ? 錯誤
需要使用 Object[]
代替:
Object[] array = new Object[10]; // ? 正確
- 不能實例化泛型類型:
public class Box<T> {T instance = new T(); // ? 錯誤
}
需要使用構造方法傳遞:
public class Box<T> {private T instance;public Box(Class<T> clazz) throws Exception {this.instance = clazz.getDeclaredConstructor().newInstance();}
}
4.3. 泛型總結
特性 | 泛型的作用 |
類型安全 | 通過編譯時檢查,避免 |
代碼復用 | 相同邏輯可適用于不同的數據類型 |
可讀性提高 | 代碼更清晰,無需強制類型轉換 |
性能優化 | 避免不必要的類型檢查,提高運行效率 |
泛型是 Java 通用編程的強大工具,可以在類、方法、接口等場景中使用,提升代碼的安全性、復用性和可維護性。🚀
5. 在編寫接口時,選擇泛型還是具體類型?
在編寫接口時,選擇泛型還是具體類型,主要取決于以下幾個因素:
- 是否需要增強通用性(支持不同的數據類型)
- 是否需要約束返回值或參數類型(限制為某種具體類型)
- 接口的使用場景(是否依賴于特定業務邏輯)
5.1. 什么時候使用泛型?
如果接口需要適用于多種類型,且不依賴于具體實現,就應該使用泛型,這樣可以提高代碼的通用性和復用性。
5.1.1. ? 泛型適用于以下情況:
- 接口支持多種數據類型
- 不關心具體的實現類
- 希望增強代碼的靈活性和復用性
- 返回值或參數的類型由調用者決定
5.1.2. 示例 1:通用存儲接口
public interface Repository<T> {void save(T entity);T findById(int id);
}
這樣,Repository<T>
可以用于任何數據類型:
class User {}
class Product {}Repository<User> userRepo = new UserRepository();
Repository<Product> productRepo = new ProductRepository();
好處:
UserRepository
和ProductRepository
可以共用Repository<T>
邏輯。save(T entity)
保證了存入的對象類型安全。
5.1.3. 示例 2:泛型方法
有時候,方法本身可以使用泛型,而不是整個接口:
public interface Converter {<T> T convert(String input, Class<T> clazz);
}
這樣可以支持不同類型的轉換:
Converter converter = new StringConverter();
Integer num = converter.convert("123", Integer.class);
Double d = converter.convert("12.34", Double.class);
5.2. 什么時候使用具體的實例類?
如果接口的輸入或輸出只涉及固定的業務邏輯,且不需要支持多種類型,就應該使用具體類型。
5.2.1. ? 具體類型適用于以下情況:
- 接口邏輯只適用于特定數據類型
- 接口方法需要操作具體的字段
- 返回值必須是固定的類型
5.2.2. 示例 1:固定業務邏輯的接口
public interface UserService {void register(User user);User findById(int id);
}
這里 UserService
只針對 User
,不會用于其他類型,因此不需要泛型。
5.2.3. 示例 2:固定返回值
public interface PaymentService {PaymentResult processPayment(PaymentRequest request);
}
這里 processPayment
方法總是返回 PaymentResult
,不會返回其他類型,所以不需要泛型。
5.3. 泛型 vs 具體類型對比
對比項 | 使用泛型(T) | 使用具體類型 |
適用場景 | 需要支持多種類型 | 僅適用于特定類型 |
靈活性 | 高,可擴展 | 低,局限于特定類型 |
代碼復用 | 代碼可復用 | 代碼可能重復 |
安全性 | 編譯時檢查類型 | 僅適用于特定類型 |
典型示例 |
, |
, |
5.4. 設計決策總結
? 使用泛型(通用、公共方法,不涉及業務邏輯)
- 如果接口適用于多個類型,且與具體類型無關(如
Repository<T>
) - 如果返回值或參數類型可以變化(如
Converter
) - 如果方法或接口需要提供通用能力(如
List<T>
)
? 使用具體類型(涉及業務相關使用的具體實現類)
- 如果接口邏輯特定于某個實體(如
UserService
) - 如果方法返回值不需要變化(如
PaymentService
) - 如果接口涉及特定領域業務邏輯(如
OrderProcessor
)