open atlas
↑ К треку
Паттерны и качество кода CP · 09 · 01

Завистливые функции и неуместная близость

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

CP Senior ◷ 20 min
Уровень
ОсновыJuniorMiddleSenior

Ты открываешь метод и видишь, как он тянется через стол: order.lineItems, order.taxRate, order.shippingRegion, order.couponPercent. До своего собственного объекта он почти не дотрагивается — каждая строка про чужие данные. Метод живёт в InvoicePrinter, но думает он про Order. Он в чужом доме.

Это и есть завистливая функция (feature envy), а её уродливая родственница — неуместная близость (inappropriate intimacy): два класса перестали стучаться и просто заходят в чужие спальни — читают приватные поля, меняют внутреннее состояние, каждый рассчитывает на форму другого. Оба запаха — одна болезнь под разными углами: поведение уехало от данных, над которыми оно работает, и за разрыв платят связанностью.

Цель

После этого урока ты можешь распознать завистливую функцию по соотношению доступа к данным (метод трогает поля чужого объекта чаще своих собственных), распознать неуместную близость по взаимному приватному доступу между классами, применить перемещение метода (Move Method) и инкапсуляцию, чтобы вернуть поведение туда, где живут его данные, и — senior-часть — отличить настоящую зависть от законной оркестрации, чтобы не запихать сценарий использования внутрь объекта данных только ради соблюдения правила.

1

Завистливая функция — это метод, которому данные чужого объекта интереснее своих собственных. Диагностика механическая: посчитай поля, которые метод читает. Если большинство из них принадлежат одному другому объекту, метод завидует этому объекту — он хочет быть методом на нём.

class InvoicePrinter {
  // envious: every field it touches belongs to `order`, none to `this`
  amountDue(order: Order): number {
    const subtotal = order.lineItems
      .reduce((s, li) => s + li.price * li.qty, 0);
    const tax = subtotal * order.taxRate;
    const shipping = order.shippingRegion === "intl" ? 25 : 5;
    return subtotal + tax + shipping - order.couponPercent * subtotal;
  }
}

amountDue читает lineItems, taxRate, shippingRegion, couponPercent — четыре куска состояния Order — и ничего от InvoicePrinter. Поведение и данные разделены, а стык между ними теперь стал публичной поверхностью, которую Order обязан выставить наружу. Лечение — переместить метод к данным, которым он завидует.

2

Лекарство от зависти — перемещение метода (Move Method): перенеси поведение к данным, которым оно завидует. Помести amountDue на Order. Теперь он читает свои поля, Order может держать их приватными, а InvoicePrinter задаёт один вопрос вместо четырёх.

class Order {
  // behaviour now sits on the data it uses
  amountDue(): number {
    const tax = this.subtotal() * this.taxRate;
    const shipping = this.shippingRegion === "intl" ? 25 : 5;
    return this.subtotal() + tax + shipping - this.couponPercent * this.subtotal();
  }
  private subtotal(): number {
    return this.lineItems.reduce((s, li) => s + li.price * li.qty, 0);
  }
}

class InvoicePrinter {
  print(order: Order): string {
    return `Total: ${order.amountDue().toFixed(2)}`; // one call, zero envy
  }
}

Связанность измеримо упала: зависимость InvoicePrinter от Order сжалась с четырёх полей до одного метода. taxRate, couponPercent и shippingRegion теперь могут стать приватными — правило, которое их объединяет, живёт вместе с ними. Это говори, не спрашивай (tell-don’t-ask): скажи заказу вычислить сумму к оплате; не выпрашивай его потроха и не считай арифметику снаружи.

3

Неуместная близость — это взаимная завистливая функция: два класса лезут во внутренности друг друга. Зависть однонаправленна — метод хочет данные чужого объекта. Близость двунаправленна и структурна — A читает/пишет приватное состояние B, а B читает/пишет приватное состояние A. Они становятся единым спутанным узлом, притворяющимся двумя классами.

class Account {
  balance = 0;
  history: Transaction[] = [];
}

