День 13 · Разбить задачу на функции · страница 2 из 7

Какой кусок выносить и как его назвать

Разбить программу можно по-разному, и не всякое разбиение делает её понятнее. Есть три признака куска, который просится в функцию, и есть случаи, когда функция только мешает. Отдельный разговор — имена: имя функции читают в сто раз чаще, чем её тело.

Список не новый: те же признаки названы в дне 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.go
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)
}
▶ Выполните
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:

Имя не обязано быть коротким. 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