Какой кусок выносить и как его назвать
Разбить программу можно по-разному, и не всякое разбиение делает её понятнее. Есть три признака куска, который просится в функцию, и есть случаи, когда функция только мешает. Отдельный разговор — имена: имя функции читают в сто раз чаще, чем её тело.
Список не новый: те же признаки названы в дне 11 (страница 6, «Когда кусок пора выносить») — повтор, кусок, у которого есть своё имя, и кусок, который мешает читать главное. Здесь повтор — признак 1, «кусок со своим именем» — признак 3 («комментарий-заголовок»), а к ним добавлен отдельный частый случай «мешает читать» — длинное условие, признак 2.
Признак 1: повтор
Ситуация. Утренняя и вечерняя приёмка, в обеих считаем коробки.
package main
import "fmt"
func main() {
perBox := 12
morning := 50
evening := 80
morningBoxes := (morning + perBox - 1) / perBox
eveningBoxes := (evening + perBox - 1) / perBox
fmt.Println("Утро:", morningBoxes, "кор.")
fmt.Println("Вечер:", eveningBoxes, "кор.")
fmt.Println("Всего:", morningBoxes+eveningBoxes, "кор.")
}stagiaire@lab:~/gocourse/day13/scratch$ go run .
Утро: 5 кор.
Вечер: 7 кор.
Всего: 12 кор.Одна и та же формула записана дважды. Пока строк две, это терпимо. Беда начинается, когда правило меняется — скажем, неполную коробку разрешили не считать. Тогда править надо оба места, и почти всегда одно из них забывают. Такая программа начинает противоречить сама себе: утром считает по новым правилам, вечером по старым.
С функцией формула записана один раз:
package main
import "fmt"
func boxesFor(items, perBox int) int {
return (items + perBox - 1) / perBox
}
func main() {
perBox := 12
morningBoxes := boxesFor(50, perBox)
eveningBoxes := boxesFor(80, perBox)
fmt.Println("Утро:", morningBoxes, "кор.")
fmt.Println("Вечер:", eveningBoxes, "кор.")
fmt.Println("Всего:", morningBoxes+eveningBoxes, "кор.")
}stagiaire@lab:~/gocourse/day13/scratch$ go run .
Утро: 5 кор.
Вечер: 7 кор.
Всего: 12 кор.Вывод прежний. Изменилось место, где живёт правило: теперь оно одно и у него есть имя.
Признак 2: длинное условие
Ситуация. Крупной считается отгрузка, у которой коробок много, вес укладывается в норму, а зона доставки не третья. В if это выглядит так — это фрагмент, одна строка из тела main, набирать его не нужно, целая программа будет ниже:
if boxes >= 5 && weight <= 900 && zone != 3 {Читающий вынужден разбирать три сравнения и два &&, чтобы понять одну мысль: «крупная отгрузка». Условие — тоже кусок работы, и его можно назвать. Функция, которая возвращает bool, читается как утверждение:
package main
import "fmt"
func bulkDelivery(boxes, weight, zone int) bool {
return boxes >= 5 && weight <= 900 && zone != 3
}
func main() {
fmt.Println(bulkDelivery(6, 800, 1))
fmt.Println(bulkDelivery(6, 800, 3))
fmt.Println(bulkDelivery(2, 800, 1))
}stagiaire@lab:~/gocourse/day13/scratch$ go run .
true
false
falseВ main вместо трёх сравнений теперь стоит if bulkDelivery(boxes, weight, zone) {, и строка читается вслух как фраза. Заодно проверить правило «что считается крупной отгрузкой» стало можно отдельно — три вызова выше как раз это и делают.
Признак 3: кусок с комментарием-заголовком
Самый надёжный признак и самый простой в работе. Если внутри main стоит комментарий вроде // сколько коробок или // печать итога, значит, автор уже разметил границу куска и уже придумал ему имя.
Комментарий в main |
Функция |
|---|---|
// сколько коробок |
boxesFor(items, perBox int) int |
// стоимость упаковки со скидкой |
packCost(boxes, boxPrice, from int) int |
// перевод копеек в рубли для печати |
rubles(kopecks int) string |
// строка заказа |
printOrder(n, items, boxes, cost int) |
// итог смены |
printTotal(orders, boxes, cost int) |
Комментарий после выноса не нужен: его работу делает имя функции и её вызов в main. Комментарии, которые остаются полезными, — про «почему так», а не про «что здесь происходит»: // неполная коробка тоже коробка.
Попробуйте сейчас: одна строка или функция.
Цель: предсказать вывод программы, в которой функция вызывается на каждом проходе цикла, и сдать ответ.
1. Не запуская, посчитайте на бумаге, что напечатает программа.
halfвозвращает целую часть от деления пополам: в день 03 разобрано, что1 / 2у целых чисел — это 0.Код для чтения · разбираем, набирать не нужноpackage main import "fmt" func half(n int) int { return n / 2 } func main() { total := 0 for box := 1; box <= 5; box++ { total = total + half(box) } fmt.Println(total) }2. Сдайте число:
▶ Выполните · выделенное замените своимcourse answer day13.q1 ЧИСЛОВместо
ЧИСЛО— то, что, по-вашему, напечатает программа.3. Проверьте запуском — наберите ту же программу в
scratch/main.go:✎ Наберите в файлscratch/main.gopackage main import "fmt" func half(n int) int { return n / 2 } func main() { total := 0 for box := 1; box <= 5; box++ { total = total + half(box) } fmt.Println(total) }▶ Выполнитеcd ~/gocourse/day13/scratch go run .Готово, когда: пункт
q1вcourse check 13зелёный, а если число не совпало — вы можете сказать, на каком проходе цикла ошиблись в расчёте на бумаге.
Чего выносить не стоит
Обёртка ради одной строки. Функция, тело которой повторяет её имя, ничего не объясняет и добавляет лишний прыжок при чтении:
func plusOne(n int) int {
return n + 1
}plusOne(count) не понятнее, чем count + 1, — просто длиннее. Выносят не строку, а смысл: boxesFor тоже одна строка, но её имя говорит то, чего не видно в формуле.
Кусок посреди расчёта, разрезанный по числу строк. «Функция не должна быть длиннее пятнадцати строк» — правило, из-за которого появляются part1 и part2: вынесены соседние строки, между которыми нет общей мысли. Резать надо по границам подцелей.
То, что придётся звать с шестью аргументами. Если куску нужны почти все переменные main, он с main и связан — значит, граница проведена не там. Обычно помогает вынести кусок поменьше, у которого входных данных два-три.
Куски, которые и так на виду. Три строки печати в конце программы можно оставить в main: они читаются подряд и не мешают.
Имя функции
Имя — это обещание. Читающий верит имени и не открывает тело.
| Что делает функция | Как называть | Примеры |
|---|---|---|
| считает значение | существительное — то, что вернёт | boxesFor, packCost, rubles |
| выполняет действие | глагол | printOrder, loadPlan |
отвечает «да или нет», возвращает bool |
утверждение | bulkDelivery, isEmpty, hasDiscount |
Правила, на которые опираются в Go:
- Имя — на латинице, без подчёркиваний, со второго слова с большой буквы:
packCost,boxesFor. ЭтоlowerCamelCase, общий стиль Go. - Имя не врёт. Функция
boxesForне должна ничего печатать. Если печатает — либо переименовать, либо убрать печать. - Имя не про устройство, а про результат.
divideAndRound— про то, как считает;boxesFor— про то, что даёт. Формулу можно поменять, результат останется тем же, и имя менять не придётся. - Никаких
do,calc,helper,func2. Через неделю такое имя не скажет ничего ни другому, ни вам. - Имена в
mainтоже важны.cost := packCost(boxes, 90, 5)читается,c := packCost(b, 90, 5)— нет.
Имя не обязано быть коротким. boxesFor длиннее, чем bf, и это правильный выбор: писать имя приходится один раз, а читать — постоянно.
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
| функций много, каждая в одну строку | вынесены строки, а не подцели | объединить обратно, резать по смыслу |
| имя функции с «and» или «и» | дел два | разделить на две функции |
./main.go:10:14: undefined: boxesFro |
вызов с опечаткой в имени | сверить имя с объявлением; в VS Code имя подсказывается по Ctrl+пробел |
функция названа getBoxes, а внутри ещё и печать |
имя обещает не то, что делает тело | убрать печать в main или переименовать честно |
после выноса main стал длиннее |
вместо выноса куска в main добавились подготовка аргументов и разбор результата |
вынести кусок целиком, вместе с его подготовкой |
| в функцию передаются шесть чисел | граница проведена не по подцели | резать мельче |
Попробуйте сейчас: имена для трёх кусков.
Цель: назвать три будущие функции задания
splitи записать их заголовки.Вернитесь к трём парам, выписанным на странице 1:
▶ Выполнитеcd ~/gocourse/day13/split cat main.goДля каждого куска запишите на бумаге строку
func имя(аргументы) тип: что функция получает на входе и что отдаёт. Проверьте себя тремя вопросами: имя без «и»; имя говорит про результат, а не про формулу; аргументов не больше трёх.Готово, когда: на бумаге три заголовка, и в каждом вы можете назвать единицы измерения каждого аргумента: штуки, коробки или копейки.
Дальше: как читать программу, разложенную на функции — course next