class Transaction {
  apply(acct: Account) {
    acct.balance += this.amount;        // mutating Account's field
    acct.history.push(this);            // reaching into Account's internals
  }
}

class Account2 {
  reverse(tx: Transaction) {
    this.balance -= tx.amount;          // reading Transaction's field
    tx.reversed = true;                 // mutating Transaction's field
  }
}

Каждый класс рассчитывает на точную форму другого и пишет сквозь неё. Поменяй представление balance в Account (центы? денежный тип? журнал событий?) — и Transaction.apply ломается; поменяй поля Transaction — и Account.reverse ломается. Ни о том, ни о другом нельзя рассуждать, тестировать или развивать поодиночке. Близость и есть связанность.

4

Восстанови инкапсуляцию: дай каждому классу границу или выдели общую концепцию. Близость растворяют два хода. Первый — замени межобъектный доступ к полям методами, раскрывающими намерение, чтобы ни один класс не зависел от внутренностей другого — только от маленького именованного интерфейса:

class Account {
  private balance = 0;
  private history: Transaction[] = [];
  post(tx: Transaction): void {          // Account owns its own mutation
    this.balance += tx.signedAmount();
    this.history.push(tx);
  }
}

Transaction теперь выставляет signedAmount(), а Account владеет каждой записью в свои поля; никто не лезет через границу. Второй ход — когда спутанность возникает из-за общей концепции, которой не владеет никто (здесь: проведение изменения баланса вместе с его аудиторским следом), выдели эту концепцию в собственный тип — Posting или Ledger, с которым оба сотрудничают чисто. Senior-эвристика: если перемещение метода просто сдвигает зависть в другую сторону, недостающий элемент обычно — третий объект, которого ещё не существует.

5

Режим отказа: не переноси оркестрацию, законно охватывающую несколько объектов. Правило «перемести поведение к данным, которым оно завидует» предполагает, что поведение принадлежит данным одного объекта. Но некоторые методы трогают поля многих объектов по замыслу — они координируют рабочий процесс: оформление заказа, которое читает Cart, списывает через PaymentGateway, уменьшает Inventory и пишет Order. Такой метод выглядит завистливым ко всем четырём, но не принадлежит ни одному. Это сценарий использования / сервис, и оркестрация принадлежит уровню выше любого отдельного объекта данных.

// Legitimate orchestration — NOT feature envy. Do not jam this into Cart or Order.
class CheckoutService {
  async checkout(cart: Cart, payment: PaymentGateway, inventory: Inventory): Promise<Order> {
    const total = cart.total();               // each object does its own work...
    await payment.charge(total);              // ...the service only sequences them
    inventory.reserve(cart.items());
    return Order.from(cart);
  }
}

Признак: сервис выстраивает в последовательность вызовы к объектам, каждый из которых делает свою работу (cart.total(), payment.charge()), тогда как зависть делает работу чужого объекта за него (читает cart.taxRate и считает налог снаружи корзины). Если «починить» оркестратор, запихав его внутрь Cart, ты дашь Cart знание о платежах и складе, которого у него быть не должно — ты обменяешь ложный запах на настоящую связанность. Перемещай поведение к данным; оставляй координацию над данными.

Разбор примера

Посмотри, как зависть превращается в чистую границу. Начни с модуля маршрутизации, который завидует Location:

// router.ts — envious: every field belongs to `from`/`to`, none to the router
class Router {
  distanceKm(from: Location, to: Location): number {
    const R = 6371;
    const dLat = (to.lat - from.lat) * Math.PI / 180;
    const dLon = (to.lng - from.lng) * Math.PI / 180;
    const a = Math.sin(dLat / 2) ** 2 +
      Math.cos(from.lat * Math.PI / 180) * Math.cos(to.lat * Math.PI / 180) *
      Math.sin(dLon / 2) ** 2;
    return R * 2 * Math.asin(Math.sqrt(a));
  }
}

distanceKm читает lat/lng с двух Location и ничего от Router. Это поведение про локации, застрявшее в роутере. Перемести его на Location, где живут его данные:

// location.ts — behaviour now sits with its data
class Location {
  constructor(private lat: number, private lng: number) {}

