前言
設(shè)計(jì)原則----一個(gè)老生常談卻又常談常新的話題郊艘。
喚作原則,即為實(shí)際編碼、模式設(shè)計(jì)時(shí)的基本思想纱注,理解在先畏浆,使用在后。流于字面的思想經(jīng)不起推敲狞贱,融于實(shí)踐才能為己所用刻获。
開(kāi)閉原則(Open Closed Principle)
都說(shuō)“開(kāi)閉原則是最基礎(chǔ)的一個(gè)原則”,故將其放在首位斥滤,以期收提綱挈領(lǐng)之效将鸵。
Software entities like classes,modules and functions should be open for extension but closed for modification
一個(gè)軟件實(shí)體如類、模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開(kāi)放佑颇,對(duì)修改關(guān)閉
按照慣例顶掉,一步一問(wèn)
蟆肆?何為擴(kuò)展
這個(gè)好理解减宣,產(chǎn)品需求變幻莫測(cè),隨著功能迭代岭皂,上線已久的模塊也常常面臨改造茬贵,“擴(kuò)展”無(wú)處不在簿透。
舉個(gè)簡(jiǎn)單的栗子
項(xiàng)目里已定義了頁(yè)面類型A,現(xiàn)在需要新增一種頁(yè)面類型B解藻,都擁有更新圖片和文字的功能老充。好,那現(xiàn)在應(yīng)該怎么辦螟左?修改已經(jīng)受過(guò)多輪檢驗(yàn)的原有邏輯啡浊,兼容AB類型?還是新增一個(gè)B類胶背?留給下問(wèn)
巷嚣?何為開(kāi)放
顧文思義,即為允許钳吟,允許擴(kuò)展廷粒。奔著擴(kuò)展的思路,由AB類共有邏輯抽象出接口IA
public interface IA {
public void updateUI();//更新圖片和整體樣式
public void changeTipNum(int number);//更新頁(yè)面文字
}
再讓類A實(shí)現(xiàn)該接口,覆寫公共方法
public class A implements IA {
···
@Override
public void updateUI() {
···
}
@Override
public void changeTipNum(int number) {
···
}
如此一來(lái)红且,無(wú)論新增多少種頁(yè)卡坝茎,只需實(shí)現(xiàn)IA接口,覆寫公共方法完成特殊業(yè)務(wù)暇番,即可滿足需求景东。原有邏輯無(wú)需改動(dòng),新增需求完美實(shí)現(xiàn)奔誓。
斤吐?何為關(guān)閉修改
如前文所述搔涝,“軟件實(shí)體應(yīng)該通過(guò)擴(kuò)展來(lái)實(shí)現(xiàn)變化,而不是通過(guò)修改已有代碼來(lái)實(shí)現(xiàn)變化”和措。實(shí)現(xiàn)新需求絕不能以推倒舊功能為代價(jià)庄呈,編碼的高復(fù)用和可拓展便在于此∨哨澹可能會(huì)認(rèn)為诬留,“只是改了一個(gè)判斷影響不大”,“不過(guò)就加了個(gè)判斷算不上推倒”······
然而首先贫母,只要做了更改文兑,測(cè)試同學(xué)必須全面回歸,此為人力消耗腺劣。再者绿贞,如此牽一發(fā)而動(dòng)全身,可見(jiàn)項(xiàng)目結(jié)構(gòu)也處在不穩(wěn)定的有毒環(huán)境中橘原,隱患重重籍铁。可見(jiàn)開(kāi)閉原則必須遵循趾断。
開(kāi)閉原則的重要性不妨從以下 幾方面理解(引自《設(shè)計(jì)模式之禪》本篇引用皆源于此)
1.開(kāi)閉原則對(duì)測(cè)試的影響
2.開(kāi)閉原則可以提高復(fù)用性
3.開(kāi)閉原則可以提高可維護(hù)性
4.面向?qū)ο箝_(kāi)發(fā)的要求
開(kāi)閉原則正是對(duì)代碼人熟知的六字箴言---高內(nèi)聚低耦合的理論應(yīng)用拒名。
單一職責(zé)原則(Single Responsebility Principle)
There should never be more than one reason for a class to change.
應(yīng)該有且僅有一個(gè)原因引起類的變更
說(shuō)完開(kāi)閉原則,可以談?wù)剢我宦氊?zé)了芋酌。先看看上栗增显,回到問(wèn)題開(kāi)始的第一個(gè)假設(shè)
修改經(jīng)過(guò)了多次檢驗(yàn)的原有邏輯,兼容AB類型脐帝?
修改原有邏輯本身的弊病已經(jīng)分析同云,這里我們看看,若要兼容腮恩,即在updateUI或別的方法中做判斷,邏輯結(jié)構(gòu)如下
public void updateUI() {
if (type== A類) {
···
} else if (type== B類) {
···
}
}
需求確定后温兼,寫好提測(cè)秸滴,好像也沒(méi)什么問(wèn)題。那么B類若要進(jìn)行修改呢募判,比如增加一個(gè)控件荡含?B類的邏輯處理完,A類也要相應(yīng)的隱藏或處理該控件届垫。若增加更為復(fù)雜的邏輯呢释液,要知道計(jì)劃永遠(yuǎn)趕不上變化。那······那就扯來(lái)扯去装处,一團(tuán)亂麻了误债。
引一段《設(shè)》對(duì)單一職責(zé)原則優(yōu)勢(shì)的總結(jié)
1.類的復(fù)雜性降低浸船,實(shí)現(xiàn)什么職責(zé)都有清晰明確的定義
2.可讀性提高
3.可維護(hù)性提高
4.變更引起的風(fēng)險(xiǎn)降低
重看“單一職責(zé)”,針對(duì)的正是一類或一方法身兼多職的問(wèn)題寝蹈。
遵循“單一職責(zé)”李命,不僅代碼更易讀,邏輯更清晰箫老,拓展性也更高封字,項(xiàng)目結(jié)構(gòu)自然更為健壯。
里氏替換原則(Liskov Substitution Principle,LSP)
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
所有引用基類的地方必須能透明地使用其子類對(duì)象
簡(jiǎn)單說(shuō)來(lái)就是耍鬓,子類能夠代替父類阔籽,父類無(wú)法替換子類,白馬是馬牲蜀,馬不一定是白馬笆制。
有此思想后可推導(dǎo)出以下四點(diǎn)
1.子類必須完全實(shí)現(xiàn)父類的方法
2.子類可以有自己的個(gè)性
3.覆蓋或?qū)崿F(xiàn)父類的方法時(shí)輸入?yún)?shù)可以被放大
4.覆蓋或?qū)崿F(xiàn)父類的方法時(shí)輸出結(jié)果可以被縮小
看完定義,習(xí)慣性反問(wèn)
各薇?子類沒(méi)有完全實(shí)現(xiàn)父類的方法又如何
假設(shè)有如下定義项贺。父類A與子類B、C峭判,B類中實(shí)現(xiàn)A類的a开缎、b方法,C類僅實(shí)現(xiàn)b方法林螃。
public abstract class A {
public abstract void a() {}
public abstract int b() {
return 0奕删;
}
}
public class B extends A {
@Override
public void a() {
···
}
@Override
public int b() {
···
}
}
public class C extends A {
@Override
public void a() {
···
}
@Override
public int b() {
return 0;
}
}
根據(jù)里氏替換原則,(一個(gè)凡是)凡是父類出現(xiàn)的地方子類皆可出現(xiàn)疗认,于是設(shè)想如下編程場(chǎng)景
public String getParam(A object) {
···(判空 邊界檢查blabla)
object.a();
int retInt = object.b();
··· (根據(jù)retInt得到param)···
return param完残;
}
很顯然,參數(shù)object可以為任意A的子類横漏,而param的值跟b方法的執(zhí)行結(jié)果息息相關(guān)谨设。那么如果傳入的是沒(méi)有覆寫和重載b方法的類C,則默認(rèn)返回0缎浇。聽(tīng)起來(lái)好像怪怪的扎拣?對(duì)!這意味著getParam邏輯中必須對(duì)返回0的情況甚或需要判斷傳入對(duì)象的類型素跺,牽涉的邏輯越多二蓝,調(diào)整的地方就更多。
指厌?子類發(fā)揮個(gè)性有何限制刊愚?
如果子類過(guò)分個(gè)性,不但父類無(wú)法替代子類(里氏替換原則反過(guò)頭理解)踩验,別的子類也無(wú)法替代這個(gè)個(gè)性十足的子類鸥诽。業(yè)務(wù)邏輯一旦變得復(fù)雜商玫,弊端很容易顯現(xiàn)。畢竟程序員需要目光長(zhǎng)遠(yuǎn)衙传,著眼大局决帖。
如果子類不能完整地實(shí)現(xiàn)父類的方法,或者父類的某些方法在子類中已經(jīng)發(fā)生“畸變”蓖捶,則建議斷開(kāi)父子繼承關(guān)系地回,采用依賴、聚集俊鱼、組合等關(guān)系代替繼承刻像。
?子類和父類的參數(shù)界定依據(jù)什么原則并闲?
第三點(diǎn)和第四點(diǎn)的通俗理解就是细睡,因?yàn)橄蛳罗D(zhuǎn)型有危險(xiǎn),所有要想讓子類替換父類帝火,則替換時(shí)接受方的參數(shù)類型必須寬于待接入方溜徙。
迪米特法則(Law of Demeter)
Only talk to your immediate friends
只與直接的朋友通信
迪米特法則的要求總結(jié)為如下四點(diǎn):
1.只和朋友交流
2.朋友之間也是有距離的
3.是自己的就是自己的
4.謹(jǐn)慎使用Serializable
理解迪米特,謹(jǐn)記高內(nèi)聚,低耦合(又是開(kāi)閉犀填,又是開(kāi)閉)蠢壹,以上四點(diǎn)要求都為了這共同目標(biāo)服務(wù)。這里稍作解釋九巡。
其一图贸,避免A愛(ài)BC,B又愛(ài)C之類剪不斷理還亂的情況(此愛(ài)為廣義之愛(ài),切莫誤會(huì)[無(wú)辜臉])冕广。方法實(shí)現(xiàn)不提倡“博愛(ài)”疏日。
其二,避免A為X戴帽,B為X戴領(lǐng)結(jié),C為X穿上衣,D為穿褲...之類繁瑣冗雜的理事步驟撒汉,可以想見(jiàn)一旦其中有一個(gè)環(huán)節(jié)出了問(wèn)題解決起來(lái)有多麻煩(比如戴領(lǐng)結(jié)的跟穿上衣的打架了...)
搬運(yùn)幾段簡(jiǎn)約版代碼以作補(bǔ)充
問(wèn)題代碼1:
public class Teacher {
public void commond(GroupLeader groupLeader){ //老師對(duì)學(xué)生發(fā)布命令,清一下女生
List<Girl> listGirls = new ArrayList();
···//初始化女生 ···
groupLeader.countGirls(listGirls); //告訴體育委員開(kāi)始執(zhí)行清查任務(wù)
}
}
public class GroupLeader {
public void countGirls(List<Girl> listGirls){ //有清查女生的工作
···//清查女生···
}
}
嗯沟优,很顯然犯了“其一”所指的問(wèn)題,Teacher 類怎么還依賴了Girl睬辐,“初始化女生”必須換個(gè)地方挠阁。
修正問(wèn)題代碼1如下:
public class Teacher {
public void commond(GroupLeader groupLeader){
groupLeader.countGirls(listGirls); //告訴體育委員開(kāi)始執(zhí)行清查任務(wù)
}
}
public class GroupLeader {
public void countGirls(){
List<Girl> listGirls = new ArrayList<Girl>();
···//初始化女生 ···
···//清查女生···
}
}
這樣邏輯線條才夠直嘛。
問(wèn)題代碼2:
public class Wizard {
public int first(){ //第一步
···
}
public int second(){ //第二步
···
}
public int third(){ //第三個(gè)方法
···
}
}
public class InstallSoftware {
public void installWizard(Wizard wizard){
int first = wizard.first();
if(first>50){ //根據(jù)first返回的結(jié)果溉委,看是否需要執(zhí)行second
int second = wizard.second();
if(second>50){
int third = wizard.third();
if(third >50){
wizard.first();
}
}
}
}
}
很明顯鹃唯,又犯了“其二”描述的問(wèn)題爱榕,耦合關(guān)系如此緊密瓣喊,豈不是牽一動(dòng)百?
修改問(wèn)題2如下:
public class Wizard {
private Random rand = new Random(System.currentTimeMillis());
private int first(){ //第一步
···
}
private int second(){ //第二步
···
}
private int third(){ //第三個(gè)方法
···
}
//軟件安裝過(guò)程
public void installWizard(){
int first = this.first();
//根據(jù)first返回的結(jié)果黔酥,看是否需要執(zhí)行second
if(first>50){
int second = this.second();
if(second>50){
int third = this.third();
if(third >50){
this.first();
}
}
}
}
}
public class InstallSoftware {
public void installWizard(Wizard wizard){
wizard.installWizard(); //不廢話藻三,直接調(diào)用
}
}
好嘛洪橘,出什么事直接找installWizard(),妙棵帽,真是妙熄求。
迪米特法則的核心觀念就是類間解耦,弱耦合逗概,只有弱耦合了以后弟晚,類的復(fù)用率才可以提高。但是解耦是有限度的逾苫,除非是計(jì)算機(jī)的最小符號(hào)二進(jìn)制的 0 和1卿城,那才是完全解耦,我們?cè)趯?shí)際的項(xiàng)目中時(shí)铅搓,需要適度的考慮這個(gè)法則瑟押,別為了套用法則而做項(xiàng)目,法則只是一個(gè)參考星掰,你跳出了這個(gè)法則多望,也不會(huì)有人判你刑,項(xiàng)目也未必會(huì)失敗氢烘,這就需要大家使用的是考慮如何度量法則了怀偷。
接口隔離原則(Interface Segregation Priciple)
Client should not be forced to depend upon interfaces that they don't use.
客戶端不應(yīng)該依賴它不需要的接口
The dependency of one class to another one should depend on the smallest possible interface.
類間的依賴關(guān)系應(yīng)該建立在最小的接口上
引一段對(duì)其含義的精辟總結(jié)
1.接口要盡量小
2.接口要高內(nèi)聚
3.定制服務(wù)
4.接口的設(shè)計(jì)是有限度的
在逐條分析之前,再回憶一遍威始,“開(kāi)閉原則是最基礎(chǔ)的一個(gè)原則”枢纠。可以想見(jiàn)黎棠,這四點(diǎn)含義的根本目的就是讓接口足夠靈活晋渺,可維護(hù)性足夠高,以實(shí)現(xiàn)“對(duì)擴(kuò)展開(kāi)放脓斩,對(duì)修改關(guān)閉”木西。
解釋了半天,不如直接搬運(yùn)兩段代碼随静,孰優(yōu)孰劣自有分辨
//美女實(shí)現(xiàn)類
public class PettyGirl implements IPettyGirl{
private String name;
public PettyGirl(String name){
this.name = name;
}
public void goodLooking(){
System.out.println(name + "---有好的面孔");
}
public void niceFigure(){
System.out.println(name + "---有好身材");
}
public void goodTemperament(){
System.out.println(name + "---有好氣質(zhì)");
}
}
//抽象星探類
public abstract class AbstractSearcher{
protected IPettyGirl pettyGirl;
public AbstractSearcher(IPettyGirl pettyGirl){
this.pettyGirl=pettyGirl;
}
public abstract void show();//顯示美女信息
}
//星探具體實(shí)現(xiàn)類
public class Searcher extends AbstractSearcher{
public Searcher(IPettyGirl pettyGirl){
super(pettyGirl);
}
public void show(){ //顯示美女信息
System.out.println("----美女的信息如下:---");
super.pettyGirl.goodLooking();//顯示好的面孔
super.pettyGirl.niceFigure();//顯示好身材
super.pettyGirl.goodTemperament();//顯示好氣質(zhì)
}
}
//實(shí)現(xiàn)找美女過(guò)程
public class Client{
public static void main(Strings[] args){
IPettyGirl xiaoHong = new PettyGirl("小紅");//定義一個(gè)美女
AbstractSearcher searcher = new Searcher(xiaoHong );
searcher.show();
}
}
乍一看好像沒(méi)啥問(wèn)題八千,細(xì)想想,“找美女”這個(gè)標(biāo)準(zhǔn)似乎還能細(xì)分燎猛,所謂接口還不夠“單純”不夠“小”
修改后代碼如下:
//兩種類型的美女定義
public interface IGoodBodyGirl{
public void goodLooking();//要有好的面孔
public void niceFigure();//要有好身材
}
public interface IGoodTemperamentGirl{
public void goodTemperament();//要有好氣質(zhì)
}
public class PettyGirl implements IGoodBodyGirl, IGoodTemperamentGirl{
private String name;
public PettyGirl(String name){
this.name = name;
}
public void goodLooking(){
System.out.println(name + "---有好的面孔");
}
public void niceFigure(){
System.out.println(name + "---有好身材");
}
public void goodTemperament(){
System.out.println(name + "---有好氣質(zhì)");
}
}
看完小栗子恋捆,好像對(duì)“接口隔離”這個(gè)抽象概念有那么點(diǎn)抽象了解了。編程還需靠實(shí)踐重绷,經(jīng)驗(yàn)積累是王道沸停,謹(jǐn)記“高內(nèi)聚,低耦合昭卓,接口要小還要純”愤钾,運(yùn)用到實(shí)際編碼中瘟滨,定會(huì)收獲奇效。
依賴倒置原則(Dependence Inversion Priciple)
High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstraction should not depend upon details.Details should depend upon abstractions.
1.高層模塊不應(yīng)該依賴低層模塊能颁,兩者都應(yīng)該依賴其抽象
2.抽象不應(yīng)該依賴細(xì)節(jié)
3.細(xì)節(jié)應(yīng)該依賴抽象
依賴倒置原則在Java語(yǔ)言中的表現(xiàn)就是:
1.模塊間的依賴通過(guò)抽象發(fā)生杂瘸,實(shí)現(xiàn)類之間不發(fā)生直接的 依賴關(guān)系,其依賴關(guān)系是通過(guò)接口或抽象類產(chǎn)生的伙菊;
2.接口或抽象類不依賴于實(shí)現(xiàn)類
3.實(shí)現(xiàn)類依賴接口或抽象類
簡(jiǎn)而言之就是“面向接口編程(OOD)”----接口只跟接口交流败玉,實(shí)現(xiàn)類通過(guò)依賴接口或抽象類實(shí)現(xiàn)擴(kuò)展。
依賴倒置和其他五條原則一脈相承镜硕,讓代碼結(jié)構(gòu)更包容绒怨,“擁抱變化”。其實(shí)谦疾,即便不知道這條規(guī)則南蹂,我們也更傾向?qū)懗龇弦蕾嚨怪玫某绦颉_€是舉一個(gè)簡(jiǎn)單的栗子
沒(méi)有接口時(shí)這樣實(shí)現(xiàn)功能
public class A{
public void a(B b){
···
}
}
public static void main(String[] args){
A objectA = new A();
B objectB = new B();
objectA .a(objectB );
}
AB類間的緊耦合呼之欲出念恍。這時(shí)候六剥,若需要a方法對(duì)C類對(duì)象作同樣的操作,要怎么寫呢峰伙?
public class A{
public void a(B b){
···
}
public void a2(C c){
···
}
}
天了嚕疗疟,太麻煩了!還是接口高屋建瓴瞳氓,感受一下
public interface IA{
public void a(IB b);
}
public class A implements IA{
public void a(IB b){
···
}
}
public interface IB{
···
}
public class B implements IB{
···
}
public class C implements IB{
···
}
這么一來(lái)策彤,每次擴(kuò)展,只需要增加一個(gè)實(shí)現(xiàn)類匣摘,靈不靈巧機(jī)不機(jī)智
引總結(jié)依賴的三種寫法
1.構(gòu)造函數(shù)傳遞依賴對(duì)象
2.Setter方法傳遞依賴對(duì)象
3.接口聲明依賴對(duì)象
還是那句話店诗,沒(méi)必要為了原則而原則,畢竟過(guò)猶不及音榜。再次默念“對(duì)擴(kuò)展開(kāi)發(fā)庞瘸,對(duì)修改封閉”,“高內(nèi)聚赠叼,低耦合”擦囊,抓住根本原則才是編碼之本。
最后
編碼這事兒嘴办,雖立意創(chuàng)新瞬场,仍是規(guī)則之治,優(yōu)秀的結(jié)構(gòu)設(shè)計(jì)能助我們應(yīng)對(duì)變化時(shí)游刃有余涧郊。當(dāng)然贯被,理論歸理論,應(yīng)用到實(shí)際問(wèn)題中還需具體問(wèn)題具體分析,不宥于形刃榨,融會(huì)貫通。
另外双仍,關(guān)于寫代碼枢希,渾渾噩噩地寫叫搬磚,條理清晰地寫是技術(shù)朱沃。我們當(dāng)然希望隨著日復(fù)一日辛勤coding苞轿,編碼技能不斷增長(zhǎng),綜合素養(yǎng)逐漸深厚逗物,以對(duì)得起所付出精力與汗水的成長(zhǎng)效率不斷逼近技術(shù)圣殿搬卒。這些理論原則如同內(nèi)功心法,武俠世界里翎卓,凡欲為大師契邀,苦練內(nèi)功勢(shì)在必行。
進(jìn)擊吧程序員~coding不輟,祝我們的代碼清晰靈動(dòng)失暴,堅(jiān)不可摧坯门,真正SOLID. 老哥,穩(wěn)逗扒!
學(xué)藝尚淺古戴,歡迎各路大佬多多指正
參考書目:秦小波《設(shè)計(jì)模式之禪第2版》