单一职责原则

简介

单一职责原则(SRP:Single responsibility principle)又称单一功能原则,面向对象五个基本原则(SOLID)之一。它规定一个类应该只有一个发生变化的原因。该原则由罗伯特·C·马丁(Robert C. Martin)于《敏捷软件开发:原则、模式与实践》一书中给出的。马丁表示此原则是基于汤姆·狄马克(Tom DeMarco)和Meilir Page-Jones的著作中的内聚性原则发展出的。
所谓职责是指类变化的原因。如果一个类有多于一个的动机被改变,那么这个类就具有多于一个的职责。而单一职责原则就是指一个类或者模块应该有且只有一个改变的原因。

概括来说,就是一次行为只拥有一种功能。

例子

比如女朋友叫你买菜,那你就去买菜,但是你看到有垃圾就顺手拿了想在路上丢了。

反例

如果不遵循单一职责原则的话,买菜的功能里也加上倒垃圾。

  1. public class Action {
  2. public void maiCai() {
  3. // 执行买菜
  4. // 执行倒垃圾
  5. }
  6. }
  7. public static void main(String[] args) {
  8. // 执行买菜跟倒垃圾
  9. Action a = new Action();
  10. a.maiCai();
  11. }

正例

买菜跟倒垃圾区分开来,以后如果又加上一个买水果的话,再加个买水果即可,不需要变动无关的方法,这样使得整个业务逻辑也清晰明了。

public class Action {
    public void  maiCai() {
        // 执行买菜
    } 
    public void  daoLaJi() {
        // 执行倒垃圾
    }
    // 可继续添加买水果等等...
}
public static void main(String[] args) {
    // 执行买菜跟倒垃圾
    Action a = new Action();
    a.maiCai();
    a.daoLaJi();
    // 可继续添加买水果等等...
}

里氏替换原则

简介

里氏替换原则(Liskov Substitution Principle,LSP)由麻省理工学院计算机科学实验室的里斯科夫(Liskov)女士在 1987 年的“面向对象技术的高峰会议”(OOPSLA)上发表的一篇文章《数据抽象和层次》(Data Abstraction and Hierarchy)里提出来的,她提出:继承必须确保超类所拥有的性质在子类中仍然成立(Inheritance should ensure that any property proved about supertype objects also holds for subtype objects)。

概括来说,就是子类不能改写父类的任何方法。

例子

比如你家族是一直做厨师的,你父亲发明了红烧茄子,挂牌对外售卖。后来他把这道菜传授给你了,你无意间发现加上肉末又是另一种风味。

反例

你在客人点红烧茄子时,你私自加上了肉末。这样的话,父辈原滋原味的红烧茄子就没了。

public class FatherMenu {

    public void hongshaoqiezi() {
        // 红烧茄子
    }
}

public class SonMenu extend FatherMenu {

    @Override
    public void hongshaoqiezi() {
        // 红烧茄子肉末
    }

}

public static void main(String[] args) {
    // 客人点红烧茄子
    SonMenu s = new SonMenu();
    // 儿子做了红烧茄子肉末给客人
    s.hongshaoqiezi();
}

正例

既然是儿子发现加了肉末又是另一种风味,那这个应该算是新的一道菜,而不应该改父辈的菜谱。以后如果发明了红烧鱼香茄子等等,都是一道新菜。
这么做的话,就完全保留了父类的信息,业务逻辑也会清晰明了。

public class FatherMenu {

    public void hongshaoqiezi() {
        // 红烧茄子
    }
}

public class SonMenu extend FatherMenu {

    public void hongshaoqieziroumo() {
        // 红烧茄子肉末
    }

    // 以后有新菜都可以加,但不会去改动父类的方法
    public void hongshaoyuxiangqiezi() {
        // 红烧鱼香茄子
    }

}

public static void main(String[] args) {

    SonMenu s = new SonMenu();
    // 客人点红烧茄子,儿子做了红烧茄子给客人
    s.hongshaoqiezi();
    // 客人点红烧茄子肉末,儿子做了红烧茄子肉末给客人
    s.hongshaoyuxiangqiezi();

}

