JAVA和Nginx 教程大全

网站首页 > 精选教程 正文

用最通俗易懂的例子讲明白继承和组合的本质区别

wys521 2024-11-27 12:18:20 精选教程 20 ℃ 0 评论

前言

JAVA是一门纯面向对象的语言,OOP思想在JAVA中能很好的体现出来。面向对象编程的其中一个特点就是可以代码复用,一个好的对象设计可以减少30%的代码量。而JAVA中的继承和组合又是代码复用的最根本的体现。平时我们在实际开发中,多多少少会用到继承,组合就更加不用说了,但是你真的可以真正的区别到底用继承还是用组合呢?本文将通过一个入门级的通俗例子,跟你讲明白继承和组合的优缺点以及他们两者之间的本质区别,在实际开发中可以灵活的运用这两个方式写出更优质的代码。

演示场景

在有戏开发中,会有英雄,怪兽,精灵等各种游戏角色,这些角色可以通过战斗来提升自己消耗对方。本文将以游戏为例子,讲述这些游戏角色在实际中应当使用怎样的设计原则让代码更具有复用性和灵活性。

代码演示

定义一个英雄父类,这个类有一个方法和一个属性,这是英雄具备的基本属性和行为

public class Hero {
    // 定义英雄的名称
    protected String name;
    public Hero(String name) {
        this.name = name;
    }
    //定义一个具体基本的战斗方法,子类可以继承也可以重写
    protected void attack() {
        System.out.println(name + "战斗技巧");
    }
}

定义一个具体的1号英雄人物,通过继续英雄父类,直接具备英雄的基本属性和行为

// 继承Hero 父类
public class Hero1 extends Hero {
    public Hero1(String name) {
        super(name);
    }
  //定义一个新的 走路 行为
    public void walk() {
        System.out.println("英雄走路");
    }
}

游戏中一般有很多的英雄,所以还需要再定义一个具体的2号英雄类

//同样是继承英雄父类
public class Hero2 extends Hero {
    public Hero2(String name) {
        super(name);
    }
   //重写了父类的战斗行为,因为这个应用的战斗武器是使用长刀,所以需要重新战斗行为
    @Override
    protected void attack() {
        System.out.println(super.name +"战斗技巧,擅长使用的武器是长刀");
    }
}

现在具体的英雄类创建完了,我们接着创建一个测试类用来测试

public class ClientTest {
    public static void main(String[] args) {
       // 实例化1号英雄
        Hero h1 = new Hero1("英雄1");
      // 实例化2号英雄
        Hero h2 = new Hero2("英雄2");
       // 分别调用1号 2号英雄的战斗方法
        h1.attack();
        h2.attack();
    }
}

运行以上代码后,最终输出如下结果:

可以看到,通过继承可以复用父类的功能,子类直接具备了父类的属性和行为,还可以通过重写父类方法实现子类自己的行为。如果还需要其他个性的英雄,可以继续继承英雄这个类,选择性的重写战斗这个方法即可。在具体使用的时候向上转型为Hero 这个类型。在大多数场景下,通过继承可以解决代码复用的问题,但是在某些场景下使用继承真的是最合适吗?

我们考虑另外一个场景,在游戏中有英雄就一定有敌人,这里假设敌人是怪兽吧。现在我们想要在代码中增加一个怪兽,当然怪兽作为反角肯定也有战斗力的,所以怪兽也有战斗这个行为。按上面的例子,我们可以想到快速的创建一个怪兽类来实现我们的功能。

// 怪兽类继承英雄
public class Monster1 extends Hero {
    public Monster1(String name) {
        super(name);
    }
    // 重写战斗方法
    @Override
    protected void attack() {
        System.out.println(super.name + "兽类战斗技巧,使用战斧武器");
    }
}

接着修改测类,把怪兽角色添加进来

public class ClientTest {
    public static void main(String[] args) {
        Hero h1 = new Hero1("英雄1");
        Hero h2 = new Hero2("英雄2");
         // 新实例化一个怪兽
        Hero m1 = new Monster1("怪兽1");
        h1.attack();
        h2.attack();
      // 调用怪兽的战斗方法
        m1.attack();
    }
}

运行后,输出的结果如下:

可以看到,通过继承也能快速的让怪兽具备了战斗的这个方法,但是如果我们认真思考后发现这样设计合理吗?虽然这里勉强可以实现功能,但是设计上有几个问题:

  1. 怪兽和英雄是对立角色,按理怪兽不应该继承英雄这个类
  2. 实际设计中怪兽应该有自己的父类,那肯定就不能再继承英雄父类,JAVA不支持多继承
  3. 进一步抽象的话战斗这个行为不应该是英雄特有的,它应该是更通用的一种行为,比如精灵也可以拥有战斗能力,但是精灵明显不是英雄,所以肯定不能继承英雄父类具备战斗这个行为
  4. 不管是英雄,怪兽,还是精灵,战斗中其实是可以更换武器的以上的继承方式显然无法通过继承来实现。

为了避免上面继承的缺点以及继承不能实现的设计,我们换一种设计思想去实现,比如可以使用接口以及组合这种设计原则也许会更合理。上面已经分析过,战斗这个行为是一个通用的行为,它不和某一类对象绑定,而是由任何对象去持有,所以应该把战斗这个行为抽象为一个接口,然后实现不同的战斗方法。

首先,定义一个战斗的接口类

// 这里把上面的战斗方法定义为一个战斗接口类
public interface IAttack {
   // 只有一个战斗方法
    public void attack();
}

