Где спряталась логика?
03 Mar 2011Вопрос
Очень часто при обсуждении программ употребляется термин «логика» или «бизнес-логика». Например:
- (о юнит-тестах) не обязательно добиваться стопроцентного покрытия кода тестами, достаточно тестировать лишь логику.
- (о веб-приложениях) контроллер не должен содержать никакой бизнес-логики, а должен только вызывать методы других классов
- В слое VIEW (то есть в JSP-файлах) не должно быть бизнес-логики
Так вот, кто скажет мне, что такое «логика»?
Надо ли понимать под этим любой IF в коде? Но разве бывает код без IF’ов? Или «бизнес-логика» означает любую информацию, которая исходит от клиента? Но разве можем мы на деньги клиента делать что-то, чего он не заказывал? Не можем. Стало быть, весь наш код — это целиком «бизнес-логика» от клиента. Вот поэтому я никогда не мог понять, что же такое эта чёртова логика.
Ответ
Возможно, ответить на этот вопрос проще, если вместо слова «логика» использовать слово «знание». Логика — это любое знание о коде, или о том, как должен вести себя код. Можно ещё сформулировать так: «логика» — это всё, что вам хотелось хоть раз откомментировать.
Пример
Возьмём для примера класс Account, который я недавно откопал в своём проекте.
Было это так. В один прекрасный день у нас в офисе отрубился интернет на пару часов. Работать я фактически не мог — ну там, svn, jira, база знаний и прочее не были доступны. И решил я в качестве упражнения написать юнит-тест для какого-нибудь класса. И наткнулся на Account:
public class Account {
  // Негативное значение означает, что счёт предоплачен
  private BigDecimal amount;
  public BigDecimal getAmount() {
    return amount;
  }
  // И ещё два десятка геттеров и сеттеров
}
Казалось бы, что тут тестировать? Где IF’ы? Где тут логика? Казалось бы, нет её, и тестировать нечего. Я уже хотел было бросить этот класс и найти что-нибудь посложнее, как вдруг заметил комментарий около переменной amount:
Негативное значение означает, что счёт предоплачен
Это что ещё такое? Получается, что «amount» — это суммарная задолженность данного клиента, а если клиент внёс предоплату, то эта «задолженность» со знаком минус. Ну вот, это уже какое-то знание о коде. Попробуем-ка превратить этот комментарий в юнит-тест:
public class AccountTest {
  @Test
  public void negativeAmountMeansThatAccountIsPrepaid() {
    Account prepaidAccount = new Account(-123);
    assertTrue(prepaidAccount.isPrepaid());
  }
}
Раз мы проверяем случай негативного «amount», хорошо бы проверить и случай позитивного:
@Test
public void positiveAmountMeansThatAccountIsInDebt() {
  Account indebtAccount = new Account(456);
  assertFalse(indebtAccount.isPrepaid());
  assertTrue(indebtAccount.isInDebt());
}
Вот так ненароком у нас возникло два новых метода isPrepaid() и isInDebt(), реализация которых, конечно, очевидна:
public class Account {
  private BigDecimal amount;
  public boolean isInDebt() {
    return amount > 0;
  }
  public boolean isPrepaid() {
    return amount < 0;
  }
}
Вот видите, уже и IF’ы появились в коде! Следующая мысль, которая должна при этом прийти в голову: наверное, эти IF’ы уже были где-то раньше?
Пришлось немножко поискать, но это того стоило. Оказалось, что эти IF’ы действительно были в коде, причём не где-нибудь, а в JSP-файле, то есть в слое «view», в котором по определению никакой логики быть не должно:
<% Positive amount means that account is in debt %>
<c:if test="${account.amount > 0}">
  <span style="color: red;">Оплатите задолженность!</span>
</c:if>
<c:if test="${account.amount < 0}">
  <span style="color: green;">Предоплата</span>
</c:if>
Опаньки, как интересно! Вот и мой второй тест-кейс всплыл. Как замечательно. 
Теперь, поскольку у нас есть методы isInDebt() и isPrepaid(), мы можем убрать логику из JSP:
<c:if test="${account.inDebt}">
 <span style="color: red;">Оплатите задолженность!</span>
</c:if>
<c:if test="${account.prepaid}">
  <span style="color: green;">Предоплата</span>
</c:if>
Стало лучше? Я думаю, что стало. Это уже чуть больше похоже на инкапсуляцию — скрытие реализации от посторонних глаз. Только класс Amount знает, в каком случае клиент должен или не должен денег. Никто больше в целом мире не знает, как это происходит — клиент получает уже готовое решение с помощью методов isInDebt и isPrepaid. Теоретически значения «долг» и «предоплата» вообще могут храниться в разных колонках базы данных, или даже вообще в разных таблицах и вообще в разных базах. Теперь благодаря наличию двух методов эта логика спрятана (инкапсулирована) в классе Account.
Если вы всё ещё не верите, сравните объём кода.
ДО
<%-- Негативное значение означает, что счёт предоплачен %-->
<c:if test="${account.amount > 0}">
  <span style="color: red;">Оплатите задолженность!</span>
</c:if>
ПОСЛЕ
<c:if test="${account.inDebt}">
  <span style="color: red;">Оплатите задолженность!</span>
</c:if>
Можно пойти ещё дальше и вместо метода getAmount() сделать два разных метода getPrepaidAmount() и getDoubt().
Причём внутри них можно проверять, действительно ли у клиента есть долг или предоплата. 
То есть теперь невозможно будет, например, написать такой код:
<%-- Мало ли что у тебя предоплата, ты всё равно должен. %>
<c:if test="${account.amount != 0}">
  <font color="red">Оплатите задолженность!</font>
</c:if>
Да, это реальный код из того же проекта. Взгляните на него ещё раз, взгляните внимательно. “Мало ли что у тебя предоплата”. Блестяще! Вот где спраяталась аццкая логика! Пойди пойми теперь, что там было у людей на уме - у людей, которые не писали юнит-тест.
В идеале метод getAmount() теперь можно вообще стереть. 
Это и есть настоящая инкапсуляция: скрыть данные и код и дать доступ только через методы. 
Я думаю, если бы интернет не починили, я бы избавился и от остальных 20 геттеров-сеттеров.
Мораль
Вот видите, как юнит-тесты приводят к читаемому, объектно-ориентированному коду. А вы говорите: «В моём коде нет логики, там нечего тестировать…» Ещё как есть!
Логика есть, её не может не быть.
 Complexity is the path to the dark side
      Complexity is the path to the dark side
    