设计模式概述以及七大设计原则

什么是设计模式

设计模式是指经过多年编程实践验证的,针对面向对象语言的一套有用的编程模式。

设计模式的作用

正确的使用设计模式可以降低代码间的耦合层度,便于代码的扩展以及维护。

设计模式使用原则

使用设计模式前应该明白:

  • 设计模式是有用的,前提是你在正确的场景下使用正确的设计模式.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对象可以作为参数。

只和朋友通信是理想情况,实际开发中要视自己的业务需求来做判断。可以适当违背这一条。

合成复用原则

尽量使用组合而不是继承。继承关系会将类紧紧耦合在一起,而且在某些业务场景下会导致类数目的爆炸性增长。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值