既然是战斗,那肯定就有不用的战斗方式,有的使用斧头,有的使用刀,有的甚至使用木棍,不同的武器战斗技巧不一样,伤害也就不一样。下面是具体的战斗实现类:

// 这个是使用长刀进行战斗的实现类
public class LongKnifeAttack implements IAttack {
    @Override
    public void attack() {
        System.out.println("用长刀战斗");
    }
}
// 这个是用斧头进行战斗的实现类
public class AxeAttack implements IAttack {
    @Override
    public void attack() {
        System.out.println("用斧头攻击");
    }
}
// 这个使用木头进行战斗的实现类
public class WoodenAttack implements IAttack {
    @Override
    public void attack() {
        System.out.println("用棍子攻击");
    }
}

现在战斗的接口和战斗的三个实现类已经完成,接着我们看如何使用接口加组合的方式解决继承带来的问题。

  1. 英雄、怪兽、精灵分属于不同的角色类目,不属于同一个父类
  2. 英雄、怪兽、精灵 都可以拥有战斗这个行为
  3. 英雄、怪兽、精灵 都可以随时切换战斗方式

根据以上三个目标,需要改造原来的英雄相关角色代码

// 新的英雄父类
public class Hero {
    protected String name;
    //内部持有战斗这个接口类,默认实现类是木棍战斗武器
    private IAttack attack = new WoodenAttack();
    //可以切换战斗方法
    protected void setAttack(IAttack attack) {
        this.attack = attack;
    }

    public Hero(String name) {
        this.name = name;
    }
  //英雄的战斗方法委托具体的战斗接口实现
    protected void attack() {
       System.out.print(super.name+",");
        attack.attack();
    }
}

// 一号英雄类,不需要改动
public class Hero1 extends Hero {
    public Hero1(String name) {
        super(name);
    }
    public void walk() {
        System.out.println("英雄");
    }
}

// 2号英雄类
public class Hero2 extends Hero {
    public Hero2(String name) {
        super(name);
    }
    @Override
    protected void attack() {
      // 直接调用父类战斗方法
        super.attack();
    }
}

其次,需要重新定义一个怪兽的父类

// 怪兽的父类,所有的怪兽都具备这个父类的属性和行为
public class Monster {
    protected String name;

    public Monster(String name) {
        this.name = name;
    }
   // 和英雄一样,内部持有战斗这个接口类,默认实现类是木棍战斗武器
    private IAttack attack = new WoodenAttack();
   // 可以切换战斗方式
    protected void setAttack(IAttack attack) {
        this.attack = attack;
    }
   // 怪兽具备的走路行为
    protected void walk() {
        System.out.println("魔兽走路");
    }
   // 委托战斗这个接口去实现战斗,为了和英雄方法区分,这里起一个不同名字
    protected void fight() {
      System.out.print(super.name+",");
        attack.attack();
    }
}

// 1号怪兽,继承了怪兽父类,这里不再继承英雄父类
public class Monster1 extends Monster {
    public Monster1(String name) {
        super(name);
    }
}

// 2号怪兽,继承怪兽父类,不继承英雄父类
public class Monster2 extends Monster {
    public Monster2(String name) {
        super(name);
    }

  // 重写战斗方法
    @Override
    protected void fight() {
        System.out.print(super.name+",");
        super.fight();
    }
}

英雄,怪兽类分别改造完后,需要再改造测试类

public class ClientTest {
    public static void main(String[] args) {
        // 分别创建两个英雄,两个怪兽,他们内部都默认使用木棍作为战斗武器
        Hero h1 = new Hero1("英雄1");
        Hero h2 = new Hero2("英雄2");
        Monster m1 = new Monster1("怪兽1");
        Monster m2 = new Monster1("怪兽2");
        // 调用战斗方法
        h1.attack();
        h2.attack();
        // 调用战斗方法
        m1.fight();
        m2.fight();
    }
}

运行后输入结果如下:

可以看到,英雄和怪兽都可以使用战斗接口去实现自己的战斗方法。接着可以让角色自由切换战斗方式。

public class ClientTest {
    public static void main(String[] args) {
        // 创建长刀战斗
        IAttack  a1=new LongKnifeAttack();
        // 创建战斧战斗
        IAttack  a2=new AxeAttack();
        Hero h1 = new Hero1("英雄1");
        Hero h2 = new Hero2("英雄2");
       // 2号英雄切换战斗方式为长刀
        h2.setAttack(a1);
        Monster m1 = new Monster1("怪兽1");
      // 2号怪兽切换战斗方式为斧头
        Monster m2 = new Monster1("怪兽2");
        m2.setAttack(a2);
        h1.attack();
        h2.attack();
        m1.fight();
        m2.fight();
    }
}

运行后,最终输出结果如下:

可以看到,最终不管是英雄还是怪兽都可以默认持有战斗这个行为,但是都可以自由的切换战斗方式。

总结

  1. 继承和接口都可以复用代码,减少代码实现
  2. 继承强调的“是”一个,接口(组合)强调的是“有”一个
  3. 继承会让行为绑定在同一类对象上,其他类别对象无法共享行为
  4. 接口可以把通用的行为抽象封装,让不同类对象可以共享这些行为,进一步复用代码
  5. 接口配合组合原则,可以让类的设计更具有松耦合的特性,因为具体的行为可以延迟到在运行时切换
  6. 多用组合少用继承,能用组合就不用继承

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表