Когда разбиение мешает, памятка и словарик
Разбиение на функции — не самоцель. Программу можно испортить и им тоже. Пять случаев, которые встречаются чаще всего, — и памятка дня.
Случай 1: функция, которая считает и печатает
Ситуация. Кусок «посчитать коробки и напечатать» вынесли целиком, вместе с печатью.
package main
import "fmt"
func printBoxes(items, perBox int) {
fmt.Println("Коробок:", (items+perBox-1)/perBox)
}
func main() {
printBoxes(50, 12)
printBoxes(7, 12)
}stagiaire@lab:~/gocourse/day13/scratch$ go run .
Коробок: 5
Коробок: 1Работает. И всё же так делать не стоит, и вот почему.
Её нельзя проверить, не глядя на экран. printBoxes ничего не возвращает: чтобы узнать, верно ли она считает, надо запустить программу и прочитать вывод глазами. Сравнить «то, что вернула функция», с «тем, что посчитано на бумаге» здесь не с чем.
Её нельзя использовать второй раз. Понадобилось то же число, но в другой строке — например, в итоге смены: Заказов: 3, коробок: 7. С printBoxes придётся писать формулу заново.
Правило: считает одна функция, печатает другая (или сам main).
func boxesFor(items, perBox int) int {
return (items + perBox - 1) / perBox
}Теперь число можно и напечатать, и сложить с другим, и сравнить с посчитанным на бумаге. Завтра, в день 14, вы напишете к такой функции тест — программу, которая сравнивает результат с ожидаемым сама. К функции вроде printBoxes теста не напишешь.
Печатающие функции нужны и полезны — printOrder, printTotal, — но у них одно дело: печать. Расчёт в них не живёт.
Случай 2: обёртка, которая ничего не объясняет
func plusOne(n int) int {
return n + 1
}count = plusOne(count) вместо count++ — на один прыжок при чтении больше, понимания ноль. Функция оправдана, когда её имя объясняет формулу, которую иначе пришлось бы разбирать: boxesFor объясняет, что значит (items + perBox - 1) / perBox.
Проверка простая: можете ли вы назвать функцию по смыслу, а не по действиям? Если единственное честное имя — пересказ тела, функция не нужна.
Случай 3: функция, которой нужно полпрограммы
Признак — длинный список параметров:
func handle(items, perBox, boxPrice, from, orders, boxesTotal, costTotal int) int {Семь аргументов означают, что вынесен не кусок работы, а случайный отрезок main: этот кусок связан со всем вокруг. Вызов такой функции читать труднее, чем исходные строки, а перепутать порядок аргументов — проще простого: все семь int, компилятор промолчит.
Что делать: резать мельче. Обычно внутри такого куска есть два-три самостоятельных: расчёт коробок, расчёт стоимости, печать. Каждому нужно два-три числа.
Ориентир на курс: до трёх параметров — спокойно, четыре — повод подумать, пять и больше — почти всегда неверная граница. Настоящие способы передавать много данных одним аргументом появятся в блоке 5, когда будут структуры.
Случай 4: функция, которая помнит
Уже разобран на странице 4: функция, которая читает и меняет пакетную переменную, возвращает разное на одних и тех же аргументах. Такую нельзя ни проверить отдельно, ни переставить местами с другим вызовом. В курсе меняющихся пакетных переменных нет.
Случай 5: разбиение без снимка поведения
Этот случай не про имена и не про границы — про порядок работы, и он обходится дороже всех остальных.
Ситуация. Программа приёмки со страницы 1 работала. За один заход вынесли три куска и заодно поправили формат первой строки. Запустили — и получили вот что:
stagiaire@lab:~/gocourse/day13/scratch$ diff baseline.txt after.txt
1c1
< Штук: 50
---
> Штук : 50
3c3
< Упаковка: 4.05 руб.
---
> Упаковка: 4.50 руб.Расхождения два, правок четыре, и какая правка дала какое расхождение — из этого вывода не видно. Первое, Штук : вместо Штук:, нашлось бы глазами. Второе, 4.50 вместо 4.05, — это потерянная скидка, и она может прятаться в любом из трёх выносов.
Дальше есть два пути. Первый: читать все три выноса подряд и искать, где исчезло условие. Второй: вернуться к последнему коммиту и вынести куски по одному, проверяя каждый. Второй быстрее, даже с учётом того, что работу придётся повторить: каждый шаг даёт ответ сразу, а не в конце.
| Как делали | Сколько кандидатов на ошибку | Как искать |
|---|---|---|
| три выноса и правка формата, один прогон | четыре | читать всё, сравнивать с оригиналом |
| один вынос, один прогон, коммит | один | смотреть только его |
Отсюда и порядок со страницы 5 — вынести, прогнать, diff, коммит, — и его цена: без него у расхождения нет адреса. И вторая причина поломки здесь та же, что в правиле дня: «заодно поправил формат». Заодно — нельзя; при выносе по одному куску это стало бы видно на первом же diff.
Пять вопросов к готовому разбиению
Разложили программу, diff молчит, коммиты на месте. Осталось проверить, что разбиение получилось не только безвредным, но и полезным. Пять вопросов, по одному на каждое правило дня:
- Читается ли
mainкак оглавление? Пройдите его глазами сверху вниз и скажите вслух, что делает программа. Если для ответа пришлось заходить внутрь функции, у неё врёт имя. - Есть ли функция, которую нельзя объяснить одной фразой без «потом»? Такая делает больше одного дела.
- Есть ли функция, которую нельзя позвать с числами и сверить с бумагой? Значит, внутри печать или чтение ввода вперемешку с расчётом.
- Есть ли функция с пятью и больше параметрами? Граница проведена не по подцели.
- Осталась ли в программе формула, записанная дважды? Найдите глазами: если одно и то же выражение встречается в двух местах, вынос ещё не закончен.
Четыре «нет» и одно «да» на первый вопрос — разбиение готово. Любой другой ответ говорит, какую именно страницу перечитать: 1, 2, 4 или 7.
Разбиение в чужом коде: три предосторожности
Сегодня вы разбирали свою программу: её видно целиком, и всё, что в ней есть, вызывается из неё же. В рабочем сервисе это не так, и у рефакторинга появляются три правила сверх сегодняшних.
1. Сначала посмотрите, кто зовёт. Прежде чем менять функцию, нажмите на её имени Shift+F12: редактор покажет все места вызова. Меняете то, что зовут из пяти мест, — проверять придётся все пять. В своей однофайловой программе этот список всегда пуст или короток, в сервисе — нет.
2. Не меняйте заголовок заодно. Переименовать функцию, переставить её параметры местами или добавить четвёртый — это отдельная работа и отдельный коммит. Особенно опасна перестановка параметров одного типа: все вызовы продолжают собираться, а считают другое. Ровно этот дефект вы чинили в fixcall, только там единицы разъехались внутри одного файла.
3. Поведение сверяют на границах, а не на удобном примере. Ваш baseline.txt снят на example.txt — три заказа, последний нулевой. Программа умеет ещё две вещи: пустой ввод и строку не с числом. Обе вы на странице 5 тоже сняли в файлы — base-empty.txt и base-bad.txt, их сверяет тот же diff. Проверка курса гоняет все эти границы ещё и скрытыми случаями. Прогоняйте такие вводы руками после каждого выноса:
| Ввод | Что должно остаться прежним |
|---|---|
go run . < /dev/null |
одна строка «Заказов не было» |
| ноль штук в заказе | 0 коробок и 0.00 руб., а не пропуск строки |
| строка не с числом | сообщение в поток ошибок и код возврата 1 |
| пустые строки вперемешку с числами | пустые пропускаются и не сдвигают номера заказов |
Правило простое: чем шире вы проверили поведение до правок, тем меньше шансов, что расхождение найдёт кто-то другой. Завтра эту работу возьмёт на себя тест — он проверит всё это сам и на каждом запуске.
Памятка дня
| Вопрос | Ответ |
|---|---|
| Когда выносить кусок | повтор; длинное условие; кусок, подписанный комментарием-заголовком |
| Когда не выносить | обёртка ради одной строки; кусок, которому нужны почти все переменные main; три строки печати подряд в конце |
| Сколько дел у функции | одно; имя без «и», объяснение без «потом» |
| Как назвать | результат — существительным (packCost), действие — глаголом (printOrder), «да или нет» — утверждением (bulkDelivery) |
| Как передать данные | аргументами; вернуть результатом; пакетных переменных не заводить |
| Как не спутать единицы | единица в имени: priceKop, priceRub, weightG |
| Как читать чужую программу | main сверху вниз; вглубь по вызову, когда нужен ответ; F12 и Alt+← |
| Как вести таблицу с вызовами | правила 16–22 дней 11 и 12 плюс сегодняшние 23 и 24: колонка на переменную каждой функции, вложенный вызов — отдельной таблицей |
| Где искать неверное число | по дереву вызовов сверху вниз, проверяя листья отдельными вызовами |
| Как менять работающую программу | снимок вывода, один кусок, diff, коммит — и снова |
| Что значит «поведение не изменилось» | diff молчит на всех трёх снимках: пример, пустой ввод, строка не с числом |
| Когда коммитить | после каждого вынесенного куска, у которого diff промолчал |
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
| функция ничего не возвращает и всё печатает | считает и показывает одновременно | расчёт — в функцию с результатом, печать — в main |
| у функции пять и больше параметров | граница проведена не по подцели | резать мельче, по два-три аргумента |
| вызов читается хуже, чем исходные строки | имя не объясняет смысла | переименовать или вернуть строки в main |
| один и тот же вызов даёт разные ответы | функция зависит от пакетной переменной или от порядка вызовов | передавать данные аргументами |
после разбиения main стал длиннее |
в main добавились подготовка аргументов и разбор результата |
выносить кусок целиком, вместе с подготовкой |
| функций десять, а понятнее не стало | вынесены строки, а не подцели | собрать обратно и разрезать по комментариям-заголовкам |
Словарик ошибок
За сегодня в словарик ~/errors.md добавляются сообщения, которые встречаются при работе с функциями. Строка — «текст ошибки — что значило», между ними тире с пробелами:
undefined: boxesFro - вызвал функцию с опечаткой в имени, такого имени в пакете нет
missing return - в функции есть путь, на котором return не встретился
not enough arguments in call to packCost - передал меньше аргументов, чем параметров в заголовке
boxes redeclared in this block - объявил две функции с одинаковым именем
syntax error: unexpected name boxesFor, expected ( - объявил функцию внутри mainЗаписывайте своими словами и только то, что видели сами: чужая строка в словарике не помогает.
Попробуйте сейчас: словарик до двадцати шести строк.
Цель: в
~/errors.mdне меньше двадцати шести разборов за весь курс.Откройте словарик и посчитайте разборы — так же, как их считает проверка: строка длиннее двадцати символов и с тире-разделителем.
▶ Выполнитеcode ~/errors.md grep -E '.{20,}' ~/errors.md | grep -cE ' (-|—|–) 'Обычный
grep -c . ~/errors.mdпокажет больше: он считает все строки подряд, в том числе заголовки и заметки без разбора.Допишите то, что встретили сегодня:
undefinedпри опечатке в имени,missing return, сообщения про число аргументов,redeclared. Записывайте настоящие строки — те, что видели на своём экране. Разделитель между сообщением и разбором — тире с пробелами вокруг, двоеточие не считается: оно есть в каждом сообщении компилятора.Готово, когда: пункт
errorsвcourse check 13зелёный — в файле не меньше двадцати шести разборов.
Итог дня
Что уносите:
- Подцель — кандидат в функцию. Комментарий-заголовок внутри
mainпочти всегда обозначает границу будущей функции и её имя. - Одна функция — одно дело. Признаки: имя без «и», объяснение одной фразой, возможность проверить отдельно.
- Имя — обещание. Результат существительным, действие глаголом, «да или нет» утверждением; единица измерения — в имени.
- Данные передают аргументами и возвращают результатом. Пакетная переменная, которую меняют по ходу работы, отнимает возможность прочитать и проверить функцию отдельно.
- Чужую программу читают от
mainвглубь по вызовам, а не подряд по файлу;F12— вглубь,Alt+←— обратно. - Рефакторинг не меняет поведения. Снимок вывода до правок, вынос по одному куску,
diffпосле каждого, коммит на каждый удачный шаг.
Завтра, в день 14, сверять поведение будет не diff руками, а тест — программа, которая проверяет вашу функцию сама и делает это каждый раз. Всё, что сегодня вынесено в функции с результатом, завтра окажется готовым к проверке.
Материал дня закончен. Задания — course lab 13, дополнительное чтение — course extra 13