设计模式之我见(一)--SOLID设计原则

前言

设计原则—-一个老生常谈却又常谈常新的话题。
唤作原则,即为实际编码、模式设计时的基本思想,理解在先,使用在后。流于字面的思想经不起推敲,融于实践才能为己所用。

开闭原则(Open Closed Principle)

都说“开闭原则是最基础的一个原则”,故将其放在首位,以期收提纲挈领之效。

Software entities like classes,modules and functions should be open for extension but closed for modification
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

按照惯例,一步一问
何为扩展
这个好理解,产品需求变幻莫测,随着功能迭代,上线已久的模块也常常面临改造,“扩展”无处不在。
举个简单的栗子
项目里已定义了页面类型A,现在需要新增一种页面类型B,都拥有更新图片和文字的功能。好,那现在应该怎么办?修改已经受过多轮检验的原有逻辑,兼容AB类型?还是新增一个B类?留给下问
何为开放
顾文思义,即为允许,允许扩展。奔着扩展的思路,由AB类共有逻辑抽象出接口IA

public interface IA {
    public void updateUI();//更新图片和整体样式
    public void changeTipNum(int number);//更新页面文字
}

再让类A实现该接口,覆写公共方法

public class A implements IA {
···
@Override
public void updateUI() {
···
}
@Override
public void changeTipNum(int number) {
···
}

如此一来,无论新增多少种页卡,只需实现IA接口,覆写公共方法完成特殊业务,即可满足需求。原有逻辑无需改动,新增需求完美实现。
何为关闭修改
如前文所述,“软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现变化”。实现新需求绝不能以推倒旧功能为代价,编码的高复用和可拓展便在于此。可能会认为,“只是改了一个判断影响不大”,“不过就加了个判断算不上推倒”······
然而首先,只要做了更改,测试同学必须全面回归,此为人力消耗。再者,如此牵一发而动全身,可见项目结构也处在不稳定的有毒环境中,隐患重重。可见开闭原则必须遵循。
开闭原则的重要性不妨从以下 几方面理解(引自《设计模式之禅》本篇引用皆源于此)

1.开闭原则对测试的影响
2.开闭原则可以提高复用性
3.开闭原则可以提高可维护性
4.面向对象开发的要求

开闭原则正是对代码人熟知的六字箴言—高内聚低耦合的理论应用。

单一职责原则(Single Responsebility Principle)

There should never be more than one reason for a class to change.
应该有且仅有一个原因引起类的变更

说完开闭原则,可以谈谈单一职责了。先看看上栗,回到问题开始的第一个假设

修改经过了多次检验的原有逻辑,兼容AB类型?

修改原有逻辑本身的弊病已经分析,这里我们看看,若要兼容,即在updateUI或别的方法中做判断,逻辑结构如下

public void updateUI() {
        if (type== A类) {
            ···
        } else if (type== B类) {
            ···
        }
}

需求确定后,写好提测,好像也没什么问题。那么B类若要进行修改呢,比如增加一个控件?B类的逻辑处理完,A类也要相应的隐藏或处理该控件。若增加更为复杂的逻辑呢,要知道计划永远赶不上变化。那······那就扯来扯去,一团乱麻了。
引一段《设》对单一职责原则优势的总结

1.类的复杂性降低,实现什么职责都有清晰明确的定义
2.可读性提高
3.可维护性提高
4.变更引起的风险降低

重看“单一职责”,针对的正是一类或一方法身兼多职的问题。
遵循“单一职责”,不仅代码更易读,逻辑更清晰,拓展性也更高,项目结构自然更为健壮。

里氏替换原则(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.
所有引用基类的地方必须能透明地使用其子类对象

简单说来就是,子类能够代替父类,父类无法替换子类,白马是马,马不一定是白马。

有此思想后可推导出以下四点

1.子类必须完全实现父类的方法
2.子类可以有自己的个性
3.覆盖或实现父类的方法时输入参数可以被放大
4.覆盖或实现父类的方法时输出结果可以被缩小

看完定义,习惯性反问
子类没有完全实现父类的方法又如何
假设有如下定义。父类A与子类B、C,B类中实现A类的a、b方法,C类仅实现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;
    }
}

根据里氏替换原则,(一个凡是)凡是父类出现的地方子类皆可出现,于是设想如下编程场景

