本文章将会介绍我对一些设计原则的理解,包括:开闭原则
、里氏替换原则
、迪米特法则
、单一职责
、接口隔离
、合成复用
,依赖倒置
等进行讲解。
对扩展开放,对修改关闭
,这是对开闭原则的基本定义,这个原则存在的意义在于我们需要对一个类的功能进行扩展、增加方法的时候,不用对原本的类进行修改,而是通过继承,去重写,将父类原本方法的行为改造为我们现在阶段需要的行为;也可以通过实现和父类一样的借口来完成一样的效果。造成的结果就是,我们没有去修改原来的类(对修改关闭
),却实现了功能的扩展(对扩展开放
)。
有一个这样的场景,饭店里面有各式各样的菜品,为了好管理他们,我们定一个最顶层的接口来规范他们应该有什么属性。
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
而出现问题。
指设计代码结构时,高层模块不应该
依赖底层模块,二者都应该
依赖其抽象
。抽象不应该
依赖细节;细节应该
依赖抽象。
简单来说就是。
高层模块不应该依赖底层模块
:类不能和类直接关联起来,这样耦合性太强,不利于以后的升级和拓展。
二者都应该依赖其抽象
:他们应该都有一个共同的父类,抽象,接口是个很好的选择,因为具体行为还未实现,有着无限的可能性。
抽象不应该依赖细节
:作为定义的规范,我并不在意你到底会去如何实现我的规范,对你的实现细节不感兴趣。
细节应该依赖抽象
:作为实现的一方,必须严格按照指定的规范去完成接口和抽象类的前置参数
和后置返回值
。
还是用饭店举例子,我去饭店吃饭。我自己想吃打折的番茄炒蛋
还有甜点
。
那么这个我(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();
}