什么是设计模式
设计模式是指经过多年编程实践验证的,针对面向对象语言的一套有用的编程模式。
设计模式的作用
正确的使用设计模式可以降低代码间的耦合层度,便于代码的扩展以及维护。
设计模式使用原则
使用设计模式前应该明白:
- 设计模式是有用的,前提是你在正确的场景下使用正确的设计模式.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;
@Override
public 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;
@Override
public int getLength() {
return length;
}
@Override
public void setLength(int length) {
length = length;
}
@Override
public int getWidth() {
return length;
}
@Override
public void setWidth(int width) {
length = width;
}
@Override
public 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对象可以作为参数。
只和朋友通信是理想情况,实际开发中要视自己的业务需求来做判断。可以适当违背这一条。
合成复用原则
尽量使用组合而不是继承。继承关系会将类紧紧耦合在一起,而且在某些业务场景下会导致类数目的爆炸性增长。