Краткое содержание:

has-A используется везде, где возможно,

is-A ~ наследование,

has-A ~ интерфейсы.

Отношения Is-A и Has-A

В программной разработке при создании сопровождаемых, масштабируемых и гибких систем важно понимать взаимосвязи между объектами. Две фундаментальные концепции объектно-ориентированного программирования  —  is-a наследование и has-a композиция. Этими отношениями разработчики эффективно моделируют реальные задачи, соблюдая принципы проектирования SOLID.

Когда

Is-A

  • Иерархические отношения: когда имеется четкая иерархия «родитель-потомок», например собака Dog  —  это животное Animal.
  • Переиспользуемость кода: от общего базового класса наследуется общее поведение.
  • Полиморфизм: реализуются родительские методы, переопределяемые дочерними классами для специализированного поведения.
  • Четко определенные иерархии: для стабильных, неизменяемых структур вроде таксономии.

Has-A

  • Отношения «часть-целое»: когда один объект находится во владении другого или используется им, например у автомобиля Car имеется двигатель Engine.
  • Динамические изменения поведения: для изменения поведения объекта используются различные компоненты во время выполнения.
  • Инкапсуляция: абстрактные детали реализации.
  • Гибкость: уменьшается сильная связанность в коде, чем упрощается изменение систем.

Для чего

  • Is-A: обеспечивается переиспользование кода, поддерживается полиморфизм, упрощаются иерархические отношения.
  • Has-A: разделением компонентов повышается модульность, обеспечивается гибкость, упрощается тестирование.

Где

Is-A:

  • Иерархическая таксономия, например животные, автомобили или организационные роли.
  • Фреймворки, для расширения функциональности которых активно применяется наследование, например фреймворки с графическим интерфейсом.
  • Случаи, когда подклассы органично вписываются в родительский класс.

Has-A:

  • Системы с взаимозаменяемым поведением, такие как платежные операторы или системы аутентификации.
  • Где требуется совместная работа объектов, например автомобиля с двигателем, колесами и сиденьями.
  • Сценарии, в которых требуются динамические изменения компонентов без влияния на ядро системы.

Как

Is-A

  • Наследование: создается подкласс, которым наследуются атрибуты и поведение расширяемого таким образом базового класса.
  • Полиморфизм: в подклассе переопределяются методы для специализированного поведения.
  • Естественные иерархии: поддерживаются четкие отношения, в которых подклассы естественно вписываются в базовый класс.

has-A

  • Композиция: используются переменные экземпляра, в которых содержатся ссылки на другие объекты.
  • Инкапсуляция: в методах инкапсулируется функциональность объекта, на который ссылаются.
  • Внедрение зависимостей: для уменьшения связанности необходимые объекты передаются во время построения.

Код

