本文主要是介绍设计模式概述以及七大设计原则,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 什么是设计模式
- 设计模式的作用
- 设计模式使用原则
- 设计模式七大原则
- 单一职责原则
- 接口隔离原则
- 依赖倒置原则
- 里氏替换原则
- 开闭原则
- 最少知道原则
- 合成复用原则
什么是设计模式
设计模式是指经过多年编程实践验证的,针对面向对象语言的一套有用的编程模式。
设计模式的作用
正确的使用设计模式可以降低代码间的耦合层度,便于代码的扩展以及维护。
设计模式使用原则
使用设计模式前应该明白:
- 设计模式是有用的,前提是你在正确的场景下使用正确的设计模式.23种设计模式针对23种不同的场景,应该根据自己的场景来选择使用设计模式。
- 设计模式的使用可能是有利有弊的,比如在保证系统的可扩展性的同时,可能导致类个数的快速膨胀,因此不要为了使用设计模式而使用,避免模式滥用。
- 设计模式的背后是七大设计原则,七大设计原则的背后就是一个字:分!!
设计模式七大原则
单一职责原则
每个方法,每个类,每个框架都只做一件事.尽可能的切割方法,类,避免将不同的功能代码块放在一个函数中.
这样也就要求方法尽可能的短,类尽可能的精简.根据马丁福勒的理论:一个函数甚至不应该超过6行,虽然我觉得有点太极端,但是往这方面追求总是没错的。
- 场景:
读取文件然后判断其中有多少个字符,应该将加载文件和分割字符分在两个函数,便于后面变化带来的需求调整.如果需要判断单词个数,句子个数,可以复用加载文件的函数.
接口隔离原则
在定义接口的时候,要尽量小,将真正需要放在一起的接口放在一起.比如我写一个动物的接口:
interface Animal{void eat();void fly();void swim();
}
这个接口明显没有达到接口隔离的标准,因为并不是所有的动物都会飞,都会游泳,所以最好的方式就是:
interface Eatable{void eat();
}
interface Fliable{void fly();
}
interface Swimable{void swim();
}
这样就将接口之间隔离开来,不定义总接口.这也暗合了Go中的接口尽量小的设计思想.
依赖倒置原则
上层代码不应该依赖下层,他们都应该依赖抽象.上层是指调用其他方法的是上层,被其他方法调用的是下层.
反例:
public class Dog {public Dog() {}void eat() {System.out.println("Dog is eating...");}
}public class Person {private Dog dog;public Person(Dog dog) {this.dog = dog;}void feed() {System.out.println("Person starts to feed dog...");this.dog.eat();}
}public class Main {public Main() {}public static void main(String[] args) {Dog dog = new Dog();Person person = new Person(dog);person.feed();}
}
上述代码可以完成人喂狗的操作,但是如果有一天,想要人喂猫,应该怎么处理呢?如果沿着上述代码的思路,就是:
public class Cat {void eat() {System.out.println("Cat is eating...");}
}public class Dog {void eat(){System.out.println("Dog is eating...");}
}public class Person {private Dog dog;private Cat cat;public Person(Dog dog) {this.dog = dog;}public Person(Cat cat) {this.cat = cat;}void feedDog() {System.out.println("Person starts to feed dog...");dog.eat();}void feedCat() {System.out.println("Person starts to feed cat...");cat.eat();}
}public class Main {public static void main(String[] args){Dog dog = new Dog();Person person = new Person(dog);person.feedDog();Person person1 = new Person(new Cat());person1.feedCat();}
}
这倒是满足了现在的需求,但如果现在要求person喂鸟,喂鱼,喂…等,难道每个都要新建类然后改动Person里面的函数?这显然不是好的设计.此时每当下游发生改动,上层代码都要更改,违反了依赖倒转原则.
此时正确的写法为:新建一个Animal接口,让Person依赖于Animal接口,下层的动物如猫狗等应该实现该Animal接口,此时原来的上下层都依赖该接口Animal.
只要实现该接口的类就可以传进去.
里氏替换原则
所有父类对象出现的位置都可以使用子类对象无条件替换,且要保证业务不受影响。
在继承中有两个限制:
- 子类方法不允许比父类方法的访问限制更严格
- 子类方法不允许比父类方法抛出更多的异常
上述的限制其实就是在要求继承关系要满足里式替换原则。
关于继承,我们通常说他们需要满足is-a
关系,但是里式替换要求我们不光要满足is-a
关系,还要满足业务场景逻辑的正确。所有,正方形是长方形吗?不一定。
在下面的业务逻辑下,计算长方形的面积,此时Squre确实是Rectangle的子类,满足继承关系,因为他们的业务逻辑是一样的。
class Rectangle {private int length;private int width;public int area() {return length * width;}
}class Squre extends Rectangle {private int length;@Overridepublic int area() {return length * length;}
}
但下面的业务逻辑下就不成立了:
public class Main {public static void main(String[] args) {Rectangle rectangle = new Rectangle();rectangle.setWidth(12);rectangle.setLength(20);Util.op(rectangle);}
}class Rectangle {private int length;private int width;public int getLength() {return length;}public void setLength(int length) {this.length = length;}public int getWidth() {return width;}public void setWidth(int width) {this.width = width;}public int area() {return length * width;}
}class Squre extends Rectangle {private int length;@Overridepublic int getLength() {return length;}@Overridepublic void setLength(int length) {length = length;}@Overridepublic int getWidth() {return length;}@Overridepublic void setWidth(int width) {length = width;}@Overridepublic int area() {return length * length;}
}class Util {/*业务场景:不断累加宽,直到它比长多1为止*/public static void op(Rectangle rectangle) {while (rectangle.getWidth() <= rectangle.getLength()) {rectangle.setWidth(rectangle.getWidth() + 1);System.out.println(rectangle.getLength()+ " "+ rectangle.getWidth());}}
}
在上面的情形下,如果将父类main函数(业务逻辑实现处)Rectangle对象替换为Squre对象,会照成代码的死循环,这显然是不符合里式替换原则的。所以在这种情况下,并不能简单的认为正方形是长方形的子类。
继承关系中,除了看是否有is-a关系外,更重要的是在替换的情况下,代码的业务逻辑是否准确无误。
开闭原则
对扩展开放,对修改关闭.开闭原则在七大原则中优先级很高,有时会为了符合该原则而牺牲其他原则.
加新功能的同时必须保证原有代码的稳定,保证原有功能的稳定.
如果全是自己的源代码,不允许修改原来的源代码,如果是导入的jar包,你甚至想改也改不到.
程序员分为两种,以某框架为例,可以分为框架的作者和框架的用户.但是这两个角色都应该严格符合开闭原则.
最少知道原则
- 上层类对下层类的实现细节要知道的越少越好
主要体现封装的思想。一个类应该尽量将自己的实现细节留在自己的内部,不要暴露太多细节给其他类。
现在有一个Computer类,现在要模仿它关机的执行流程,反例代码如下:
public class Computer {public void saveData() {//保存数据System.out.println("saveData");}public void closeScreen() {//关闭屏幕System.out.println("closeScreen");}public void killProcess() {//杀死进程System.out.println("killProcess");}public void powerOff() {//关掉电源System.out.println("powerOff");}
}class Person{private Computer computer = new Computer();public void shutDownComputer() {//太多实现细节在这里暴露computer.saveData();computer.killProcess();computer.closeScreen();computer.powerOff();}
}
如上面的例子,Person对象只是想关掉电脑,他并不需要知道电脑关机的具体流程,这样的架构不但让代码冗余,而且可能会带来其他的问题。倘若客户并不清楚电脑关闭的操作步骤呢?可以直接关闭电源而没有保存数据呢?这都是暴露太多细节带来的问题。正确的做法应该是:
public class Computer {private void saveData() {System.out.println("saveData");}private void closeScreen() {System.out.println("closeScreen");}private void killProcess() {System.out.println("killProcess");}private void powerOff() {System.out.println("powerOff");}public void shutDown() {this.saveData();this.killProcess();this.closeScreen();this.powerOff();}
}class Person{private Computer computer = new Computer();public void shutDownComputer() {computer.shutDown();}
}
重构之后,电脑关机的细节全部定义在Computer类内部,保证了操作步骤的正确性,毕竟该类的作者才是最最懂关机细节的人。Computer对外暴露shutDown函数,Person只需调用该方法就完成整个关机流程。代码更整洁漂亮,而且不会出错。
- 只和朋友进行通信
朋友的定义:- 类中的字段类型
- 类中的方法的参数类型
- 类中的方法的返回值类型
- 方法中直接实例化出来的类型(如使用new)
总结一句话就是:该类依赖的所有类都是自己的朋友。
那么,哪种类不算自己的朋友呢?
public class AppTest {private Foo foo;void f1() {Bee bee = foo.getBee();}
}class Foo{Bee getBee() {return new Bee();}
}class Bee{}
Bee类就不是AppTest类的朋友,不符合上述朋友的任何一条。我们应该尽量不在AppTest中调用Bee的方法。此时Bee对象可以作为参数。
只和朋友通信是理想情况,实际开发中要视自己的业务需求来做判断。可以适当违背这一条。
合成复用原则
尽量使用组合而不是继承。继承关系会将类紧紧耦合在一起,而且在某些业务场景下会导致类数目的爆炸性增长。
这篇关于设计模式概述以及七大设计原则的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!