依赖倒置原则

简介

  • 高级模块不应当依赖于低级模块。它们都应当依赖于抽象。
  • 抽象不应当依赖于实现,实现应当依赖于抽象。

常提到的面向接口 编程(OOP),就是基于这个原则。所以接口是关键的因素。
为什么叫倒置呢?举个小例子,比如一个需求是A调用B,B调用C1、C2,C1、C2是同类型细节不同对象,所以流程应该是 A->B->C1、C2。现在加一个接口类aC,然后C1、C2都去继承aC,aC具备同类型的功能,那么流程就变成了 A->B->aC<-C1、C2,可以看到流程中C1、C2的箭头反过来了,这就是倒置。

例子

奶茶店卖奶茶,有珍珠奶茶,有葡萄味奶茶。

反例

如果不按照依赖倒置原则,则新增一个口味的话,Shop就要进行装修,新增一个sell来对应新的口味。

public class Shop {


    public void sell(Zhenzhu zhenzhu) {
        // 制作珍珠奶茶
        zhenzhu.make();
    }
     public void sell(Putaowei putaowei) {
        // 制作葡萄味奶茶
         putaowei.make();
    }
    // ... 后续有芒果味的再加。
}

public class Zhenzhu {
    public void make(){
        // 制作珍珠奶茶
    }

}

public class Putaowei {
    public void make() {
        // 制作葡萄味奶茶
    }

}

public static void main(String[] args) {

    Shop s = new Shop();
    // 卖珍珠奶茶
    s.sell(new Zhenzhu());
    // 卖葡萄味奶茶
    s.sell(new Putaowei());

   // .. 如果还想卖芒果味的话,就得动Shop、新增芒果味

}

正例

如果我们把制作奶茶作为一个接口的话,这样每次新加口味就不用改动Shop,只需在使用时跟店员说我要哪种口味即可。

public class Shop {


    public void sell(Kouwei kouwei) {
        // 制作某种口味奶茶
        kouwei.make();
    }
}

public interface Kouwei {

    public void make();    
}


public class Zhenzhu implements Kouwei {
    public void make(){
        // 制作珍珠奶茶
    }

}

public class Putaowei implements Kouwei {
    public void make() {
        // 制作葡萄味奶茶
    }

}