public String getParam(A object) {
        ···(判空 边界检查blabla)
        object.a();
        int retInt = object.b();
        ··· (根据retInt得到param)···
        return param;
}

很显然,参数object可以为任意A的子类,而param的值跟b方法的执行结果息息相关。那么如果传入的是没有覆写和重载b方法的类C,则默认返回0。听起来好像怪怪的?对!这意味着getParam逻辑中必须对返回0的情况甚或需要判断传入对象的类型,牵涉的逻辑越多,调整的地方就更多。
子类发挥个性有何限制?
如果子类过分个性,不但父类无法替代子类(里氏替换原则反过头理解),别的子类也无法替代这个个性十足的子类。业务逻辑一旦变得复杂,弊端很容易显现。毕竟程序员需要目光长远,着眼大局。

如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。

子类和父类的参数界定依据什么原则?
第三点和第四点的通俗理解就是,因为向下转型有危险,所有要想让子类替换父类,则替换时接受方的参数类型必须宽于待接入方。

迪米特法则(Law of Demeter)

Only talk to your immediate friends
只与直接的朋友通信

迪米特法则的要求总结为如下四点:

1.只和朋友交流
2.朋友之间也是有距离的
3.是自己的就是自己的
4.谨慎使用Serializable

理解迪米特,谨记高内聚,低耦合(又是开闭,又是开闭),以上四点要求都为了这共同目标服务。这里稍作解释。
其一,避免A爱BC,B又爱C之类剪不断理还乱的情况(此爱为广义之爱,切莫误会[无辜脸])。方法实现不提倡“博爱”。
其二,避免A为X戴帽,B为X戴领结,C为X穿上衣,D为穿裤…之类繁琐冗杂的理事步骤,可以想见一旦其中有一个环节出了问题解决起来有多麻烦(比如戴领结的跟穿上衣的打架了…)
搬运几段简约版代码以作补充
问题代码1:

public class Teacher { 
public void commond(GroupLeader groupLeader){ //老师对学生发布命令,清一下女生 
    List<Girl> listGirls = new ArrayList(); 
    ···//初始化女生 ···
    groupLeader.countGirls(listGirls); //告诉体育委员开始执行清查任务 
} 
} 
public class GroupLeader { 
public void countGirls(List<Girl> listGirls){ //有清查女生的工作 
        ···//清查女生···  
} 
} 

嗯,很显然犯了“其一”所指的问题,Teacher 类怎么还依赖了Girl,“初始化女生”必须换个地方。
修正问题代码1如下:

public class Teacher { 
public void commond(GroupLeader groupLeader){     
    groupLeader.countGirls(listGirls); //告诉体育委员开始执行清查任务 
} 
} 
public class GroupLeader { 
public void countGirls(){ 
    List<Girl> listGirls = new ArrayList<Girl>();     
    ···//初始化女生 ···
    ···//清查女生···  
} 
} 

这样逻辑线条才够直嘛。
问题代码2:

public class Wizard { 
public int first(){ //第一步  
    ···
} 
public int second(){ //第二步 
    ···
} 
public int third(){ //第三个方法 
    ···
} 
}
public class InstallSoftware { 
public void installWizard(Wizard wizard){ 
    int first = wizard.first();   
    if(first>50){ //根据first返回的结果,看是否需要执行second 
        int second = wizard.second(); 
        if(second>50){ 
            int third = wizard.third(); 
                if(third >50){ 
                    wizard.first(); 
                }  
        }  
    } 
} 
}

很明显,又犯了“其二”描述的问题,耦合关系如此紧密,岂不是牵一动百?
修改问题2如下:

public class Wizard { 
private Random rand = new Random(System.currentTimeMillis()); 
private int first(){ //第一步 
    ···
} 
private int second(){ //第二步 
    ···
} 
private int third(){ //第三个方法 
    ···
} 
//软件安装过程   
public void installWizard(){     
    int first = this.first();   
    //根据first返回的结果,看是否需要执行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(); //不废话,直接调用     
} 
} 

好嘛,出什么事直接找installWizard(),妙,真是妙。

迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。但是解耦是有限度的,除非是计算机的最小符号二进制的 0 和1,那才是完全解耦,我们在实际的项目中时,需要适度的考虑这个法则,别为了套用法则而做项目,法则只是一个参考,你跳出了这个法则,也不会有人判你刑,项目也未必会失败,这就需要大家使用的是考虑如何度量法则了。

