JavaRush/Java блог/Архив info.javarush/Наследование против композиции в Java
dio
16 уровень

Наследование против композиции в Java

Статья из группы Архив info.javarush
участников
Эта статья иллюстрирует концепции наследования и композиции в Java. Первый пример демонстрирует наследование, а затем показывается как улучшить дизайн наследования с использованием композиции. О том как выбрать между ними, мы подытожим в конце. Наследование против композиции в Java - 1

1. Наследование

Давайте предположим, что у нас есть класс Insect (англ насекомое) Этот класс содержит два метода: 1. move() (с англ. передвигаться) и 2. attack() (с англ. атаковать)
class Insect {
	private int size;
	private String color;

	public Insect(int size, String color) {
		this.size = size;
		this.color = color;
	}

	public int getSize() {
		return size;
	}

	public void setSize(int size) {
		this.size = size;
	}

	public String getColor() {
		return color;
	}

	public void setColor(String color) {
		this.color = color;
	}

	public void move() {
		System.out.println("Move");
	}

	public void attack() {
		move(); //assuming an insect needs to move before attacking
		System.out.println("Attack");
	}
}
Теперь вы хотите определить класс Bee(англ пчела), который является одним из видов Insect, но имеет различные реализации attack() and move(). Это может быть оформленно с помощью наследования:
class Bee extends Insect {
	public Bee(int size, String color) {
		super(size, color);
	}

	public void move() {
		System.out.println("Fly");
	}

	public void attack() {
		move();
		super.attack();
	}
}
public class InheritanceVSComposition {
	public static void main(String[] args) {
		Insect i = new Bee(1, "red");
		i.attack();
	}
}
Диаграмма иерархии классов довольно проста: Наследование против композиции в Java - 2Результат выполнения:
Fly
Fly
Attack
"Fly" напечатано дважды, следовательно метод move() вызывается два раза. Но он должен вызываться только один раз. Проблема вызвана методом super.attack(). Метод attack () вызывает метод move() класса Insect. Когда подкласс вызывает super.attack (), он также вызывает переопределенный метод move(). Что бы исправить проблему мы можем:
  1. Устранить метод attack() подкласса. Это сделает подкласс зависимым от реализации метода attack() суперкласса. Если attack()attack() суперкласса начнет использовать другой метод для перемещения, подкласс нужно будет тоже изменить. Это плохо инкапсуляции.
  2. Переписать метод attack() следующим образом:

    public void attack() {
    	move();
    	System.out.println("Attack");
    }
  3. Это гарантирует правильный результат, потому что подкласс больше не зависит от суперкласса. Однако код является дубликатом суперкласса. ( метод attack() делает более сложные вещи, чем просто вывод строки). Это не соответствует правильному конструированию программного обеспечения, повторяющегося кода не должно быть.

Эта конструкция наследования плоха тем что подкласс зависит от деталей реализации своего суперкласса. Если изменится суперкласс, подкласс не будет верно работать.

2. Композиция

Вместо наследования можно использовать композицию. Давайте посмотрим на решение с ее помощью. Функция attack() абстрагируется как интерфейс.
interface Attack {
	public void move();
	public void attack();
}
Различные виды атаки могут быть определены путем реализации интерфейса Attack.
class AttackImpl implements Attack {
	private String move;
	private String attack;

	public AttackImpl(String move, String attack) {
		this.move = move;
		this.attack = attack;
	}

	@Override
	public void move() {
		System.out.println(move);
	}

	@Override
	public void attack() {
		move();
		System.out.println(attack);
	}
}
Поскольку функция атаки внешняя, класс Insect ее больше не содержит.
class Insect {
	private int size;
	private String color;

	public Insect(int size, String color) {
		this.size = size;
		this.color = color;
	}

	public int getSize() {
		return size;
	}

	public void setSize(int size) {
		this.size = size;
	}

	public String getColor() {
		return color;
	}

	public void setColor(String color) {
		this.color = color;
	}
}
Класс Bee(c англ Пчела), как тип Insect может атаковать.
// This wrapper class wrap an Attack object
class Bee extends Insect implements Attack {
	private Attack attack;

	public Bee(int size, String color, Attack attack) {
		super(size, color);
		this.attack = attack;
	}

	public void move() {
		attack.move();
	}

	public void attack() {
		attack.attack();
	}
}
Диаграмма классов: Наследование против композиции в Java - 3
public class InheritanceVSComposition2 {
	public static void main(String[] args) {
		Bee a = new Bee(1, "black", new AttackImpl("fly", "move"));
		a.attack();

		// if you need another implementation of move()
		// there is no need to change Insect, we can quickly use new method to attack

		Bee b = new Bee(1, "black", new AttackImpl("fly", "sting"));
		b.attack();
	}
}
Результат выполнения:
fly
move
fly
sting

3. Когда использовать эти подходы?

Следующие 2 пункта могут помочь выбрать между наследованием и композицией:
  1. если имеете дело с отношением между классами вида "ЯВЛЯЕТСЯ" и класс хочет предоставить все свои интерфейсы другому классу, то наследование предпочтительнее.
  2. если отношение "ИМЕЕТ", то предпочтительнее композиция.
Таким образом, наследование и композиция имеют свои области применения и стоит разобраться в их достоинствах. Ссылки:
  1. Bloch, Joshua. Effective java. Pearson Education India, 2008
  2. https://stackoverflow.com/questions/49002/prefer-composition-over-inheritance
  3. https://www.javaworld.com/article/2076814/core-java/inheritance-versus-composition--which-one-should...