  distanceTo(other: Location): number {
    const R = 6371;
    const dLat = (other.latRad() - this.latRad());
    const dLon = (other.lngRad() - this.lngRad());
    const a = Math.sin(dLat / 2) ** 2 +
      Math.cos(this.latRad()) * Math.cos(other.latRad()) * Math.sin(dLon / 2) ** 2;
    return R * 2 * Math.asin(Math.sqrt(a));
  }
  private latRad() { return this.lat * Math.PI / 180; }
  private lngRad() { return this.lng * Math.PI / 180; }
}

// router.ts — now asks one question
class Router {
  routeLength(stops: Location[]): number {
    return stops.slice(1).reduce(
      (sum, stop, i) => sum + stops[i].distanceTo(stop), 0);
  }
}

Три выигрыша, все от одного перемещения. lat/lng стали приватнымиRouter больше не знает, что Location хранит градусы (она могла бы внутри перейти на геохеш, и Router не изменился бы). Работа Router сжалась до оркестрации (routeLength выстраивает вызовы distanceTo) — а это ровно то, где роутер и должен жить, потому что суммирование маршрута законно охватывает много локаций и не принадлежит ни одной. Зависть уехала вниз, на Location; координация осталась наверху, в Router. Это разделение — весь урок в одном диффе.

Почему это работает

Почему перемещение метода действительно снижает связанность, а не просто переносит её? Связанность считается числом деталей чужого объекта, которые ты обязан знать. Завистливый amountDue знал четыре поля Order; после перемещения InvoicePrinter знает один метод Order. Что важно, эти четыре поля теперь можно сделать приватными, а значит Order волен менять как он хранит налог и купоны, ничего не ломая — публичная поверхность сжалась. Move Method не тасует связанность туда-сюда; он превращает широкую зависимость от данных в узкую поведенческую и отдаёт остальное инкапсуляции. Это превращение — много полей наружу, один глагол внутрь — и есть механизм, стоящий за «говори, не спрашивай».

Частая ошибка

Соблазнительная ошибка — считать «трогает поля чужого объекта» доказательством зависти и механически перемещать метод. Два ложных срабатывания кусают сеньоров. Первое — оркестрация: сервис, выстраивающий в последовательность cart, payment и inventory, читает много объектов по замыслу — запихни его в один из них, и ты дашь этому объекту знание, которым он держать не должен. Второе — неправильный дом: метод может завидовать объекту B, но, будучи перемещённым, обнаружить, что теперь завидует C — настоящее лечение в недостающей третьей концепции (Posting, Money, Route), от которой оба должны зависеть, а не в перетягивании каната, какой существующий класс приютит метод. Диагностируй намерение (делает ли он работу чужого объекта или лишь выстраивает работу, которую объекты и так делают сами?) прежде, чем тянуться к Move Method.

Проверь себя
Викторина

Метод CheckoutService читает cart.total(), вызывает payment.charge() и вызывает inventory.reserve(). Ревьюер помечает его как завистливую функцию и просит переместить в Cart. Какой ответ самый senior?

Итог

Завистливая функция — это метод, которому данные чужого объекта интереснее своих собственных; диагностируй её по соотношению доступа к полям. Неуместная близость — взаимный случай: два класса лезут во внутренности друг друга, сплавляясь в один узел. Оба — поведение, уехавшее от своих данных, и за разрыв платят связанностью. Лечения — перемещение метода (перенеси поведение к данным, которым оно завидует, затем сделай эти поля приватными) и восстановление инкапсуляции (замени межобъектный доступ к полям именованными методами или выдели общую концепцию в третий тип, когда перемещение просто сдвигает зависть вбок). Senior-дисциплина — это режим отказа: метод, который выстраивает в последовательность несколько объектов, каждый из которых делает свою работу, — это оркестрация: сценарий использования или сервис, принадлежащий уровню выше любого отдельного объекта данных. Перемещай поведение к данным; никогда не хорони координацию внутри объекта данных только ради того, чтобы заткнуть правило подсчёта полей.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.