Издательский дом «Питер» corporate blog
Java
ООP
Professional literature
March 2016 4

Еще возитесь с отладкой?

Original author: Yegor Bugayenko
Translation
Здравствуйте, уважаемые читатели.

На русском языке выходит не так много универсальной неустаревающей литературы о принципах ООП. Пользуясь случаем, предлагаем скачать "Объектно-ориентированное мышление" Мэтта Вайсфельда, которой практически не осталось в бумаге. Однако подобные книги время от времени появляются, причем есть и такие, которые написаны в новаторском и прикладном стиле, а не просто перемалывают известные истины. Одна из них называется "Elegant Objects", мы серьезно задумываемся издать ее на русском языке.



Предлагаем оценить стиль и философию автора по переводу одной из последних статей из его блога.



Отладка – это «процесс интерактивного запуска программы/метода, причем поток выполнения после каждой инструкции приостанавливается, и отображается результат…». В сущности, это очень дельная техника… для плохого программиста. Или для олдскульного программиста, который все еще пишет процедурный код на C. Специалисты по ООП не отлаживают код — они пишут модульные тесты. Берусь утверждать, что модульное тестирование полностью снимет отладку с повестки дня. Если требуется отладка, значит программа спроектирована плохо.



Допустим, я плохой программист, пишу в императивном процедурном стиле и выдаю такой код на Java:

class FileUtils {
  public static Iterable<String> readWords(File f) {
    String text = new String(
      Files.readAllBytes(Paths.get(f)),
      "UTF-8"
    );
    Set<String> words = new HashSet<>();
    for (String word : text.split(" ")) {
      words.add(word);
    }
    return words;
  }
}


Этот статический вспомогательный метод считывает содержимое файла, а затем находит в нем все уникальные слова. Все просто. Однако, если он откажется работать, что мы будем делать? Допустим, вот файл:

We know what we are,
but know not what we may be.


Из него извлекаем следующий список слов:

"We"
"know"
"what"
"we"
"are,\n"
"but"
"not"
"may"
"be\n"


В данном случае мне не нравится… как раз следующий шаг — какой? Либо файл считывается с ошибкой, либо ошибка в разрыве строк. Давайте его отлаживать, так? Прогоняем файл через ввод и шаг за шагом прорабатываем его, отслеживая все переменные и наблюдая за ними. Находим баг, фиксим. Но если подобная проблема возникнет повторно, ее придется снова отлаживать! Именно такие случаи следует предотвращать при помощи модульных тестов.

Нам нужно один раз написать модульный тест и тем самым воспроизвести проблему. Затем мы исправляем проблему и убеждаемся, что тест пройден. Таким образом, проблемы решаются рационально. Нам не придется ничего фиксить заново, так как ошибка больше не произойдет. Наш тест это гарантирует.

Однако, все это работает, лишь если написать модульный тест легко. Если написать его будет сложно — я заленюсь и не буду писать. Просто займусь отладку и пофиксю проблему. В данном конкретном случае написать тест достаточно непросто. То есть, сам уровень сложности теста довольно высок. Требуется создать временный файл, наполнить его данными, запустить метод и проверить результат. Чтобы понять, что происходит, и где тут баг, понадобится не один тест. Чтобы избежать дублирования кода, мне также потребуется написать несколько утилит, которые помогут мне создавать временный файл и заполнять его данными. Много работы. Может быть, «не слишком много», но с отладкой я бы управился за несколько минут.

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

class Words implements Iterable<String> {
  private final File file;
  Words(File src) {
    this.file = src;
  }
  @Override
  public Iterator<String> iterator() {
    String text = new String(
      Files.readAllBytes(Paths.get(this.file)),
      "UTF-8"
    );
    Set<String> words = new HashSet<>();
    for (String word : words.split(" ")) {
      words.add(word);
    }
    return words.iterator();
  }
}


Уже гораздо лучше, но по-прежнему сложно. Далее разобью его на более мелкие классы:

class Text {
  private final File file;
  Text(File src) {
    this.file = src;
  }
  @Override
  public String toString() {
    return new String(
      Files.readAllBytes(Paths.get(this.file)),
      "UTF-8"
    );
  }
}
class Words implements Iterable<String> {
  private final String text;
  Words(String txt) {
    this.text = txt;
  }
  @Override
  public Iterator<String> iterator() {
    Set<String> words = new HashSet<>();
    for (String word : this.text.split(" ")) {
      words.add(word);
    }
    return words.iterator();
  }
}


Ну как? Написать тест для класса Words
– совершенно тривиальная задача:

import org.junit.Test;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
public class WordsTest {
  @Test
  public void parsesSimpleText() {
    assertThat(
      new Words("How are you?"),
      hasItems("How", "are", "you")
    );
  }
}


Сколько времени я на это потратил? Меньше минуты. Не приходится создавать временный файл, наполнять его данными, поскольку класс Words не работает с файлами. Он просто разбирает входящую строку и находит в ней уникальные слова. Теперь пофиксить ошибку легко, так как тест маленький, и нам не составит труда написать и другие тесты, например:

import org.junit.Test;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
public class WordsTest {
  @Test
  public void parsesSimpleText() {
    assertThat(
      new Words("How are you?"),
      hasItems("How", "are", "you")
    );
  }
  @Test
  public void parsesMultipleLines() {
    assertThat(
      new Words("first line\nsecond line\n"),
      hasItems("first", "second", "line")
    );
  }
}


Я считаю, что отладка необходима, когда на написание модульного теста тратится существенно больше времени, чем на нажатие кнопок Trace-In/Trace-Out. Это логично. Мы все ленивы, любим простые и быстрые решения. Но отладка – это пустая трата времени и сил. Она помогает лишь отыскивать проблемы, но не застраховаться от их повторного появления.

Отладка нужна в процедурном и алгоритмическом коде, когда код описывает, как достигается цель, а не какова эта цель. Еще раз пересмотрите вышеприведенные примеры. Весь первый статический метод рассказывает о том, как считывать файл, делать его синтаксический анализ и находить слова. Он даже называется readWords() («read» это глагол). Напротив, во втором примере указано, какая цель должна быть достигнута. Речь либо о тексте Text файла или о словах Words в тексте (это существительные).

Я считаю, что в правильном ООП отладка неуместна. Только модульное тестирование!
Опрос
57% Интересно, жду книгу 109
42.9% Спорно 82
8.3% Допечатайте пожалуйста хотя бы триста штук Вайсфельда 16
191 user voted. 52 users abstained.
0
9k 52
Comments 10
Top of the day