6

Head First 设计模式

软件开发中,不变的真理就是Change

打开Head First设计模式,书上的第一个例子就让我真正明白了我们一直说的Java的接口优点。

我们常说:Java中类不允许多继承,而采用了更加合理的接口设计。

继承是为了代码复用的,而接口不过是声明方法,那为什么接口能替代多继承呢?这是我一直以来的疑问。今天,终于找到答案。

接口

注:以下代码均省略getset方法。

原设计

/**
 * 动物
 */
public class Animal {

    private String name;      // 名称

    public void eat() {}
}

/**
 * 鸟
 */
public class Bird extends Animal {

}

/**
 * 鱼
 */
public class Fish extends Animal {
    
}

/**
 * 蝙蝠
 */
public class Bat extends Animal {

}

这是我们的原设计,一个动物类,里面有所有动物公有的eat方法,然后鸟、鱼和蝙蝠都继承自动物,复用了eat方法,减少了代码量。

需求变更

客户改需求了,我们要让能飞的动物有fly方法,也就是我们的BirdBat类生成的对象有要有fly这个方法。

然后这些飞的行为都是相同的,我们需要实现复用,最不负责任的方式就是在Animal类中添加fly方法,让多个类能继承这个方法,实现复用。

/**
 * 动物
 */
public class Animal {

    private String name;      // 名称

    public void eat() {

    }
    
    public void fly() {
        System.out.println("I can fly");
    }
}

指正实现方式极其简单,但会让以后该代码的人痛苦万分。他们会去想,我new一个鱼,怎么给我提示一个fly方法?

clipboard.png

如此,每次修改,都是对原有设计的破坏!

接口

/**
 * 接口:能飞的
 */
public interface Volitant {

    void fly();
}

/**
 * 鸟
 */
public class Bird extends Animal implements Volitant {

    @Override
    public void fly() {
        System.out.println("I can fly");
    }
}

/**
 * 蝙蝠
 */
public class Bat extends Animal implements Volitant {

    @Override
    public void fly() {
        System.out.println("I can fly");
    }
}

建立一个能飞的接口,让这两个需要飞的动物实现该接口,实现了需要飞的动物能飞,不需要飞的动物不能飞。

书中提出了一个思想:把会变化的部分取出并封装起来,以便以后可以轻易地改动或扩充此部分,而不影响不需要变化的其他部门。

我对这句话的理解就是将经常需要改动的部分抽象出来并封装,如此,用户改动只改动相关部分,而对原设计没有破坏。

但是这样要怎么实现代码复用呢?同样的代码,在鸟和蝙蝠中写了两次。

复用

/**
 * 会飞的实现类
 */
public class VolitantImpl implements Volitant {
    @Override
    public void fly() {
        System.out.println("I can fly");
    }
}

/**
 * 鸟
 */
public class Bird extends Animal implements Volitant {

    private Volitant volitant = new VolitantImpl();

    @Override
    public void fly() {
        this.volitant.fly();
    }
}

/**
 * 蝙蝠
 */
public class Bat extends Animal implements Volitant {

    private Volitant volitant = new VolitantImpl();

    @Override
    public void fly() {
        this.volitant.fly();
    }
}

写一个类,这个类去实现Volitant接口,然后在鸟和蝙蝠中引用这个实例化该类对象,调用其fly方法。如此,实现了代码复用。当然这个实现方法非常的low,这里直接去依赖实现类了,这就不好了。

控制反转

这是我自己想的解决方法,欢迎批评指正。

/**
 * 鸟
 */
public class Bird extends Animal implements Volitant {

    @Autowired
    private Volitant volitant;

    @Override
    public void fly() {
        this.volitant.fly();
    }
}

写了一段时间的Spring项目了,解除依赖的方法最常用的就是@Autowired,然后在实现类中添加@Service注入。

但是建的是普通的Java项目,我们不能被@Autowired吊死,这是一种思想,一种控制注入类的思想。

/**
 * 鸟
 */
public class Bird extends Animal implements Volitant {

    private Volitant volitant;

    public void setVolitant(Volitant volitant) {
        this.volitant = volitant;
    }

    @Override
    public void fly() {
        this.volitant.fly();
    }
}

/**
 * 蝙蝠
 */
public class Bat extends Animal implements Volitant {

    private Volitant volitant;

    public void setVolitant(Volitant volitant) {
        this.volitant = volitant;
    }

    @Override
    public void fly() {
        this.volitant.fly();
    }
}

public class Main {

    public static void main(String[] args) {
        Volitant volitant = new VolitantImpl();
        Bird bird = new Bird();
        bird.setVolitant(volitant);
        bird.fly();
        Bat bat = new Bat();
        bat.setVolitant(volitant);
        bat.fly();
    }
}

为两个需要飞的动物添加set方法,用于注入实现,在main方法中实例化的时候将需要的实现注入进去。

其实Spring的注入也与之类似,只是其更加强大,在程序运行时自动扫描需要注入的地方,然后注入实现。此处,不过是手工注入,我们学习到这种思想就是最大的收获。

Java8

其实从Java8开始,可以在接口中对方法进行默认的实现。

/**
 * 接口:能飞的
 */
public interface Volitant {

    default void fly() {
        System.out.println("I can fly");
    }
}

public class Main {

    public static void main(String[] args) {
        Bird bird = new Bird();
        bird.fly();
        Bat bat = new Bat();
        bat.fly();
    }
}

在接口中声明一个默认的方法,如果没有实现,则调用默认方法。

最佳实践

这是一句在腾讯云+社区一篇博客中借来的话。

clipboard.png

框架,就是对设计模式的最佳实践。而我们学习一个框架,不是去为了学多么新多么新的技术,而是学习其越来越好的设计模式。一个框架的流行,不是没有原因的。

张喜硕
2.1k 声望423 粉丝

浅梦辄止,书墨未浓。