目录
// 设计模式的本质是封装变化
单一职责原则
-
定义
应该有且仅有一个原因引起类的变更。即一个类只做一件事情。
-
思考
其实单一职责原则最难划分的就是职责,所以有很多时候职责的取舍是很难说的事情。从逻辑上来说,单一职责原则都适用于接口、类、方法三个层级。我们要做的就是,接口严格按单一职责划分,类可适当根据实际情况灵活应用,方法则尽量拆开,不要混在一起。
里氏替换原则
-
定义
任何父类出现的地方,都应该可以被替换为它的子类。即儿子随时可以替换爹。
-
思考
其实我们日常中的面向接口编程,即在类中调用其他类时都使用父类或接口,就已经是满足里氏替换原则了
为了满足里氏替换原则,我们需要注意以下四点:
1.子类完全实现父类的方法
不然没法用儿子去替代爹。
2.子类可以有自己的个性
比如说,子类可以扩充很多父类没有的方法。这个也就是我们常说的,向下转型不安全。不过这样做是符合里氏替换原则的。
3. 子类重载方法时,入参范围要大于等于父类。(前置条件)
即调用时优先调用父类方法。这样才能保证,用儿子替代爹时,调用子类父类的同名方法,入参同时满足子父类的方法时,执行的是父类的方法。
4.子类重写方法时,返回值范围要小于等于父类。(后置条件)
即返回时要兼容父类方法。这样才能保证,原本调用父类方法的地方,换成子类时,返回值也一定是符合条件的。当然,当违反这一条时,编译器会自动报错的。
第3、4条示例如下:
public class Father {
public Map<String, Object> cleanHome(ArrayList<String> list) {
return null;
}
public Map<String, Object> washCloth(List<String> list) {
return null;
}
}
public class Son extends Father {
// 重载,入参范围要大于等于父类,从而保证父类方法的优先调用
public Map<String, Object> cleanHome(List<String> list) {
return null;
}
// 重写,返回值范围要小于等于父类,避免子类替换父类时返回值向下转型
@Override
public HashMap<String, Object> washCloth(List<String> list) {
return null;
}
}
总之,为了避免麻烦,采用里氏替换原则时,要尽量避免子类的“个性”。
依赖倒置原则
-
定义
面向接口编程。即,能用接口表示具体类的地方,都用接口表示。这个原则其实包含3层意思:1.模块间的依赖通过抽象发生。2.接口或抽象类不依赖于实现类。3.实现类依赖接口或抽象类。
-
依赖的意思
就是持有对方啦。上面三条就变成:1.模块间持有对方的接口。2.接口不持有实现类。3实现类持有接口。不就是面向接口编程么。
-
倒置的意思
正常的思维,依赖是面向实现的,实实在在的、具体的。我开车要开奔驰就开奔驰,要开宝马就开宝马。即“正置”。而面向接口时,依赖是面向抽象的,代替了人类的传统思维,也就是“倒置”。
-
依赖倒置的优点
1.减少类间的耦合。这样扩展性就更好,这个也是后面开闭原则的前提。
2.有利于并行开发。多人开发时,只要依赖的地方有一个接口,自己的程序就可以运行。如果依赖的地方不是接口,则必须要等到那个人实现那个类以后,自己的程序才能跑起来。依赖于接口,就可以使用JMock工具等进行单元测试,测试驱动开发,先写好单元测试类,再写实现类,这个也是TDD的精髓。
-
依赖的三种写法
1,通过构造函数注入
public interface IDriver {
// 开车
public void drive();
}
public class Driver implements IDriver {
private ICar car;
// 构造函数注入
public Driver(ICar carIn) {
this.car = carIn;
}
public void drive() {
this.car.run();
}
}
2.通过Setter注入
public interface IDriver {
// 开车
public void drive();
}
public class Driver implements IDriver {
private ICar car;
// Setter注入
public void setCar(ICar carIn) {
this.car = carIn;
}
public void drive() {
this.car.run();
}
}
3.通过接口注入
public interface IDriver {
// 开车
public void drive(ICar carIn);
}
public class Driver implements IDriver {
// 接口注入
public void drive(ICar carIn) {
carIn.run();
}
}
当然,实际项目中,也要灵活运用,不是死死抓住一个原则不放。
接口隔离原则
-
定义
客户端不应该依赖它不需要的接口。其实这只是结果,本质则是,接口尽量细化。即接口中的方法尽量少,尽量使用多个专门的接口。
接口隔离原则其实包含了4层含义:
1.接口要尽量小。这个是核心定义。在不违反单一职责原则的情况下,尽量小。
2.接口要高内聚。高内聚就是,依赖于外部的信息很少。比如说,你对一个高手说,去川普办公室偷个文件,其他你啥也不用做,一个月后,文件就偷到了。具体到实际操作,就是,在接口中,尽量少的公布public方法。
3.只提供访问者需要的方法。如果有访问者不需要的其他方法,则这个接口很可能就需要再拆了。
4.接口的设计是有限度的。满足接口隔离原则最理想的情况,当然是一个方法一个接口。但是这是不可能的,在实际操作中,就需要我们根据经验来控制粒度的大小。
-
注意
接口其实分两种。1.实例的接口,也就是类。由于类是实例要遵循的标准,所以类也是一种接口。2.类的接口,就是我们常说的interface。
-
单一职责原则和接口隔离原则的区别
单一职责原则注重的是职责,是业务逻辑层面的划分,同一个接口里面有很多个方法也无所谓。而接口隔离原则则是要求接口的方法尽量少。
-
总结
总之,就是在不违背单一职责原则的情况下,让接口尽量小。一个接口只服务一个子模块或业务逻辑。粒度大小需要根据实际情况控制。
迪米特法则
-
定义
也叫最少知识原则。核心观念就是类间解耦,弱耦合。一个对象应该对其他对象有最少的了解。即,一个类需要对自己需要耦合或调用的类知道得最少。
-
理解
最少知识原则包含了以下几层含义:
1.对直接调用类的最小知识:直接调用类时,被调用的类要高内聚,不需要让我知道的方法,都不要让我知道。例如:去酒吧点一杯鸡尾酒,直接调用小哥的购酒方法,就得到了一杯鸡尾酒。而不是调用小哥的取杯子方法取杯子、调用小哥的倒酒方法倒酒、调用小哥的调酒方法调酒......这样我还要小哥干啥?小哥只需要提供给我一个获得鸡尾酒的public方法,其他的private方法要设置为private,因为我根本不关心。
2.对间接调用类的最小知识:通过中介类调用其他类时,只与中介类交互,不关心其具体的实现。例如:老师叫班长去给同学发书,他只是把班长叫过来,告诉他去发书就行了。并不需要再给班长发放的人员列表,否则就违背了最少知识原则。即,我只叫你去做,并不关心你是怎么实现的。
3.是自己的就是自己的。当一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响时,那就放置在本类中。
其他:解耦后会造成大量的中转或跳转类,在实际调用中,如果一个类跳转两次以上才能访问到另一个类,就要想办法重构了。所以最小知识原则也是需要灵活运用的。
开闭原则
-
定义
一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。
-
理解
我们要尽量通过增量修改的方式来实现变化,而不是通过修改已有的代码来实现。
开闭原则是最基本的一个原则,也是前面五个原则的精神领袖。前面五个原则只是实现开闭原则的工具和方法。也不局限与这五个原则。
-
为什么要采用开闭原则
1.对测试友好:不用再去重新改单元测试的代码了,新增测试代码即可。
2.提高了代码复用性:缩小逻辑粒度,好维护,复用性强。
3.提高了代码可维护性:毕竟研究、修改老代码是一件很痛苦的事情。
4.面向对象开发的要求。
-
如何使用开闭原则
1.抽象约束。a.通过接口或抽象类约束扩展。b.参数类型、引用对象尽量使用接口或抽象类。c.抽象层尽量保持稳定。
2.元数据控制模块行为。元数据就是描述数据的数据,其实就是配置参数。例如,Spring通过修改配置文件引入子类。
3.指定项目章程。建立团队的章程,约定由于配置。
4.封装变化。要预知变化,a.将相同的变化封装到同一个接口或抽象类。b.将不同的变化封装到不同的接口或抽象类中。
合成/复用原则
通常,Has A 比 Is A 要好。
总结
开闭原则是总原则,六大原则一句话总结如下:
开闭原则:增量修改
单一职责原则:一个类只做一件事
里氏替换原则:子类可以替换父类
依赖倒置原则:面向接口编程
接口隔离原则:满足单一职责情况下,接口方法尽量少。
迪米特法则:最少知识,不关心调用类或中间类的内部方法。高内聚。
合成/复用原则:通常,Has A 比 Is A 要好。
六大原则就讲完了。最后,设计模式的本质是封装变化。