一羊初、設(shè)計模式的目的
編寫軟件的過程中,程序員面臨著來自耦合性舶衬,內(nèi)聚性以及可維護(hù)性埠通,可擴(kuò)展性,重用性逛犹,靈活性等多方面的挑戰(zhàn)端辱,設(shè)計模式是為了讓程序具有更好的:
- 代碼重用性(即相同功能的代碼,不用多次編寫)
- 可讀性(即編程規(guī)范性虽画,便于其他程序員的閱讀和理解)
- 可擴(kuò)展性(即當(dāng)需要增加新功能時舞蔽,非常方便,稱為可維護(hù))
- 可靠性(即當(dāng)我們增加新的功能后码撰,對原來的功能沒有影響)
- 使程序呈現(xiàn)高內(nèi)聚渗柿,低耦合的特性
二、設(shè)計模式七大原則
設(shè)計模式的七大原則有:
- 單一職責(zé)原則
- 接口隔離原則
- 依賴倒轉(zhuǎn)原則
- 里氏替換原則
- 開閉原則
- 迪米特法則
- 合成復(fù)用原則
1脖岛、單一職責(zé)原則
概念:對類來說朵栖,即一個類應(yīng)該只負(fù)責(zé)一項職責(zé)。
如類A負(fù)責(zé)兩個不同職責(zé):職責(zé)1柴梆、職責(zé)2陨溅。當(dāng)職責(zé)1需求變更而改變A類時,可能造成職責(zé)2執(zhí)行錯誤绍在,因此要將類A的粒度分解為A1门扇、A2。
單一職責(zé)原則的注意事項
- 降低類的復(fù)雜性偿渡,一個類只負(fù)責(zé)一項職責(zé)
- 提高類的可讀性臼寄,可維護(hù)性
- 降低變更引起的風(fēng)險
- 通常情況下,我們應(yīng)當(dāng)遵守單一職責(zé)原則溜宽,只有邏輯足夠簡單脯厨,才可以在代碼級違反單一職責(zé)原則(只有類中方法數(shù)量足夠少,才可以在方法級別保持單一職責(zé)原則坑质。即把“降低類的復(fù)雜性”同個分解為多個方法來實現(xiàn))合武。
2、接口隔離原則
概念:客戶端不應(yīng)該依賴它不需要的接口涡扼,即一個類對另一個類的依賴應(yīng)該建立在最小的接口上稼跳。
通俗一點來說就是一個類通過接口去依賴另一個類時,這個接口不應(yīng)該存在太多多余的方法吃沪√郎疲可以將大的接口拆分成多個小的接口。
示例
我們現(xiàn)在有一個接口票彪,接口里面有五個方法红淡,然后有一個類B和類D分別實現(xiàn)了該接口。然后類A和類C分別通過這個接口去依賴類B和類D降铸,但是他們只會用到接口的部分方法在旱,第一種寫法如下:
package com.cxc.principle.segregation;
/**
* 實現(xiàn):類B和類D分別去實現(xiàn)接口1
* 然后類A通過接口1依賴類B(使用了1,2,3方法)
* 類C通過接口1依賴類D(使用了1,4,5方法)
*/
public class Segregation1 {
public static void main(String[] args) {
A a = new A();
a.depend1(new B()); //類A通過接口去依賴類B
}
}
/**
* 接口1
*/
interface Interface1{
void operation1();
void operation2();
void operation3();
void operation4();
void operation5();
}
class B implements Interface1{
@Override
public void operation1() {
System.out.println("B 實現(xiàn)了operation1");
}
@Override
public void operation2() {
System.out.println("B 實現(xiàn)了operation2");
}
@Override
public void operation3() {
System.out.println("B 實現(xiàn)了operation3");
}
@Override
public void operation4() {
System.out.println("B 實現(xiàn)了operation4");
}
@Override
public void operation5() {
System.out.println("B 實現(xiàn)了operation5");
}
}
class D implements Interface1{
@Override
public void operation1() {
System.out.println("D 實現(xiàn)了operation1");
}
@Override
public void operation2() {
System.out.println("D 實現(xiàn)了operation2");
}
@Override
public void operation3() {
System.out.println("D 實現(xiàn)了operation3");
}
@Override
public void operation4() {
System.out.println("D 實現(xiàn)了operation4");
}
@Override
public void operation5() {
System.out.println("D 實現(xiàn)了operation5");
}
}
/**
* A類通過Interface1依賴B類,但是只會用到1,2,3方法
*/
class A{
public void depend1(Interface1 i){
i.operation1();
}
public void depend2(Interface1 i){
i.operation2();
}
public void depend3(Interface1 i){
i.operation3();
}
}
/**
* C類通過Interface1依賴D類推掸,但是只會用到1,4,5方法
*/
class C{
public void depend1(Interface1 i){
i.operation1();
}
public void depend4(Interface1 i){
i.operation4();
}
public void depend5(Interface1 i){
i.operation5();
}
}
上面這種代碼實現(xiàn)的話桶蝎,就不符合我們的接口隔離原則,接口隔離原則中強(qiáng)調(diào)我們要將接口依賴降低到最小接口谅畅,而不論是類A還是類C登渣,依賴時都并沒有使用到接口的全部方法。
因此我們要進(jìn)行改進(jìn)毡泻,將大接口分解成小接口胜茧,在此我們將接口1分為接口1,接口2和接口3:
interface Interface1{
void operation1();
}
interface Interface2{
void operation2();
void operation3();
}
interface Interface3{
void operation4();
void operation5();
}
改進(jìn)后的實現(xiàn)如下:
package com.cxc.principle.segregation.improve;
public class Segregation1 {
public static void main(String[] args) {
A a = new A();
a.depend1(new B()); //A類通過接口去依賴B類
a.depend2(new B());
a.depend3(new B());
C c = new C();
c.depend1(new D());
c.depend4(new D());
c.depend5(new D());
}
}
interface Interface1{
void operation1();
}
interface Interface2{
void operation2();
void operation3();
}
interface Interface3{
void operation4();
void operation5();
}
class B implements Interface1,Interface2 {
@Override
public void operation1() {
System.out.println("B 實現(xiàn)了operation1");
}
@Override
public void operation2() {
System.out.println("B 實現(xiàn)了operation2");
}
@Override
public void operation3() {
System.out.println("B 實現(xiàn)了operation3");
}
}
class D implements Interface1,Interface3 {
@Override
public void operation1() {
System.out.println("D 實現(xiàn)了operation1");
}
@Override
public void operation4() {
System.out.println("D 實現(xiàn)了operation4");
}
@Override
public void operation5() {
System.out.println("D 實現(xiàn)了operation5");
}
}
/**
* A類通過Interface1依賴B類仇味,但是只會用到1,2,3方法
*/
class A{
public void depend1(Interface1 i){
i.operation1();
}
public void depend2(Interface2 i){
i.operation2();
}
public void depend3(Interface2 i){
i.operation3();
}
}
/**
* C類通過Interface1依賴D類呻顽,但是只會用到1,4,5方法
*/
class C{
public void depend1(Interface1 i){
i.operation1();
}
public void depend4(Interface3 i){
i.operation4();
}
public void depend5(Interface3 i){
i.operation5();
}
}
這樣實現(xiàn)就遵守了接口隔離原則,使依賴接口降低到最小接口邪铲。
3芬位、依賴倒轉(zhuǎn)原則
依賴倒轉(zhuǎn)原則是指:
- 高層模塊不應(yīng)該依賴低層模塊,二者都應(yīng)該依賴其抽象
- 抽象不應(yīng)該依賴細(xì)節(jié)带到,細(xì)節(jié)應(yīng)該依賴抽象
- 依賴倒轉(zhuǎn)的中心思想是面向接口編程
- 依賴倒轉(zhuǎn)原則是基于這樣的設(shè)計理念:相對于細(xì)節(jié)的多變性昧碉,抽象的東西要穩(wěn)定得多。以抽象為基礎(chǔ)搭建的架構(gòu)比以細(xì)節(jié)為基礎(chǔ)的架構(gòu)要穩(wěn)定得多揽惹。在Java中被饿,抽象指的是接口或抽象類,細(xì)節(jié)就是具體的實現(xiàn)類
- 使用接口或抽象類的目的就是制定好規(guī)范搪搏,而不涉及任何具體的操作狭握,把具體細(xì)節(jié)的任務(wù)交給他們的實現(xiàn)類去完成
依賴倒轉(zhuǎn)原則的注意事項和細(xì)節(jié)
- 低層模塊盡量都要有抽象類或接口,或者兩者都有疯溺,程序穩(wěn)定性更好
- 變量的聲明類型盡量是抽象類或接口论颅,這樣我們的變量引用和實際對象間哎垦,就存在一個緩沖層,利于程序擴(kuò)展和優(yōu)化
- 繼承時遵循里氏替換原則
依賴關(guān)系傳遞的三種方式
- 接口傳遞
- 構(gòu)造方法傳遞
- setter方式傳遞
示例
我們要實現(xiàn)一個簡單的用戶接收消息的功能
package com.cxc.principle.inversion;
/**
* 方式1問題
* 1.簡單恃疯,比較容易想到
* 2.如果我們獲取的對象是微信漏设,短信等,則需要新增類今妄,同時Person也要增加響應(yīng)的接收方法
* 3.解決思路:引入一個抽象的接口IReceiver郑口,表示接收者,這樣Person類與接口發(fā)現(xiàn)依賴盾鳞。
* 因為Email犬性,Weixin都屬于接收的范圍,他們各自實現(xiàn)IReceiver接口就可以了腾仅,就符合了依賴倒轉(zhuǎn)原則
*/
public class DependecyInversion {
public static void main(String[] args) {
new Person().receive(new Email());
}
}
class Email{
public String getInfo(){
return "電子郵件信息:hello,world";
}
}
//完成person接受消息的功能
class Person{
public void receive(Email email){
System.out.println(email.getInfo());
}
}
上述方式中乒裆,在Person類的接收消息方法中,直接傳入了一個Email類攒砖,這樣的話如果以后有其他方式比如微信缸兔、短信等消息方式還需要多寫幾個方法。不符合依賴倒轉(zhuǎn)原則吹艇,我們可以作如下更改:將這個類替換為一個接口惰蜜,實現(xiàn)如下:
package com.cxc.principle.inversion.improve;
public class DependecyInversion {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
person.receive(new WeiXin());
}
}
/**
* 接口
*/
interface IReceiver{
public String getInfo();
}
/**
* 實現(xiàn)類1
*/
class Email implements IReceiver{
public String getInfo(){
return "電子郵件信息:hello,world";
}
}
/**
* 實現(xiàn)類2
*/
class WeiXin implements IReceiver{
@Override
public String getInfo() {
return "微信消息:hello,ok";
}
}
//完成person接受消息的功能
class Person{
public void receive(IReceiver receiver){
System.out.println(receiver.getInfo());
}
}
4、里氏替換原則
OO中的繼承性的思考和說明
- 繼承包含這樣一層含義:父類中凡是已經(jīng)實現(xiàn)好的方法受神,實際上是在設(shè)定規(guī)范和契約抛猖,雖然它不強(qiáng)制要求所有的子類必須遵循這些契約,但是如果子類對這些已經(jīng)實現(xiàn)的方法任意修改鼻听,就會對整個繼承體系造成破壞财著。
- 繼承在給程序設(shè)計帶來便利的同時,也帶來了弊端撑碴。比如使用繼承會給程序帶來侵入性撑教,程序的可移植性降低,增加對象間的耦合性醉拓,如果一個類被其他的類所繼承伟姐,則當(dāng)這個類需要修改時,必須考慮到所有的子類亿卤,并且父類修改后愤兵,所有涉及到子類的功能都有可能產(chǎn)生故障。
里氏替換原則
- 如果對每個類型為T1的對象o1排吴,都有類型為T2的對象o2秆乳,使得以T1定義的所有程序P在所有的對象o1都代換成o2時,程序P的行為沒有發(fā)生變化,那么類型T2是類型T1的子類型屹堰。換句話說肛冶,所有引用基類的地方必須能透明的使用其子類的對象。
- 在使用繼承時双藕,遵循里氏替換原則淑趾,在子類中盡量不要重寫父類的方法。
- 里氏替換原則告訴我們忧陪,繼承實際上讓兩個類耦合性增強(qiáng)了,在適當(dāng)?shù)那闆r下近范,可以通過聚合嘶摊,組合,依賴來解決問題评矩。
- 我們可以讓原來的父類和子類都繼承一個更通俗的基類叶堆,原有的繼承關(guān)系去掉,采用依賴斥杜,聚合虱颗,組合燈關(guān)系代替。
5蔗喂、開閉原則
- 開閉原則是編程中最基礎(chǔ)忘渔、最重要的設(shè)計原則。
- 一個軟件實體如類缰儿,模塊和函數(shù)應(yīng)該對外擴(kuò)展開放(對提供方)畦粮,對修改關(guān)閉(對使用方)。用抽象構(gòu)建框架乖阵,用實現(xiàn)擴(kuò)展細(xì)節(jié)宣赔。
- 當(dāng)軟件需要變化時,盡量通過擴(kuò)展軟件實體的行為來實現(xiàn)變化瞪浸,而不是通過修改已有的代碼來實現(xiàn)變化儒将。
- 編程中遵循其它原則,以及使用涉及模式的目的就是遵循開閉原則对蒲。
示例
有一個畫圖類如下:
package com.cxc.principle.ocp;
public class Ocp {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());
}
}
class GraphicEditor {
public void drawShape(Shape s) {
if (s.m_type == 1)
drawRectangle(s);
else if (s.m_type == 2)
drawCircle(s);
else if (s.m_type == 3)
drawTriangle(s);
}
//三角形
public void drawRectangle(Shape r) {
System.out.println("三角形 ");
}
//圓形
public void drawCircle(Shape r) {
System.out.println(" 圓形 ");
}
//矩形
public void drawTriangle(Shape r) {
System.out.println(" 矩形 ");
}
}
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
}
class Triangle extends Shape {
Triangle() {
super.m_type = 3;
}
}
上述例子中钩蚊,如果要新增一個其他的圖形類,那么在GraphicEditor
類中也要更改代碼來加上掉這個圖形類的處理齐蔽,不滿足開閉原則两疚。因此我們應(yīng)該修改為:把Shape做成抽象類,并提供一個抽象的draw方法含滴,讓子類去實現(xiàn):
package com.cxc.principle.ocp.improve;
public class Ocp {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());
graphicEditor.drawShape(new OtherGraphic());
}
}
class GraphicEditor {
public void drawShape(Shape s) {
s.draw();
}
}
//抽象類
abstract class Shape {
int m_type;
public abstract void draw();
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 三角形 ");
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 圓形 ");
}
}
class Triangle extends Shape {
Triangle() {
super.m_type = 3;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 矩形 ");
}
}
class OtherGraphic extends Shape {
OtherGraphic() {
super.m_type = 4;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 其他圖形 ");
}
}
改進(jìn)后诱渤,如果想加入其他圖形類,直接繼承抽象類然后實現(xiàn)抽象方法即可谈况,不用去更改抽象類勺美。做到了對外擴(kuò)展開放(對提供方)递胧,對修改關(guān)閉(對使用方)
6、迪米特法則
- 一個對象應(yīng)該對其他對象保持最少的了解
- 類與類關(guān)系越密切赡茸,耦合度越大
- 迪米特法則又叫最少知道原則缎脾,即一個類對自己依賴的類知道的越少越好,也就是說占卧,對于被依賴的類不管多么復(fù)雜遗菠,都盡量將邏輯封裝在類的內(nèi)部,對外除了提供的public方法华蜒,不對外泄露任何信息
- 迪米特法則還有個更簡單的定義:只與直接的朋友通信
- 直接的朋友:每個對象都會與其他對象有耦合關(guān)系辙纬,只要兩個對象之間有耦合關(guān)系,我們就說這兩個對象之間是朋友關(guān)系叭喜,耦合的方式很多贺拣,依賴,關(guān)聯(lián)捂蕴,組合譬涡,聚合等。其中啥辨,我們稱出現(xiàn)成員變量涡匀,方法參數(shù),方法返回值中的類為直接的朋友委可,而出現(xiàn)在局部變量中的類不是直接的朋友渊跋。也就是說,陌生的類最好不要以全局變量的形式出現(xiàn)在類的內(nèi)部着倾。
示例
有一個學(xué)校拾酝,下屬有各個學(xué)院和總部,現(xiàn)要求打印出學(xué)锌ㄕ撸總部員工id和學(xué)院員工id蒿囤。
package com.cxc.principle.demeter;
import java.util.ArrayList;
import java.util.List;
public class Demeter1 {
public static void main(String[] args) {
SchoolManager schoolManager = new SchoolManager();
schoolManager.printAllEmployee(new CollegeManager());
}
}
//學(xué)校總部員工類
class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//學(xué)院員工
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//管理學(xué)院員工的管理類
class CollegeManager {
//返回學(xué)院的所有員工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) {
CollegeEmployee emp = new CollegeEmployee();
emp.setId("學(xué)院員工id= " + i);
list.add(emp);
}
return list;
}
}
//學(xué)校管理類
//分析 SchoolManager的直接朋友: Employee崇决、CollegeManager
//CollegeEmployee 不是直接朋友材诽,這樣違背了迪米特法則
class SchoolManager {
//返回學(xué)校總部的員工
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<Employee>();
for (int i = 0; i < 5; i++) {
Employee emp = new Employee();
emp.setId("學(xué)泻闵担總部的員工id= " + i);
list.add(emp);
}
return list;
}
//輸入學(xué)校員工和學(xué)院員工信息
void printAllEmployee(CollegeManager sub) {
//分析問題
//1. 這里的 CollegeEmployee 不是 SchoolManager的直接朋友
//2. CollegeEmployee 是以局部變量方式出現(xiàn)在 SchoolManager
//3. 違反了迪米特法則
//獲取到學(xué)院員工
List<CollegeEmployee> list1 = sub.getAllEmployee();
System.out.println("------------學(xué)院員工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
//獲取到學(xué)辛辰模總部員工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------學(xué)校總部員工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
上述示例違反了迪米特法則盈厘,那么應(yīng)該如何改進(jìn)呢睁枕?
將printAllEmployee
方法里面輸出學(xué)院員工的代碼封裝到學(xué)院員工管理類CollegeManager
中:
//管理學(xué)院員工的管理類
class CollegeManager {
//返回學(xué)院的所有員工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) {
CollegeEmployee emp = new CollegeEmployee();
emp.setId("學(xué)院員工id= " + i);
list.add(emp);
}
return list;
}
//輸出學(xué)院員工的信息
public void printEmployee(){
List<CollegeEmployee> list1 = getAllEmployee();
System.out.println("------------學(xué)院員工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
}
}
對應(yīng)的printAllEmployee
方法調(diào)用我們封裝的方法即可:
//輸入學(xué)校員工和學(xué)院員工信息
void printAllEmployee(CollegeManager sub) {
//輸出學(xué)院的員工
sub.printEmployee();
//獲取到學(xué)校總部員工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------學(xué)校總部員工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
迪米特法則注意事項和細(xì)節(jié)
- 迪米特法則的核心是降低類之間的耦合
- 但是注意:由于每個類都減少了不必要的依賴外遇,因此迪米特法則只是要求降低類間(對象間)的耦合關(guān)系注簿,并不是要求完全沒有依賴關(guān)系
7、合成復(fù)用原則
-
原則是盡量使用合成/聚合的方式跳仿,而不是使用繼承
上圖中的1就是使用繼承诡渴,這樣就會增強(qiáng)類的耦合性,而2,3,4使用的是組合/合成/聚合的方式菲语,這樣就可以降低類的耦合性妄辩。
設(shè)計原則核心思想
其實歸結(jié)到底就是要注意以下幾點:
- 找出應(yīng)用中可能需要變化之處,把他們獨立出來
- 針對接口編程山上,而不是針對實現(xiàn)編程
- 為了交互對象之間的松耦合設(shè)計而努力