1、接口和抽象类分别在什么时候使用
2、对“如果基本功能在不断变化,那么就使用抽象类,如果使用接口,那么每次变更都需要相应的去改变实现该接口的所有类。”这句话的理解
请耐心看完列子,列子很重要
首先看个例子和代码
参考文章https://blog.csdn.net/timchen525/article/details/78175365
问题提出:
在实际业务开发过程中,对于同样一种事件可能有不同的处理行为,而且在开发完成之后,后期可能会添加新的处理行为,如何在不影响旧有的业务代码增加新的行为是本文需要探讨的问题。
举个栗子:
有个模拟鸭子游戏,在游戏中有各种鸭子,鸭子可以一边游泳戏水,一边呱呱叫,然后我们还要展示鸭子的颜色等等。
已知条件:
1、所有的鸭子都会游泳;
2、有的鸭子会呱呱叫,有的鸭子不会呱呱叫;
3、后期可能会添加其他行为。
思路分析:
通常对于既有变化的行为和不变化的行为,我们通过一个抽象类来实现,在抽象类中将不变的游泳实现出来,这样所有继承该抽象类的类将不需要再去重复该代码,对于不确定的行为呱呱叫写一个抽象方法进行,具体的实现由它所继承的类来实现。
可知:上述将所有的行为都通过方法进行实现。
缺点:如果要添加一个行为(该行为具有不确定性),则在抽象类中添加一个抽象方法,所有实现该抽象类的类都要去实现该接口(注意:抽象类也是接口的一种,可以作为继承类的父类),而有些实现该抽象类的类可能并不需要该行为。
解决办法:区分出可变行为,并将可变行为封装成接口类,在抽象类,通过字段声明(实现has a 而不是 is a),以及相应的方法来表现该行为,行为的具体实现类,在具体实现抽象类中的类中进行调用。这样的有点,后期在增加新的行为时,不会影响到旧有代码的逻辑,达到低耦合的目的。
举个栗子:
Duck.java类
public abstract class Duck {
QuackBehavior quackBehavior;
FlyBehavior flyBehavior;
public void performQuack(){
quackBehavior.quack();
}
public void setQuack(QuackBehavior quackBehavior){
this.quackBehavior = quackBehavior;
}
public void performFly(){
flyBehavior.fly();
}
public void setFly(FlyBehavior flyBehavior){
this.flyBehavior = flyBehavior;
}
protected void swim(){
System.out.println("I can swim!");
}
protected abstract void display();
}
分析:
在Duck.java 类这中,游泳行为swim()是鸭子实现类共有的特征且一定会游泳,因此,将其实现出来。而display()是所有鸭子的共有特征,但是不同鸭子的行为不一样,因此,将其抽象出来,所有继承该抽象方法的鸭子都要实现该方法。鸭子的飞行行为和叫的行为,并不是所有鸭子的共有特征,因此,将其声明为接口,这样,所有实现该抽象类的类可以按需要不要去关心鸭子的飞行和叫的行为,达到低耦合的目的。比如,此时,如果有些鸭子需要跳舞的行为,则可以同样添加一个跳舞的接口。
补充:
关于接口中的行为有两种:
一种是获取接口的行为,如:performFly();
另一种是改变接口的行为,如:setFly() (改变行为实现对鸭子的行为进行动态设置)。
FlyBehavior.java类:
public interface FlyBehavior {
void fly();
}
两个实现FlyBehavior.java类如下:
Fly.java类:
public class Fly implements FlyBehavior {
@Override
public void fly() {
System.out.println("I can fly!");
}
}
QuackBehavior.java类:
public interface QuackBehavior {
void quack();
}
两个实现QuackBehavior.java类如下:
Quack.java 类:
public class Quack implements QuackBehavior {
@Override
public void quack() {
System.out.println("I can quack!");
}
}
NoQuack.java 类:
public class NoQuack implements QuackBehavior {
@Override
public void quack() {
System.out.println("I can't quack!");
}
}
一个Duck.java的实现类如下:
MallardDuck.java 类:
public class MallardDuck extends Duck {
public MallardDuck(){
quackBehavior = new Quack();
flyBehavior = new Fly();
}
@Override
protected void display() {
System.out.println("I am green!");
}
}
分析:
上述实现类对于Duck.java的实现类中,在构造函数中实例化鸭子的飞行行为和叫的行为。另外:对于鸭子的飞行行为和叫的行为可以在后期通过set函数进行更改。
实际调用的main方法如下:
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performFly();
mallard.setFly(new NoFly());
mallard.performFly();
}
输出:
I can fly!
I can't fly!
实现了运行时动态更改鸭子的飞行行为。
总结分析:
抽象类中的三种行为:
1、所有继承该抽象类都会有该行为,且行为都一样。(具体方法实现)
2、所有继承该抽象类都会有该行为,但行为都不一样。(抽象方法)
3、只有部分继承该抽象类会有该行为。(面向接口编程)
使用模式最好的方式是:“把模式装进脑子里,然后在你的设计和已有的应用中,寻找何处可以使用它们。”以往是代码复用,现在是经验复用。
OO原则:
1)封装变化(代码复用,低耦合)
2)多用组合,少用继承(推荐has a ,少用is a)
3)针对接口编程,不针对实现编程
2.1、总结
由此上述代码示例,可以看出如果使用接口的话在 Duck类 中就不能有默认的方法实现。必须全部是abstract 方法,使用接口的实现类必须重写方法才能调用。也正因为抽象类可以有默认的方法实现,故基本功能在不断变化最好选择使用抽象类,因为可以在抽象类实现默认的方法