
Краткое содержание:
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
- Сильная связанность: изменения в родительских классах сказываются на дочерних.
- Нарушение инкапсуляции: детали родительской реализации просачиваются в дочернюю.
- Сложности с рефакторингом: глубокие иерархии со временем становятся хрупкими.
Has-A
- Повышенная детализация: композиции часто требуется больше шаблонного кода.
- Потенциальные накладные расходы: делегирование вызовов между объектами-компонентами чревато увеличением сложности.
Внимание!
На Go не поддерживается отношение is-a: вместо традиционного наследования здесь используется композиция через встраивание. Таким образом реализуется принцип «композиция вместо наследования», чем обеспечиваются слабая связанность и гибкость.
Заключение
Понимая и эффективно применяя отношения is-a и has-a, разработчики создают надежные, масштабируемые и сопровождаемые системы.
Благодаря использованию вместе с отношением has-a шаблона «Стратегия», посредством инкапсуляции алгоритмов, которые становятся взаимозаменяемыми, обеспечивается модульный дизайн, а при помощи внедрения зависимостей привносится динамическое поведение.
Это делается с соблюдением ключевых принципов проектирования, например принципа единственной ответственности — разделением поведений на специализированные стратегии, принципа открытости/закрытости — добавлением новых стратегий без изменения имеющегося кода и принципа подстановки Лисков — обеспечением соответствия взаимозаменяемых стратегий общему интерфейсу.
Совместным применением таких подходов обеспечиваются гибкость, переиспользуемость, чистота кода. В современной программной разработке эти инструменты незаменимы.
Читайте также:
- Как решить реальную задачу при помощи структурированной конкурентности и виртуальных потоков Java 21
- Как узнать, допускает ли изменения коллекция в Java?
- Java: оператор try-with-resources
Читайте нас в Telegram, VK и Дзен
Перевод статьи Shantanu Saini: is-A vs has-A





