继承和组合

1.概述

继承和组合 - 以及抽象,封装和多态 - 是面向对象编程(OOP)的基石。

在本教程中,我们将介绍继承和组合的基础知识,并且我们将重点关注发现两种类型关系之间的差异。

2.继承的基础

继承是一种强大但过度使用和滥用的机制。

简单地说,通过继承,基类(基类型)定义了给定类型的公共状态和行为,并让子类(子类型)提供该状态和行为的专用版本。

为了清楚地了解如何使用继承,让我们创建一个简单的例子:基类Person定义关于人的公共字段和方法,而子类WaitressActress提供额外的,细粒度的方法实现。

这是Person类:

public class Person {
    private final String name;
 
    // other fields, standard constructors, getters
}

这些是子类:

  • Waitress类
public class Waitress extends Person {
 
    public String serveStarter(String starter) {
        return "Serving a " + starter;
    }
     
    // additional methods/constructors
}
  • Actress类
public class Actress extends Person {
     
    public String readScript(String movie) {
        return "Reading the script of " + movie;
    } 
     
    // additional methods/constructors
}

另外,让我们创建一个单元测试来验证WaitressActress类的实例也是Person的实例,从而表明在类型级别满足is-a条件:

@Test
public void givenWaitressInstance_whenCheckedType_thenIsInstanceOfPerson() {
    assertThat(new Waitress("jsaon", "[email protected]", 22))
      .isInstanceOf(Person.class);
}
     
@Test
public void givenActressInstance_whenCheckedType_thenIsInstanceOfPerson() {
    assertThat(new Actress("joker", "[email protected]", 30))
      .isInstanceOf(Person.class);
}

在这里强调继承的语义方面很重要。 除了重用Person类的实现之外,我们在基类Person和子类WaitressActress之间创建了一个明确定义的is-a关系。 女服务员和女演员实际上都是人。

我们可能会有个疑问:什么时候使用继承是正确的?

如果子类型满足is-a条件并且主要在类层次结构中提供附加功能,则使用继承。

当然,在重写方法要遵循里氏替换原则所提倡的基本类型/子类型的可替代性。

不过,子类型继承了基类的API,在某些情况下可能是不需要或者不合理。这时候我们就需要使用组合了。

3.设计模式中的继承

虽然我们应该尽可能地支持组合而不是继承,但是有一些典型的用例,使用继承会更好一些。

3.1. 层超类型模式(The Layer Supertype Pattern)

在这种情况下,我们使用继承将公共代码基于每层移动到基类(超类型)。

以下是domain层中此模式的基本实现:

public class Entity {
     
    protected long id;
     
    // setters
}
public class User extends Entity {
     
    // additional fields and methods   
}

我们可以将相同的方法应用于系统中的其他层,例如服务层和持久层。

3.2. 模版方法模式

在模板方法模式中,我们可以使用基类来定义算法的不变部分,然后在子类中实现变体部分:

public abstract class ComputerBuilder {
     
    public final Computer buildComputer() {
        addProcessor();
        addMemory();
    }
     
    public abstract void addProcessor();
     
    public abstract void addMemory();
}
public class StandardComputerBuilder extends ComputerBuilder {
 
    @Override
    public void addProcessor() {
        // method implementation
    }
     
    @Override
    public void addMemory() {
        // method implementation
    }
}

4.组合

组合是OOP提供的另一种重用实现的机制。

简而言之,组合允许我们对由其他对象组成的对象进行建模,从而在它们之间定义“has-a”关系。

此外,组合有很强的关联性,这意味着当一个对象被破坏时,由一个对象组成或包含的对象也被破坏。

为了更好地理解组合的工作原理,假设我们需要使用代表计算机的对象。

计算机由不同的部分组成,包括微处理器,存储器,声卡等,因此我们可以将计算机及其每个部分建模为单独的类。

以下是Computer类的简单实现:

public class Computer {
 
    private Processor processor;
    private Memory memory;
    private SoundCard soundCard;
 
    // standard getters/setters/constructors
     
    public Optional<SoundCard> getSoundCard() {
        return Optional.ofNullable(soundCard);
    }
}

以下类为微处理器,内存和声卡建模(为简洁起见,省略了接口):

public class StandardProcessor implements Processor {
 
    private String model;
     
    // standard getters/setters
}
public class StandardMemory implements Memory {
     
    private String brand;
    private String size;
     
    // standard constructors, getters, toString
}
public class StandardSoundCard implements SoundCard {
     
    private String brand;
 
    // standard constructors, getters, toString
}

在每个可以在给定类和其他类之间建立语义正确的“has-a”关系的场景中,组合是正确的选择。

在上面的示例中,计算机满足“has-a”条件,其中包含对其零件进行建模的类。

值得注意的是,在这种情况下,当且仅当对象不能在另一个Computer对象中重用时,Computer对象才拥有所包含对象的所有权。 如果可以重用,我们将使用聚合而不是组合,不隐含所有权。

5.没有抽象的组合

我们可以通过硬编码Computer类的依赖关系来定义组合关系,而不是在构造函数中声明它们:

public class Computer {
 
    private StandardProcessor processor
      = new StandardProcessor("Intel I3");
    private StandardMemory memory
      = new StandardMemory("Kingston", "1TB");
     
    // additional fields / methods
}

当然,这将是一种紧耦合的设计,因为我们的Computer类依赖于处理器和内存的特定实现。

我们不能利用接口和依赖注入提供的抽象级别。

通过基于接口的初始设计,我们可以获得了松散耦合设计,这也更容易测试。

6. 结论

在本文中,我们学习了Java中继承和组合的基础知识,并深入探讨了两种关系之间的差异(“is-a”与“has-a”)。