接口隔离原则(Interface Segregation Priciple)

Client should not be forced to depend upon interfaces that they don’t use.
客户端不应该依赖它不需要的接口
The dependency of one class to another one should depend on the smallest possible interface.
类间的依赖关系应该建立在最小的接口上

引一段对其含义的精辟总结

1.接口要尽量小
2.接口要高内聚
3.定制服务
4.接口的设计是有限度的

在逐条分析之前,再回忆一遍,“开闭原则是最基础的一个原则”。可以想见,这四点含义的根本目的就是让接口足够灵活,可维护性足够高,以实现“对扩展开放,对修改关闭”。
解释了半天,不如直接搬运两段代码,孰优孰劣自有分辨

//美女实现类
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 + "---有好气质");
   }
}
//抽象星探类
public abstract class AbstractSearcher{
    protected IPettyGirl pettyGirl;
    public AbstractSearcher(IPettyGirl pettyGirl){
       this.pettyGirl=pettyGirl;
    }    
    public abstract void show();//显示美女信息
}
//星探具体实现类
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();//显示好气质
   }
}
//实现找美女过程
public class Client{
   public static void main(Strings[] args){      
      IPettyGirl xiaoHong = new PettyGirl("小红");//定义一个美女
      AbstractSearcher searcher = new Searcher(xiaoHong );
      searcher.show();
   }
}

乍一看好像没啥问题,细想想,“找美女”这个标准似乎还能细分,所谓接口还不够“单纯”不够“小”
修改后代码如下:

//两种类型的美女定义
public interface IGoodBodyGirl{    
    public void goodLooking();//要有好的面孔    
    public void niceFigure();//要有好身材
}
public interface IGoodTemperamentGirl{    
    public void goodTemperament();//要有好气质
}
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 + "---有好气质");
   }
}

看完小栗子,好像对“接口隔离”这个抽象概念有那么点抽象了解了。编程还需靠实践,经验积累是王道,谨记“高内聚,低耦合,接口要小还要纯”,运用到实际编码中,定会收获奇效。

依赖倒置原则(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.高层模块不应该依赖低层模块,两者都应该依赖其抽象
2.抽象不应该依赖细节
3.细节应该依赖抽象

依赖倒置原则在Java语言中的表现就是:

1.模块间的依赖通过抽象发生,实现类之间不发生直接的 依赖关系,其依赖关系是通过接口或抽象类产生的;
2.接口或抽象类不依赖于实现类
3.实现类依赖接口或抽象类

简而言之就是“面向接口编程(OOD)”—-接口只跟接口交流,实现类通过依赖接口或抽象类实现扩展。
依赖倒置和其他五条原则一脉相承,让代码结构更包容,“拥抱变化”。其实,即便不知道这条规则,我们也更倾向写出符合依赖倒置的程序。还是举一个简单的栗子
没有接口时这样实现功能

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类间的紧耦合呼之欲出。这时候,若需要a方法对C类对象作同样的操作,要怎么写呢?

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{
    ···
}

这么一来,每次扩展,只需要增加一个实现类,灵不灵巧机不机智
引总结依赖的三种写法

1.构造函数传递依赖对象
2.Setter方法传递依赖对象
3.接口声明依赖对象

还是那句话,没必要为了原则而原则,毕竟过犹不及。再次默念“对扩展开发,对修改封闭”,“高内聚,低耦合”,抓住根本原则才是编码之本。

最后

编码这事儿,虽立意创新,仍是规则之治,优秀的结构设计能助我们应对变化时游刃有余。当然,理论归理论,应用到实际问题中还需具体问题具体分析,不宥于形,融会贯通。
另外,关于写代码,浑浑噩噩地写叫搬砖,条理清晰地写是技术。我们当然希望随着日复一日辛勤coding,编码技能不断增长,综合素养逐渐深厚,以对得起所付出精力与汗水的成长效率不断逼近技术圣殿。这些理论原则如同内功心法,武侠世界里,凡欲为大师,苦练内功势在必行。
进击吧程序员~coding不辍,祝我们的代码清晰灵动,坚不可摧,真正SOLID. 老哥,稳!
学艺尚浅,欢迎各路大佬多多指正

参考书目:秦小波《设计模式之禅第2版》

    原文作者:苹果tree
    原文地址: https://www.jianshu.com/p/9cfcdd719e0f
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