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 .