Is-A

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
// «Task» - это универсальная задача с общими атрибутами
abstract class Task {
private String id;
private LocalDateTime createdAt;
private String description;

public Task(String id, String description) {
this.id = id;
this.createdAt = LocalDateTime.now();
this.description = description;
}
public String getId() {
return id;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public String getDescription() {
return description;
}
// Абстрактный метод для выполнения задачи реализуется подклассами
public abstract void executeTask();
}
// Операции базы данных управляются в «DatabaseTask»
class DatabaseTask extends Task {
private String query;
private String dbName;
private boolean connected = false;
public DatabaseTask(String id, String description, String query, String dbName) {
super(id, description);
this.query = query;
this.dbName = dbName;
}
@Override
public void executeTask() {
if (!connected) {
System.out.printf("[DatabaseTask] Connecting to database: %s%n", dbName);
connected = true;
}
System.out.printf("[DatabaseTask] Task ID: %s - Description: %s%n", getId(), getDescription());
System.out.printf("[DatabaseTask] Executing query: %s%n", query);
}
}
// Операции, связанные с API, управляются в «APITask»
class APITask extends Task {
private String url;
private Map<String, String> headers;
public APITask(String id, String description, String url, Map<String, String> headers) {
super(id, description);
this.url = url;
this.headers = headers;
}
@Override
public void executeTask() {
System.out.printf("[APITask] Task ID: %s - Description: %s%n", getId(), getDescription());
System.out.printf("[APITask] Sending request to URL: %s with headers: %s%n", url, headers);
}
}
// Файловые операции управляются в «FileProcessingTask»
class FileProcessingTask extends Task {
private String filePath;
private String fileType;
public FileProcessingTask(String id, String description, String filePath, String fileType) {
super(id, description);
this.filePath = filePath;
this.fileType = fileType;
}
@Override
public void executeTask() {
System.out.printf("[FileProcessingTask] Task ID: %s - Description: %s%n", getId(), getDescription());
System.out.printf("[FileProcessingTask] Processing file: %s of type: %s%n", filePath, fileType);
}
}
// Основной класс
public class IsARelationshipExample {
public static void main(String[] args) {
// Конфигурирование «DatabaseTask»
Task dbTask = new DatabaseTask(
"DB001",
"Run a database query",
"SELECT * FROM users",
"UserDB"
);
dbTask.executeTask();
// Конфигурирование «APITask»
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer token");
headers.put("Content-Type", "application/json");
Task apiTask = new APITask(
"API001",
"Send an API request",
"https://api.example.com/data",
headers
);
apiTask.executeTask();
// Конфигурирование «FileProcessingTask»
Task fileTask = new FileProcessingTask(
"FILE001",
"Process a CSV file",
"/data/files/report.csv",
"CSV"
);
fileTask.executeTask();
}
}

has-A

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

// «Task» - это универсальная задача с общими атрибутами
class Task {
private String id;
private LocalDateTime createdAt;
private String description;
private TaskStrategy strategy; // Отношение «Has-a» с «TaskStrategy»

public Task(String id, String description, TaskStrategy strategy) {
this.id = id;
this.createdAt = LocalDateTime.now();
this.description = description;
this.strategy = strategy; // Внедрение зависимостей для стратегии
}

public String getId() {
return id;
}

public LocalDateTime getCreatedAt() {
return createdAt;
}

public String getDescription() {
return description;
}

public void executeTask() {
if (strategy == null) {
throw new IllegalStateException("Task strategy not set!");
}
strategy.execute(this); // Выполнение делегируется стратегии
}
}

// Поведение для выполняемых задач определяется в «TaskStrategy»
interface TaskStrategy {
void execute(Task task);
}

// Операции базы данных управляются в «DatabaseTask»
class DatabaseTaskStrategy implements TaskStrategy {
private String query;
private String dbName;
private boolean connected = false;

public DatabaseTaskStrategy(String query, String dbName) {
this.query = query;
this.dbName = dbName;
}

@Override
public void execute(Task task) {
if (!connected) {
System.out.printf("[DatabaseTask] Connecting to database: %s%n", dbName);
connected = true;
}
System.out.printf("[DatabaseTask] Task ID: %s - Description: %s%n", task.getId(), task.getDescription());
System.out.printf("[DatabaseTask] Executing query: %s%n", query);
}
}

// Операции, связанные с API, управляются в «APITask»
class APITaskStrategy implements TaskStrategy {
private String url;
private Map<String, String> headers;

public APITaskStrategy(String url, Map<String, String> headers) {
this.url = url;
this.headers = headers;
}

@Override
public void execute(Task task) {
System.out.printf("[APITask] Task ID: %s - Description: %s%n", task.getId(), task.getDescription());
System.out.printf("[APITask] Sending request to URL: %s with headers: %s%n", url, headers);
}
}

// Файловые операции управляются в «FileProcessingTask»
class FileProcessingTaskStrategy implements TaskStrategy {
private String filePath;
private String fileType;

public FileProcessingTaskStrategy(String filePath, String fileType) {
this.filePath = filePath;
this.fileType = fileType;
}

@Override
public void execute(Task task) {
System.out.printf("[FileProcessingTask] Task ID: %s - Description: %s%n", task.getId(), task.getDescription());
System.out.printf("[FileProcessingTask] Processing file: %s of type: %s%n", filePath, fileType);
}
}

// Основной класс
public class StrategyPatternHasARelationship {
public static void main(String[] args) {
// Конфигурирование «DatabaseTask»
Task dbTask = new Task(
"DB001",
"Run a database query",
new DatabaseTaskStrategy("SELECT * FROM users", "UserDB")
);
dbTask.executeTask();

// Конфигурирование «APITask»
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer token");
headers.put("Content-Type", "application/json");
Task apiTask = new Task(
"API001",
"Send an API request",
new APITaskStrategy("https://api.example.com/data", headers)
);
apiTask.executeTask();

// Конфигурирование «FileProcessingTask»
Task fileTask = new Task(
"FILE001",
"Process a CSV file",
new FileProcessingTaskStrategy("/data/files/report.csv", "CSV")
);
fileTask.executeTask();
}
}

Проблемы при неправильном использовании

Is-A

  1. Сильная связанность: изменения в родительских классах сказываются на дочерних.
  2. Нарушение инкапсуляции: детали родительской реализации просачиваются в дочернюю.
  3. Сложности с рефакторингом: глубокие иерархии со временем становятся хрупкими.

Has-A

  1. Повышенная детализация: композиции часто требуется больше шаблонного кода.
  2. Потенциальные накладные расходы: делегирование вызовов между объектами-компонентами чревато увеличением сложности.

Внимание!

На Go не поддерживается отношение is-a: вместо традиционного наследования здесь используется композиция через встраивание. Таким образом реализуется принцип «композиция вместо наследования», чем обеспечиваются слабая связанность и гибкость.

Заключение

Понимая и эффективно применяя отношения is-a и has-a, разработчики создают надежные, масштабируемые и сопровождаемые системы.

Благодаря использованию вместе с отношением has-a шаблона «Стратегия», посредством инкапсуляции алгоритмов, которые становятся взаимозаменяемыми, обеспечивается модульный дизайн, а при помощи внедрения зависимостей привносится динамическое поведение.

Это делается с соблюдением ключевых принципов проектирования, например принципа единственной ответственности  —  разделением поведений на специализированные стратегии, принципа открытости/закрытости  —  добавлением новых стратегий без изменения имеющегося кода и принципа подстановки Лисков  —  обеспечением соответствия взаимозаменяемых стратегий общему интерфейсу.

Совместным применением таких подходов обеспечиваются гибкость, переиспользуемость, чистота кода. В современной программной разработке эти инструменты незаменимы.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Shantanu Saini: is-A vs has-A

Предыдущая статья10 конструкций для написания Bash-скриптов
Следующая статьяApple убивает Swift