Рубрики
Без рубрики

Наследование и композиция (Is-a vs Has-a relationship) в Java

Изучите различия между отношениями наследования и композиции в Java.

Автор оригинала: baeldung.

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 Optional 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 удовлетворяет условию “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 .