JavaRush /Java блог /Random UA /Спадкування проти композиції в Java
dio
16 рівень
Москва

Спадкування проти композиції в Java

Стаття з групи Random UA
Ця стаття ілюструє концепцію спадкування та композиції в 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(з англ Бджола) як тип 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. Ефективний 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...
Посилання на оригінал статті Переклав
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