Два предыдущих принципа звучат как прямая противоположность друг другу. High Cohesion требует собрать связанное вместе, Low Coupling – наоборот, всё развязать и разнести. Любой из них легко довести до абсурда поодиночке: свалить систему в один класс – связность предельная; растащить каждый метод в отдельный класс за интерфейсом – зацепления почти нет. Оба предела дают негодный код.
Противоречие исчезает, если заметить, что принципы считают разные связи:
- связность – это связи внутри модуля, и их должно быть много: что меняется вместе, то и лежит вместе;
- зацепление – это связи, пересекающие границу модуля, и их должно быть мало.
Одна и та же связь между двумя классами попадёт то в одну корзину, то в другую – смотря по какую сторону границы эти классы оказались. Поэтому спорят принципы не между собой: они вдвоём задают вам один вопрос – где провести границу. Провели по смыслу – связи остались внутри и стали связностью. Провели поперёк смысла – те же самые связи вылезли наружу и стали зацеплением.
Отсюда главное: связи не убирают, их перекладывают. Разрезали класс пополам – связность каждой половины упала, а между половинами появилось зацепление, которого раньше не было.
Четыре угла
Если развести оба свойства по осям, получается четыре типа кода:
Дальше – каждый угол по отдельности, от худшего к лучшему. На схемах цвет кружка это смысл (что к чему относится по задаче), а рамка – граница модуля: класса, папки, сервиса. Под каждой схемой – один и тот же заказ из интернет-магазина, разложенный по границам четырьмя разными способами: сначала три способа промахнуться, потом тот, к которому мы идём.
Кружки на схемах живые: их можно таскать и перекидывать – но только внутри своей рамки. Класс не переедет в другой модуль оттого, что вам так удобнее смотреть.
Божественный объект
Единственный угол, где высокая связность не спасает. Внутри божественного объекта действительно всё связано со всем: методы дёргают друг друга, общие поля читают все подряд, разрезать по живому не выходит. Но и наружу он цепляется за полсистемы: базу, почту, отчёты, планировщик. В литературе такой код называют ещё big ball of mud – большим комом грязи.
Так получается не сразу. Класс начинается нормальным, а дальше каждое «ну куда ещё это
положить, пусть пока здесь полежит» добавляет по методу. Через год выходит UserManager на
три тысячи строк, у которого пятнадцать причин меняться и ни одной причины, по которой в
него не добавить шестнадцатую.
// Тот же заказ, но класс отвечает ещё и за деньги, базу, почту, печать и доставку
public class OrderManager
{
private readonly List<OrderLine> _lines = new();
public void AddLine(Product product, int quantity) =>
_lines.Add(new OrderLine(product, quantity));
public decimal Total => _lines.Sum(l => l.Cost) - Discount();
private decimal Discount() => _lines.Count > 10 ? 500 : 0;
public void Save() =>
new SqlConnection(Config.Db).Execute("INSERT INTO orders ...", _lines);
public void SendConfirmation(string email) =>
new SmtpClient("smtp.example.com").Send("noreply@shop.io", email, "Заказ принят", "...");
public byte[] RenderInvoicePdf() => PdfBuilder.From(_lines).Build();
public void ScheduleDelivery(DateTime when) =>
new Scheduler().Add(new DeliveryJob(_lines, when));
}
// Тот же заказ, но класс отвечает ещё и за деньги, базу, почту, печать и доставку
class OrderManager {
public:
void AddLine(const Product& product, int quantity) {
lines_.emplace_back(product, quantity);
}
double Total() const { return Sum() - Discount(); }
void Save() { SqlConnection(Config::Db()).Execute("INSERT INTO orders ...", lines_); }
void SendConfirmation(const std::string& email) {
SmtpClient("smtp.example.com").Send("noreply@shop.io", email, "Заказ принят", "...");
}
std::vector<char> RenderInvoicePdf() const { return PdfBuilder(lines_).Build(); }
void ScheduleDelivery(Date when) { Scheduler().Add(DeliveryJob(lines_, when)); }
private:
double Discount() const { return lines_.size() > 10 ? 500 : 0; }
std::vector<OrderLine> lines_;
};
// Тот же тип отвечает ещё и за деньги, базу, почту, печать и доставку
type OrderManager struct {
lines []OrderLine
}
func (m *OrderManager) AddLine(product Product, quantity int) {
m.lines = append(m.lines, OrderLine{Product: product, Quantity: quantity})
}
func (m *OrderManager) Total() float64 { return m.sum() - m.discount() }
func (m *OrderManager) discount() float64 {
if len(m.lines) > 10 {
return 500
}
return 0
}
func (m *OrderManager) Save() error {
return db.Exec("INSERT INTO orders ...", m.lines)
}
func (m *OrderManager) SendConfirmation(email string) error {
return smtp.SendMail("smtp.example.com:25", nil, "noreply@shop.io", []string{email}, confirmation(m.lines))
}
func (m *OrderManager) RenderInvoicePDF() ([]byte, error) { return pdf.Build(m.lines) }
func (m *OrderManager) ScheduleDelivery(when time.Time) error {
return scheduler.Add(DeliveryJob{Lines: m.lines, At: when})
}
# Тот же заказ, но класс отвечает ещё и за деньги, базу, почту, печать и доставку
class OrderManager:
def __init__(self) -> None:
self._lines: list[OrderLine] = []
def add_line(self, product: Product, quantity: int) -> None:
self._lines.append(OrderLine(product, quantity))
@property
def total(self) -> float:
return sum(line.cost for line in self._lines) - self._discount()
def _discount(self) -> float:
return 500 if len(self._lines) > 10 else 0
def save(self) -> None:
psycopg.connect(Config.DB).execute("INSERT INTO orders ...", self._lines)
def send_confirmation(self, email: str) -> None:
smtplib.SMTP("smtp.example.com").sendmail("noreply@shop.io", email, "...")
def render_invoice_pdf(self) -> bytes:
return PdfBuilder(self._lines).build()
def schedule_delivery(self, when: datetime) -> None:
Scheduler().add(DeliveryJob(self._lines, when))
// Тот же заказ, но класс отвечает ещё и за деньги, базу, почту, печать и доставку
class OrderManager {
private readonly lines: OrderLine[] = [];
addLine(product: Product, quantity: number): void {
this.lines.push(new OrderLine(product, quantity));
}
get total(): number {
return this.lines.reduce((sum, line) => sum + line.cost, 0) - this.discount();
}
private discount(): number {
return this.lines.length > 10 ? 500 : 0;
}
save(): void {
db.execute('INSERT INTO orders ...', this.lines);
}
sendConfirmation(email: string): void {
new SmtpClient('smtp.example.com').send('noreply@shop.io', email, 'Заказ принят', '...');
}
renderInvoicePdf(): Buffer {
return new PdfBuilder(this.lines).build();
}
scheduleDelivery(when: Date): void {
scheduler.add(new DeliveryJob(this.lines, when));
}
}
// Тот же заказ, но класс отвечает ещё и за деньги, базу, почту, печать и доставку
public class OrderManager {
private final List<OrderLine> lines = new ArrayList<>();
public void addLine(Product product, int quantity) {
lines.add(new OrderLine(product, quantity));
}
public BigDecimal getTotal() {
return sum().subtract(discount());
}
private BigDecimal discount() {
return lines.size() > 10 ? BigDecimal.valueOf(500) : BigDecimal.ZERO;
}
public void save() {
db.execute("INSERT INTO orders ...", lines);
}
public void sendConfirmation(String email) {
new SmtpClient("smtp.example.com").send("noreply@shop.io", email, "Заказ принят", "...");
}
public byte[] renderInvoicePdf() {
return new PdfBuilder(lines).build();
}
public void scheduleDelivery(LocalDate when) {
scheduler.add(new DeliveryJob(lines, when));
}
}
// Тот же заказ, но класс отвечает ещё и за деньги, базу, почту, печать и доставку
class OrderManager {
private val lines = mutableListOf<OrderLine>()
fun addLine(product: Product, quantity: Int) {
lines += OrderLine(product, quantity)
}
val total: Double get() = lines.sumOf { it.cost } - discount()
private fun discount() = if (lines.size > 10) 500.0 else 0.0
fun save() = db.execute("INSERT INTO orders ...", lines)
fun sendConfirmation(email: String) =
SmtpClient("smtp.example.com").send("noreply@shop.io", email, "Заказ принят", "...")
fun renderInvoicePdf(): ByteArray = PdfBuilder(lines).build()
fun scheduleDelivery(at: LocalDate) = scheduler.add(DeliveryJob(lines, at))
}
Каждый метод по отдельности написан нормально – проблема в том, что они собрались в одном классе. Скидки приходят от маркетинга, шаблон письма от поддержки, формат счёта от бухгалтерии, расписание доставки от логистики: пять хозяев у одного файла.
Чем это оборачивается:
- любая правка рискованная – метод, который вы трогаете, читают ещё двадцать соседних, и что отвалится, заранее не видно;
- тестировать нечего по частям – чтобы проверить одну функцию, нужно поднять базу, почту и всё остальное;
- работать вдвоём невозможно – весь отдел правит один файл и разбирает конфликты;
- переиспользовать нельзя вообще – класс тащит за собой всю систему;
- новичок в нём тонет – чтобы понять один сценарий, приходится прочитать три тысячи строк.
Лечится он не тем, что «разрезать на классы поменьше»: нарежете не по смыслу – переедете в соседний угол, к плохим границам. Резать нужно по причинам изменения, и об этом – весь модуль GRASP: Information Expert подсказывает, кому отдать данные, Creator – кому создание, Controller – кому приём событий.
Плохие границы
Классика «разрезали по слоям»: Services, Helpers, Utils, Models. Рамки есть, но
проведены не по смыслу задачи, а по типу файла – и в каждой лежат кусочки разных задач.
Связи, которые должны были остаться внутренними, теперь пересекают границы.
// Models: данные без поведения
public class OrderModel
{
public List<OrderLineModel> Lines { get; set; } = new();
public bool IsPaid { get; set; }
}
// Helpers: поведение заказа, вынесенное из заказа
public static class OrderHelper
{
public static decimal CalculateTotal(OrderModel order) =>
order.Lines.Sum(l => l.Price * l.Quantity);
}
// Services: сценарий, которому приходится ходить и туда, и сюда
public class OrderService
{
public void Confirm(OrderModel order)
{
var total = OrderHelper.CalculateTotal(order);
order.IsPaid = PaymentHelper.Charge(total);
}
}
// Models: данные без поведения
struct OrderModel {
std::vector<OrderLineModel> lines;
bool is_paid = false;
};
// Helpers: поведение заказа, вынесенное из заказа
namespace order_helper {
double CalculateTotal(const OrderModel& order) {
double total = 0;
for (const auto& line : order.lines) total += line.price * line.quantity;
return total;
}
} // namespace order_helper
// Services: сценарий, которому приходится ходить и туда, и сюда
class OrderService {
public:
void Confirm(OrderModel& order) {
order.is_paid = payment_helper::Charge(order_helper::CalculateTotal(order));
}
};
// Models: данные без поведения
type OrderModel struct {
Lines []OrderLineModel
IsPaid bool
}
// Helpers: поведение заказа, вынесенное из заказа
func CalculateTotal(order OrderModel) float64 {
total := 0.0
for _, line := range order.Lines {
total += line.Price * float64(line.Quantity)
}
return total
}
// Services: сценарий, которому приходится ходить и туда, и сюда
type OrderService struct{}
func (OrderService) Confirm(order *OrderModel) error {
paid, err := Charge(CalculateTotal(*order))
order.IsPaid = paid
return err
}
# Models: данные без поведения
@dataclass
class OrderModel:
lines: list[OrderLineModel] = field(default_factory=list)
is_paid: bool = False
# Helpers: поведение заказа, вынесенное из заказа
def calculate_total(order: OrderModel) -> float:
return sum(line.price * line.quantity for line in order.lines)
# Services: сценарий, которому приходится ходить и туда, и сюда
class OrderService:
def confirm(self, order: OrderModel) -> None:
order.is_paid = charge(calculate_total(order))
// Models: данные без поведения
interface OrderModel {
lines: OrderLineModel[];
isPaid: boolean;
}
// Helpers: поведение заказа, вынесенное из заказа
function calculateTotal(order: OrderModel): number {
return order.lines.reduce((sum, line) => sum + line.price * line.quantity, 0);
}
// Services: сценарий, которому приходится ходить и туда, и сюда
class OrderService {
confirm(order: OrderModel): void {
order.isPaid = charge(calculateTotal(order));
}
}
// Models: данные без поведения
public class OrderModel {
private List<OrderLineModel> lines = new ArrayList<>();
private boolean paid;
// геттеры и сеттеры на каждое поле
}
// Helpers: поведение заказа, вынесенное из заказа
public final class OrderHelper {
public static BigDecimal calculateTotal(OrderModel order) {
return order.getLines().stream()
.map(l -> l.getPrice().multiply(BigDecimal.valueOf(l.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
// Services: сценарий, которому приходится ходить и туда, и сюда
public class OrderService {
public void confirm(OrderModel order) {
order.setPaid(PaymentHelper.charge(OrderHelper.calculateTotal(order)));
}
}
// Models: данные без поведения
data class OrderModel(
val lines: MutableList<OrderLineModel> = mutableListOf(),
var isPaid: Boolean = false,
)
// Helpers: поведение заказа, вынесенное из заказа
object OrderHelper {
fun calculateTotal(order: OrderModel) = order.lines.sumOf { it.price * it.quantity }
}
// Services: сценарий, которому приходится ходить и туда, и сюда
class OrderService {
fun confirm(order: OrderModel) {
order.isPaid = PaymentHelper.charge(OrderHelper.calculateTotal(order))
}
}
Заказ разрезан не по смыслу, а по типу файла: данные в Models, расчёт в Helpers,
сценарий в Services. Добавить скидку – значит открыть все три, а сумму заказа теперь
считает кто угодно, только не сам заказ (привет,
Information Expert).
Чем это оборачивается:
- одна задача размазана по трём папкам – правка «добавить поле в заказ» открывает четыре файла в разных местах дерева;
- границы не держат – раз связи и так ходят через них, никто не мешает добавить ещё одну;
- имена ни о чём не говорят – по
OrderHelperне понять, что внутри; - свойства модуля не измерить – ни связность, ни зацепление у папки «прочее» не посчитать даже на глаз.
Разрушительное расцепление
Здесь границ, наоборот, слишком много. Кружки одного цвета – то есть части одного смыслового целого – разложены по отдельным рамкам, каждая со своим интерфейсом. Формально зацепление слабое: все зависят от абстракций. На деле связи никуда не делись, просто теперь каждая пересекает границу.
// Тот же заказ, но каждая часть – за своим интерфейсом
public class Order
{
private readonly IOrderLineFactory _lineFactory;
private readonly ICostCalculator _calculator;
private readonly List<IOrderLine> _lines = new();
public Order(IOrderLineFactory lineFactory, ICostCalculator calculator)
{
_lineFactory = lineFactory;
_calculator = calculator;
}
public void AddLine(IProduct product, int quantity) =>
_lines.Add(_lineFactory.Create(product, quantity));
public decimal Total => _calculator.Calculate(_lines);
}
// Тот же заказ, но каждая часть – за своим интерфейсом
class Order {
public:
Order(IOrderLineFactory& line_factory, ICostCalculator& calculator)
: line_factory_(line_factory), calculator_(calculator) {}
void AddLine(const IProduct& product, int quantity) {
lines_.push_back(line_factory_.Create(product, quantity));
}
double Total() const { return calculator_.Calculate(lines_); }
private:
IOrderLineFactory& line_factory_;
ICostCalculator& calculator_;
std::vector<std::unique_ptr<IOrderLine>> lines_;
};
// Тот же заказ, но каждая часть – за своим интерфейсом
type Order struct {
lineFactory OrderLineFactory
calculator CostCalculator
lines []OrderLine
}
func NewOrder(lineFactory OrderLineFactory, calculator CostCalculator) *Order {
return &Order{lineFactory: lineFactory, calculator: calculator}
}
func (o *Order) AddLine(product Product, quantity int) {
o.lines = append(o.lines, o.lineFactory.Create(product, quantity))
}
func (o *Order) Total() float64 { return o.calculator.Calculate(o.lines) }
# Тот же заказ, но каждая часть – за своим интерфейсом
class Order:
def __init__(self, line_factory: OrderLineFactory, calculator: CostCalculator) -> None:
self._line_factory = line_factory
self._calculator = calculator
self._lines: list[OrderLine] = []
def add_line(self, product: Product, quantity: int) -> None:
self._lines.append(self._line_factory.create(product, quantity))
@property
def total(self) -> float:
return self._calculator.calculate(self._lines)
// Тот же заказ, но каждая часть – за своим интерфейсом
class Order {
private readonly lines: IOrderLine[] = [];
constructor(
private readonly lineFactory: IOrderLineFactory,
private readonly calculator: ICostCalculator,
) {}
addLine(product: IProduct, quantity: number): void {
this.lines.push(this.lineFactory.create(product, quantity));
}
get total(): number {
return this.calculator.calculate(this.lines);
}
}
// Тот же заказ, но каждая часть – за своим интерфейсом
public class Order {
private final OrderLineFactory lineFactory;
private final CostCalculator calculator;
private final List<OrderLine> lines = new ArrayList<>();
public Order(OrderLineFactory lineFactory, CostCalculator calculator) {
this.lineFactory = lineFactory;
this.calculator = calculator;
}
public void addLine(Product product, int quantity) {
lines.add(lineFactory.create(product, quantity));
}
public BigDecimal getTotal() {
return calculator.calculate(lines);
}
}
// Тот же заказ, но каждая часть – за своим интерфейсом
class Order(
private val lineFactory: OrderLineFactory,
private val calculator: CostCalculator,
) {
private val lines = mutableListOf<OrderLine>()
fun addLine(product: Product, quantity: Int) {
lines += lineFactory.create(product, quantity)
}
val total: Double get() = calculator.calculate(lines)
}
Делает этот заказ ровно то же, что и заказ из последнего кейса, – но вместо
new OrderLine(...) и Sum(...) здесь две зависимости, четыре интерфейса и ни одной
второй реализации.
Чем это оборачивается:
- чтобы понять одно действие, нужно открыть четыре файла – и в трёх из них не будет логики, только объявления;
- абстракции ничего не дают – у каждого интерфейса ровно одна реализация, менять её никто не собирается;
- правка расползается – добавили поле в позицию заказа, поправили интерфейс, фабрику и калькулятор;
- тесты проверяют заглушки – в них подставлены моки, а не настоящее поведение.
Этот случай встречается реже остальных, зато обычно у тех, кто уже прочитал про Low Coupling.
Расцепление – не бесплатное. Каждый интерфейс, фабрика и слой – это ещё одна граница,
которую читателю нужно пересечь, чтобы понять, что происходит. И в последнем листинге
формально всё хорошо: зацепление слабое, зависимости приходят снаружи, каждый интерфейс
подменяется в тестах. На деле у IOrderLineFactory и ICostCalculator по одной
реализации, менять их никто не собирается, а заказ, позиция и товар – понятия, которые
всегда меняются вместе, – разложены по разным файлам за абстракциями.
Вариант, к которому мы придём в следующем разделе, зацеплен сильнее: Order знает про
OrderLine, а тот про Product. Но это связи внутри одного смыслового целого – и кода в
нём втрое меньше. Разница между двумя листингами не в количестве зависимостей, а в том,
совпадают ли границы со смыслом.
Интерфейс имеет смысл там, где реально ожидается вторая реализация или где граница проходит по чужой системе – СУБД, платёжный шлюз, почта. Интерфейс над классом, который никогда не подменят, добавляет границу, но не даёт свободы.
Low Coupling и High Cohesion
А теперь тот же заказ с границами по смыслу. Всё, что относится к оформлению заказа, лежит в одной рамке и плотно связано внутри; уведомления – в другой. Наружу торчит одна связь: заказ просит отправить письмо и больше ничего про почту не знает.
// Заказ владеет позициями – он же и считает сумму
public class Order
{
private readonly List<OrderLine> _lines = new();
public decimal Total => _lines.Sum(l => l.Cost);
public void AddLine(Product product, int quantity) =>
_lines.Add(new OrderLine(product, quantity));
public void MarkPaid() => IsPaid = true;
public bool IsPaid { get; private set; }
}
// Наружу – одна связь: заказ просит уведомить, но не знает, чем и как
public class Checkout
{
private readonly INotifier _notifier;
public Checkout(INotifier notifier) => _notifier = notifier;
public void Confirm(Order order)
{
order.MarkPaid();
_notifier.OrderConfirmed(order);
}
}
// Заказ владеет позициями – он же и считает сумму
class Order {
public:
void AddLine(const Product& product, int quantity) {
lines_.emplace_back(product, quantity);
}
double Total() const {
double total = 0;
for (const auto& line : lines_) total += line.Cost();
return total;
}
void MarkPaid() { is_paid_ = true; }
private:
std::vector<OrderLine> lines_;
bool is_paid_ = false;
};
// Наружу – одна связь: заказ просит уведомить, но не знает, чем и как
class Checkout {
public:
explicit Checkout(Notifier& notifier) : notifier_(notifier) {}
void Confirm(Order& order) {
order.MarkPaid();
notifier_.OrderConfirmed(order);
}
private:
Notifier& notifier_;
};
// Заказ владеет позициями – он же и считает сумму
type Order struct {
lines []OrderLine
isPaid bool
}
func (o *Order) AddLine(product Product, quantity int) {
o.lines = append(o.lines, OrderLine{Product: product, Quantity: quantity})
}
func (o *Order) Total() float64 {
total := 0.0
for _, line := range o.lines {
total += line.Cost()
}
return total
}
func (o *Order) MarkPaid() { o.isPaid = true }
// Наружу – одна связь: заказ просит уведомить, но не знает, чем и как
type Checkout struct {
notifier Notifier
}
func (c Checkout) Confirm(order *Order) error {
order.MarkPaid()
return c.notifier.OrderConfirmed(order)
}
# Заказ владеет позициями – он же и считает сумму
class Order:
def __init__(self) -> None:
self._lines: list[OrderLine] = []
self.is_paid = False
def add_line(self, product: Product, quantity: int) -> None:
self._lines.append(OrderLine(product, quantity))
@property
def total(self) -> float:
return sum(line.cost for line in self._lines)
def mark_paid(self) -> None:
self.is_paid = True
# Наружу – одна связь: заказ просит уведомить, но не знает, чем и как
class Checkout:
def __init__(self, notifier: Notifier) -> None:
self._notifier = notifier
def confirm(self, order: Order) -> None:
order.mark_paid()
self._notifier.order_confirmed(order)
// Заказ владеет позициями – он же и считает сумму
class Order {
private readonly lines: OrderLine[] = [];
isPaid = false;
addLine(product: Product, quantity: number): void {
this.lines.push(new OrderLine(product, quantity));
}
get total(): number {
return this.lines.reduce((sum, line) => sum + line.cost, 0);
}
markPaid(): void {
this.isPaid = true;
}
}
// Наружу – одна связь: заказ просит уведомить, но не знает, чем и как
class Checkout {
constructor(private readonly notifier: Notifier) {}
confirm(order: Order): void {
order.markPaid();
this.notifier.orderConfirmed(order);
}
}
// Заказ владеет позициями – он же и считает сумму
public class Order {
private final List<OrderLine> lines = new ArrayList<>();
private boolean paid;
public void addLine(Product product, int quantity) {
lines.add(new OrderLine(product, quantity));
}
public BigDecimal getTotal() {
return lines.stream().map(OrderLine::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
}
public void markPaid() {
paid = true;
}
}
// Наружу – одна связь: заказ просит уведомить, но не знает, чем и как
public class Checkout {
private final Notifier notifier;
public Checkout(Notifier notifier) {
this.notifier = notifier;
}
public void confirm(Order order) {
order.markPaid();
notifier.orderConfirmed(order);
}
}
// Заказ владеет позициями – он же и считает сумму
class Order {
private val lines = mutableListOf<OrderLine>()
var isPaid = false
private set
fun addLine(product: Product, quantity: Int) {
lines += OrderLine(product, quantity)
}
val total: Double get() = lines.sumOf { it.cost }
fun markPaid() {
isPaid = true
}
}
// Наружу – одна связь: заказ просит уведомить, но не знает, чем и как
class Checkout(private val notifier: Notifier) {
fun confirm(order: Order) {
order.markPaid()
notifier.orderConfirmed(order)
}
}
Order, OrderLine и Product меняются вместе и лежат вместе – это связность. С почтой
заказ общается через INotifier из одного метода – это слабое зацепление, и здесь
интерфейс оправдан: за ним чужая система, которую действительно меняют (SMTP, мессенджер,
пуш).
Так выглядит код, который дёшево менять: правка внутри одной рамки не выходит за её пределы, а чтобы разобраться в задаче, достаточно открыть одну папку. Ни один из трёх предыдущих вариантов не был «слишком связным» или «слишком расцепленным» сам по себе – все три просто провели границы не там.
А чем это отличается от SRP
SRP и высокая связность – про одно и то же, но с разной строгостью. SRP требует одной причины для изменения; связность допускает несколько обязанностей, если они про одно и то же и меняются вместе. Проще считать SRP частным, самым жёстким случаем высокой связности.
Как этим пользоваться
- Цель – не ноль связей, а их правильное расположение. Слабое зацепление, а не полное; высокая связность, а не абсолютная.
- Границы проводите по смыслу задачи, а не по технике. Папка «оформление заказа» лучше папки «сервисы», в которой лежат оформление заказа, рассылка и импорт прайсов.
- Принцип работает на всех уровнях – метода, класса, модуля, папки и отдельного сервиса. Вопрос везде один: что меняется вместе, а что – по разным поводам.
- Заметили много интерфейсов с одной реализацией – скорее всего, вы в левом нижнем углу.