Вам когда-нибудь приходилось перемещаться по нескольким файлам, чтобы узнать поведение простой функции? Впрочем, приходилось ли вам углубляться в основательный такой класс, выполняющий буквально все, в попытке добавить небольшое улучшение? Если вы сталкивались с такими ситуациями, то, скорее всего, работали с кодом, который обладал сильной связанностью (англ. coupling) и слабой связностью (англ. cohesion).
В связи с этим возникают вопросы: что такое сильная связанность и слабая связность? Почему код с такими характеристиками считается плохим? Как писать код получше?
Чтобы ответить на поставленные вопросы, рассмотрим понятия связанности и связности.
Связанность
В объектно-ориентированном программировании (ООП) под связанностью понимается степень прямой осведомленности одного элемента о другом. Другими словами, как часто изменения в классе
A
приводят к соответствующим изменениям в классеB
.
Рассмотрим следующий фрагмент кода:
public class Order {
private CashPayment payment = new CashPayment();
public void processPayment() {
payment.process();
}
}
public class CashPayment {
public void process() {
// Логика процесса
}
}
Класс Order
напрямую зависит от класса CashPayment
, что затрудняет внесение изменений в класс CashPayment
без воздействия на класс Order
. В этом случае считается, что класс Order
сильно связан с классом CashPayment
.
Чтобы ослабить связанность кода, можно ввести абстракции в класс CashPayment
. Ниже обновленный вариант кода:
public class Order {
private PaymentType payment;
public Order(PaymentType paymentType) {
payment = paymentType;
}
public void processPayment() {
payment.process();
}
}
public interface PaymentType {
public void process();
}
public class CashPayment implements PaymentType {
public void process() {
// Логика процесса
}
}
В обновленном варианте представлен интерфейс PaymentType
, который определяет метод process()
. Теперь класс Order
зависит от интерфейса PaymentType
, а не от класса CashPayment
. С помощью интерфейса мы отвязываем класс Order
от конкретной реализации класса CashPayment
.
Это изменение обеспечивает большую гибкость. Оно также позволяет классу Order
взаимодействовать с любым классом, который реализует интерфейс PaymentType
. В данном случае можно легко добавить класс CreditCardPayment
без изменения класса Order
.
Итак, признак хорошего кода — слабая связанность, поскольку изменения осуществляются в сопряженном классе.
Связность
В ООП связность определяет то, как спроектирован отдельный класс. Она гарантирует, что класс разработан с единой и четко сформулированной целью. Другими словами, если все составляющие класса похожи по многим аспектам, то он обладает сильной связностью.
Рассмотрим фрагмент кода:
public class Circle {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getArea() {
return 3.14 * radius * radius;
}
public double getPerimeter() {
return 2 * 3.14 * radius;
}
public String toString() {
return "Circle (radius: " + radius + ")";
}
public void printDetails() {
System.out.println("Shape: " + this.toString());
System.out.println("Area: " + getArea());
System.out.println("Perimeter: " + getPerimeter());
}
}
У класса Circle
есть атрибут radius
, а также методы для вычисления площади, периметра и вывода данных окружности. Однако метод printDetails()
нарушает принцип единственной ответственности, поскольку совмещает вывод данных с вычислением площади и периметра. В таком случае считается, что код обладает слабой связностью.
Чтобы улучшить связность, следует разделить ответственность путем создания нового класса для отображения данных окружности:
public class Circle {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getArea() {
return 3.14 * radius * radius;
}
public double getPerimeter() {
return 2 * 3.14 * radius;
}
public String toString() {
return "Circle (radius: " + radius + ")";
}
}
public class CircleDetailsPrinter {
private Circle circle;
public CircleDetailsPrinter(Circle circle) {
this.circle = circle;
}
public void printDetails() {
System.out.println("Shape: " + circle.toString());
System.out.println("Area: " + circle.getArea());
System.out.println("Perimeter: " + circle.getPerimeter());
}
}
В обновленном варианте кода представлен класс CircleDetailsPrinter
, который обрабатывает вывод данных окружности. Теперь класс Circle
передает ответственность за вывод данных классу CircleDetailsPrinter
.
Отделяя функциональность вывода в обособленный класс мы добиваемся высокой связности. Каждый класс имеет одну ответственность, что способствует повышению чистоты кода и удобства его сопровождения.
Обратите внимание, что мы улучшили связность, разделив ответственность между двумя классами. Однако в этом деле важно соблюдать баланс и избегать излишнего разделения классов. Задача в том, чтобы создавать специализированные классы с четкими зонами ответственности без ненужного разделения их на множество небольших частей.
Признак хорошего кода — это сильная связность, поскольку изменения осуществляются в сопряженном классе.
С понятием связности мы разобрались, теперь применим полученные знания о связанности к последнему примеру. Как видно, класс CircleDetailsPrinter
обладает сильной связанностью с классом Circle
. Отделим класс CircleDetailsPrinter
от конкретной реализации класса Circle
:
public interface Shape {
public double getArea();
public double getPerimeter();
}
public class Circle implements Shape {
// ...
}
public class Square implements Shape {
// ...
}
public class Rectangle implements Shape {
// ...
}
public class ShapeDetailsPrinter {
private Shape shape;
public ShapeDetailsPrinter(Shape shape) {
this.shape = shape;
}
public void printDetails() {
System.out.println("Shape: " + shape.toString());
System.out.println("Area: " + shape.getArea());
System.out.println("Perimeter: " + shape.getPerimeter());
}
}
Введение интерфейса Shape
позволило отделить класс CircleDetailsPrinter
от реализации класса Circle
. Кроме того, мы используем новый класс ShapeDetailsPrinter
для обслуживания классов Square
и Rectangle
без дублирования кода.
Заключение
В заключении отметим, что характеристиками наилучшего кода являются слабая связанность и сильная связность. Мы рассмотрели примеры кода с сильной связанностью и слабой связностью, выявили трудности, сопряженные с обслуживанием такого кода, и предложили варианты решения проблемы.
Читайте также:
- Знакомство с фабричным методом
- Принципы SOLID в инженерии данных. Часть 1
- Краткое руководство по ООП в JS
Читайте нас в Telegram, VK и Дзен
Перевод статьи Drioueche Mohammed: Coupling and Cohesion in Object-Oriented Programming