1. Обзор
Наследование и композиция — наряду с абстракцией, инкапсуляцией и полиморфизмом — являются краеугольными камнями объектно-ориентированного программирования (ООП).
В этом уроке мы рассмотрим основы наследования и композиции, а также сосредоточимся на выявлении различий между двумя типами отношений.
2. Основы наследования
Наследование – это мощный, но чрезмерно используемый и неправильно используемый механизм.
Проще говоря, при наследовании базовый класс (он же базовый тип) определяет состояние и поведение, общие для данного типа, и позволяет подклассам (он же подтипы) предоставлять специализированные версии этого состояния и поведения.
Чтобы иметь четкое представление о том, как работать с наследованием, давайте создадим наивный пример: базовый класс 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); }
Здесь важно подчеркнуть семантический аспект наследования . Помимо повторного использования реализации класса Person , мы создали четко определенную связь “is-a” между базовым типом Person и подтипами Официантка и Актриса . Официантки и актрисы-это, по сути, личности.
Это может заставить нас спросить: в каких случаях использования наследование является правильным подходом?
Если подтипы удовлетворяют условию “is-a” и в основном обеспечивают аддитивную функциональность дальше по иерархии классов, тогда наследование-это путь.
Конечно, переопределение методов допускается до тех пор, пока переопределенные методы сохраняют подстановку базового типа/подтипа, поддерживаемую принципом подстановки Лискова .
Кроме того, мы должны иметь в виду , что подтипы наследуют 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 OptionalgetSoundCard() { 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 удовлетворяет условию “has-a” с классами, моделирующими его части.
Также стоит отметить, что в этом случае содержащий Компьютер объект имеет право собственности на содержащиеся объекты тогда и только тогда, когда объекты не могут быть повторно использованы в другом компьютере объекте. Если бы они могли, мы бы использовали агрегацию, а не композицию, где владение не подразумевается.
5. Композиция Без Абстракции
В качестве альтернативы мы могли бы определить отношения композиции, жестко закодировав зависимости класса Computer , вместо того, чтобы объявлять их в конструкторе:
public class Computer { private StandardProcessor processor = new StandardProcessor("Intel I3"); private StandardMemory memory = new StandardMemory("Kingston", "1TB"); // additional fields / methods }
Конечно, это была бы жесткая, тесно связанная конструкция, как мы бы делали Компьютер сильно зависит от конкретных реализаций Процессор и Память .
Мы бы не воспользовались уровнем абстракции, предоставляемым интерфейсами и внедрением зависимостей .
При первоначальном дизайне, основанном на интерфейсах, мы получаем слабо связанный дизайн, который также легче протестировать.
6. Заключение
В этой статье мы изучили основы наследования и композиции в Java, а также подробно исследовали различия между двумя типами отношений (“is-a” и “has-a”).
Как всегда, все примеры кода, показанные в этом руководстве, доступны на GitHub .