Наследование и композиция (отношение Is-a против Has-a) в Java

Наследование и состав (отношение Is-a против Has-a) в Java

1. обзор

Inheritance и состав - наряду с абстракцией, инкапсуляцией и полиморфизмом - являются краеугольными камнямиobject-oriented programming (ООП).

В этом руководстве мы рассмотрим основы наследования и композиции и сосредоточимся на выявлении различий между двумя типами отношений.

2. Основы наследования

Наследование - это мощный механизм, которым злоупотребляют и используют неправильно.

Проще говоря, с наследованием, базовый класс (a.k.a. базовый тип) определяет общее состояние и поведение для данного типа и позволяет подклассам (a.k.a. подтипы) предоставляют специализированные версии этого состояния и поведения.

Чтобы иметь четкое представление о том, как работать с наследованием, давайте создадим наивный пример: базовый классPerson, который определяет общие поля и методы для человека, а подклассыWaitress иActress предоставляют дополнительные детализированные реализации методов.

Вот классPerson:

public class Person {
    private final String name;

    // other fields, standard constructors, getters
}

И это подклассы:

public class Waitress extends Person {

    public String serveStarter(String starter) {
        return "Serving a " + starter;
    }

    // additional methods/constructors
}
public class Actress extends Person {

    public String readScript(String movie) {
        return "Reading the script of " + movie;
    }

    // additional methods/constructors
}

Кроме того, давайте создадим модульный тест, чтобы убедиться, что экземпляры классовWaitress иActress также являются экземплярамиPerson, тем самым показывая, что условие «is-a» выполняется в уровень типа:

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

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

It’s important to stress here the semantic facet of inheritance. Помимо повторного использования реализацииPerson class,we’ve created a well-defined “is-a” relationship между базовым типомPerson и подтипамиWaitress иActress. Официантки и актрисы, по сути, люди.

Это может заставить нас спросить:in which use cases is inheritance the right approach to take?

Если подтипы удовлетворяют условию «is-a» и в основном обеспечивают дополнительную функциональность ниже по иерархии классов, тогда наследование - это правильный путь.

Конечно, переопределение метода разрешено, пока переопределенные методы сохраняют заменяемость базового типа / подтипа, продвигаемуюLiskov Substitution Principle.

Кроме того, мы должны помнить, чтоthe subtypes inherit the base type’s API, что в некоторых случаях может быть чрезмерным или просто нежелательным.

В противном случае мы должны использовать композицию вместо этого.

3. Наследование в шаблонах проектирования

Хотя все согласны с тем, что мы должны отдавать предпочтение композиции, а не наследованию, есть несколько типичных случаев использования, где наследование имеет свое место.

3.1. Паттерн супертипа слоя

В этом случае weuse inheritance to move common code to a base class (the supertype), on a per-layer basis.

Вот базовая реализация этого шаблона на уровне домена:

public class Entity {

    protected long id;

    // setters
}
public class User extends Entity {

    // additional fields and methods
}

Мы можем применить тот же подход к другим уровням системы, таким как уровни обслуживания и постоянства.

3.2. Шаблон метода шаблона

В шаблоне шаблонного метода мы можемuse a base class to define the invariant parts of an algorithm, and then implement the variant parts in the subclasses:

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. Основы композиции

Композиция - это еще один механизм, предоставляемый ООП для повторного использования реализации.

Вкратце,composition allows us to model objects that are made up of other objects, таким образом, определяя связь «имеет» между ними.

Кроме того,the composition is the strongest form of association, что означает, чтоthe object(s) that compose or are contained by one object are destroyed too when that object is destroyed.

Чтобы лучше понять, как работает композиция, предположим, что нам нужно работать с объектами, которые представляют компьютеры.

Компьютер состоит из разных частей, включая микропроцессор, память, звуковую карту и т. Д., Поэтому мы можем моделировать компьютер и каждую его часть как отдельные классы.

Вот как может выглядеть простая реализация классаComputer:

public class Computer {

    private Processor processor;
    private Memory memory;
    private SoundCard soundCard;

    // standard getters/setters/constructors

    public Optional 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
}

Легко понять мотивы, стоящие за отталкиванием композиции от наследования. In every scenario where it’s possible to establish a semantically correct “has-a” relationship between a given class and others, the composition is the right choice to make.с

В приведенном выше примереComputer удовлетворяет условию «has-a» с классами, которые моделируют его части.

Также стоит отметить, что в этом случаеthe containing Computer object has ownership of the contained objects if and only if the objects can’t be reused within another Computer object.. Если они могут, мы будем использовать агрегирование, а не состав, где право собственности не подразумевается.

5. Композиция без абстракции

В качестве альтернативы мы могли бы определить отношение композиции, жестко закодировав зависимости классаComputer, вместо того, чтобы объявлять их в конструкторе:

public class Computer {

    private StandardProcessor processor
      = new StandardProcessor("Intel I3");
    private StandardMemory memory
      = new StandardMemory("Kingston", "1TB");

    // additional fields / methods
}

Конечно, это будет жесткая, тесно связанная конструкция, поскольку мы сделаемComputer сильно зависимым от конкретных реализацийProcessor иMemory.

Мы бы не воспользовались уровнем абстракции, обеспечиваемым интерфейсами иdependency injection.

С первоначальным дизайном, основанным на интерфейсах, мы получаем слабосвязанный дизайн, который также легче тестировать.

6. Заключение

В этой статье мы изучили основы наследования и композиции в Java, а также подробно изучили различия между двумя типами отношений («is-a» против "имеет").

Как всегда, все примеры кода, показанные в этом руководстве, доступныover on GitHub.