0、单一职责原则(Single Responsibility Principle)
单一职责原则很简单,每个类都只有一种职责,一个类应该只有一个被修改的理由。这是常识,几乎所有的程序员都遵循这个原则。
优点:减少类之间的耦合,提高可读性,增加可维护性和可扩展性,降低可变性风险。
1、开闭原则(Open Close Principle)
开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。
优点:单一原则告诉我们,每个类都有自己的职责,里氏替代原则不能破坏继承制度。
2、里氏代换原则(Liskov Substitution Principle)
里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
优点:增加程序的健壮性,即使增加了子类,原来的子类也能继续运行,互不影响。
3、依赖倒转原则(Dependence Inversion Principle)
这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。
优点:可以减少需求变更带来的工作量,并行开发更加友好。
4、接口隔离原则(Interface Segregation Principle)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。
优点:提高程序的灵活性,提高内聚性,减少外部交互,让最小的接口做最多的事情。
5、迪米特法则,又称最少知道原则(Demeter Principle)
最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
优点:低耦合,高内聚。
6、合成复用原则(Composite Reuse Principle)
合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。
优点:继承复用会破坏系统的封装性,合成复用原则维持了类的封装性,新旧类之间的耦合度低,复用的灵活性高。
定义:抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。
小明开宝马车上路:
正常的思维:
public class BMW{
public void run(){
System.out.println("宝马车开动了....");
}
}
public class Person{
public void drive(BMW bmw){
System.out.println("小明开始开车...");
bmw.run();
}
}
public class Client{
public static void main(String[] args) {
new Person().drive(new BMW());
}
}
但是现在更改需求了,小明嫌弃宝马车不好,现在想开法拉利。那么如果在这个类的基础上更改,我们需要给司机提供一个drive(Ferrari ferrari)的方法。同时再提供一个Ferrari类,提供一个run方法。那么就存在类之间的依赖性太大,不利于类的拓展。
public interface IDriver {
//构造函数传递依赖
public void drive();
}
public interface ICar {
public void run();
}
public class BMW implements ICar {
@Override
public void run() {
System.out.println("开着宝马上路!");
}
}
public class Ferrari implements ICar {
@Override
public void run() {
System.out.println("开着法拉利上路!");
}
}
public class Person implements IDriver {
//构造函数传递依赖
private ICar car;
public Person(ICar car){
this.car = car;
}
@Override
public void drive() {
System.out.println("小明开始开车。。。");
car.run();
}
}
public class Client {
public static void main(String[] args) {
//构造函数传递依赖
ICar car = new BMW();
IDriver driver = new Person(car);
driver.drive();
ICar car1 = new Ferrari();
IDriver driver = new Person(car1);
driver.drive();
}
public interface IDriver {
//setter方法传递依赖
public void drive();
public void setCar(ICar car);
}
public interface ICar {
public void run();
}
public class BMW implements ICar {
@Override
public void run() {
System.out.println("开着宝马上路!");
}
}
public class Ferrari implements ICar {
@Override
public void run() {
System.out.println("开着法拉利上路!");
}
}
public class Person implements IDriver {
//setter方法传递依赖
private ICar car;
@Override
public void setCar(ICar car) {
this.car = car;
}
@Override
public void drive() {
System.out.println("小明开始开车。。。");
car.run();
}
}
public class Client {
public static void main(String[] args) {
//setter方法传递依赖
ICar car = new Ferrari();
IDriver driver = new Person();
driver.setCar(car);
driver.drive();
ICar car2 = new BMW();
IDriver driver2 = new Person();
driver.setCar(car2);
driver.drive();
}
public interface IDriver {
//接口传递依赖
public void drive(ICar car);
}
public interface ICar {
public void run();
}
public class BMW implements ICar {
@Override
public void run() {
System.out.println("开着宝马上路!");
}
}
public class Ferrari implements ICar {
@Override
public void run() {
System.out.println("开着法拉利上路!");
}
}
public class Person implements IDriver {
//接口传递依赖
@Override
public void drive(ICar car){
System.out.println("小明开始开车。。。");
car.run();
}
}
public class Client {
public static void main(String[] args) {
//接口传递依赖
IDriver driver = new Person();
driver.drive(new BMW());
driver.drive(new Ferrari());
}
}
在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入) 和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过Setter方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。
依赖倒转首先要求类之间具有联系,将其联系抽象成接口或者抽象类,这样降低类之间的依赖。从而产生抽象类或接口之间的依赖,将具体事物时间的依赖转化成抽象接口的依赖。
在实际编程中,我们一般需要做到如下3点:
依赖倒转原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒转。
很多初学编程的小伙伴在编程时会发现,自己写的类总是频繁的用到(依赖)其他类,一旦被依赖的类需要修改,那么其他的类也统统都要修改一遍,让人感觉烦不胜烦。若是小型的程序也紧紧是觉得烦而已,可一旦是大型的工程,这种强耦合的程序一旦有某一个细节放生改变,那是砸电脑的心都有。
各个具体类之间发生了直接的依赖关系,使得这些类紧紧地耦合在了一起,从而降低了程序的稳定性、可维护性和可读性。要解决这个问题,我们可以用一些方法来将这些程序解耦,降低其耦合性。这种方法就是我即将要讲到的依赖倒置原则(Dependence Inversion Principle ,DIP)。
High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
也就是说,高层模块、低层模块、细节都应该依赖抽象。
依赖倒置原则在java中的三种含义:
这三种含义在java的精髓之一——面向接口编程中体现得淋漓尽致。
既然一开始就说到不用依赖倒置原则有多糟糕,那么下面我们就用一个简单的程序来证明一下。
不使用依赖倒置原则:
程序1
/**
* 大众汽车类
* @author 叶汉伟
*/
public class DaZhong {
public void run(){
System.out.println("开大众汽车");
}
}
/**
* 司机类
* @author 叶汉伟
*/
public class Driver {
public void drive(DaZhong daZhong){
daZhong.run();
}
}
public class Client {
public static void main(String[] args){
Driver Tom=new Driver();
DaZhong daZhong=new DaZhong();
Tom.drive(daZhong);
}
}
看上去没什么嘛,这不是好好的吗?好像也是哦。那么既然司机会开大众汽车,那应该会开宝马吧。我们让它开一下宝马试试。
先生产一辆宝马给他:
程序2
/**
* 宝马车类
* @author 叶汉伟
*/
public class BaoMa {
public void run(){
System.out.println("开宝马车");
}
}
当我们要让司机开宝马的时候,他确开不了,程序报错了。感情他考的是大众驾照,有宝马都开不了啊。
其实要让司机开宝马车也容易,把司机类开车方法的参数改一下就成了呗。的确可以。但又想想,现在知识多了个宝马车,如果我再多个奔驰、本田什么的,那不就是要经常改,大改特改?对于大型的项目来说,这是致命的。
使用依赖倒置原则
那么接下来,我们看一下使用依赖倒置原则有什么优势。
接口声明依赖对象
在接口处就声明了依赖的对象。如程序3中的司机接口IDriver,其方法drive()的形参是ICar类型的。那么我们可以说IDrive与ICar放生了依赖关系,这个依赖对象在接口处已经声明了。接口声明依赖的方法也叫接口注入。
程序3
/**
* 车子接口
* @author 叶汉伟
*/
public interface ICar {
public void run();
}
/**
* 大众汽车类
* @author 叶汉伟
*/
public class DaZhong implements ICar{
public void run(){
System.out.println("开大众汽车");
}
}
/**
* 宝马车类
* @author 叶汉伟
*/
public class BaoMa implements ICar{
public void run(){
System.out.println("开宝马车");
}
}
/**
* 司机接口
* @author 叶汉伟
*/
public interface IDriver {
public void drive(ICar car);
}
/**
* 司机类
* @author 叶汉伟
*/
public class Driver implements IDriver{
public void drive(ICar car){
car.run();
}
}
public class Client {
public static void main(String[] args){
IDriver Tom=new Driver();
//Tom开大众汽车
ICar daZhong=new DaZhong();
Tom.drive(daZhong);
//Tom开宝马
ICar baoMa=new BaoMa();
Tom.drive(baoMa);
}
}
看,现在不仅可以开大众,而且可以开宝马了。要还有什么本田、奔驰汽车,一个drive方法都能开,而开其他的车只需要修改一下客户端就可以了。
上面的程序在实现类之间不发生依赖关系,他们的依赖关系是在接口处发生的,这样就可以大大的降低了类之间的耦合。其实这里不仅用到了依赖倒置原则,还用到了里氏替换原则,从类Client中可以看出来。
对象的依赖关系可以通过三种方法来实现:
在接口处就声明了依赖的对象。如程序3中的司机接口IDriver,其方法drive()的形参是ICar类型的。那么我们可以说IDrive与ICar放生了依赖关系,这个依赖对象在接口处已经声明了。接口声明依赖的方法也叫接口注入。
在类中通过构造函数声明依赖对象。具体实现如下:
程序4
/**
* 司机接口
* @author 叶汉伟
*/
public interface IDriver {
public void drive();
}
/**
* 司机类
* @author 叶汉伟
*/
public class Driver implements IDriver{
private ICar car;
//通过构造函数注入依赖对象
public Driver(ICar car){
this.car=car;
}
public void drive(){
this.car.run();
}
}
如果我们想要司机开宝马车,只需要将宝马车对象传入构造函数即可。这种方法又叫做构造函数注入。
这种方法通过在抽象中增加一个setter方法实现。具体实现如下:
程序5
/**
* 司机接口
* @author 叶汉伟
*/
public interface IDriver {
public void setCar(ICar car);
public void drive();
}
/**
* 司机类
* @author 叶汉伟
*/
public class Driver implements IDriver{
private ICar car;
//setter方法传递依赖对象
public void setCar(ICar car){
this.car=car;
}
public void drive(){
this.car.run();
}
}
若要司机开那种车,只需要将车子的对象通过Driver对象的setCar()方法传入即可。这种方法又叫setter依赖注入。
通过依赖倒置原则,我们可以实现模块中的松耦合。具体来说总结为一下几点规则:
还没有形成面向接口编程的java学习者们,赶紧将这个规则用起来吧,用过一段之间,你会感觉水平飙升,在代码间游走更加柔韧有余。
依赖倒置原则(Dependence Inversion Principle),简称DIP
High level modules should depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.
即
1、高层模块不应该依赖低层模块,两者都应该依赖于抽象(抽象类或接口)
2、抽象(抽象类或接口)不应该依赖于细节(具体实现类)
3、细节(具体实现类)应该依赖抽象
抽象:即抽象类或接口,两者是不能够实例化的
细节:即具体的实现类,实现接口或者继承抽象类所产生的类,两者可以通过关键字new直接被实例化
而依赖倒置原则的本质其实就是通过抽象(抽象类或接口)使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合。但是这个原则也是6个设计原则中最难以实现的了,如果没有实现这个原则,那么也就意味着开闭原则(对扩展开发,对修改关闭)也无法实现。
1、通过构造函数传递依赖对象
比如在构造函数中的需要传递的参数是抽象类或接口的方式实现。
2、通过setter方法传递依赖对象
即在我们设置的setXXX方法中的参数为抽象类或接口,来实现传递依赖对象
3、接口声明实现依赖对象
涂涂是个女僧
public class Tutu {
//涂涂是个女孩,会煮面
public void cook(Noodles noodles)
{
noodles.eat();
}
}
面条(目前只会煮面)
public class Noodles {
//吃面条
public void eat()
{
System.out.println("涂涂吃面条...");
}
}
涂涂坐在家里吃面(场景类)
public class Home {
public static void main(String args[])
{
Tutu tutu = new Tutu();
Noodles food = new Noodles();
tutu.cook(food);
}
}
运行结果:涂涂吃面条...
但是这有个问题,涂涂只会做面条,不可能每次都吃面条吧,天天吃面吃死你,所以在上面的Tutu类中的cook方法中,如果涂涂会做其他吃的,那岂不是更好。于是她向家庭主妇迈进了一步,使用了依赖倒置原则。
也就是涂涂通过学习还可以焖米饭,炒鱿鱼(虽然听着不爽,但是很好吃),京酱肉丝啊等等。要想在代码中实现,就需要实现两个接口:ITutu和IFood
public interface ITutu {
//这样就会做很多饭菜了
public void cook(IFood food);
}
实现类
public class Tutu implements ITutu {
@Override
public void cook(IFood food) {
food.eat();
}
}
食物接口
public interface IFood {
public void eat();
}
这样就为扩展留出了很大的空间,方面扩展其他的类。也不会对细节有变动。以后涂涂想吃什么学一下就可以自己做了
实现面条
public class Noodles implements IFood {
@Override
public void eat() {
System.out.println("涂涂吃面条...");
}
}
实现米饭
public class Rice implements IFood {
@Override
public void eat() {
System.out.println("涂涂吃米饭(终于吃上米饭了)...");
}
}
场景类:涂涂在家里开吃了,想吃什么直接做就是了
public class Home {
public static void main(String args\[\])
{
//接口是不能实例化滴
ITutu tutu = new Tutu();
//实例化米饭,涂涂可以吃米饭了
IFood rice = new Rice();
//吃面条
//IFood noodles = new Noodles();
tutu.cook(rice);
}
}
这样各个类或模块的实现彼此独立,不互相影响,实现了模块间的松耦合。