萬字總結(jié)之設(shè)計模式(掃盲篇)
前言
今天我們來看設(shè)計模式。話不多說,let's go。
什么是設(shè)計模式?
設(shè)計模式是對軟件設(shè)計普遍存在的問題,所提出的解決方案。與項(xiàng)目本身沒有關(guān)系,不管是電商,ERP,OA 等,都可以利用設(shè)計模式來解決相關(guān)問題。
當(dāng)然如果這個軟件就只有一小部分人用,并且功能非常簡單,在未來可預(yù)期的時間內(nèi),不會做任何大的修改和添加,即可以不使用設(shè)計模式。但是這種的太少了,所以設(shè)計模式還是非常重要的。
為什么要使用設(shè)計模式?
使用設(shè)計模式的最終目的是“高內(nèi)聚低耦合”。
- 代碼重用性:相同功能的代碼,不多多次編寫
- 代碼可讀性:編程規(guī)范性,便于其他程序員閱讀
- 代碼可擴(kuò)展性:當(dāng)增加新的功能后,對原來的功能沒有影響
設(shè)計模式的七大原則
設(shè)計模式有7大原則,具體如下,即這些不僅是設(shè)計模式的依據(jù),也是我們平常編程中應(yīng)該遵守的原則。
1.單一職責(zé)原則
見名知意,我們設(shè)計的類盡量負(fù)責(zé)一項(xiàng)功能,如A類只負(fù)責(zé)功能A,B類只負(fù)責(zé)功能B,不要讓A類既負(fù)責(zé)功能A,又負(fù)責(zé)功能B,這樣會導(dǎo)致代碼混亂,容易產(chǎn)生bug。
a.未使用單一職責(zé)原則
Single類:
- public class single {
- public static void main(String[] args) {
- Vehicle vehicle = new Vehicle();
- vehicle.run("汽車");
- vehicle.run("輪船");
- vehicle.run("飛機(jī)");
- }
- }
Vehicle類:
- public class Vehicle {
- void run(String type){
- System.out.println(type+"在公路上開");
- }
- }
運(yùn)行結(jié)果:
我們看下運(yùn)行結(jié)果,汽車是在公路上開,但是輪船和飛機(jī)并不是在公路上。因?yàn)閂ehicle類負(fù)責(zé)了不止一個功能,所以該設(shè)計是有問題的。
b.已使用單一職責(zé)原則
對于上面的例子,我們采用單一職責(zé)原則重寫一下,將Vehicle類拆分成三個類,分別是Car,Ship,Plane,讓他們各自負(fù)責(zé)陸地上,水上,空中的交通工具,使其互不影響。如果我們需要對水上交通做“風(fēng)級大于8級,禁止出海”的限制,就只需要對Ship類進(jìn)行修改。
single類:
- public class single {
- public static void main(String[] args) {
- Car car = new Car();
- car.run("汽車");
- Ship ship=new Ship();
- ship.run("輪船");
- Plane plane=new Plane();
- plane.run("飛機(jī)");
- }
- }
Car類:
- public class Car {
- void run(String type){
- System.out.println(type+"在公路上開");
- }
- }
Ship類:
- public class Ship {
- void run(String type){
- System.out.println(type+"在水里開");
- }
- }
Plane類:
- public class Plane {
- void run(String type){
- System.out.println(type+"在天空開");
- }
- }
運(yùn)行結(jié)果:
c.優(yōu)化
我們可以發(fā)現(xiàn)單一職責(zé)原則有點(diǎn)代碼太多了,顯得冗余。畢竟我們程序員是能少寫就少寫,決不能多寫代碼。那我們對其優(yōu)化下,上面每個類只有一個方法,我們可以合并為一個類,其中有三個方法,每個方法對應(yīng)著在公路上,在水上,在天空中的交通工具,將單一職責(zé)原則落在方法層面,而不再是類層面,代碼如下:
single類:
- public class single {
- public static void main(String[] args) {
- Vehicle vehicle = new Vehicle();
- vehicle.runOnRoad("汽車");
- vehicle.runOnWater("輪船");
- vehicle.runOnAir("飛機(jī)");
- }
- }
Vehicle類:
- public class Vehicle {
- void runOnRoad(String type){
- System.out.println(type+"在公路上開");
- }
- void runOnWater(String type){
- System.out.println(type+"在水里開");
- }
- void runOnAir(String type){
- System.out.println(type+"在天空開");
- }
- }
運(yùn)行結(jié)果:
d.優(yōu)缺點(diǎn)總結(jié)
優(yōu)點(diǎn):
- 降低類的復(fù)雜性,一個類只負(fù)責(zé)一個職責(zé)。
- 提高代碼的可讀性,邏輯清楚明了。
- 降低風(fēng)險,只修改一個類,并不影響其他類的功能。
缺點(diǎn):代碼量增多。(可將單一職責(zé)原則落在方法層面進(jìn)行優(yōu)化)
2.接口隔離原則
類不應(yīng)該依賴他不需要的接口,接口盡量小顆粒劃分。
a.未使用接口隔離原則
People類:
- public interface People {
- void exam();
- void teach();
- }
Student類:
- public class Student implements People {
- @Override
- public void exam() {
- System.out.println("學(xué)生考試");
- }
- @Override
- public void teach() {
- }
- }
Teacher類:
- public class Teacher implements People{
- @Override
- public void exam() {
- }
- @Override
- public void teach() {
- System.out.println("教師教書");
- }
- }
test類:
- public class test {
- public static void main(String[] args){
- People student=new Student();
- student.exam();
- People teacher=new Teacher();
- teacher.teach();
- }
- }
運(yùn)行結(jié)果:
注:此處代碼并沒有報錯,正常運(yùn)行的,但是看得代碼冗余且奇怪。Student只需要實(shí)現(xiàn)People的exam方法,而Teacher只需要實(shí)現(xiàn)People的teach方法,但是現(xiàn)在Student實(shí)現(xiàn)了People接口,就必須重寫exam和teach方法,Teacher也是如此。
b.已使用接口隔離原則
我們將People接口的兩個方法拆分開,分為兩個接口People1和People2,并且讓Sudent實(shí)現(xiàn)People1接口,Teacher實(shí)現(xiàn)People2接口,使其互不干擾,具體代碼如下:
People1類:
- public interface People1 {
- void exam();
- }
People2類:
- public interface People2 {
- void teach();
- }
Student類:
- public class Student implements People1 {
- @Override
- public void exam() {
- System.out.println("學(xué)生考試");
- }
- }
Teacher類:
- public class Teacher implements People2 {
- @Override
- public void teach() {
- System.out.println("教師教書");
- }
- }
test類:
- public class test {
- public static void main(String[] args){
- People1 student=new Student();
- student.exam();
- People2 teacher=new Teacher();
- teacher.teach();
- }
- }
運(yùn)行結(jié)果:
c.總結(jié)
某人要問了,那奇怪礙什么事,能正常運(yùn)行就行?此處需要敲頭,產(chǎn)品經(jīng)理認(rèn)為能跑就行我可以理解,但是咱身為程序員,不能就這點(diǎn)追求,要求代碼優(yōu)雅。。。(手動調(diào)侃產(chǎn)品經(jīng)理)
言歸正傳,如果將多個方法合并為一個接口,再提供給其他系統(tǒng)使用的時候,就必須實(shí)現(xiàn)該接口的所有方法,那有些方法是根本不需要的,造成使用者的混淆。
3.依賴倒轉(zhuǎn)原則
高層模塊不應(yīng)該依賴底層模塊,二者都應(yīng)該依賴接口或抽象類。其核心就是面向接口編程
依賴倒轉(zhuǎn)原則主要基于如下的設(shè)計理念:相對于細(xì)節(jié)的多變性,抽象的東西要穩(wěn)定的多,以抽象為基礎(chǔ)搭建的架構(gòu)比以細(xì)節(jié)為基礎(chǔ)的架構(gòu)要穩(wěn)定的多。
抽象指接口或抽象類,細(xì)節(jié)指具體的實(shí)現(xiàn)類。
這樣講太干澀,照搬宣科,沒有靈魂,說了等于沒說。接下來我們用例子來說明。
a.未使用依賴倒轉(zhuǎn)原則
由于現(xiàn)在是特殊時期,我們先來一個買菜的例子。如下是傻白甜的例子,未使用到依賴倒轉(zhuǎn)原則。
Qingcai類:
- public class Qingcai {
- public void run(){
- System.out.println("買到了青菜");
- }
- }
People類:
- public class People {
- public void bug(Qingcai qingcai){
- qingcai.run();
- }
- }
test類:
- public class test {
- public static void main(String[] args){
- People people=new People();
- people.bug(new Qingcai());
- }
- }
運(yùn)行結(jié)果:
b.提出問題,思路轉(zhuǎn)變(重點(diǎn))
上述看著沒啥問題,但是如果他不想買青菜,想買蘿卜怎么辦?我們當(dāng)然可以新建一個蘿卜類,再給他弄一個run方法,但是問題是People并沒有操作蘿卜類的方法,我們還需要在People添加對蘿卜類的依賴。這樣代碼要修改的代碼量太多了,模塊與模塊之間的耦合性太高,只要需要稍微有點(diǎn)變化,就要大面積重構(gòu),所以該設(shè)計不合理,我們看下其類圖,如下:
這種設(shè)計是一般設(shè)計的思考方式,而依賴倒轉(zhuǎn)原則中的倒轉(zhuǎn)是指和平常的思考方式完全相反,先從底部開始,即先從Qingcai和Luobo開始,然后想是否能抽象出什么。很明顯,他們都是蔬菜,然后我們再回頭重新思考如何來設(shè)計,新的設(shè)計圖如下:(請原諒我手殘黨,畫圖都畫不好。。。)
我們可以看到將低層的類抽象出一個接口Shucai,其直接和高層進(jìn)行交互,而低層的一些類則不參與,這樣能降低代碼的耦合性,提高穩(wěn)定性。
c.已使用依賴倒轉(zhuǎn)原則
思路有了,那就來代碼耍耍把。
Shucai類:
- public interface Shucai {
- public void run();
- }
Qingcai類:
- public class Qingcai implements Shucai{
- public void run(){
- System.out.println("買到了青菜");
- }
- }
Luobo類:
- public class Luobo implements Shucai {
- @Override
- public void run() {
- System.out.println("買到了蘿卜");
- }
- }
People類:
- public class People {
- public void bug(Shucai shucai){
- shucai.run();
- }
- }
test類:
- public class test {
- public static void main(String[] args){
- People people=new People();
- people.bug(new Qingcai());
- people.bug(new Luobo());
- }
- }
運(yùn)行結(jié)果:
d.總結(jié)
該原則重點(diǎn)在“倒轉(zhuǎn)”,要從低層往上思考,盡量抽象抽象類和接口。此例子很好的解釋了“上層模塊不應(yīng)該依賴低層模塊,他們都應(yīng)該依賴于抽象”。在最開始的設(shè)計中,上層模塊依賴了低層模塊,調(diào)整后,上層模塊和低層模塊都依賴于接口Shucai,依賴關(guān)系從圖中可以看出來了“倒轉(zhuǎn)”。
4.里氏替換原則
a.繼承的優(yōu)缺點(diǎn)
里氏替換原則是1988年麻省理工姓李的女士提出,它是闡述了對繼承extends的一些看法。
繼承的優(yōu)點(diǎn):
- 提高代碼的重用性,子類也有父類的屬性和方法。
- 提高代碼的可擴(kuò)展性,子類有自己特有的方法。
繼承的缺點(diǎn):
當(dāng)父類發(fā)生改變的時候,要考慮子類的修改。
里氏替換原則是繼承的基礎(chǔ),只有當(dāng)子類替換父類時,軟件功能仍然不受到影響,才說明父類真正被復(fù)用啦。
a.使用里氏替換原則1
子類必須實(shí)現(xiàn)父類的抽象方法,但不得重寫(覆蓋)父類的非抽象(已實(shí)現(xiàn))方法。
反例
父類A:
- public class A {
- public void run(){
- System.out.println("父類執(zhí)行");
- }
- }
子類B:
- public class B extends A{
- public void run(){
- System.out.println("子類執(zhí)行");
- }
- }
測試類test:
- public class test {
- public static void main(String[] args) {
- A a = new A();
- a.run();
- System.out.println("將子類替換成父類:");
- B b = new B();
- b.run();
- }
- }
運(yùn)行結(jié)果:
注:我每次使用子類替換父類的時候,還要擔(dān)心這個子類有沒有可能導(dǎo)致問題。此處子類不能直接替換成父類,故沒有遵循里氏替換原則。
b.使用里氏替換原則2
子類中可以增加自己特有的方法
父類A:
- public class A {
- public void run(){
- System.out.println("父類執(zhí)行");
- }
- }
子類B:
- public class B extends A{
- public void runOwn(){
- System.out.println("子類執(zhí)行");
- }
- }
測試類test:
- public class test {
- public static void main(String[] args) {
- A a = new A();
- a.run();
- System.out.println("將子類替換成父類:");
- B b = new B();
- b.run();
- b.runOwn();
- }
- }
運(yùn)行結(jié)果:
注:父類A 有run方法,繼承父類A的子類B有runOwn方法,測試類test先是調(diào)用A類的run方法,接著用B類替換A類,發(fā)現(xiàn)還是執(zhí)行的是父類A的run方法,最后再調(diào)用子類B特有的方法runOwn方法。如上,說明該段代碼已使用了里氏替換原則。
c.使用里氏替換原則3
當(dāng)子類覆蓋或?qū)崿F(xiàn)父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松。
父類A:
- public class A {
- public void run(HashMap hashMap){
- System.out.println("父類執(zhí)行");
- }
- }
子類B :
- public class B extends A{
- public void run(Map map){
- System.out.println("子類執(zhí)行");
- }
- }
測試類test:
- public class test {
- public static void main(String[] args) {
- A a = new A();
- a.run(new HashMap());
- System.out.println("將子類替換成父類:");
- B b = new B();
- b.run(new HashMap());
- }
- }
運(yùn)行結(jié)果:
我們可以看到在測試類test中,將父類A替換成子類B的時候,還是顯示的執(zhí)行結(jié)果“父類執(zhí)行”,我們可以發(fā)現(xiàn)他并不是重寫,而是方法重載,因?yàn)閰?shù)不一樣,所以他其實(shí)是對繼承的規(guī)范化,為了更好的使用繼承。關(guān)于是否為方法重載或重寫,我們從下圖看:
如果是重寫,在上圖標(biāo)紅的位置會出現(xiàn)箭頭,我們可以看出是實(shí)際為重載。
那如果沒有使用這個規(guī)則,會是什么樣?看下面的代碼:
父類A:
- public class A {
- public void run(Map map){
- System.out.println("父類執(zhí)行");
- }
- }
子類B:
- public class B extends A{
- public void run(HashMap hashMap){
- System.out.println("子類執(zhí)行");
- }
- }
測試test:
- public class test {
- public static void main(String[] args) {
- A a = new A();
- a.run(new HashMap());
- System.out.println("將子類替換成父類:");
- B b = new B();
- b.run(new HashMap());
- }
- }
運(yùn)行結(jié)果:
我們可以看到將子類的范圍比父類大的時候,替換的子類還是執(zhí)行自己的子類方法。此不符合里氏替換原則。
d.總結(jié)
我們平常好像也沒有遵循這些里氏替換原則,程序還是正常跑。其實(shí)如果不遵循里氏替換原則,你寫的代碼出問題的幾率會大大增加。
5.開閉原則(重點(diǎn))
a.基本介紹
前面四個原則,單一職責(zé)原則,接口屏蔽原則,依賴倒轉(zhuǎn)原則,里氏替換原則可以說都是為了開閉原則做鋪墊,其是編程匯總最基礎(chǔ),最重要的設(shè)計原則,核心為對擴(kuò)展開發(fā),對修改關(guān)閉,簡單來說,通過擴(kuò)展軟件的行為來實(shí)現(xiàn)變化,而不是通過修改來實(shí)現(xiàn),盡量不修改代碼,而是擴(kuò)展代碼。
b.未使用開閉原則
接口transport:
- public interface transport {
- public void run();
- }
Bus:
- public class Bus implements transport {
- @Override
- public void run() {
- System.out.println("大巴在公路上跑");
- }
- }
當(dāng)我們修改需求,讓大巴也能有在水里開的屬性,我們可以對Bus類添加一個方法即可。但是這個已經(jīng)違背了開閉原則,如果業(yè)務(wù)復(fù)雜,這樣子的修改很容易出問題的。
c.已使用開閉原則
我們可以新增一個類,實(shí)現(xiàn)transport接口,并繼承Bus類,寫自己的需求即可。
- public class universalBus extends Bus implements transport {
- @Override
- public void run() {
- System.out.println("大巴既然在公路上開,又能在水里開");
- }
- }
6.迪米特原則
a.介紹
- 一個對象應(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)部。
把上面的概念一一翻譯成人話就是:
- 我們這個類姑娘啊,因?yàn)樘娉至瞬簧朴谏缃唬詫ζ渌惢锇閭儾辉趺词煜ぁ?/li>
- 類姑娘實(shí)在是太害羞了,一旦與別人多說幾句話就會緊張的不知所措,頻頻犯錯。
- 矜持的類姑娘盡管心思很活躍,愛多想。但是給別人的感覺都是純潔的像一張白紙。
- 因?yàn)轭惞媚锾^于矜持,害怕陌生人,認(rèn)為陌生人都是壞人,所以只與自己熟悉的朋友交流。
- 類姑娘熟悉的朋友有:成員變量,方法參數(shù),方法返回值的對象。而出現(xiàn)在其他地方的類都是陌生人,壞人!本姑娘拒絕與你交流!!!
哈哈,這樣應(yīng)該大家都能理解了。總而言之就一句話:一個類應(yīng)該盡量不要知道其他類太多的東西,不要和陌生的類有太多接觸。
b.未使用迪米特原則
總公司員工Employee類:
- public class Employee {
- private String id;
- public String getId() {
- return id;
- }
- public void setId(String id) {
- this.id = id;
- }
- }
分公司員工SubEmployee類:
- public class SubEmployee {
- private String id;
- public String getId() {
- return id;
- }
- public void setId(String id) {
- this.id = id;
- }
- }
總公司員工管理EmployeeManager類:
- public class EmployeeManager {
- public List<Employee> setValue(){
- List<Employee> employees=new ArrayList<Employee>();
- for(int i=0;i<10;i++){
- Employee employee=new Employee();
- employee.setId("總公司"+i);
- employees.add(employee);
- }
- return employees;
- }
- public void printAllEmployee(SubEmployeeManager sub){
- List<SubEmployee> list1 = sub.setValue();
- for(SubEmployee e:list1){
- System.out.println(e.getId());
- }
- List<Employee> list2 = this.setValue();
- for(Employee e:list2){
- System.out.println(e.getId());
- }
- }
- }
分公司員工管理SubEmployeeManager類:
- public class SubEmployeeManager {
- public List<SubEmployee> setValue(){
- List<SubEmployee> subEmployees=new ArrayList<SubEmployee>();
- for(int i=0;i<10;i++){
- SubEmployee subEmployee=new SubEmployee();
- subEmployee.setId("分公司"+i);
- subEmployees.add(subEmployee);
- }
- return subEmployees;
- }
- }
測試類:
- public class test {
- public static void main(String[] args){
- EmployeeManager employeeManager=new EmployeeManager();
- SubEmployeeManager subEmployeeManager=new SubEmployeeManager();
- employeeManager.printAllEmployee(subEmployeeManager);
- }
- }
運(yùn)行結(jié)果:
上面的代碼是正常運(yùn)行的,但是可以看到一個問題,EmployeeManager類的printAllEmployee方法中使用的局部變量SubEmployee是不符合迪米特法則的,其是陌生朋友,應(yīng)該拒絕溝通。
b.已使用迪米特原則
EmployeeManager類:
- public class EmployeeManager {
- public List<Employee> setValue() {
- List<Employee> employees = new ArrayList<Employee>();
- for (int i = 0; i < 10; i++) {
- Employee employee = new Employee();
- employee.setId("總公司" + i);
- employees.add(employee);
- }
- return employees;
- }
- public void printAllEmployee(SubEmployeeManager sub) {
- sub.printAllSubEmployee();
- List<Employee> list2 = this.setValue();
- for (Employee e : list2) {
- System.out.println(e.getId());
- }
- }
- }
SubEmployeeManager類:
- public class SubEmployeeManager {
- public List<SubEmployee> setValue(){
- List<SubEmployee> subEmployees=new ArrayList<SubEmployee>();
- for(int i=0;i<10;i++){
- SubEmployee subEmployee=new SubEmployee();
- subEmployee.setId("分公司"+i);
- subEmployees.add(subEmployee);
- }
- return subEmployees;
- }
- public void printAllSubEmployee(){
- List<SubEmployee> list1 = setValue();
- for(SubEmployee e:list1){
- System.out.println(e.getId());
- }
- }
- }
我們將EmployeeManager類printAllEmployee方法中的打印分公司的代碼移到了分公司的管理類SubEmployeeManager類中,再在方法中顯示的調(diào)用SubEmployeeManager類的方法,這符合迪米特法則的。
7.合成復(fù)用原則
盡量使用合成/集合,不要用繼承。
如果使用繼承,會使得耦合性加強(qiáng),盡量作為方法的輸入?yún)?shù)或類的成員變量,這樣可以避免耦合。
結(jié)語
所有的原則只是規(guī)范,為了代碼更加優(yōu)雅,為了讓人一目了然。如果一定不遵循原則,那代碼還是可以跑的,只是日后出bug的可能性提高。
以上,簡單來說,主要包括兩點(diǎn):
1.找出應(yīng)用中需要變化的獨(dú)立出來,不要和固定的混合在一起。
2.面向接口編程,而不是面向?qū)崿F(xiàn)編程。