更新時(shí)間:2021-10-14 09:50:14 來源:動(dòng)力節(jié)點(diǎn) 瀏覽697次
設(shè)計(jì)模式是為了更好的代碼重用性,可讀性,可靠性,可維護(hù)性。
(1)單一職責(zé)原則
(2)里氏替換原則
(3)依賴倒轉(zhuǎn)原則
(4)接口隔離原則
(5)迪米特法則
(6)開閉原則
該原則是針對(duì)類來說的,即一個(gè)類應(yīng)該只負(fù)責(zé)一項(xiàng)職責(zé)。
如類T負(fù)責(zé)兩個(gè)不同職責(zé):職責(zé)P1,職責(zé)P2。當(dāng)職責(zé)P1需求變更而改變T時(shí),可能造成職責(zé)P2發(fā)生故障,所以需要將類T的粒度分解為T1,T2。
示例如下:
用一個(gè)類秒數(shù)動(dòng)物呼吸這個(gè)場(chǎng)景
class Animal {
public void breathe(string animal)
{
Console.WriteLine(animal+"呼吸空氣");
}
}
class Program
{
static void Main(string[] args)
{
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("豬");
animal.breathe("魚");
Console.ReadLine();
}
}
輸出結(jié)果:
我們發(fā)現(xiàn)不是所有動(dòng)物都是呼吸空氣的,比如魚就是呼吸水的,根據(jù)單一職責(zé)原則,我們將Animal類細(xì)分為陸生動(dòng)物類和水生動(dòng)物類,如下所示:
class Terrestrial
{
public void breathe(string animal)
{
Console.WriteLine(animal+"呼吸空氣");
}
}
class Aquatic
{
public void breathe(string animal)
{
Console.WriteLine(animal + "呼吸水");
}
}
class Program
{
static void Main(string[] args)
{
Terrestrial terrestrial = new Terrestrial();
terrestrial.breathe("牛");
terrestrial.breathe("羊");
terrestrial.breathe("豬");
Aquatic aquatic = new Aquatic();
aquatic.breathe("魚");
Console.ReadLine();
}
}
我們發(fā)現(xiàn)這樣修改的花銷很大,既要將原來的類分解,又要修改客戶端。而直接修改Animal類雖然違背了單一職責(zé)原則,但花銷小的多,如下所示:
class Animal
{
public void breathe(string animal)
{
if ("魚".Equals(animal))
{
Console.WriteLine(animal + "呼吸水");
}
else {
Console.WriteLine(animal + "呼吸空氣");
}
}
}
class Program
{
static void Main(string[] args)
{
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("豬");
animal.breathe("魚");
Console.ReadLine();
}
}
可以看到,這種修改方式簡單的多。但卻存在隱患,一天需要將魚分為淡水魚,海水魚,又需要修改Animal類的breathe方法。可能給“豬牛羊”等相關(guān)功能帶來風(fēng)險(xiǎn),這種修改直接在代碼級(jí)別違背了單一職責(zé)原則,雖然修改起來最簡單,但隱患最大。還有一種修改方式:
class Animal
{
public void breathe(string animal)
{
Console.WriteLine(animal + "呼吸空氣");
}
public void breathe2(string animal)
{
Console.WriteLine(animal + "呼吸水");
}
}
class Program
{
static void Main(string[] args)
{
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("豬");
animal.breathe2("魚");
Console.ReadLine();
}
}
這種修改方式?jīng)]有改動(dòng)原來的方法,而是在類中新加了一個(gè)方法,這樣雖然違背了單一職責(zé)原則,但在方法級(jí)別上卻是符合單一職責(zé)原則的。
該原則是在1988年,由麻省理工學(xué)院的以為姓里的女士提出的。
如果對(duì)每個(gè)類型為T1的對(duì)象o1,都有類型為T2的對(duì)象o2,使得以T1定義的所有程序P在所有的對(duì)象o1都代換成o2時(shí),程序P的行為沒有發(fā)生變化,那么類型T2是類型T1的子類型。
換句話說,所有引用基類的地方必須能透明地使用其子類的對(duì)象。
由定義可知,在使用繼承時(shí),遵循里氏替換原則,在子類中盡量不要重寫和重載父類的方法。
繼承包含這樣一層含義:父類中凡是已經(jīng)實(shí)現(xiàn)好的方法(相對(duì)抽象方法而言),實(shí)際上是在設(shè)定一系列的規(guī)范和契約,雖然它不強(qiáng)制要求所有的子類必須遵循這些契約,但是如果子類對(duì)這些非抽象方法任意修改,就會(huì)對(duì)整個(gè)繼承體系造成破壞。而里氏替換原則就是表達(dá)了這一層含義。
繼承作為面向?qū)ο笕筇匦灾唬诮o程序設(shè)計(jì)帶來巨大遍歷的同時(shí),也帶來了弊端。比如使用繼承會(huì)給程序帶來侵入性,程序的可移植性降低,增加對(duì)象間的耦合性,如果一個(gè)類被其他的類所繼承,則當(dāng)這個(gè)類需要修改時(shí),必須考慮到所有的子類,并且父類修改后,所有涉及到子類的功能都有可能產(chǎn)生故障。
舉例說明繼承的風(fēng)險(xiǎn),我們需要完成一個(gè)兩數(shù)相減的功能,由類A來負(fù)責(zé)。
class A{
public int func1(int a,int b){
return a-b;
}
}
public class Client{
public static void main(string[] args){
A a=new A();
System.out.println("100-50="+a.func1(100,50));
System.out.println("100-80="+a.func1(100,80));
}
}
運(yùn)行結(jié)果:
100-50=50
100-80=20
后來,我們需要增加一個(gè)新的功能:完成兩數(shù)相加,然后再與100求和,由類B來負(fù)責(zé)。
Class B extends A{
public int func1(int a,int b){
return a+b;
}
public int func2(int a,int b){
return func1(a,b)+100;
}
}
public class Client{
public static void main(string[] args){
B a=new B();
System.out.println("100-50="+b.func1(100,50));
System.out.println("100-80="+b.func1(100,80));
System.out.println("100+20+100="+b.func2(100,20));
}
}
運(yùn)行結(jié)果:
100-50=150
100-80=180
100+20+100=220
我們發(fā)現(xiàn)原來運(yùn)行正常的相減功能發(fā)生了錯(cuò)誤。原因就是類B無意中重寫了父類的方法,造成原有功能出現(xiàn)錯(cuò)誤。在實(shí)際編程中,我們常常會(huì)通過重寫父類的方法完成新的功能,這樣寫起來雖然簡單,但整個(gè)繼承體系的復(fù)用性會(huì)比較差。特別是運(yùn)行多態(tài)比較頻繁的時(shí)候,如果非要重寫父類的方法,通用的做法是:原來的父類和子類都繼承一個(gè)更通俗的基類,原有的繼承關(guān)系去掉,采用依賴,聚合,組合等關(guān)系代替。
高層模塊不應(yīng)該依賴低層模塊,二者都應(yīng)該依賴其抽象;抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象。
類A直接依賴類B,如果要將類A改為依賴類C,則必須通過修改類A的代碼來達(dá)成。此時(shí),類A一般是高層模塊,負(fù)責(zé)復(fù)雜的業(yè)務(wù)邏輯,類B和類C是低層模塊,負(fù)責(zé)基本的原子操作;修改A會(huì)給程序帶來風(fēng)險(xiǎn)。
將類A修改未依賴接口I,類B和類C各自實(shí)現(xiàn)接口I,類A通過接口I間接與類B或類C發(fā)生聯(lián)系,則會(huì)大大降低修改類A的記幾率。
依賴倒置原則基于這樣一個(gè)事實(shí):相對(duì)于細(xì)節(jié)的多變性,抽象的東西要穩(wěn)定的多。以抽象為基礎(chǔ)搭建的架構(gòu)比以細(xì)節(jié)為基礎(chǔ)的架構(gòu)要穩(wěn)定的多。在java中,抽象指的是接口或抽象類,細(xì)節(jié)就是具體的實(shí)現(xiàn)類,使用接口或抽象類的目的是制定好規(guī)范,而不涉及任何具體的操作,把展現(xiàn)細(xì)節(jié)的任務(wù)交給他們的實(shí)現(xiàn)類去完成。
依賴倒置的中心思想是面向接口編程。
代碼示例如下:
class Book {
public string getContent() {
return "很久很久以前。。。。。";
}
}
class Mother {
public void narrate(Book book)
{
Console.WriteLine(book.getContent());
}
}
class Program
{
static void Main(string[] args)
{
Mother monther = new Mother();
monther.narrate(new Book());
Console.ReadLine();
}
}
運(yùn)行結(jié)果:
如果讀的對(duì)象是報(bào)紙,雜志,卻發(fā)現(xiàn)客戶端不適用了。
我們引入一個(gè)抽象的接口IReader,代表讀物
interface IReader{
public string getContent();
}
這樣Mother類與接口IReader發(fā)生依賴關(guān)系,而Book和Newspaper都屬于讀物的范疇,他們各自都去實(shí)現(xiàn)IReader接口,這樣就符合依賴倒置原則了,修改代碼如下:
interface IReader {
string getContent();
}
class Newspaper: IReader
{
public string getContent()
{
return "切爾西豪取12連勝";
}
}
class Book:IReader
{
public string getContent()
{
return "很久很久以前。。。。";
}
}
class Mother
{
public void narrate(IReader reader)
{
Console.WriteLine(reader.getContent());
}
}
class Program
{
static void Main(string[] args)
{
Mother monther = new Mother();
monther.narrate(new Book());
monther.narrate(new Newspaper());
Console.ReadLine();
}
}
運(yùn)行結(jié)果:
采用依賴倒置原則給多人并行開發(fā)帶來極大的便利,比如上列中Mother類與Book類直接耦合,Mother必須等Book類編碼完成后才可以進(jìn)行編碼,因?yàn)镸other類依賴于Book類。修改后的程序可以同時(shí)開工,互不影響。
依賴關(guān)系的傳遞有三種方式,接口傳遞,構(gòu)造方法傳遞和setter方法傳遞。
接口傳遞:
interface IDriver{
public void drive(ICar car);
}
public class Driver:IDriver{
public void drive(ICar car){
car.run();
}
}
構(gòu)造方法傳遞:
interface IDriver{
public void drive();
}
public class Driver implements IDriver{
public ICar car;
public Driver(ICar _car){
this.car=_car;
}
public void drive(){
this.car.run();
}
}
setter方式傳遞:
interface IDriver{
public void setCar(ICar car);
public void drive();
}
public class Driver:IDriver{
PRIVATE ICar car;
public void setCar(ICar car){
this.car=car;
}
public void drive(){
this.car.run();
}
}
在實(shí)際編程中,一般需要做到如下3點(diǎn):
低層模塊盡量都要有抽象類或接口,或者兩者都有。
變量的聲明類型盡量是抽象類或接口。
使用繼承時(shí)遵循里氏替換原則
客戶端不應(yīng)該依賴它不需要的接口;一個(gè)類對(duì)另一個(gè)類的依賴應(yīng)該建立在最小的接口上。
類A通過接口I依賴類B,類C通過接口I依賴類D,如果接口I對(duì)于類A和類C來說不是最小接口,則類B和類D必須去實(shí)現(xiàn)他們不需要的方法。
將臃腫的接口I拆分為獨(dú)立的幾個(gè)接口,類A和類C分別與他們需要的接口建立依賴關(guān)系。也就是采用接口隔離原則。
舉例說明接口隔離原則:
這個(gè)圖的意思是:類A依賴接口I中的方法1,方法2,方法3,類B是對(duì)類A依賴的實(shí)現(xiàn);類C依賴接口I中的方法1,方法4,方法5,類D是對(duì)類C依賴的實(shí)現(xiàn)。對(duì)于類B和類D來說,雖然存在用不到的方法(紅色標(biāo)記所示),但由于實(shí)現(xiàn)了接口I,所以也必須要實(shí)現(xiàn)這些用不到的方法。代碼如下:
interface I{
void method1();
void method2();
void method3();
void method4();
void method5();
}
class A{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method2();
}
public void depend3(I i){
i.method3();
}
}
class C{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method4();
}
public void depend3(I i){
i.method5();
}
}
class B:I{
public void method1(){
Console.WriteLine("類B實(shí)現(xiàn)接口I的方法1");
}
public void method2(){
Console.WriteLine("類B實(shí)現(xiàn)接口I的方法2");
}
public void method3(){
Console.WriteLine("類B實(shí)現(xiàn)接口I的方法3");
}
public void method4(){}
public void method5(){}
}
class D:I{
public void method1(){
Console.WriteLine("類B實(shí)現(xiàn)接口I的方法1");
}
public void method2(){}
public void method3(){}
public void method4(){
Console.WriteLine("類B實(shí)現(xiàn)接口I的方法4");
}
public void method5(){
Console.WriteLine("類B實(shí)現(xiàn)接口I的方法5");
}
}
class Program
{
static void Main(string[] args)
{
A a=new A();
a.depend1(new B());
a.depend2(new B());
a.depend3(new B());
C c=new C();
c.depend1(new D());
c.depend2(new D());
c.depend3(new D());
Console.ReadLine();
}
}
可以看到,接口中出現(xiàn)的方法,不管對(duì)依賴于它的類有沒有作用,實(shí)現(xiàn)類中都必須去實(shí)現(xiàn)這些方法。于是我們將原接口I拆分為三個(gè)接口:
代碼如下所示:
interface I1{
void method1();
}
interface I2{
void method2();
void method3();
}
interface I3{
void method4();
void method5();
}
class A{
public void depend1(I1 i){
i.method1();
}
public void depend2(I2 i){
i.method2();
}
public void depend3(I2 i){
i.method3();
}
}
class C{
public void depend1(I1 i){
i.method1();
}
public void depend2(I3 i){
i.method4();
}
public void depend3(I3 i){
i.method5();
}
}
class B:I1,I2{
public void method1(){
Console.WriteLine("類B實(shí)現(xiàn)接口I1的方法1");
}
public void method2(){
Console.WriteLine("類B實(shí)現(xiàn)接口I2的方法2");
}
public void method3(){
Console.WriteLine("類B實(shí)現(xiàn)接口I2的方法3");
}
}
class D:I1,I3{
public void method1(){
Console.WriteLine("類B實(shí)現(xiàn)接口I的方法1");
}
public void method4(){
Console.WriteLine("類B實(shí)現(xiàn)接口I的方法4");
}
public void method5(){
Console.WriteLine("類B實(shí)現(xiàn)接口I的方法5");
}
}
class Program
{
static void Main(string[] args)
{
A a=new A();
a.depend1(new B());
a.depend2(new B());
a.depend3(new B());
C c=new C();
c.depend1(new D());
c.depend2(new D());
c.depend3(new D());
Console.ReadLine();
}
}
說到這里,可能會(huì)覺得接口隔離原則和之前的單一職責(zé)原則很相似,其實(shí)不然。一,單一職責(zé)注重職責(zé),而接口隔離原則注重對(duì)接口依賴的隔離;二,單一職責(zé)是約束類,其次是方法,針對(duì)的是程序中的實(shí)現(xiàn)和細(xì)節(jié);而接口隔離原則約束的是接口,針對(duì)的是抽象,程序整體框架的構(gòu)建。
一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象保持最少的了解。
類與類關(guān)系越密切,耦合度越大。
迪米特法則又叫最少知道原則,即一個(gè)類對(duì)自己依賴的類知道的越少越好。也就是說,對(duì)于被依賴的類不管多么復(fù)雜,都盡量將邏輯封裝在類的內(nèi)部。對(duì)外除了提供的public 方法,不對(duì)外泄露任何信息。
迪米特法則還有個(gè)更簡單的定義:只與直接的朋友通信。
什么是直接的朋友:每個(gè)對(duì)象都會(huì)與其他對(duì)象由耦合關(guān)系,只要兩個(gè)對(duì)象之間有耦合關(guān)系,我們就說這兩個(gè)對(duì)象之間是朋友關(guān)系。耦合的方式很多,依賴,關(guān)聯(lián),組合,聚合等。其中,我們稱出現(xiàn)成員變量,方法參數(shù),方法返回值中的類為直接的朋友,而出現(xiàn)在局部變量中的類不是直接的朋友。也就是說,陌生的類最好不要以局部變量的形式出現(xiàn)在類的內(nèi)部。
舉例額說明如下,有一個(gè)集團(tuán)公司,下屬單位有分公司和直屬部門,現(xiàn)要求打印出所有下屬單位的員工ID。
class Employee{
private string id;
public void setId(string id){
this.id=id;
}
public string getId(){
return id;
}
}
class SubEmployee{
private string id;
public void setId(string id){
this.id=id;
}
public string getId(){
return id;
}
}
class SubCompanyManager{
public List<SubEmployee> getAllEmployee(){
List<SubEmployee> list=new ArrayList(SubEmployee);
for(int i=0;i<100;i++){
SubEmployee emp=new SubEmployee();
emp.setId("分公司"+i);
list.add(emp);
}
return list;
}
}
class CompanyManager{
public List<Employee> getAllEmployee(){
List<Employee> list=new ArrayList<Employee>();
for(int i=0;i<30;i++)
{
Employee emp=new Employee();
emp.setId("總公司"+i);
list.add(emp);
}
return list;
}
publi void printAllEmployee(SubCompanyManager sub){
List<SubEmployee> list1=sub.getAllEmployee();
foreach(SubEmployee e in list1){
Console.WriteLine(e.getId());
}
List<Employee> list2=this.getAllEmployee();
foreach(Employee e in list2){
Console.WriteLine(e.getId());
}
}
}
class Program
{
static void Main(string[] args)
{
CompanyManager e=new CompanyManager();
e.printAllEmployee(new SubCompanyManager());
Console.ReadLine();
}
}
這個(gè)設(shè)計(jì)的問題在于CompanyManager中,SubEmployee類并不是CompanyManager類的直接朋友,按照迪米特法則,應(yīng)該避免類中出現(xiàn)這樣非直接朋友關(guān)系的耦合。修改后的代碼如下:
class SubCompanyManager{
public List<SubEmployee> getAllEmployee(){
List<SubEmployee> list = new ArrayList<SubEmployee>();
for(int i=0; i<100; i++){
SubEmployee emp = new SubEmployee();
//為分公司人員按順序分配一個(gè)ID
emp.setId("分公司"+i);
list.add(emp);
}
return list;
}
public void printEmployee(){
List<SubEmployee> list = this.getAllEmployee();
for(SubEmployee e:list){
System.out.println(e.getId());
}
}
}
class CompanyManager{
public List<Employee> getAllEmployee(){
List<Employee> list = new ArrayList<Employee>();
for(int i=0; i<30; i++){
Employee emp = new Employee();
//為總公司人員按順序分配一個(gè)ID
emp.setId("總公司"+i);
list.add(emp);
}
return list;
}
public void printAllEmployee(SubCompanyManager sub){
sub.printEmployee();
List<Employee> list2 = this.getAllEmployee();
for(Employee e:list2){
System.out.println(e.getId());
}
}
}
迪米特法則的初衷是降低類之間的耦合,由于每個(gè)類都減少了不必要的依賴,因此的確可以降低耦合關(guān)系。
一個(gè)軟件實(shí)體如類,模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉。用抽象構(gòu)建框架,用實(shí)現(xiàn)擴(kuò)展細(xì)節(jié)。
當(dāng)軟件需要變化時(shí),盡量通過擴(kuò)展軟件實(shí)體的行為來實(shí)現(xiàn)變化,而不是通過修改已有的代碼來實(shí)現(xiàn)變化。
當(dāng)我們遵循前面介紹的5大原則,以及使用23中設(shè)計(jì)模式的目的就是遵循開閉原則。
以上就是關(guān)于“6大設(shè)計(jì)原則”的介紹,如果大家想了解更多相關(guān)信息,可以關(guān)注一下動(dòng)力節(jié)點(diǎn)的面向?qū)ο笤O(shè)計(jì)原則,里面有更多的知識(shí)在等待著大家,希望對(duì)大家能夠有所幫助。
0基礎(chǔ) 0學(xué)費(fèi) 15天面授
有基礎(chǔ) 直達(dá)就業(yè)
業(yè)余時(shí)間 高薪轉(zhuǎn)行
工作1~3年,加薪神器
工作3~5年,晉升架構(gòu)
提交申請(qǐng)后,顧問老師會(huì)電話與您溝通安排學(xué)習(xí)