设计原则-开闭原则与依赖倒置

设计原则

本文章将会介绍我对一些设计原则的理解,包括:开闭原则里氏替换原则迪米特法则单一职责接口隔离合成复用依赖倒置等进行讲解。


开闭原则(Open-Closed Principle, OCP)

对扩展开放,对修改关闭,这是对开闭原则的基本定义,这个原则存在的意义在于我们需要对一个类的功能进行扩展、增加方法的时候,不用对原本的类进行修改,而是通过继承,去重写,将父类原本方法的行为改造为我们现在阶段需要的行为;也可以通过实现和父类一样的借口来完成一样的效果。造成的结果就是,我们没有去修改原来的类(对修改关闭),却实现了功能的扩展(对扩展开放)。

有一个这样的场景,饭店里面有各式各样的菜品,为了好管理他们,我们定一个最顶层的接口来规范他们应该有什么属性。

public interface IFood {
    Integer getId();
    String getName();
    Double getPrice();
}

当然,如果具体到某一道菜的时候,我们就需要去实现这个接口成为一个真正的类才行。

public class TomatoEggFood implements IFood{
    private Integer Id;
    private String name;
    private Double price;
    public TomatoEgg(Integer id, String name, Double price) {
        this.Id = id;
        this.name = name;
        this.price = price;
    }
    public Integer getId() {
    	return this.Id;
	}
    public String getName() {
		return this.name;
	}
	public Double getPrice() {
		return this.price;
	}

这样,我们的西红柿鸡蛋就定义好了,我们可以把这个看做是已经在我们系统中已经运行很顺畅的了,可是这个时候,饭店需要打折促销,原来TomatoEgg里面的getPrice方法,也就是这个获得原来价格的行为已经不再满足我们打折促销的需求了,根据开闭原则我们不能随便对这个进行修改,因为这个在系统上运行良好的类,如果因为我们的擅自修改而产生了问题,会是一件很麻烦的事情。

所以,我们通过继承这个TomatoEgg来重新修改他的获得价钱的行为。

public class TomatoEggDiscountFood extends TomatoEggFood {
    public JavaDiscountCourse(Integer id, String name, Double price) {
    	super(id, name, price);
    }
    public Double getOriginPrice(){//这里其实违背了里氏替换法则,后会讲解,这里留个坑
    	return super.getPrice();
    }
    public Double getPrice(){
    	return super.getPrice() * 0.8;//修改的行为
    }
}

这里,当我们想要使用打折后的TomatoEgg的时候,完全可以使用我们扩展TomatoEggDiscountFood来获得一个打折后的价钱,而不需要对着TomatoEgg进行再次的修改而避免系统其它地方因为使用了他的getPrice而出现问题。


依赖倒置原则(Dependence Inversion Principle,DIP)

指设计代码结构时,高层模块不应该依赖底层模块,二者都应该依赖其抽象。抽象不应该依赖细节;细节应该依赖抽象。

简单来说就是。

高层模块不应该依赖底层模块:类不能和类直接关联起来,这样耦合性太强,不利于以后的升级和拓展。

二者都应该依赖其抽象:他们应该都有一个共同的父类,抽象,接口是个很好的选择,因为具体行为还未实现,有着无限的可能性。

抽象不应该依赖细节:作为定义的规范,我并不在意你到底会去如何实现我的规范,对你的实现细节不感兴趣。

细节应该依赖抽象:作为实现的一方,必须严格按照指定的规范去完成接口和抽象类的前置参数和后置返回值

还是用饭店举例子,我去饭店吃饭。我自己想吃打折的番茄炒蛋还有甜点

那么这个我(Pop)的例子就可以简单写成这样。

public class Pop {
    public void eatTomatoEgg(){
        System.out.println("我吃番茄炒蛋");
    }
    public void eatCake(){
        System.out.println("我吃甜点");
    }
}

那么主函数里面,我要吃东西,就需要进行调用方法操作。

public static void main(String[] main){
    Pop pop = new Pop();
    pop.eatTomatoEgg();
    pop.eatCake();
}

以上,我完成了吃饭的动作,不过这个时候,我吃番茄炒蛋太腻了,我还想吃一碗米饭,这个时候我们需要怎么做呢,最简单的方法就是在Pop类新添加eatRice方法,接着再到主函数调用即可。可是凡事我们都需要站到更高的角度去思考问题,如果我是一个大吃货,我要吃的东西非常多,那么这个类中的方法会非常的冗长,并且还是那句话,这个类如果在系统上运行良好,你去修改这个类必定会伴随着相应的风险,而这些风险可能是我们无法承受的,所以这个架构就存在着问题,我们需要重新设计层级关系。

回到依赖倒置的原则的第一条:高层模块不应该依赖底层模块,高层模块指的是调用方,我们调用的时候非常舒服,因为吃东西这个业务逻辑已经被我们自己封装起来了,只需要调用一个方法就可以完成我们的吃东西动作,但是我们发现这是不利于我们拓展的,借用开闭原则这是对修改很不友好的,我们紧紧的将吃饭的人食物这两个概念紧紧耦合在一起,我们调用(高层模块)的时候太依赖一个调用方(底层模块)来帮我们完成操作,来导致这个类中所涉及的诸多业务解耦起来困哪。

那么如何解决,二者都应该依赖其抽象,现在我们来看看那些概念可以被抽象,首先食物依旧可以被抽象,那么我们可以这样做。

public interface IFood{
    void eat();
}

我们把食物单独抽象出来,不让他依赖与于食客

public class TomatoEgg implements IFood{
    @Override
    public void eat(){
        System.out.println("吃番茄炒蛋");
    }
}
public class Cake implements IFood{
    @Override
    public void eat(){
        System.out.println("吃蛋糕");
    }
}
public class Rice implements IFood{
    @Override
    public void eat(){
        System.out.println("吃饭饭");
    }
}

将对象的修改一下。

public class Pop{
    public void eat(IFood food){
        food.eat();
    }
}

现在主方法的调用就变成为了。

public static void main(String[] ars){
    Pop pop = new Pop();
    pop.eat(new TomatoEgg());
    pop.eat(new Rice());//扩展也变得方便,也不需要直接去修改底层类。
    pop.eat(new Cake());
}

以上,我们都在依赖IFood这个接口,只要我们一增加需求,只需要直接new给食客就可以。

抽象不应该依赖细节的解释可以得到更好的体现,对于IFood中对应的eat方法,并不关心你到底要吃什么,怎么吃,反正这是一个吃的动作,并不关心你的具体细节,话虽这么说,但是很多业务场景的抽象出来的规范很难拿捏,你并不确定这个你制定的规范是否适用于绝大部分场景,这个时候我们是否就需要抽出更高层的接口,又或者通过继承接口来产生适用性更强大的后代。

来说几个衍生的概念,依赖注入setter注入

其实这并不算是什么的新的概念,依赖注入我相信很多人第一个想到的就是spring中的DI

虽然后者的DI是通过IOC容器中存储的BeanDefintion信息来实例化并注入到相应声明的变量中去,但是我现在说的依赖注入没有spring那么复杂。

这个依赖注入我的理解就是实例化时必须依赖某个类,并且调用方法的时候依赖那个被注入类的具体实现。

我们将Pop改造一下。

public class Pop {
	private IFood food;
	public Pop(IFood food) {
		this.food = food;
	}
	public void eat(){
		food.eat();
	}
}
public static void main(String[] ars){
    Pop pop = new Pop(new Cake());
    pop.eat();
}

在构造方法中,我们必须传入IFood的一个具体实现,才允许你实例Pop ,接着我们的eat真正的实现也是依赖于我们传入具体实现细节的。

一眼看过去可能觉得这个依赖注入也没什么复杂的,甚至觉得有些多余,不过很多源码的设计,都有这种设计的影子,这样的设计我觉得还是为了隐藏,和封装实现的细节,而让调用者无须关心更多的内容,我只要传入了参数,调用就可以获得我需要的吃东西效果,即便传入的所依赖的东西各式各样,我也只需要调用一个简单的eat就可以让他自我发挥了。

接下来是setter注入,本质上其实还是一个东西,这个情况建立在Pop为单例的情况下,因为不能new出来,所以放入所依赖的模块,必须设置进去,于是就有了。

public class Pop {
	private IFood food;
	public void setFood(IFood food) {
		this.food = food;
	}
	public void eat(){
		food.eat();
	}
    private Pop(){}
    public static Pop getInstance(){
        return new Pop();
    }
}
public static void main(String[] ars){
    Pop pop = Pop.getInstance();
    pop.setFood(new Cake());
    pop.eat();
}

你可能感兴趣的:(设计模式,设计原则,开闭原则,依赖倒置)