Рефакторинг: меняем устройство, не трогаем поведение
Разбить на функции задачу, которой ещё нет, легко: пишете сразу так. Гораздо чаще приходится другое — программа уже написана, работает, ею пользуются, и надо привести её в порядок, ничего не сломав. Это называется рефакторинг: код меняется, поведение остаётся прежним.
Правило дня
Пока идёт рефакторинг, поведение не меняется ни на букву.
Не «почти то же самое», не «а заодно я поправил округление». Заодно — нельзя. Если по ходу вы увидели ошибку в расчёте, у вас теперь две задачи: сначала до конца разложить на функции, ничего не меняя, а потом отдельно починить расчёт. Иначе, когда вывод разойдётся, вы не будете знать, из-за чего: из-за переноса куска или из-за «заодно».
Отсюда весь порядок работы:
| Шаг | Что делаете | Зачем |
|---|---|---|
| 1 | снимок поведения: прогнали программу на примерах, вывод сохранили в файл | появилось, с чем сравнивать |
| 2 | вынесли один кусок в функцию | одна правка — одна причина расхождения |
| 3 | прогнали те же примеры, сравнили со снимком | убедились, что ничего не сломали |
| 4 | закоммитили этот шаг | есть точка, к которой можно вернуться |
| 5 | обратно к шагу 2, пока куски не кончатся |
Шаг 1: снимок поведения
Вывод программы можно не разглядывать глазами, а сохранить в файл. Знак > после команды отправляет её вывод в файл вместо экрана. Вы знаете его с дня 07 (страница 2): go run . < input.txt > out.txt; сегодня он работает по делу. Это пара к знаку <, который подаёт программе ввод из файла.
stagiaire@lab:~/gocourse/day13/scratch$ go run . > baseline.txt
stagiaire@lab:~/gocourse/day13/scratch$ cat baseline.txt
Штук: 50
Коробок: 5
Упаковка: 4.05 руб.На экране после первой команды не появилось ничего: весь вывод ушёл в файл. Вторая команда показывает, что в файле.
Если программа читает ввод, оба перенаправления ставят в одну строку:
stagiaire@lab:~/gocourse/day13/split$ go run . < example.txt > baseline.txtЧитается слева направо: запустить программу, ввод взять из example.txt, вывод положить в baseline.txt.
Имя baseline — общепринятое название такого снимка: «базовая линия», то, с чем сравнивают. Снимок делается один раз, до правок, и больше не переснимается: в этом весь смысл.
Шаг 2: вынести один кусок
Как выносить, вы знаете с дня 11 (страница 6): обвести кусок, понять, что входит и что выходит, дать имя, перенести, заменить вызовом, проверить. Сегодня к этим шагам одно ограничение: за один заход — один кусок. Какой кусок выносить и как его назвать — страницы 1 и 2. А последний шаг, «проверить», делается уже не глазами, а сверкой со снимком — это шаг 3.
Шаг 3: сверка через diff
diff сравнивает два файла построчно. Команду вы видели в дне 08, когда смотрели на свою починку; здесь у неё другая работа — сравнить вывод до и после.
stagiaire@lab:~/gocourse/day13/scratch$ go run . > after.txt
stagiaire@lab:~/gocourse/day13/scratch$ diff baseline.txt after.txt
stagiaire@lab:~/gocourse/day13/scratch$diff промолчал — поведение не изменилось. Это единственный ответ, который годится.
А вот как выглядит расхождение: в вынесенной функции скидка потерялась, и третья строка стала другой.
stagiaire@lab:~/gocourse/day13/scratch$ diff baseline.txt after.txt
3c3
< Упаковка: 4.05 руб.
---
> Упаковка: 4.50 руб.Как это читать:
3c3— третья строка первого файла changed, заменена на третью строку второго;- строки со знаком
<— из первого файла,baseline.txt, то есть «как было»; - строки со знаком
>— из второго,after.txt, «как стало».
Бывают ещё 2d1 (строка удалилась) и 2a3 (строка добавилась). Что бы ни было напечатано, вывод на шаге 3 — это сигнал «откатить последний вынос и сделать его заново, аккуратнее».
Попробуйте сейчас: снимок поведения.
Цель: в
split/baseline.txtлежит вывод программы до всяких правок.1. Сделайте снимок и посмотрите на него:
▶ Выполнитеcd ~/gocourse/day13/split go run . < example.txt > baseline.txt cat baseline.txt2. Сразу проверьте, что сверка работает: прогоните ещё раз в другой файл и сравните.
▶ Выполнитеgo run . < example.txt > after.txt diff baseline.txt after.txt3. В
example.txtтри заказа, последний нулевой, а программа умеет ещё две вещи: пустой ввод и строку не с числом. Снимите и их, иначе «поведение не изменилось» будет сказано только про удобный пример:▶ Выполнитеgo run . < /dev/null > base-empty.txt printf '50\n12шт\n' > bad.txt go run . < bad.txt > base-bad.txt 2>&1 cat base-empty.txt base-bad.txtЗнак
2>&1отправляет поток ошибок (номер 2) туда же, куда уже идёт обычный вывод (номер 1), — в файл; номера потоков — из дня 07, страница 5, а&значит, что1— номер потока, а не файл с таким именем. На плохом вводе программа печатает именно в поток ошибок, и без2>&1в файле осталась бы только первая строка. Дальше все три снимка сверяются одним и тем жеdiffпосле каждого выноса, а переснимать их нельзя.Последней строкой в
base-bad.txtбудет ещё иexit status 1. Её пишет не ваша программа, а самgo run: так он сообщает, каким кодом завершилось то, что он запустил. В снимке она не мешает — проверка эту строку не считает, — и после выносов она останется на месте.Готово, когда: в
baseline.txtчетыре строки — три заказа и итог, вbase-empty.txtодна строка «Заказов не было», вbase-bad.txt— строка первого заказа, сообщение об ошибке иexit status 1отgo run, аdiffничего не напечатал (пунктыt_base,t_diff,base).
Шаг 4: коммит на каждый вынесенный кусок
Git вы завели в дне 11. Сегодня он нужен по делу: каждый вынесенный кусок — отдельный коммит. Тогда, если через три выноса вывод разошёлся, видно, какой именно шаг его сломал, и можно вернуться к последнему рабочему состоянию.
В папке дня репозитория ещё нет — заведите его. Называться (user.name, user.email) и задавать имя первой ветки не нужно: это сделано один раз в дне 11, настройки лежат в домашнем каталоге и действуют на все папки.
stagiaire@lab:~/gocourse/day13$ git init
Initialized empty Git repository in /home/stagiaire/gocourse/day13/.git/
stagiaire@lab:~/gocourse/day13$ git add .
stagiaire@lab:~/gocourse/day13$ git commit -m "день 13: стенд как есть"
[main (root-commit) ea412df] день 13: стенд как есть
11 files changed, 232 insertions(+)
create mode 100644 fixcall/main.go
create mode 100644 go.mod
create mode 100644 read/program.go
create mode 100644 report/TASK.txt
create mode 100644 report/main.go
create mode 100644 scratch/main.go
create mode 100644 split/TASK.txt
create mode 100644 split/after.txt
create mode 100644 split/baseline.txt
create mode 100644 split/example.txt
create mode 100644 split/main.goСемизначный номер в квадратных скобках у вас будет свой: это начало имени коммита, и оно считается от содержимого и времени. Список файлов и число строк — тоже свои: в первый коммит попадает всё, что лежит в папке дня на этот момент, включая черновик scratch и снимки.
Если git init вместо одной строки напечатал десяток строк со словом hint про имя ветки master, значит, настройка init.defaultBranch из дня 11 не сделана. Ветка в квадратных скобках тогда будет называться master; на работу дня это не влияет.
Дальше — вынесли функцию, сверили diff, закоммитили:
stagiaire@lab:~/gocourse/day13$ git add .
stagiaire@lab:~/gocourse/day13$ git commit -m "split: вынес расчёт коробок"
[main d89cab9] split: вынес расчёт коробок
1 file changed, 5 insertions(+), 2 deletions(-)Одна строка вместо списка файлов — изменился только split/main.go: три строки новой функции плюс её вызов вместо двух прежних строк расчёта. Числа у вас будут другие, а форма та же.
История коммитов читается одной командой:
stagiaire@lab:~/gocourse/day13$ git log --oneline
d89cab9 split: вынес расчёт коробок
ea412df день 13: стенд как естьСообщение коммита пишут о том, что сделано: «вынес расчёт коробок», «вынес печать строки заказа». Не «правки», не «фикс», не «работа». Через неделю такую историю читаете вы сами.
Совет, который экономит время: коммитьте только когда diff промолчал. Коммит с уже сломанным поведением сохраняет поломку, и откатываться будет некуда.
Попробуйте сейчас: разложить split на функции.
Цель:
splitразложен на функции, вывод не изменился, каждый шаг закоммичен.1. Прочитайте условие целиком:
▶ Выполнитеcd ~/gocourse/day13/split cat TASK.txt2. Заведите репозиторий в папке дня и сделайте первый коммит — состояние «как было»:
▶ Выполнитеcd ~/gocourse/day13 git init git add . git commit -m "день 13: стенд как есть"Если
gitотвечает, что не знает, кто вы, — подпись автора задаётся так же, как в дне 11:git config --global user.name "…"иgit config --global user.email "…".3. Вынесите первый кусок — тот, что подписан комментарием «сколько коробок». Сверьте и закоммитьте:
▶ Выполнитеcd ~/gocourse/day13/split go run . < example.txt > after.txt diff baseline.txt after.txt cd ~/gocourse/day13 git add . git commit -m "split: вынес расчёт коробок"4. Так же — второй кусок и третий. Каждый раз: вынести, прогнать,
diff, коммит. Всего должно получиться не меньше четырёх коммитов. После каждого выноса сверяйте и границы — это те же две команды и дваdiff:▶ Выполнитеgo run . < /dev/null > after-empty.txt diff base-empty.txt after-empty.txt go run . < bad.txt > after-bad.txt 2>&1 diff base-bad.txt after-bad.txt5. В конце проверьте формат,
go vetи историю:▶ Выполнитеcd ~/gocourse/day13/split gofmt -l . go vet . cd ~/gocourse/day13 git log --onelineГотово, когда: в
splitне меньше трёх своих функций, каждая вызывается, вmainот расчётов остались только вызовы, телоmainне длиннее 34 строк (это порог «вынос сделан», а не правило про длину функции: в заготовке телоmain— 38 строк, страница 1),diffмолчит на всех трёх снимках,git log --onelineпоказывает четыре и больше строк — пунктыsplit,funcs,base,vet,code_fmt,t_log,git.Порядок тут жёсткий:
funcs— про само разбиение, аsplit,vetиcode_fmt— про то, что при разбиении ничего не сломалось. Пока расчёты не уехали изmain, эти три пункта красные и пишут «сначала пунктfuncs»: на нетронутой простыне вывод не менялся,go vetмолчал и формат был в порядке сам собой, а «ничего не сломал» — это про работу, а не про то, что работы не было.
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
diff печатает 3c3 и две строки |
последний вынос изменил поведение | смотреть только последний кусок: потерянное условие, перепутанный порядок, забытый += |
diff: after.txt: No such file or directory |
прогон после правки не сделан или ушёл на экран | повторить с > after.txt |
baseline.txt совпал, а проверка ругается на скрытые случаи |
пример не покрывает границы: ноль, пустой ввод, строку с ошибкой | прогнать руками go run . < /dev/null и ввод с не-числом |
| вывод «почти тот же», отличается пробел | для сверки нет «почти» | вернуть прежний формат строки до буквы |
закоммитить нечего: nothing to commit |
git add не сделан или правок не было |
git status, потом git add . |
Author identity unknown |
не задана подпись автора | git config --global user.name и user.email, день 11 |
| после трёх выносов всё сломалось, непонятно где | коммитов не было, шаги слились в один | дальше коммитить каждый шаг; сейчас — выносить обратно по одному |
Дальше: написать функцию по чужому описанию — course next