Эта статья иллюстрирует концепции наследования и композиции в Java. Первый пример демонстрирует наследование, а затем показывается как улучшить дизайн наследования с использованием композиции. О том как выбрать между ними, мы подытожим в конце.
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();
}
}
Диаграмма иерархии классов довольно проста:
Результат выполнения:
Fly
Fly
Attack
"Fly" напечатано дважды, следовательно метод move()
вызывается два раза. Но он должен вызываться только один раз.
Проблема вызвана методом super.attack()
. Метод attack ()
вызывает метод move()
класса Insect
. Когда подкласс вызывает super.attack ()
, он также вызывает переопределенный метод move()
.
Что бы исправить проблему мы можем:
- Устранить метод
attack()
подкласса. Это сделает подкласс зависимым от реализации методаattack()
суперкласса. Еслиattack()attack()
суперкласса начнет использовать другой метод для перемещения, подкласс нужно будет тоже изменить. Это плохо инкапсуляции. Переписать метод
attack()
следующим образом:public void attack() { move(); System.out.println("Attack"); }
Это гарантирует правильный результат, потому что подкласс больше не зависит от суперкласса. Однако код является дубликатом суперкласса. ( метод
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();
}
}
Диаграмма классов:
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 пункта могут помочь выбрать между наследованием и композицией:- если имеете дело с отношением между классами вида "ЯВЛЯЕТСЯ" и класс хочет предоставить все свои интерфейсы другому классу, то наследование предпочтительнее.
- если отношение "ИМЕЕТ", то предпочтительнее композиция.
- Bloch, Joshua. Effective java. Pearson Education India, 2008
- https://stackoverflow.com/questions/49002/prefer-composition-over-inheritance
- https://www.javaworld.com/article/2076814/core-java/inheritance-versus-composition--which-one-should...
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Почему не сработает такая конструкция
Потому что this будет указывать на объект Bee?
И второй вопрос.
Если сделать метод move статическим, то вызовы класса наследника будут идти к методу класса родителя, в случае если же нам надо будет вызвать метод у наследника, то это можно сделать по имени класса Bee?