Перейти к основному содержимому

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

· 5 мин. чтения

Задача: Сумма двух

Дано массив целых чисел и целая сумма. Нужно найти индексы двух чисел, сумма которых равна заданной ...

ANDROMEDA

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
}

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

@Test
public void givenWaitressInstance_whenCheckedType_thenIsInstanceOfPerson() {
assertThat(new Waitress("Mary", "mary@domain.com", 22))
.isInstanceOf(Person.class);
}

@Test
public void givenActressInstance_whenCheckedType_thenIsInstanceOfPerson() {
assertThat(new Actress("Susan", "susan@domain.com", 30))
.isInstanceOf(Person.class);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вот как может выглядеть простая реализация класса 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
}

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

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

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

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

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

public class Computer {

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

// additional fields / methods
}

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

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

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

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

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

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