前言
設(shè)計(jì)模式是在開發(fā)中必須要掌握的一種技能搂擦,包括各個(gè)項(xiàng)目的結(jié)構(gòu)設(shè)計(jì)以及一些框架源碼的解讀庇配,里面都包含有設(shè)計(jì)模式歌逢,設(shè)計(jì)模式也是面試中經(jīng)常問(wèn)到签杈,從這篇文章開始記錄全部23種設(shè)計(jì)模式的解析,23種設(shè)計(jì)模式根據(jù)準(zhǔn)則來(lái)分類總共分為三大類沽一,創(chuàng)建型盖溺、結(jié)構(gòu)形以及行為型。此篇文章的大部分源代碼出自于Java設(shè)計(jì)模式精講 Debug方式+內(nèi)存分析铣缠,并結(jié)合我自己的理解烘嘱,已獲得課程老師同意昆禽。
設(shè)計(jì)模式的七大原則
在講設(shè)計(jì)模式之前,首先就要講設(shè)計(jì)模式的七大原則:
- 開閉原則(open closed principle)
- 依賴倒置原則(dependence inversion principle)
- 單一職責(zé)原則(Single Responsibility Principle)
- 接口隔離原則(interface segregation principle)
- 迪米特原則(law of demeter LOD)
- 里氏替換原則(LSP liskov substitution principle)
- 合成復(fù)用原則(Composite Reuse Principle)
創(chuàng)建型設(shè)計(jì)模式,共5種:?jiǎn)卫J健⒐S方法模式堕花、抽象工廠模式、建造者模式盗棵、原型模式。 - Java日記之設(shè)計(jì)模式總結(jié)(創(chuàng)建型)
結(jié)構(gòu)型設(shè)計(jì)模式北发,共7種:外觀模式纹因、裝飾者模式、適配器模式琳拨、享元模式瞭恰、組合模式、橋接模式狱庇、代理模式惊畏。 - Java日記之設(shè)計(jì)模式總結(jié)(結(jié)構(gòu)型)
行為型設(shè)計(jì)模式,共11種:模板方法模式密任、迭代器模式颜启、策略模式、解釋器模式浪讳、觀察者模式缰盏、備忘錄模式 、命令模式驻债、中介者模式、責(zé)任鏈模式形葬、訪問(wèn)者模式合呐、狀態(tài)模式。 - Java日記之設(shè)計(jì)模式總結(jié)(行為型)
1. 開閉原則(open closed principle)
定義:一個(gè)軟件的實(shí)體比如類笙以、模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開放淌实,修改和關(guān)閉,所謂的開閉也正是對(duì)擴(kuò)展和修改這兩個(gè)行為的原則猖腕,強(qiáng)調(diào)的是用抽象構(gòu)建框架拆祈,用實(shí)現(xiàn)來(lái)擴(kuò)展細(xì)節(jié),它是面向設(shè)計(jì)模式中最基礎(chǔ)的原則倘感,它知道我們?nèi)绾谓⒎€(wěn)定靈活的系統(tǒng)放坏,例如版本更新,盡量不修改源代碼老玛,但是可以增加新功能淤年,實(shí)現(xiàn)開閉原則的思想就是面向抽象編程钧敞。
優(yōu)點(diǎn): 提高軟件的可復(fù)用性及可維護(hù)性。
代碼舉例:
public interface ICourse {
Integer getId();
String getName();
Double getPrice();
}
//實(shí)現(xiàn)類
public class JavaCourse implements ICourse{
private Integer Id;
private String name;
private Double price;
public JavaCourse(Integer id, String name, Double price) {
this.Id = id;
this.name = name;
this.price = price;
}
public Integer getId() {
return this.Id;
}
public String getName() {
return this.name;
}
public Double getPrice() {
return this.price;
}
}
首先創(chuàng)建一個(gè)接口和它的實(shí)現(xiàn)類麸粮,這時(shí)候假如說(shuō)要對(duì)JavaCourse這個(gè)類進(jìn)行拓展溉苛,但是不修改原來(lái)的代碼,該怎么做呢弄诲?很簡(jiǎn)單愚战,繼承這個(gè)類,并進(jìn)行拓展就行齐遵,以下的例子:
//拓展類
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(Integer id, String name, Double price) {
super(id, name, price);
}
public Double getDiscountPrice(){
return super.getPrice()*0.8;
}
}
//測(cè)試類
public class Test {
public static void main(String[] args) {
ICourse iCourse = new JavaDiscountCourse(96, "Java從零到企業(yè)級(jí)電商開發(fā)", 348d);
JavaDiscountCourse javaCourse = (JavaDiscountCourse) iCourse;
System.out.println("課程ID:" + javaCourse.getId() + " 課程名稱:" + javaCourse.getName()
+ " 課程原價(jià):" + javaCourse.getPrice() + " 課程折后價(jià)格:" + javaCourse.getDiscountPrice() + "元");
}
}
從代碼就可以看出來(lái)了寂玲,這就很符合開閉原則,我們只對(duì)類進(jìn)行拓展和修改洛搀,如果是修改方法的話敢茁,通過(guò)接口去進(jìn)行抽象出來(lái),然后去實(shí)現(xiàn)就可以留美,這就是面向接口編程彰檬。
2.依賴倒置原則(dependence inversion principle)
定義:高層模塊不應(yīng)該依賴底層模塊,兩者都應(yīng)該依賴其抽象谎砾。意思就是抽象不應(yīng)該依賴細(xì)節(jié)逢倍,細(xì)節(jié)應(yīng)該依賴抽象,還是最主要的景图,就是針對(duì)接口來(lái)進(jìn)行編程较雕,不要針對(duì)實(shí)現(xiàn)來(lái)進(jìn)行編程。 通過(guò)抽象可以使各個(gè)類或者模塊的實(shí)現(xiàn)彼此獨(dú)立互不影響挚币,從而實(shí)現(xiàn)模塊之間的松耦合亮蒋,降低模塊的耦合性。使用這個(gè)原則也有一些注意的點(diǎn)妆毕,就是每個(gè)類都盡量繼承接口或者抽象類慎玖。
優(yōu)點(diǎn):可以減少類間的耦合,提高系統(tǒng)的而穩(wěn)定性笛粘,提高代碼的可讀性和可維護(hù)性趁怔,也降低了修改程序所造成的的風(fēng)險(xiǎn)。
代碼舉例:
定義接口
public interface ICourse {
void studyCourse();
}
實(shí)現(xiàn)類
public class JavaCourse implements ICourse {
@Override
public void studyCourse() {
System.out.println("學(xué)習(xí)Java課程");
}
}
public class PythonCourse implements ICourse {
@Override
public void studyCourse() {
System.out.println("學(xué)習(xí)Python課程");
}
}
首先定義接口和它的實(shí)現(xiàn)類薪前,接著我們面向接口編程呢润努,我只需要要?jiǎng)?chuàng)建一個(gè)傳遞接口實(shí)現(xiàn)的類就好了。
public class Ju {
public void setiCourse(ICourse iCourse) {
this.iCourse = iCourse;
}
private ICourse iCourse;
public void studyImoocCourse(){
iCourse.studyCourse();
}
}
//測(cè)試類實(shí)現(xiàn)
public class Test {
public static void main(String[] args) {
Ju ju = new Ju();
ju.setiCourse(new JavaCourse());
ju.studyImoocCourse();
ju.setiCourse(new FECourse());
ju.studyImoocCourse();
}
}
從代碼就可以看出來(lái)示括,這就是面向接口編程铺浇,你需要實(shí)現(xiàn)接口什么樣的方法,就只要把實(shí)現(xiàn)這個(gè)接口的類實(shí)例化就可以輕松的進(jìn)行調(diào)用垛膝,這樣就降低了耦合度随抠,也就不用如果需要替換類裁着,就需要從核心邏輯上替換實(shí)現(xiàn)的類了,也提高了代碼的可維護(hù)性拱她。
3.單一職責(zé)原則(Single Responsibility Principle)
定義:不要存在多于一個(gè)導(dǎo)致類變更的原因二驰。假設(shè)我們有一個(gè)類負(fù)責(zé)兩個(gè)職責(zé), 但是一旦有其中一個(gè)需求變更秉沼,就要修改里面的代碼桶雀,但是修改的話有可能就導(dǎo)致原版運(yùn)行正常的另一個(gè)職責(zé)發(fā)生問(wèn)題。也就是說(shuō)這個(gè)類有兩個(gè)職責(zé)唬复,對(duì)于這樣子的類的話矗积,我們就需要分別建立兩個(gè)類,分別負(fù)責(zé)職責(zé)敞咧,這樣就不會(huì)發(fā)生故障或者變更棘捣。一個(gè)類/接口/方法只負(fù)責(zé)一項(xiàng)原則。
優(yōu)點(diǎn):降低類的復(fù)雜度休建,提高類的可讀性乍恐,提高系統(tǒng)的可維護(hù)性和降低變更引起的風(fēng)險(xiǎn)。
缺點(diǎn):有可能類會(huì)非常的多测砂,可讀性就差茵烈。
代碼舉例:
private void updateUserInfo(String userName,String address,boolean bool){
if(bool){
//todo something1
}else{
//todo something2
}
}
比如說(shuō)一個(gè)有新的業(yè)務(wù)需求(如上代碼),你可能就需要在代碼上面直接更改砌些,這樣子如此長(zhǎng)期改下去可能會(huì)發(fā)生問(wèn)題呜投。我們就推薦各創(chuàng)建一個(gè)類并負(fù)責(zé)各自的職責(zé)。
//各自類的實(shí)現(xiàn)
public class FlyBird {
public void mainMoveMode(String birdName){
System.out.println(birdName+"用翅膀飛");
}
}
public class WalkBird {
public void mainMoveMode(String birdName){
System.out.println(birdName+"用腳走");
}
}
//測(cè)試類
public class Test {
public static void main(String[] args) {
FlyBird flyBird = new FlyBird();
flyBird.mainMoveMode("大雁");
WalkBird walkBird = new WalkBird();
walkBird.mainMoveMode("鴕鳥");
}
}
一些公共的方法可以使用抽象類來(lái)進(jìn)行實(shí)現(xiàn)存璃,不同的業(yè)務(wù)仑荐,就創(chuàng)建各自的類來(lái)進(jìn)行負(fù)責(zé)其職責(zé)就好。
4.接口隔離原則(interface segregation principle)
定義:用多個(gè)專門的接口纵东,而不使用單一的接口粘招,客戶端不應(yīng)該依賴它不需要的接口。一個(gè)類對(duì)一個(gè)類的依賴應(yīng)該建立在最小的接口上篮迎。建立單一接口男图,不要建立龐大臃腫的接口示姿,盡量細(xì)化接口甜橱,接口的方法,盡量少栈戳,太少也不行岂傲,就是一定要適度。
優(yōu)點(diǎn):符合我們常說(shuō)的高內(nèi)聚低耦合的設(shè)計(jì)思想子檀,從而使得類具有很好的可讀性镊掖,可擴(kuò)展性和可維護(hù)性乃戈。
代碼舉例:
//接口定義
public interface IAnimalAction {
void eat();
void fly();
void swim();
}
public interface IEatAnimalAction {
void eat();
}
public interface IFlyAnimalAction {
void fly();
}
public interface ISwimAnimalAction {
void swim();
}
//實(shí)現(xiàn)類
public class Bird implements IAnimalAction {
@Override
public void eat() {
}
@Override
public void fly() {
}
@Override
public void swim() {
}
}
public class Dog implements ISwimAnimalAction,IEatAnimalAction {
@Override
public void eat() {
}
@Override
public void swim() {
}
}
這就是一個(gè)接口隔離原則的設(shè)計(jì)了,因?yàn)楸容^簡(jiǎn)單所有沒(méi)什么好說(shuō)的亩进,就是通過(guò)一個(gè)接口來(lái)對(duì)應(yīng)一個(gè)類以及使用多繼承症虑。
5.迪米特原則(law of demeter LOD)
定義:一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象保持最少的了解,又叫最少知道原則归薛,盡量降低類的耦合谍憔,就是盡量不要對(duì)外公開過(guò)多的方法和變量,多使用private權(quán)限來(lái)約束主籍。
優(yōu)點(diǎn):降低類之間的耦合习贫。
代碼舉例:
比如Boss說(shuō)有一個(gè)需求就是查詢現(xiàn)在有多少門線上課程,這就關(guān)系了三個(gè)類千元,TeamLeader苫昌、Course和Boss。
public class Boss {
public void commandCheckNumber(TeamLeader teamLeader){
teamLeader.checkNumberOfCourses();
}
}
public class Course {
}
public class TeamLeader {
public void checkNumberOfCourses(){
List<Course> courseList = new ArrayList<Course>();
//模擬查詢課程邏輯
for(int i = 0 ;i < 20;i++){
courseList.add(new Course());
}
System.out.println("在線課程的數(shù)量是:"+courseList.size());
}
}
//測(cè)試類
public class Test {
public static void main(String[] args) {
Boss boss = new Boss();
TeamLeader teamLeader = new TeamLeader();
boss.commandCheckNumber(teamLeader);
}
}
從這段代碼我們看到Course是有TeamLeader來(lái)生成的幸海,Course是不和Boss發(fā)生接觸的祟身,Boss也不需要知道Course,這就是迪米特原則涕烧,Boss不需要關(guān)心那么多細(xì)節(jié)月而,只要知道結(jié)果就行。
6. 里氏替換原則(LSP liskov substitution principle)
定義:如果對(duì)每個(gè)類型為T1的對(duì)象o1议纯,都有類型為T2的對(duì)象o2父款,使得T1定義的所有程序P在所有的對(duì)象o1都替換成o2時(shí),程序P的行為沒(méi)有發(fā)生變化瞻凤,那么類型T2是類型T1的子類型憨攒。這個(gè)原則相比其他幾個(gè)是比較難理解的,簡(jiǎn)單來(lái)說(shuō)就是T1這個(gè)類生成了o1對(duì)象阀参,T2生成了o2對(duì)象肝集,我們現(xiàn)在寫一個(gè)程序,里面使用T1的類型蛛壳,同時(shí)創(chuàng)建了o1杏瞻,當(dāng)我們把所有程序中的o1都替換成T2生成的o2時(shí),這時(shí)程序的行為沒(méi)有發(fā)生變化衙荐,那么就可以認(rèn)為T2是T1的子類型捞挥。有沒(méi)有發(fā)現(xiàn)跟開閉原則有點(diǎn)類似,它其實(shí)就是開閉原則的補(bǔ)充忧吟。
定義擴(kuò)展:一個(gè)軟件實(shí)體如果適用一個(gè)父類的話砌函,那一定適用于其子類,所有引用父類的地方必須能透明的適用其子類的對(duì)象,子類對(duì)象能夠替換父類對(duì)象讹俊,而程序邏輯不變垦沉。子類可以擴(kuò)展父類的功能,但不能改變父類原有的功能仍劈。子類可以實(shí)現(xiàn)父類的抽象方法厕倍,但不能覆蓋父類的非抽象方法。子類可以增加自己特有的方法贩疙。當(dāng)子類的方法重載父類的方法绑青,方法的前置條件要比父類的輸入?yún)?shù)更寬松。當(dāng)子類的方法實(shí)現(xiàn)父類的方法時(shí)屋群,方法的后置條件要比父類更加嚴(yán)格或者相等闸婴。
優(yōu)點(diǎn):約束繼承泛濫,開閉原則的一種體現(xiàn)芍躏。也加強(qiáng)了程序的健壯性邪乍,同時(shí)變更時(shí)也可以做到非常好的兼容性,提高程序的維護(hù)性对竣,擴(kuò)展性庇楞,降低需求變更時(shí)引入的風(fēng)險(xiǎn)。
代碼舉例:
舉個(gè)例子說(shuō)明下繼承的風(fēng)險(xiǎn)(以下例子來(lái)源其他博客否纬,侵刪)
public class Father {
public int subtraction(int a, int b) {
return a - b;
}
}
public class Test {
public static void main(String[] args) {
Father father = new Father();
System.out.println("100-50=" + father.subtraction(100, 50));
System.out.println("100-80=" + father.subtraction(100, 80));
}
}
上面就是一個(gè)很簡(jiǎn)單減法運(yùn)算吕晌,現(xiàn)在我們需要增加一個(gè)新的功能,完成兩數(shù)相加临燃,然后再與100求和睛驳,由類Sun來(lái)負(fù)責(zé)。
public class Sun extends Father {
@Override
public int subtraction(int a, int b) {
return a + b;
}
public int addition(int a, int b) {
//重寫后subtraction(a,b)改為a+b
return subtraction(a, b) + 100;
}
}
public class Test {
public static void main(String[] args) {
Sun sun = new Sun();
System.out.println("100-50="+sun.subtraction(100, 50));
System.out.println("100-80="+sun.subtraction(100, 80));
System.out.println("100+20+100="+sun.addition(100, 20));
}
}
我們發(fā)現(xiàn)原本運(yùn)行正常的相減功能發(fā)生了錯(cuò)誤膜廊。原因就是類Sun在給方法起名時(shí)無(wú)意中重寫了父類的方法乏沸,造成所有運(yùn)行相減功能的代碼全部調(diào)用了類Sun重寫后的方法,造成原本運(yùn)行正常的功能出現(xiàn)了錯(cuò)誤爪瓜。在本例中蹬跃,引用基類Father完成的功能,換成子類Sun之后铆铆,發(fā)生了異常蝶缀。在實(shí)際編程中,我們常常會(huì)通過(guò)重寫父類的方法來(lái)完成新的功能薄货,這樣寫起來(lái)雖然簡(jiǎn)單翁都,但是整個(gè)繼承體系的可復(fù)用性會(huì)比較差,特別是運(yùn)用多態(tài)比較頻繁時(shí)菲驴,程序運(yùn)行出錯(cuò)的幾率非常大荐吵。如果非要重寫父類的方法骑冗,比較通用的做法是:原來(lái)的父類和子類都繼承一個(gè)更通俗的基類赊瞬,原有的繼承關(guān)系去掉先煎,采用依賴、聚合巧涧,組合等關(guān)系代替薯蝎。
7. 合成復(fù)用原則(Composite Reuse Principle)
定義:盡量使用對(duì)象組合/聚合,而不是使用繼承關(guān)系來(lái)達(dá)到軟件復(fù)用的目的谤绳。
優(yōu)點(diǎn):可以使系統(tǒng)更加的靈活占锯,降低類與類之間的耦合度,一個(gè)類的變化對(duì)其他類造成的影響相對(duì)較少缩筛。
代碼舉例:
//定義接口
public abstract class DBConnection {
public abstract String getConnection();
}
//實(shí)現(xiàn)接口類
public class MySQLConnection extends DBConnection {
@Override
public String getConnection() {
return "MySQL數(shù)據(jù)庫(kù)連接";
}
}
public class PostgreSQLConnection extends DBConnection {
@Override
public String getConnection() {
return "PostgreSQL數(shù)據(jù)庫(kù)連接";
}
}
public class ProductDao{
private DBConnection dbConnection;
public void setDbConnection(DBConnection dbConnection) {
this.dbConnection = dbConnection;
}
public void addProduct(){
String conn = dbConnection.getConnection();
System.out.println("使用"+conn+"增加產(chǎn)品");
}
}
public class Test {
public static void main(String[] args) {
ProductDao productDao = new ProductDao();
productDao.setDbConnection(new PostgreSQLConnection());
productDao.addProduct();
}
}
從代碼就可以看出消略,通過(guò)ProductDao類注入你需要的對(duì)象,就能實(shí)現(xiàn)不同的業(yè)務(wù)邏輯瞎抛,通過(guò)setDbConnection()
就可以實(shí)現(xiàn)對(duì)象的組合和復(fù)用艺演,當(dāng)然也可以通過(guò)構(gòu)造方法來(lái)進(jìn)行,而以后我們?nèi)绻黾赢a(chǎn)品的話桐臊,只需要寫一個(gè)和MySQLConnection類一樣平級(jí)的類胎撤,繼承DBConnection接口,具體的選擇就可以額交給客戶端就好了断凶。