???前言:聽說有本很牛的關(guān)于Java設(shè)計模式的書——重學Java設(shè)計模式,然后買了(*^▽^*)
開始跟著小傅哥學Java設(shè)計模式吧,本文主要記錄筆者的學習筆記和心得。
打卡!打卡!
六大設(shè)計原則
單一職責原則、開閉原則、里氏替換原則、迪米特法則、接口隔離原則、依賴倒置原則。
(引讀:這里的節(jié)奏是,先說一下概念定義,然后是模擬場景,最后是反例、正例。)
一、單一職責原則
1、定義
????????單一職責原則,它規(guī)定一個類應(yīng)該只有一個發(fā)生變化的原因。
????????為什么?
????????因為如果開發(fā)的一個功能不是一次性的,當一個Class類負責超過兩個及以上職責時,當需求不斷迭代、實現(xiàn)類持續(xù)擴張,就會出現(xiàn)難以維護、不好擴展、測試難度大和上線風險高等問題。
2、模式場景
????????一個視頻網(wǎng)站用戶分類的例子:
- 訪問用戶,只能看480P的高清視頻,有廣告
- 普通會員,可以看720P的超清視頻,有廣告
- VIP會員,付費的大哥,可以看1080P的藍光視頻,無廣告
3、違背原則方案(反例)
????????根據(jù)上面的需求,直接編碼,實現(xiàn)一個最簡單的基本功能:根據(jù)不同的用戶類型,判斷用戶可以觀看的視頻類型。
public class VideoUserService {
public void serveGrade(String userType){
if ("VIP用戶".equals(userType)){
System.out.println("VIP用戶,視頻1080P藍光");
} else if ("普通用戶".equals(userType)){
System.out.println("普通用戶,視頻720P超清");
} else if ("訪客用戶".equals(userType)){
System.out.println("訪客用戶,視頻480P高清");
}
}
}
? ? ? ? 如上,這一個類包含著多個不同的行為,多種用戶職責,如果在這樣的類上繼續(xù)擴展功能就會顯得很臃腫。比如再加一個“超級VIP會員”,可以超前點播,按上面的實現(xiàn)方式,只能繼續(xù)ifelse。這樣的代碼結(jié)構(gòu)每次迭代,新需求的實現(xiàn)都可能會影響到其他邏輯。
4、單一職責原則改善代碼(正例)
? ? ? ? 視頻播放是視頻網(wǎng)站的核心功能,當完成核心功能的開發(fā)后,就需要不斷地完善用戶權(quán)限,才能更好運營網(wǎng)站。其實就是不斷建設(shè)用戶權(quán)益,根據(jù)不同的用戶類型提供差異化服務(wù)。
? ? ? ? 為了滿足不斷迭代的需求,就不能向上面一樣把所有職責行為混為一談,而是應(yīng)該提供一個上層的接口類,對不同的差異化用戶給出單獨的實現(xiàn)類,拆分各自的職責。
(1)定義接口
public interface IVideoUserService {
// 視頻清晰級別;480P、720P、1080P
void definition();
// 廣告播放方式;無廣告、有廣告
void advertisement();
}
? ? ? ? 定義出上層接口IVideoUserService,統(tǒng)一定義需要實現(xiàn)的功能,包括視頻清晰級別接口definition()、廣告播放方式接口advertisement()。然后三種不同類型的用戶就可以分別實現(xiàn)自己的服務(wù)類,做到職責統(tǒng)一。
(2)實現(xiàn)類
????????1)訪問用戶,只能看480P的高清視頻,有廣告
public class GuestVideoUserService implements IVideoUserService {
public void definition() {
System.out.println("訪客用戶,視頻480P高清");
}
public void advertisement() {
System.out.println("訪客用戶,視頻有廣告");
}
}
? ? ? ? 2)普通會員,可以看720P的超清視頻,有廣告
public class OrdinaryVideoUserService implements IVideoUserService {
public void definition() {
System.out.println("普通用戶,視頻720P超清");
}
public void advertisement() {
System.out.println("普通用戶,視頻有廣告");
}
}
? ? ? ? 3)VIP會員,付費的大哥,可以看1080P的藍光視頻,無廣告
public class VipVideoUserService implements IVideoUserService {
public void definition() {
System.out.println("VIP用戶,視頻1080P藍光");
}
public void advertisement() {
System.out.println("VIP用戶,視頻無廣告");
}
}
5、易擴展示例
? ? ? ? 假設(shè)有新的需求如下:7天試用VIP會員,可以試用看1080P的藍光視頻,但是有廣告。
// 7天試用VIP用戶
public class TryVipVideoUserService implements IVideoUserService {
public void definition() {
System.out.println("7天試用VIP用戶,視頻1080P藍光");
}
public void advertisement() {
System.out.println("7天試用VIP用戶,視頻有廣告");
}
}
????????在項目開發(fā)的過程中,盡可能保證接口的定義、類的實現(xiàn)以及方法開發(fā)保持單一職責,對項目后期的迭代和維護是很好的。
二、開閉原則
1、定義
????????在面向?qū)ο缶幊填I(lǐng)域中,開閉原則規(guī)定軟件的對象、類、模塊和函數(shù)對擴展應(yīng)該是開放的,但是對于修改是封閉的。
????????這就意味著應(yīng)該用抽象定義結(jié)構(gòu),用具體實現(xiàn)擴展細節(jié),以此確保軟件系統(tǒng)開發(fā)和維護過程的可靠性。
????????開閉原則的核心思想可以理解為面向抽象編程。
????????小結(jié):對擴展是開放的,對修改是封閉的。
2、模擬場景
?????????對于外部調(diào)用方,只要能體現(xiàn)出面向抽象編程,定義出接口并實現(xiàn)其方法,即不修改原有方法體,只通過繼承方式進行擴展,都可以體現(xiàn)出開閉原則。
?(1)場景案例
????????計算三種形狀的面積,長方形、三角形,圓形。其中圓的π=3.14,但后續(xù)由于π的取值精度不適用于后面的場景,需要再擴展,接下來模擬這個場景來體現(xiàn)開閉原則。
(2)定義接口
public interface ICalculationArea {
/**
* 計算面積,長方形
*
* @param x 長
* @param y 寬
* @return 面積
*/
double rectangle(double x, double y);
/**
* 計算面積,三角形
* @param x 邊長x
* @param y 邊長y
* @param z 邊長z
* @return 面積
*
* 海倫公式:S=√[p(p-a)(p-b)(p-c)] 其中:p=(a+b+c)/2
*/
double triangle(double x, double y, double z);
/**
* 計算面積,圓形
* @param r 半徑
* @return 面積
*
* 圓面積公式:S=πr2
*/
double circular(double r);
}
?(3)實現(xiàn)類
????????特別地,這里的π取3.14D,這也是要擴展精度的方法和體現(xiàn)開閉原則的地方。
public class CalculationArea implements ICalculationArea {
private final static double π = 3.14D;
public double rectangle(double x, double y) {
return x * y;
}
public double triangle(double x, double y, double z) {
double p = (x + y + z) / 2;
return Math.sqrt(p * (p - x) * (p - y) * (p - z));
}
public double circular(double r) {
return π * r * r;
}
}
3、違背原則方案
????????如果不考慮開閉原則,也不考慮整個工程服務(wù)的使用情況,直接改π值。
private final static double π = 3.141592653D;
4、開閉原則改善代碼
????????更好的做法,按照開閉原則。繼承父類,擴展需要的方法,同保留原有的方法,新增自己需要的方法。它的主要目的是不能因為個例需求的變化二改變預(yù)定的實現(xiàn)類。
public class CalculationAreaExt extends CalculationArea {
private final static double π = 3.141592653D;
@Override
public double circular(double r) {
return π * r * r;
}
}
????????擴展后的方法滿足了π精度變化的需求,需要使用此方法的用戶可以直接調(diào)用。而其他的方法,也不影響繼續(xù)使用。
三、里氏替換原則
1、定義
????????(1)里氏替換原則由芭芭拉·利斯科夫,1987年發(fā)表的文章《數(shù)據(jù)抽象和層次》提出:繼承必須確保超類所擁有的性質(zhì)在子類中仍然成立。
????????(2)舉個例子:如果S是T的子類,S extends T,那么所有的T類對象都可以在不破壞程序的情況下被S類的對象替換。
? ? ? ? 簡單來說,子類可以擴展父類的功能,但是不能改變父類原有的功能。
????????也就是說:當子類繼承父類時,除添加新的方法且完成新增功能外,盡量不要重寫父類的方法。這句話有四點含義:
- 子類可以實現(xiàn)父類的抽象方法,但不能覆蓋和重寫父類的非抽象方法。
- 子類可以增加自己特有的方法
- 當子類的方法重載父類的方法時,方法的輸入形參要比父類的方法更寬松。
- 當子類的方法實現(xiàn)父類的方法(重寫、重載或?qū)崿F(xiàn)抽象方法)時,方法的輸出或返回值要比父類發(fā)方法更嚴格或與父類的方法相等。
????????(3)里氏替換原則的作用
- 里氏替換原則是實現(xiàn)開閉原則的方式之一。
- 解決繼承中重寫父類造成的可復(fù)用性變差的問題。
- 類的擴展不會給已有系統(tǒng)引入新錯誤,降低了代碼出錯的可能性。
2、模擬場景
????????不同種類的銀行卡,如儲蓄卡、信用卡等都具備一定的消費功能,但又有所不同,假設(shè)構(gòu)建一個銀行系統(tǒng)。
3、違背原則方案
????????儲蓄卡和信用卡在使用功能上類似,都有支付、提現(xiàn)、還款、充值等功能。也有不同,例如支付,儲蓄卡做的是賬戶扣款動作,信用卡做的是生成貸款單動作。下面模擬先有儲蓄卡的類,之后繼承儲蓄卡類的基本功能來實現(xiàn)信用卡的功能。
(1)儲蓄卡類
/**
* 模擬儲蓄卡功能
*/
public class CashCard {
private Logger logger = LoggerFactory.getLogger(CashCard.class);
/**
* 提現(xiàn)
*
* @param orderId 單號
* @param amount 金額
* @return 狀態(tài)碼 0000成功、0001失敗、0002重復(fù)
*/
public String withdrawal(String orderId, BigDecimal amount) {
// 模擬支付成功
logger.info("提現(xiàn)成功,單號:{} 金額:{}", orderId, amount);
return "0000";
}
/**
* 儲蓄
*
* @param orderId 單號
* @param amount 金額
*/
public String recharge(String orderId, BigDecimal amount) {
// 模擬充值成功
logger.info("儲蓄成功,單號:{} 金額:{}", orderId, amount);
return "0000";
}
/**
* 交易流水查詢
* @return 交易流水
*/
public List<String> tradeFlow() {
logger.info("交易流水查詢成功");
List<String> tradeList = new ArrayList<String>();
tradeList.add("100001,100.00");
tradeList.add("100001,80.00");
tradeList.add("100001,76.50");
tradeList.add("100001,126.00");
return tradeList;
}
}
(2)信用卡類
/*
* 模擬信用卡功能
*/
public class CreditCard extends CashCard {
private Logger logger = LoggerFactory.getLogger(CashCard.class);
@Override
public String withdrawal(String orderId, BigDecimal amount) {
// 校驗
if (amount.compareTo(new BigDecimal(1000)) >= 0){
logger.info("貸款金額校驗(限額1000元),單號:{} 金額:{}", orderId, amount);
return "0001";
}
// 模擬生成貸款單
logger.info("生成貸款單,單號:{} 金額:{}", orderId, amount);
// 模擬支付成功
logger.info("貸款成功,單號:{} 金額:{}", orderId, amount);
return "0000";
}
@Override
public String recharge(String orderId, BigDecimal amount) {
// 模擬生成還款單
logger.info("生成還款單,單號:{} 金額:{}", orderId, amount);
// 模擬還款成功
logger.info("還款成功,單號:{} 金額:{}", orderId, amount);
return "0000";
}
@Override
public List<String> tradeFlow() {
return super.tradeFlow();
}
}
? ? ? ?信用卡的功能實現(xiàn)是繼承了儲蓄卡類后,進行方法重寫。這種繼承父類方式的好處是復(fù)用了父類的核心功能邏輯,但也破壞了原有的方法。此時的信用卡類不滿足里氏替換原則。因為此時的子類不能承擔原父類的功能,直接給儲蓄卡使用。
4、里氏替換原則改善代碼
(1)抽象銀行卡類
? ? ? ? 該類提供了卡的基本屬性:卡號、開卡時間。三個核心方法:正向入賬 +錢,逆向入賬 -錢
/**
* 抽象銀行卡類
*/
public abstract class BankCard {
private Logger logger = LoggerFactory.getLogger(BankCard.class);
private String cardNo; // 卡號
private String cardDate; // 開卡時間
public BankCard(String cardNo, String cardDate) {
this.cardNo = cardNo;
this.cardDate = cardDate;
}
abstract boolean rule(BigDecimal amount);
// 正向入賬,+ 錢
public String positive(String orderId, BigDecimal amount) {
// 入款成功,存款、還款
logger.info("卡號{} 入款成功,單號:{} 金額:{}", cardNo, orderId, amount);
return "0000";
}
// 逆向入賬,- 錢
public String negative(String orderId, BigDecimal amount) {
// 入款成功,存款、還款
logger.info("卡號{} 出款成功,單號:{} 金額:{}", cardNo, orderId, amount);
return "0000";
}
/**
* 交易流水查詢
*
* @return 交易流水
*/
public List<String> tradeFlow() {
logger.info("交易流水查詢成功");
List<String> tradeList = new ArrayList<String>();
tradeList.add("100001,100.00");
tradeList.add("100001,80.00");
tradeList.add("100001,76.50");
tradeList.add("100001,126.00");
return tradeList;
}
public String getCardNo() {
return cardNo;
}
public String getCardDate() {
return cardDate;
}
}
(2)儲蓄卡類實現(xiàn)
????????儲蓄卡類繼承了抽象銀行卡父類,實現(xiàn)的核心功能包括規(guī)則過濾rule()、體現(xiàn)withdrawal()、儲蓄recharge()和新增的擴展方法風控校驗checkRisk()。
/**
* 儲蓄卡
*/
public class CashCard extends BankCard {
private Logger logger = LoggerFactory.getLogger(CashCard.class);
public CashCard(String cardNo, String cardDate) {
super(cardNo, cardDate);
}
boolean rule(BigDecimal amount) {
return true;
}
/**
* 提現(xiàn)
*
* @param orderId 單號
* @param amount 金額
* @return 狀態(tài)碼 0000成功、0001失敗、0002重復(fù)
*/
public String withdrawal(String orderId, BigDecimal amount) {
// 模擬支付成功
logger.info("提現(xiàn)成功,單號:{} 金額:{}", orderId, amount);
return super.negative(orderId, amount);
}
/**
* 儲蓄
*
* @param orderId 單號
* @param amount 金額
*/
public String recharge(String orderId, BigDecimal amount) {
// 模擬充值成功
logger.info("儲蓄成功,單號:{} 金額:{}", orderId, amount);
return super.positive(orderId, amount);
}
/**
* 風險校驗
*
* @param cardNo 卡號
* @param orderId 單號
* @param amount 金額
* @return 狀態(tài)
*/
public boolean checkRisk(String cardNo, String orderId, BigDecimal amount) {
// 模擬風控校驗
logger.info("風控校驗,卡號:{} 單號:{} 金額:{}", cardNo, orderId, amount);
return true;
}
}
????????這樣的實現(xiàn)方式滿足了里氏替換原則,既實現(xiàn)抽象類的抽象方法,又沒有破壞父類中原有的方法。
(3)信用卡類實現(xiàn)
????????信用卡類可以繼承儲蓄卡,也可以繼承抽象銀行卡父類,無論哪種實現(xiàn)都需遵從里氏替換原則,不能破壞父類原有的方法。
/**
* 信用卡
*/
public class CreditCard extends CashCard {
private Logger logger = LoggerFactory.getLogger(CreditCard.class);
public CreditCard(String cardNo, String cardDate) {
super(cardNo, cardDate);
}
boolean rule2(BigDecimal amount) {
return amount.compareTo(new BigDecimal(1000)) <= 0;
}
/**
* 提現(xiàn),信用卡貸款
*
* @param orderId 單號
* @param amount 金額
* @return 狀態(tài)碼
*/
public String loan(String orderId, BigDecimal amount) {
boolean rule = rule2(amount);
if (!rule) {
logger.info("生成貸款單失敗,金額超限。單號:{} 金額:{}", orderId, amount);
return "0001";
}
// 模擬生成貸款單
logger.info("生成貸款單,單號:{} 金額:{}", orderId, amount);
// 模擬支付成功
logger.info("貸款成功,單號:{} 金額:{}", orderId, amount);
return super.negative(orderId, amount);
}
/**
* 還款,信用卡還款
*
* @param orderId 單號
* @param amount 金額
* @return 狀態(tài)碼
*/
public String repayment(String orderId, BigDecimal amount) {
// 模擬生成還款單
logger.info("生成還款單,單號:{} 金額:{}", orderId, amount);
// 模擬還款成功
logger.info("還款成功,單號:{} 金額:{}", orderId, amount);
return super.positive(orderId, amount);
}
}
小結(jié):使用繼承要遵從里氏替換原則。繼承作為面向?qū)ο蟮闹匾卣鳎o開發(fā)帶來很大便利,但也有可能給代碼帶來入侵性,降低可移植性。里氏替換的目的是使用約定的方式,讓使用繼承的代碼具備良好的擴展性和兼容性。
四、迪米特法則原則
1、定義
????????迪米特法則又稱最少知道原則,是指一個對象類對其他對象類來說,知道的越少越好。兩個類之間不要有過多的耦合關(guān)系,保持最少關(guān)聯(lián)性。
????????迪米特法則經(jīng)典語錄:只和朋友通信,不和陌生人說話。也就是說,有內(nèi)在關(guān)聯(lián)的類要內(nèi)聚,沒有直接關(guān)系的類要低耦合。
2、模擬場景
????????模擬學生、老師和校長的關(guān)系來說明迪米特法則。校長對接老師,老師管著學生,如果校長想知道一個班的分數(shù),應(yīng)該是直接問老師要,還是跟每一位學生要再進行統(tǒng)計?正常來說,是直接跟老師要。如果我們在實際開發(fā)中忽略了這種真實情況就會開發(fā)出邏輯錯誤的程序。
3、違背原則方案
(1)先簡單定義一個學生信息類,有學生姓名、考試排名、總分。
public class Student {
private String name; // 學生姓名
private int rank; // 考試排名(總排名)
private double grade; // 考試分數(shù)(總分)
// get和set方法...
}
(2)再定義老師類,初始化學生信息、提供基本的信息獲取接口
public class Teacher {
private String name; // 老師名稱
private String clazz; // 班級
private static List<Student> studentList; // 學生
public Teacher() {
}
public Teacher(String name, String clazz) {
this.name = name;
this.clazz = clazz;
}
static {
studentList = new ArrayList<>();
studentList.add(new Student("花花", 10, 589));
studentList.add(new Student("豆豆", 54, 356));
studentList.add(new Student("秋雅", 23, 439));
studentList.add(new Student("皮皮", 2, 665));
studentList.add(new Student("蛋蛋", 19, 502));
}
public static List<Student> getStudentList() {
return studentList;
}
public String getName() {
return name;
}
public String getClazz() {
return clazz;
}
}
(3)定義校長類,校長管理全局,在校長類中獲取學生人數(shù)、總分、平均分
/*
* 校長
/
public class Principal {
private Teacher teacher = new Teacher("麗華", "3年1班");
// 查詢班級信息,總分數(shù)、學生人數(shù)、平均值
public Map<String, Object> queryClazzInfo(String clazzId) {
// 獲取班級信息;學生總?cè)藬?shù)、總分、平均分
int stuCount = clazzStudentCount();
double totalScore = clazzTotalScore();
double averageScore = clazzAverageScore();
// 組裝對象,實際業(yè)務(wù)開發(fā)會有對應(yīng)的類
Map<String, Object> mapObj = new HashMap<>();
mapObj.put("班級", teacher.getClazz());
mapObj.put("老師", teacher.getName());
mapObj.put("學生人數(shù)", stuCount);
mapObj.put("班級總分數(shù)", totalScore);
mapObj.put("班級平均分", averageScore);
return mapObj;
}
// 總分
public double clazzTotalScore() {
double totalScore = 0;
for (Student stu : teacher.getStudentList()) {
totalScore += stu.getGrade();
}
return totalScore;
}
// 平均分
public double clazzAverageScore(){
double totalScore = 0;
for (Student stu : teacher.getStudentList()) {
totalScore += stu.getGrade();
}
return totalScore / teacher.getStudentList().size();
}
// 班級人數(shù)
public int clazzStudentCount(){
return teacher.getStudentList().size();
}
}
小結(jié):以上方案違背了迪米特法則(最小知道),這里所有班級都讓校長類統(tǒng)計,代碼臃腫,不利于維護和擴展。
4、迪米特法則改善代碼
? ? ? ? 使用迪米特法則,把原來違背迪米特法則的服務(wù)接口交給老師類。這樣每個老師會提供相應(yīng)的功能,校長類只需調(diào)用即可,而不需要了解每個學生的分數(shù)。
(1)學生信息類不變
public class Student {
private String name; // 學生姓名
private int rank; // 考試排名(總排名)
private double grade; // 考試分數(shù)(總分)
// get和set方法...
}
(2)老師類:我們要把校長要的信息交給老師類去管理
public class Teacher {
private String name; // 老師名稱
private String clazz; // 班級
private static List<Student> studentList; // 學生
public Teacher() {
}
public Teacher(String name, String clazz) {
this.name = name;
this.clazz = clazz;
}
static {
studentList = new ArrayList<>();
studentList.add(new Student("花花", 10, 589));
studentList.add(new Student("豆豆", 54, 356));
studentList.add(new Student("秋雅", 23, 439));
studentList.add(new Student("皮皮", 2, 665));
studentList.add(new Student("蛋蛋", 19, 502));
}
// 總分
public double clazzTotalScore() {
double totalScore = 0;
for (Student stu : studentList) {
totalScore += stu.getGrade();
}
return totalScore;
}
// 平均分
public double clazzAverageScore(){
double totalScore = 0;
for (Student stu : studentList) {
totalScore += stu.getGrade();
}
return totalScore / studentList.size();
}
// 班級人數(shù)
public int clazzStudentCount(){
return studentList.size();
}
public String getName() {
return name;
}
public String getClazz() {
return clazz;
}
}
(3)校長類:直接調(diào)用老師類的接口獲取相應(yīng)信息
public class Principal {
private Teacher teacher = new Teacher("麗華", "3年1班");
// 查詢班級信息,總分數(shù)、學生人數(shù)、平均值
public Map<String, Object> queryClazzInfo(String clazzId) {
// 獲取班級信息;學生總?cè)藬?shù)、總分、平均分
int stuCount = teacher.clazzStudentCount();
double totalScore = teacher.clazzTotalScore();
double averageScore = teacher.clazzAverageScore();
// 組裝對象,實際業(yè)務(wù)開發(fā)會有對應(yīng)的類
Map<String, Object> mapObj = new HashMap<>();
mapObj.put("班級", teacher.getClazz());
mapObj.put("老師", teacher.getName());
mapObj.put("學生人數(shù)", stuCount);
mapObj.put("班級總分數(shù)", totalScore);
mapObj.put("班級平均分", averageScore);
return mapObj;
}
}
(4)自測
public class ApiTest {
private Logger logger = LoggerFactory.getLogger(ApiTest.class);
@Test
public void test_Principal() {
Principal principal = new Principal();
Map<String, Object> map = principal.queryClazzInfo("3年1班");
logger.info("查詢結(jié)果:{}", JSON.toJSONString(map));
}
}
五、接口隔離原則
1、定義
????????《代碼整潔之道》的作者Robert C.Martin 于2002年給“接口隔離原則”的定義是:客戶端不應(yīng)該被迫依賴于它不使用的方法。
2、模擬場景
? ? ? ? 舉個某農(nóng)藥游戲中英雄技能的例子,這里為了說明問題簡化了一下,與真實游戲有出入。
? ? ? ? 假設(shè)由我們來開發(fā)英雄技能的功能,游戲中有很多英雄,可以分為射手、戰(zhàn)士、刺客等,每個英雄有三個技能。
3、違背原則方案
?(1)定義技能接口,實現(xiàn)的英雄都需要實現(xiàn)這個接口,進而實現(xiàn)自己的技能
/**
* 英雄技能
*/
public interface ISkill {
//灼日之矢
void doArchery();
// 隱襲
void doInvisible();
// 技能沉默
void doSilent();
// 眩暈
void doVertigo();
}
(2)后羿實現(xiàn)了三個技能,眩暈技能不需要實現(xiàn)
/**
* 后羿
*/
public class HeroHouYi implements ISkill{
@Override
public void doArchery() {
System.out.println("后裔的灼日之矢");
}
@Override
public void doInvisible() {
System.out.println("后裔的隱身技能");
}
@Override
public void doSilent() {
System.out.println("后裔的沉默技能");
}
@Override
public void doVertigo() {
// 無此技能的實現(xiàn)(眩暈)
}
}
(3)廉頗實現(xiàn)了三個技能,射箭技能不需要實現(xiàn)
/**
* 廉頗
*/
public class HeroLianPo implements ISkill{
@Override
public void doArchery() {
// 無此技能的實現(xiàn)(射箭)
}
@Override
public void doInvisible() {
System.out.println("廉頗的隱身技能");
}
@Override
public void doSilent() {
System.out.println("廉頗的沉默技能");
}
@Override
public void doVertigo() {
System.out.println("廉頗的眩暈技能");
}
}
小結(jié):以上,每個英雄都有一個和自己無關(guān)的接口實現(xiàn)方法,不符合接口隔離原則。不僅無法控制外部調(diào)用,還需要維護對應(yīng)的接口文檔來說明這個接口不需要實現(xiàn),非常麻煩。
4、接口隔離原則改善代碼
????????按照接口隔離原則的約定,在確保合理的情況下,把接口細分。也就是把技能拆分出來,每個英雄按需繼承和實現(xiàn)。
(1)分別定義四個技能接口ISkillArchery、ISkillInvisible、ISkillSilent、ISkillVertigo
/**
* 技能:射箭
*/
public interface ISkillArchery {
//灼日之矢
void doArchery();
}
/**
* 技能:隱身
*/
public interface ISkillInvisible {
// 隱襲
void doInvisible();
}
/**
* 技能:沉默
*/
public interface ISkillSilent {
// 技能沉默
void doSilent();
}
/**
* 技能:眩暈
*/
public interface ISkillVertigo {
// 眩暈
void doVertigo();
}
有了四個技能細分的接口,英雄的類可以自由組合實現(xiàn)
(2)英雄后羿實現(xiàn)
public class HeroHouYi implements ISkillArchery, ISkillInvisible, ISkillSilent {
@Override
public void doArchery() {
System.out.println("后裔的灼日之矢");
}
@Override
public void doInvisible() {
System.out.println("后裔的隱身技能");
}
@Override
public void doSilent() {
System.out.println("后裔的沉默技能");
}
}
(3)英雄廉頗實現(xiàn)
public class HeroLianPo implements ISkillInvisible, ISkillSilent, ISkillVertigo {
@Override
public void doInvisible() {
System.out.println("廉頗的隱身技能");
}
@Override
public void doSilent() {
System.out.println("廉頗的沉默技能");
}
@Override
public void doVertigo() {
System.out.println("廉頗的眩暈技能");
}
}
? ? ? ? 這兩個英雄的類都按需實現(xiàn)了自己需要的技能接口,這樣的實現(xiàn)方式就可以避免一些本身不屬于自己的技能還需要不斷地用文檔的方式進行維護,同時提高了代碼的可靠性,在別人接手或者修改時,可以降低開發(fā)成本和維護風險。
六、依賴倒置原則
1、定義
? ? ? ? 依賴倒置原則是Robert C.Martin于1996年在C++Report上發(fā)表的文章中提出的。
? ? ? ? 依賴倒置原則是指在設(shè)計代碼架構(gòu)時,高層模塊不應(yīng)該依賴于底層模塊,二者都應(yīng)該依賴于抽象。抽象不應(yīng)該依賴于細節(jié),細節(jié)應(yīng)該依賴于抽象。
? ? ? ? 依賴倒置原則是實現(xiàn)開閉原則的重要途徑之一,它降低了類之間的耦合,提高了系統(tǒng)的穩(wěn)定性和可維護性,同時這樣的代碼一般更易讀,且便于傳承。
2、模擬場景
? ? ? ? 模擬一個抽獎的系統(tǒng),有隨機抽獎、權(quán)重抽獎等
3、違背原則方案
? ? ? ? 最直接的方式,按不同的抽獎邏輯定義不同的接口,讓外部的服務(wù)調(diào)用。
(1)定義抽獎用戶類
? ? ? ? 一個普通的對象類,包括了用戶姓名和對應(yīng)的用戶權(quán)重,方便滿足不同的抽獎方式。
public class BetUser {
private String userName; // 用戶姓名
private int userWeight; // 用戶權(quán)重
// 此處略寫了getter和setter方法
}
? ? ? ? ?接下來在一個類用兩個方法實現(xiàn)兩種不同的抽獎邏輯
public class DrawControl {
// 隨機抽取指定數(shù)量的用戶,作為中獎用戶
public List<BetUser> doDrawRandom(List<BetUser> list, int count) {
// 集合數(shù)量很小直接返回
if (list.size() <= count) return list;
// 亂序集合
Collections.shuffle(list);
// 取出指定數(shù)量的中獎用戶
List<BetUser> prizeList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
prizeList.add(list.get(i));
}
return prizeList;
}
// 權(quán)重排名獲取指定數(shù)量的用戶,作為中獎用戶
public List<BetUser> doDrawWeight(List<BetUser> list, int count) {
// 按照權(quán)重排序
list.sort((o1, o2) -> {
int e = o2.getUserWeight() - o1.getUserWeight();
if (0 == e) return 0;
return e > 0 ? 1 : -1;
});
// 取出指定數(shù)量的中獎用戶
List<BetUser> prizeList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
prizeList.add(list.get(i));
}
return prizeList;
}
}
? ? ? ? 該類包括了兩個方法,隨機抽獎和權(quán)重抽獎:
- 隨機抽獎,這里是把集合中的元素使用Collections.shuffle(list) 進行亂序,之后隨機選取三個元素。
- 權(quán)重抽獎,這里是使用list.sort 的方法,并按自定義排序,最終選擇權(quán)重最高的前三名作為中獎用戶。
(2)測試類
public class ApiTest {
private Logger logger = LoggerFactory.getLogger(ApiTest.class);
@Test
public void test_DrawControl(){
List<BetUser> betUserList = new ArrayList<>();
betUserList.add(new BetUser("花花", 65));
betUserList.add(new BetUser("豆豆", 43));
betUserList.add(new BetUser("小白", 72));
betUserList.add(new BetUser("笨笨", 89));
betUserList.add(new BetUser("丑蛋", 10));
DrawControl drawControl = new DrawControl();
List<BetUser> prizeRandomUserList = drawControl.doDrawRandom(betUserList, 3);
logger.info("隨機抽獎,中獎用戶名單:{}", JSON.toJSON(prizeRandomUserList));
List<BetUser> prizeWeightUserList = drawControl.doDrawWeight(betUserList, 3);
logger.info("權(quán)重抽獎,中獎用戶名單:{}", JSON.toJSON(prizeWeightUserList));
}
}
????????小結(jié):這樣的實現(xiàn)方式,擴展性和可維護性都差。當業(yè)務(wù)發(fā)展需要不斷調(diào)整和新增時,對于調(diào)用方來說需要新增調(diào)用接口的代碼;對于服務(wù)類來說,隨著接口數(shù)量的增加,代碼行數(shù)會不斷暴增,最后難于維護。
4、依賴倒置原則改善代碼
? ? ? ? 為了良好的擴展性,使用依賴倒置、面向?qū)ο缶幊痰姆绞綄崿F(xiàn)。
public class BetUser {
private String userName; // 用戶姓名
private int userWeight; // 用戶權(quán)重
// 此處略寫了getter和setter方法
}
(1)抽獎接口
? ? ? ? 這里只有一個抽獎接口,接口包括了需要傳輸?shù)膌ist集合,以及中獎用戶數(shù)量。
/* 抽獎接口
*/
public interface IDraw {
// 獲取中獎用戶接口
List<BetUser> prize(List<BetUser> list, int count);
}
(2)隨機抽獎實現(xiàn)
隨機抽獎邏輯和上面一樣,只是放到了接口實現(xiàn)中
/* 隨機抽取指定數(shù)量的用戶,作為中獎用戶
*/
public class DrawRandom implements IDraw {
@Override
public List<BetUser> prize(List<BetUser> list, int count) {
// 集合數(shù)量很小直接返回
if (list.size() <= count) return list;
// 亂序集合
Collections.shuffle(list);
// 取出指定數(shù)量的中獎用戶
List<BetUser> prizeList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
prizeList.add(list.get(i));
}
return prizeList;
}
}
(3)權(quán)重抽獎實現(xiàn)
? 權(quán)重抽獎邏輯和上面一樣,只是放到了接口實現(xiàn)中
/* 權(quán)重排名獲取用戶中獎名單,指定數(shù)量
*/
public class DrawWeightRank implements IDraw {
@Override
public List<BetUser> prize(List<BetUser> list, int count) {
// 按照權(quán)重排序
list.sort((o1, o2) -> {
int e = o2.getUserWeight() - o1.getUserWeight();
if (0 == e) return 0;
return e > 0 ? 1 : -1;
});
// 取出指定數(shù)量的中獎用戶
List<BetUser> prizeList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
prizeList.add(list.get(i));
}
return prizeList;
}
}
這樣一來,任何一種抽獎都有自己的實現(xiàn)類,既可以不斷完善,也可以不斷新增。
(4)創(chuàng)建抽獎服務(wù)
public class DrawControl {
private IDraw draw;
public List<BetUser> doDraw(IDraw draw, List<BetUser> betUserList, int count) {
return draw.prize(betUserList, count);
}
}
? ? ? ? 在這個類中體現(xiàn)了依賴倒置的重要性,可以把任何一種抽獎邏輯傳遞給這個類。這樣實現(xiàn)的好處是可以不斷擴展。但不需要在外部新增調(diào)用接口,降低了一套代碼的維護成本,提高了可擴展性和可維護性。
? ? ? ? 特別地,這里把實現(xiàn)邏輯的接口作為參數(shù)傳遞,這在一些框架源碼中會經(jīng)常遇到。
(5)測試
public class ApiTest {
private Logger logger = LoggerFactory.getLogger(ApiTest.class);
@Test
public void test_DrawControl() {
List<BetUser> betUserList = new ArrayList<>();
betUserList.add(new BetUser("花花", 65));
betUserList.add(new BetUser("豆豆", 43));
betUserList.add(new BetUser("小白", 72));
betUserList.add(new BetUser("笨笨", 89));
betUserList.add(new BetUser("丑蛋", 10));
DrawControl drawControl = new DrawControl();
List<BetUser> prizeRandomUserList = drawControl.doDraw(new DrawRandom(), betUserList, 3);
logger.info("隨機抽獎,中獎用戶名單:{}", JSON.toJSON(prizeRandomUserList));
List<BetUser> prizeWeightUserList = drawControl.doDraw(new DrawWeightRank(), betUserList, 3);
logger.info("權(quán)重抽獎,中獎用戶名單:{}", JSON.toJSON(prizeWeightUserList));
}
}
????????這里新增了實現(xiàn)抽獎的入?yún)ew DrawRandom()、new DrawWeightRank(),在這兩個抽獎功能邏輯作為入?yún)⒑螅瑪U展起來會非常方便。文章來源:http://www.zghlxwxcb.cn/news/detail-776640.html
????????以這種抽象接口為基準搭建起來的框架結(jié)構(gòu)會更加穩(wěn)定,算程已經(jīng)建設(shè)好了,外部只需實現(xiàn)自己的算子即可,最終把算子交給算程處理。文章來源地址http://www.zghlxwxcb.cn/news/detail-776640.html
總結(jié)
到了這里,關(guān)于(學習打卡2)重學Java設(shè)計模式之六大設(shè)計原則的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!