本文最后更新于:4 个月前

前言

其实没有设计模式我们也能完成开发工作。但是为什么需要设计模式呢?让你看起来很牛,没错这个算一个。让你的代码层次感分明,可读性强而且容易维护。让你像我一样有更多的摸鱼划水时间。
可能有人说我一个类或者方法就干完的东西,你搞了七八个。当然使用设计模式也是要斟酌的。一些简单稳定的业务也不推荐使用设计模式。设计模式多用于复杂多变的业务或者要求适配性、扩展性更强的场景中。不要为了设计模式而设计模式。

接下来我们结合实际开探讨一下设计模式的一些原则。

1、开闭原则

public class Seller {
    public BigDecimal sellCar(Car car) {
        return car.getPrice();
    }
}

上面模拟4S店一个销售在卖车。突然老板搞了一个促销:在双十一要开展打折活动。在sellCar方法内增加一个计算可行吗?这势必影响整个业务,导致所有车都打折。不行不行!那么在Car里面操作?然后你改啊改!结果各种逻辑流程判断。才实现了业务要求。如果后续打折活动结束了或者升级了,你还要再进行各种改动。你发现一个打折让你的代码面目全非、臃肿不堪。上面说了对于复杂而多变的业务使用设计模式就可以解决。
那么设计模式最重要的一个原则就是开闭原则。也就是说一个软件模型实体如类、模块和函数应该对扩展开放,对修改关闭。也就是需要我们将业务行为抽象出来,使用抽象来构建。具体的业务通过抽象的实现来解决。那么我们就搞一个DiscountCar来extends Car.这样sellCar是什么具体的实现就执行什么具体的逻辑。不会影响以前的逻辑,而且不会因为改动原来的代码影响其他逻辑。保证接口可靠性和稳定性。如下:

public class DiscountCar extends Car{
       
   private BigDecimal price;
   private BigDecimal discount;
   @Override
   public BigDecimal getPrice() {
       return price.multiply(discount);
   }
}

2、依赖倒置原则

还拿上面的例子来说。经过一系列的打折活动4S店的生意蒸蒸日上。老板突然想扩展一下周边,同时压榨一下销售。让他们卖车的同时卖点玻璃水、防冻液之类的。这个需求当然又抛给了苦逼的程序员。sellCar太具体了不能满足需要了。很多情况下你会增加一个卖玻璃水、卖防冻液的方法。如果以后增加了卖大米,甚至买起了鸡蛋饼呢?总不能一直增加方法吧。我们需要考虑这种问题。我们可以抽象所有卖东西的场景。然后我们把卖的物品抽象成了一个抽象化的概念(java对应的是接口,把卖的行为抽象成了sell方法:

public interface Any {
String getName();
BigDecimal getPrice();
}
public class Seller {
public BigDecimal sell(Any any) {
return any.getPrice();
    }
}

这样随便老板以后卖什么你都可以通过该方法进行处理了,只需要关注于Any的实现。

3、职责单一原则

4S店销售卖了一段东西后,发现对客户的吸引力度不大。突然脑子比较灵活的老板又想起了电影中的一句台词:少林功夫加唱歌跳舞有没有搞头?对啊你们销售能不能搞搞什么唱、跳、Rap,当然打篮球就不要了别砸坏了车玻璃。但是人与人是不一样的,有的人只会唱,有的人只会跳,有的人可能唱跳Rap都会甚至篮球都很溜。所以为了适配这么多情况,我们必须把每种技能独立出来,根据不同的人来组合这些技能。

public class Seller implements Sing, Jump, Rap {
public BigDecimal sell(Any any) {
return any.doSell();
    }
@Override
public void sing() {
        System.out.println("seller sing ");
    }
@Override
public void jump() {
        System.out.println("seller jumping ");
    }
@Override
public void rap() {
        System.out.println("seller raping ");
    }
}

但是注意一定要适度,根据业务来细分。否则会导致接口过多反而增大开发难度以及代码的复杂度。

4、迪米特法则

新的销售方法搞了一段时间后,老板想看看检验一下他这个馊主意的效果。于是就叫了一个销售让他提供一份报表出来看看。那么程序员该如何实现老板查看报表功能呢,很可能有人会这么写:

public class Boss {
    private Seller seller;
    private Report report;
    public void read() {
        seller.apply(report);
    }
}

乍看功能实现了,细看会发现逻辑不对。哪里不对呢?老板已经持有了报表,如果老板已经知道了你的业绩还叫你干什么?这种逻辑肯定是不对的!也就是说Boss直接依赖了Report;而这个Report不应该直接由Boss处理,而应由Seller控制。

public class Boss {
    private Seller seller;
    
    public void read(){
         seller.apply();
    }   
}
public class Seller {
      private Report report;
      
      public void apply(){
         report.show();
      }
}

这种最大化隔离了类与类之间的关系。降低了类之间的耦合。迪米特法则因此又得名最少知道原则。

5、接口隔离原则

用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。一个类对一个类的依赖应该建立在最小的接口上。
建立单一接口,不要建立庞大臃肿的接口尽量细化接口,接口中的方法尽量少,尽量细化接口。注意适度原则,一定要适度。不能滥用
就像上面的唱跳 rap,分离是最好的。

6、里氏代换原则

这里主要针对类的继承关系而言。比较正式的定义:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2 时,程序P的行为没有发生变化,那么类型 S 是类型 T 的子类型。
在4S店老板眼里,只要新来的能在销售岗位上像销售老手一样卖出汽车,他就是一名合格的销售。感觉这种定义就像一句名言:不管你黑猫白猫,能抓老鼠的都是好猫。
从某种含义上里氏代换有着以下的契约:

  1. 子类必须完全实现父类的方法。父类出现的地方子类都可以代替父类。
  2. 子类可以有自己的个性定义。里氏替换原则 可以正着用,但是不能反过来用。在子类出现的地方,父类未必就可以胜任。子类一般比父类有个性。
  3. 覆盖或实现父类的方法时输入参数可以被放大。如果4S店老板规定基础车谈价的折扣最多九折,销售打个九五折没有问题,打八折老板肯定要跟你说道说道了。
  4. 覆写或实现父类的方法时输出结果可以被缩小。同样是15W本来只能卖出给客户一个乞丐版,结果换了个销售结果给出了一辆旗舰版。怕不是过不了试用期哦。

7、合成/复用原则

它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
欢迎大家关注我的公种浩【程序员追风】,文章都会在里面更新,整理的资料也会放在里面。

总结

这七种设计原则是软件设计模式必须尽量遵循的原则,各种原则要求的侧重点不同。其中,开闭原则是总纲,它告诉我们要对扩展开放,对修改关闭;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;单一职责原则告诉我们实现类要职责单一;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合度;合成复用原则告诉我们要优先使用组合或者聚合关系复用,少用继承关系复用。在实际开发中我们可以根据业务来进行设计模式的使用,但是很重要的一点千万不要被这些条条框框束缚了你的手脚。


 目录