最近在复习设计模式,找了老韩的视频看了看,想顺便记录一下学习的过程,因此决定写点学习笔记,废话不多说开始。
1.单一职责原则
单一职责是指:一个类尽量只负责一个职责.如果类负责多个职责的话当其中一个职责发生变化就有可能影响到其他的职责的正常运行。
2.接口隔离原则
接口隔离原则定义如下:
- 客户端不应该依赖它不需要的接口
- 类间的依赖关系应该建立在最小的接口上
例如:
接口1中有5个方法
类A实现了接口1 ; 类C通过接口1依赖类A,并调用接口中的方法1,2,3
类B也实现了接口1;类D同样通过接口1依赖类B,但是类D只调用接口中的1,4,5方法
这个时候问题就来了,类C只用了1,2,3方法,但是类A必须实现接口1的全部5个方法
类B也有同样的问题,这样造成了无意义的实现
针对上面的问题,接口隔离原则就要发挥它的作用了:
我们首先将接口1拆分成3个接口:
- 接口1:只有方法1
- 接口2:包含方法2和3
- 接口3:包含方法4和5
这样我们的类A就需要实现接口1和2,类B就要实现接口1和3,然后类C就要可以通过接口1,2来依赖类A,类D就可以通过接口1,3类依赖类B.
这样就没有实现多余的方法,也同样达成了依赖关系
3.依赖倒转(倒置)
定义:
- 高层模块不依赖底层模块,两者都依赖抽象
- 抽象不应该依赖于细节,细节应该依赖于抽象
例如:
public static class Email{
public String getMessage(){
return "Email message";
}
}
public void getMessage(Email email){
System.out.println(email.getMessage());
}
我们在类中的方法getMessage(Email email)
它的参数的Email类,这意味着,如果我们想新增其他的方法输入渠道的时候我们必须重写getMessage
方法,这就造成不必要的麻烦,就像这样:
public static class Email{
public String getMessage(){
return "Email message";
}
}
public static class WeChat{
public String getMessage(){
return "WeChat message";
}
}
public void getMessage(Email email){
System.out.println(email.getMessage());
}
//要想获取新增的渠道的信息,必须重写getMessage方法
public void getMessage(WeChat weChat){
System.out.println(weChat.getMessage());
}
因此我们需要使用到依赖倒转,于是我们抽象一个接口Message
然后方法getMessage
使用的是Message接口,就像下面这样:
public interface Message{
String getMessage();
}
public static class Email implements Message{
@Override
public String getMessage(){
return "Email message";
}
}
public static class WeChat implements Message{
@Override
public String getMessage(){
return "WeChat message";
}
}
//使用接口来进行调用
public void getMessage(Message message){
System.out.println(message.getMessage());
}
这样无论增加多少中信息渠道,只要它实现了Message接口就不会对我们的getMessage
方法造成影响
除了这种直接在调用方法的时候传入的方法,还可以通过构造方法传入以及通过setter方法传入的实现
4.里氏替换原则
定义1: 如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
定义2: 所有引用基类的地方必须能透明地使用其子类的对象。
用一句话说明就是:子类可以当作父类使用。或者说子类可以扩展父类的功能但是不能改变父类原有的功能。
例如:
private static class A{
//原有功能,返回10
public int func1(){
return 10;
}
}
//现在让子类来实现一个新的功能func,其功能由fun1和func2联合完成
private static class B extends A{
//fanc实现func1与func2的和
public int func(){
return func1()+func2();
}
public int func1(){
return 5;
}
public int func2(){
return 95;
}
}
private static class C{
//引用基类
public static void invoke(A a){
System.out.println(a.func1());
}
}
public static void main(String[] args) {
A a = new A();
B b = new B();
//新的功能正常
System.out.println("新功能:"+b.func());
C.invoke(a);
//引用基类使用子类时只能得到5而不是10,因此发生故障
C.invoke(b);
}
如上面的列子,当B继承了A之后实现了新的功能func这个功能由func1和func2共同的计算结果来完成。但是在实现的过程中我们没有发现func1是原有的类A中的一个方法。此时我们还想通过子类B调用类A的func1方法就有可能得不到我们期望的结果。
这个时候我们就需要让类A和类B成为一种平行的关系,而不是原来的继承关系,这个时候如果我们还要在类B中使用类A的一些功能,我们可以将类A聚合到内B中。
private static class A{
//原有功能,返回10
public int func1(){
return 10;
}
}
//现在让子类来实现一个新的功能func,其功能由fun1和func2联合完成
private static class B {
private A a = new A();
//fanc实现func1与func2的和
public int func(){
return func1()+func2();
}
public int func1(){
return 5;
}
public int func2(){
return 95;
}
public int func1A(){
return a.func1();
}
}
private static class C{
public static void invoke(A a){
System.out.println(a.func1());
}
}
public static void main(String[] args) {
A a = new A();
B b = new B();
System.out.println("原功能:"+a.func1());
//通过聚合对象调用方法就不会发生故障
System.out.println("原功能:"+b.func1A());
//新的功能正常
System.out.println("新功能:"+b.func());
C.invoke(a);
//此时类B不是类A的子类,因此不存在引用子类的情况,故障不会发生
// C.invoke(b);
}
5.开闭原则
定义:一个软件实体如类,模块和函数应该对扩展开放(提供方),对修改关闭(使用方)。
开闭原则也是设计模式中最基础、最重要的原则。
例如:
public static class GraphicEditor{
public void draw(Shape shape){
//根据图形的类型确定调用哪个方法
if(shape.shapeType == 1){
drawRectangle();
}else if (shape.shapeType == 2){
drawTriangle();
}
}
private void drawRectangle(){
System.out.println("画长方形");
}
private void drawTriangle(){
System.out.println("画三角形");
}
}
public static class Shape{
int shapeType;
}
public static class Rectangle extends Shape{
Rectangle(int shapeType){
this.shapeType = shapeType;
}
}
public static class Triangle extends Shape{
Triangle(int shapeType){
this.shapeType = shapeType;
}
}
public static void main(String[] args) {
GraphicEditor editor = new GraphicEditor();
editor.draw(new Rectangle(1));
editor.draw(new Triangle(2));
}
可以看出如果我们想针对上面的代码添加新的图形,如:我们想要添加一个圆形,我们必须:
//添加一个新的shape的子类,用于画圆形
public static class Circle extends Shape{
Circle(int shapeType){
this.shapeType = shapeType;
}
}
public static class GraphicEditor{
.....
public void draw(Shape shape){
//根据图形的类型确定调用哪个方法
if(shape.shapeType == 1){
drawRectangle();
}else if (shape.shapeType == 2){
drawTriangle();
}else if(shape.shapeType == 3){//修改GraphicEditor类的draw方法
drawCircle();
}
}
//添加一个画圆形的方法
private void drawCircle(){
System.out.println("画圆形");
}
.....
}
public static void main(String[] args) {
GraphicEditor editor = new GraphicEditor();
editor.draw(new Rectangle(1));
editor.draw(new Triangle(2));
editor.draw(new Circle(3));
}
可以看出这样的方案虽然比较简单易懂,但是也非常不利于扩展,每次我们想添加一个图形都必须修改大量的代码
如果我们使用开闭原则的话,我们可以这样写:
public static class GraphicEditor{
public void draw(Shape shape){
//直接调用draw
shape.draw();
}
}
public static abstract class Shape{
int shapeType;
//将draw抽象成一个抽象方法
public abstract void draw();
}
public static class Rectangle extends Shape{
Rectangle(int shapeType){
this.shapeType = shapeType;
}
@Override
public void draw() {
System.out.println("画长方形");
}
}
public static class Triangle extends Shape{
Triangle(int shapeType){
this.shapeType = shapeType;
}
@Override
public void draw() {
System.out.println("画三角形");
}
}
public static void main(String[] args) {
GraphicEditor editor = new GraphicEditor();
editor.draw(new Rectangle(1));
editor.draw(new Triangle(2));
}
此时我们还想新增一个画圆形的方法的话:
//添加一个新的shape的子类,用于画圆形
public static class Circle extends Shape{
Circle(int shapeType){
this.shapeType = shapeType;
}
@Override
public void draw() {
System.out.println("画圆形");
}
}
//我们甚至还可以继续添加话正方形的类
public static class Square extends Shape{
Square(int shapeType){
this.shapeType = shapeType;
}
@Override
public void draw() {
System.out.println("画正方形");
}
}
//无需修改GraphicEditor这个类
public static class GraphicEditor{
.....
}
public static void main(String[] args) {
GraphicEditor editor = new GraphicEditor();
editor.draw(new Rectangle(1));
editor.draw(new Triangle(2));
editor.draw(new Circle(3));
editor.draw(new Square(4));
}
可以看出我们使用OCP原则之后,代码简介了不少。而且相比于不使用OCP时,我们需要修改的部分也少了很多。
6.迪米特法则
迪米特法则的定义:
也被称为最少知识原则(Least knowledge Principle,LKP)
也可以表述为 一个对象应该对其他对象有最少的了解,即一个类应该对自己需要耦合或调用的类知道的最少
4层含义:
1、只和朋友交流(Only talk to your immediate friends)
在类之间,什么样的类算作朋友呢?
出现在成员变量、方法的输入输出参数中的类称为成员朋友类。而出现在方法体内部的类不属于朋友类。
2、朋友之间也是有距离的
不能暴露太多,否则二次修改的时候,会让影响的范围增大。
这也要求类间public方法不能肆无忌惮的暴露
3、是自己的就是自己的
如果一个方法在类间关系中,放在自身类中既不增加类间关系,也对本类不产生负面影响就放置在自身类中。
4、谨慎进行序列化操作
针对RMI(Remote Method Invocation)
最佳实践:
迪米特法则的核心在于类间的解耦,只有弱耦合之后类的复用率才会提高。其要求的结果就是产生大量的中转或跳转类。
迪米特法则的核心也是为了降低类与类之间的耦合
例如:
public static class Employee{
private int id;
private String name;
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public static class EmployeeManager{
public List<Employee> getAllEmployee(){
List<Employee> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(new Employee(i,"Employee"+i));
}
return list;
}
}
public static class Manager{
public static void printAll(EmployeeManager manager){
//不符合迪米特法则,暴露了过多的信息
List<Employee> allEmployee = manager.getAllEmployee();
for (Employee employee:allEmployee) {
System.out.println(employee.toString());
}
}
}
public static void main(String[] args) {
Manager.printAll(new EmployeeManager());
}
可以看到,我们的Manager类的printAll方法拿到了EmployeeManager暴露的Employee数组,但是这个printAll的逻辑写在Manager类中和写在EmployeeManager类中没有任何其他的影响,但是写在Manager类中确暴露了更多的信息。因此我们可以使用迪米特法则做如下修改:
......
public static class EmployeeManager{
//将原来暴露多余信息的方法私有化
private List<Employee> getAllEmployee(){
List<Employee> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(new Employee(i,"Employee"+i));
}
return list;
}
//只对外提供一个printAll的方法
public void printAll(){
//这样就不会将List<Employee>暴露出去了
List<Employee> allEmployee = getAllEmployee();
for (Employee employee:allEmployee) {
System.out.println(employee.toString());
}
}
}
public static class Manager{
public static void printAll(EmployeeManager manager){
//只能调用EmployeeManager的printAll方法,无法再获得其他多余的信息
manager.printAll();
}
}
public static void main(String[] args) {
Manager.printAll(new EmployeeManager());
}
这样原来暴露的信息,就被隐藏了起来,并且原来的功能也没有受到影响
7.合成复用原则
定义:合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
问题由来:通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。
如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
解决方案:合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用
8.总结
设计原则的核心思想:
- 找出应用中可能需要变化之处,把他们独立出来,不要和那些不需要变化的代码混在一起。
- 针对接口编程,而不是针对实现编程。
- 为了交互对象之间的松耦合设计而努力。