«1. Обзор

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

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

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

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

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

Чтобы иметь четкое представление о том, как работать с наследованием, давайте создадим наивный пример: базовый класс 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
}

@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);
}

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

Здесь важно подчеркнуть семантический аспект наследования. Помимо повторного использования реализации класса Person, мы создали четко определенное отношение «является» между базовым типом Person и подтипами официантки и актрисы. Официантки и актрисы, по сути, личности.


Это может вызвать у нас вопрос: в каких случаях наследование является правильным подходом?

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

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

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

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

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

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

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

В этом случае мы используем наследование для переноса общего кода в базовый класс (супертип) для каждого уровня.

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

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

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

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

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

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

4. Композиция Основы

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

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

Кроме того, композиция является самой сильной формой ассоциации, что означает, что объект(ы), которые составляют или содержатся в одном объекте, также уничтожаются при уничтожении этого объекта.

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
}

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

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

Следующие классы моделируют микропроцессор, память и звуковую карту (интерфейсы для краткости опущены):

«

public class Computer {

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

«Легко понять мотивы приоритета композиции над наследованием. В каждом сценарии, где возможно установить семантически правильное отношение «имеет-а» между данным классом и другими, композиция является правильным выбором.

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

Также стоит отметить, что в этом случае содержащий объект Computer имеет право собственности на содержащиеся объекты тогда и только тогда, когда объекты нельзя повторно использовать в другом объекте Computer. Если бы они могли, мы бы использовали агрегацию, а не композицию, где право собственности не подразумевается.

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

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

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