Ссылка на оригинал статьи Перевел
Комментарии (10)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Vlad V
Уровень 35
29 декабря 2022, 16:55
На мой взгляд тут проблема не в наследовании а в кривой реализации методов, как минимум в данном примере.
Александр
Уровень 111
26 сентября 2022, 18:23
Хорошая статья.
xgrothx
Уровень 30
4 февраля 2017, 20:53
А я думал, что композиция — это включение в объект объект другого класса.
class A {
private B b = new B();
}

A a = new A();
Maxym
Уровень 108
Expert
27 мая 2023, 09:27
Так и есть. В данном примере мы не можем создать пчелу не задав ей определенную реализацию Attack через конструктор. Если бы Attack можно было задать после создания объекта пчелы, например через сеттер, тогда бы это была агрегация. Или если бы существовал конструктор без параметра Attack, это также была бы агрегация
ttt
Уровень 30
28 июня 2014, 16:34
У меня еще немного вопросов появилось.
Почему не сработает такая конструкция
public void attack() {
                this.move(); //assuming an insect needs to move before attacking
                System.out.println("Attack");
        }

Потому что this будет указывать на объект Bee?
И второй вопрос.
Если сделать метод move статическим, то вызовы класса наследника будут идти к методу класса родителя, в случае если же нам надо будет вызвать метод у наследника, то это можно сделать по имени класса Bee?
blacky
Уровень 23
28 июня 2014, 17:45
Конструкция this.move(); не сработает потому что она по факту такая и есть =)
IMHO: Тут вместе с наследованием хорошо показана полиморфность методов. Однако этой возможностью автор воспользовался как-то криво. Я понимаю, что статья о наследовании против композиции, но рассматривать отдельно наследование без полиморфизма и композицию без абстракции — это нелепо. А вот это просто шедевр:
«Fly» напечатано дважды, следовательно метод move() вызывается два раза. Но он должен вызываться только один раз.
Это особенность полиморфных методов и можно сделать вывод, что проектировщик ошибся.
На заметку о полиморфизме:
Полиморфизм — это возможность объектов выполнять методы, определенные в интерфейсе (базовый класс или интерфейс), в зависимости от самого типа объекта.
* Классы, связанные отношением наследования и переопределяющие методы, показывают полиморфизм.
* Полиморфные методы должны быть определены в базовом и производных классах.
* Полиморфизм можно реализовать и при помощи классов (в т.ч. абстрактных), и при помощи интерфейсов.
* Для полиморфизма необходимо переопределение методов в производных классах.
* При наследовании связывание переменных экземпляра происходит во время компиляции, а методов — во время выполнения (т.е. для методов это динамическое связывание и проверка типа на этапе выполнения).

Насчет статических методов:
* они не наследуются, т.е. это методы класса и только класса (никаких this и super)
* при вызове статического метода не используется информация об созданном объекте, т.е. используется информация только о типе ссылки
Insect ins = new Insect();
ins.staticMethod(); // будет вызван метод класса Insect
Insect ins2 = new Bee();
ins2.staticMethod(); // будет вызван метод класса Insect
Bee bee = new Bee();
bee.staticMethod(); // будет вызван метод класса Bee
Bee bee2
blacky
Уровень 23
29 июня 2014, 22:09
Да, статические методы следует вызывать используя имя класса, а не ссылку на объект как в примерах выше.
Insect.staticMethod(); // правильно
Bee.staticMethod(); // правильно
blacky
Уровень 23
29 июня 2014, 23:16
Кстати, выделение поведения в интерфейс Attack мне нравится, годно =)
В общем, автор «сией» архитектуры воспользовался несколькими принципами проектирования.
Кратко и лаконично они описаны в хабрастатье "Восемь принципов программирования, которые могут облегчить вам жизнь".

Заметьте, помимо Блоха и stackoverflow, он использовал материалы по Java 1998 года. Как-то странно.
Следующие 2 пункта могут помочь выбрать между наследованием и композицией
1. если имеете дело с отношением между классами вида «ЯВЛЯЕТСЯ» и класс хочет предоставить все свои интерфейсы другому классу, то наследование предпочтительнее.
2. если отношение «ИМЕЕТ», то предпочтительнее композиция.
— это базовые принципы и для этого не обязательно делать UML-диаграммы, а только правильно выделить абстракции, основываясь на правильно сформулированных вопросах. Нужно делегировать поведение или нет? Есть четкая иерархия или нет? Объект состоит из составных частей или нет? Заказчику эта функциональность нужна или можно спроектировать проще? Что сейчас нужно заказчику? И ещё куча правильных вопросов, на которые в разное время мы получим разные ответы, из-за чего программу, возможно, придется перепроектировать. Если кратко, то «нет правильных решений, но есть неправильные решения». Тут напрашивается определение хорошей программы: «хорошая программа хорошо спроектирована, хорошо запрограммирована, проста в сопровождении, повторном использовании и расширении» и самое главное — удовлетворяет требованиям заказчика.

У автора «X Wang», который учится в «University of Delaware» есть хорошие статьи, но конкретно эта — незачет, поскольку тема наследования и композиции не раскрыта.
ttt
Уровень 30
28 июня 2014, 15:47
Вот интересно как раньше этот вопрос решали, например, программисты на с++, ведь интерфейсы появились не так уж и давно, в отличии от классов.
blacky
Уровень 23
28 июня 2014, 15:58
Там эта абстракция просто по-другому называется. Насколько понимаю, аналогом интерфейса из Java будет абстрактный класс с виртуальными функциями в c++. Но лучше всего множественное наследование реализовано в Python.