Понятия

Вам когда-нибудь приходилось перемещаться по нескольким файлам, чтобы узнать поведение простой функции? Впрочем, приходилось ли вам углубляться в основательный такой класс, выполняющий буквально все, в попытке добавить небольшое улучшение? Если вы сталкивались с такими ситуациями, то, скорее всего, работали с кодом, который обладал сильной связанностью (англ. 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 без дублирования кода. 

Заключение 

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

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

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


Перевод статьи Drioueche Mohammed: Coupling and Cohesion in Object-Oriented Programming

Предыдущая статьяПостквантовая криптография на Python, C и Linux
Следующая статьяRust: рефакторинг для новичков