public static void main(String[] args) {public static void main(args String[]) {
    Shop s = new Shop();
    // 卖珍珠奶茶
    s.sell(new Zhenzhu());
    // 卖葡萄味奶茶
    s.sell(new Putaowei());

   // .. 如果还想卖芒果味的话,不需要动Shop、只需要新增芒果味的class即可

}

接口隔离原则

简介

接口隔离原则(Interface Segregation Principle,ISP)要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。
2002 年罗伯特·C.马丁给“接口隔离原则”的定义是:客户端不应该被迫依赖于它不使用的方法(Clients should not be forced to depend on methods they do not use)。该原则还有另外一个定义:一个类对另一个类的依赖应该建立在最小的接口上(The dependency of one class to another one should depend on the smallest possible interface)。

概括来说:

  • 接口尽量定义得更小,更具体。
  • 对象不应该继承不需要的接口。

    例子

    在一个公司里,可以干的事情很多,比如发薪资、辞退员工、工作、划水、收薪资…等等。

    反例

    我把公司的这些行为都写到一个接口里,然后老板跟员工都继承接口,只要分别实现自己需要的方法即可。 ```java public interface CompanyAction { // 发薪资 public void faxinzi(); // 辞退员工 public void cituiyuangong(); // 工作 public void gongzuo(); // 划水 public void huashui(); // 收薪资 public void shouxinzi(); }

public class Boss implements CompanyAction {

// 发薪资
 @Override
public void faxinzi(){
    // 执行发薪资逻辑
}

 // 辞退员工
 @Override
public void cituiyuangong(){
    // 执行辞退员工逻辑
}

 // 工作
 @Override
public void gongzuo(){
    //  老板不需要工作,所以不需要写逻辑
}

 // 划水
 @Override
public void huashui(){
    //  老板不需要划水,所以不需要写逻辑
}

 // 收薪资
 @Override
public void shouxinzi(){
    //  老板不需要收薪资,所以不需要写逻辑
}

}

public class Staff implements CompanyAction {

// 发薪资
 @Override
public void faxinzi(){
    //  员工没权利发薪资,所以不需要写逻辑
}

 // 辞退员工
 @Override
public void cituiyuangong(){
    //  员工没权利辞退员工,所以不需要写逻辑
}

 // 工作
 @Override
public void gongzuo(){
    //  执行工作逻辑
}

 // 划水
 @Override
public void huashui(){
    //  执行划水逻辑
}

 // 收薪资
 @Override
public void shouxinzi(){
    //  执行收薪资逻辑
}

}

public static void main(String[] args) { // 老板的活 CompanyAction boss = new Boss(); boss.fagongzi(); boss.cituiyuangong(); // 员工的活 CompanyAction staff = new Staff(); staff.gongzuo(); … }


<a name="aQSVe"></a>
### 正例
从反例代码看出,虽然老板跟员工不需要去做某些事情,但是依然要去实现它们的空方法,就显得很笨。所以这里就把这些职责拆分出来,单独做成2个不同的接口,老板跟员工各一个,这样的话,业务逻辑就清晰很多。
```java
// 老板可以做的事情的接口
public interface CompanyBossAction {
    // 发薪资
    public void faxinzi();
     // 辞退员工
    public void cituiyuangong();
}

// 员工可以做的事情的接口
public interface CompanyStaffAction {
     // 工作
    public void gongzuo();
     // 划水
    public void huashui();
     // 收薪资
    public void shouxinzi();
}

public class Boss implements CompanyBossAction {

    // 发薪资
     @Override
    public void faxinzi(){
        // 执行发薪资逻辑
    }

     // 辞退员工
     @Override
    public void cituiyuangong(){
        // 执行辞退员工逻辑
    }
}

public class Staff implements CompanyStaffAction {
     // 工作
     @Override
    public void gongzuo(){
        //  执行工作逻辑
    }

     // 划水
     @Override
    public void huashui(){
        //  执行划水逻辑
    }

     // 收薪资
     @Override
    public void shouxinzi(){
        //  执行收薪资逻辑
    }

}

public static void main(String[] args) {
   // 老板的活
    CompanyBossAction boss = new Boss();
    boss.fagongzi();
     boss.cituiyuangong();
    // 员工的活
    CompanyStaffAction staff = new Staff();
    staff.gongzuo();
    ...
}

迪米特法则

简介

迪米特法则(Law of Demeter,LoD)又叫作最少知识原则(Least Knowledge Principle,LKP),产生于 1987 年美国东北大学(Northeastern University)的一个名为迪米特(Demeter)的研究项目,由伊恩·荷兰(Ian Holland)提出,被 UML 创始者之一的布奇(Booch)普及,后来又因为在经典著作《程序员修炼之道》(The Pragmatic Programmer)提及而广为人知。

迪米特法则的定义是:只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。

概括来说:
一个对象只会依赖有直接关系的对象。

例子

我有一个朋友A,在一次相见过程中,我发现A居然有个漂亮的女性朋友B,于是我想跟B交流。不遵循迪米特法则的话,我是可以直接跟B聊天的。

反例


public class Me {
    private B b = new B();

    // 跟b聊天
    public void talkWithB() {
        // B说话了
        b.talk(); 
    } 
}

public class B {
    // 说话
    public void talk() {
    } 
}

public static void main(String[] args) {
    Me me = new Me();
    me.talkWithB()
}

正例

一看反例就离谱,我不认识B,我怎么可能直接跟B进行聊天呢,B还不当我变态么。所以,我应该是通过A的介绍,才能跟B搭的上话。这样的话,业务逻辑才说的过去。

public class Me {
    private A a = new A();

    // 跟b聊天
    public void talkWithB() {
        // 先通过A认识B
        B b = a.jieshaoB();
        // B说话了
        b.talk(); 
    } 
}

public class A {
    private B b = new B();
    // 介绍A
    public B jieshaoB() {
        return b;
    } 
}


public class B {
    // 说话
    public void talk() {
    } 
}

public static void main(String[] args) {
    Me me = new Me();
    me.talkWithB()
}

开闭原则

简介

开闭原则(Open Closed Principle,OCP)由勃兰特·梅耶(Bertrand Meyer)提出,他在 1988 年的著作《面向对象软件构造》(Object Oriented Software Construction)中提出:软件实体应当对扩展开放,对修改关闭(Software entities should be open for extension,but closed for modification),这就是开闭原则的经典定义。

这个原则很重要的2点:

  • 对扩展开放:用户在使用功能时,可以基于原功能进行扩展,即使是新加功能也可以。
  • 对修改关闭:用户可以使用功能,但是不能对原功能进行修改。

    例子

    上面说的非常绕口,要举例子才行。小红是一个手办收藏家,每次双11都忍不住割手买个一两个手办,这样家里的手办就越来越多了,于是他给每个手办都起了一个名字,这样就更有辨识度了。但随着手办的增加,家里快装不下了,于是小红只能准备把一些挂在网上卖掉,于是他又给手办加了一个原价的属性,在售卖时第三方通过这个价格来决定卖价。

    反例

    一开始手办的属性只有名字一个,后面要售卖了,于是便又加上了一个原价的属性。 ```java public class Shouban { // 名字 private String name; // 原价 private Integer price;

    public String getName() {

    }

    public Integer getSellPrice(){

      // 根据原价处理售价
    

    } }

public static void main(String[] args) { Shouban shouban = new Shouban(); // 获取售价 shouban.getSellPrice(); }

后来小红发现,在不同的第三方渠道,它们的定价是不一样的。于是只能通过修改getSellPrice的逻辑来针对不同渠道获取售价。

```java
public class Shouban {
    // 名字
    private String name;
    // 原价
    private Integer price;

    public String getName() {

    }

    public Integer getSellPrice(String name){
        // 根据渠道来获取售价
        if (name.equals("1")) {
            // 渠道1的售价   
        } else if (name.equals("2")) {
            // 渠道2的售价
        }
        // 如果有渠道3、4...,就无限else if下去
    }
}

public static void main(String[] args) {
    Shouban shouban = new Shouban();
    // 获取渠道1售价
    shouban.getSellPrice("1");
    // 获取渠道2售价
    shouban.getSellPrice("2");
}

正例

反例是分来2种来举例的,第一个是对扩展开放。本来作为一个手办收藏家,其手办的属性包含名字是理所当然,而后面加了价格是无奈之举,所以价格不应该是手办的原始属性才对。要分开新建个对象作为子类,这样就是扩展开放了,无论小红以后做了什么新的操作,都是基于原始手办的基础上去扩展,而无需改动原始手办的属性。

public class Shouban {
    // 名字
    private String name;

    public String getName() {

    }
}

public class SellShouban {
      // 原价
    private Integer price;

    public Integer getSellPrice(){
        // 根据原价处理售价
    }
}

public static void main(String[] args) {
    SellShouban shouban = new SellShouban();
    // 获取售价
    shouban.getSellPrice();
}

第二个就是对修改关闭,在上面的反例中,每挂到一个渠道进行售卖的话,都需要对getSellPrice进行修改,这样就违反原则了,正确的应该是给每个渠道都设定新的方法。这样在新增渠道时也无需改动getSellPrice。这么做的好处也防止了在新增功能时改动旧逻辑导致旧功能出错的风险。

public class Shouban {
    // 名字
    private String name;
    // 原价
    private Integer price;

    public String getName() {

    }

    public Integer get1SellPrice(){
            // 渠道1的售价   
    }

     public Integer get2SellPrice(String name){
            // 渠道2的售价
    }
     // 如果有渠道3、4...,就新增getXxSellPrice
}

public static void main(String[] args) {
    Shouban shouban = new Shouban();
    // 获取渠道1售价
    shouban.get1SellPrice();
    // 获取渠道2售价
    shouban.get2SellPrice();
}