Разработка приложений на Go: повторное использование логики

в 16:46, , рубрики: command, Go, reusable logic, повторное использование

На мой взгляд, написание библиотек на Go — довольно хорошо освещенная тема… а вот о написании приложений (команд) статей гораздо меньше. Когда дело до этого доходит, весь код на Go представляет собой команду. Так давайте об этом и поговорим! Этот пост будет первым в серии, т.к. у меня много информации, которой я еще не делился.

В сегодняшней статье речь пойдет о базовой компоновке проекта, будем улучшать повторное использование и тестирования кода.

Когда я разрабатываю не библиотеку, а программу, у меня существуют три уникальных правила организации кода:

Пакет main

Это единственный пакет в программе на Go, который обязательно должен быть. Помимо указания инструменту go создавать исполняемый файл, у этого пакета есть еще одна уникальная штука — из него невозможно импортировать код. Это значит, что любой код, который вы помещаете в пакет main, не может быть использован напрямую другим проектом, и это расстраивает богов открытого ПО. Поскольку одна из главных причин, по которым я пишу проекты с открытым исходным кодом, заключается в том, что другие разработчики могут его использовать, эта невозможноть переиспользовать код из main противоречит моим желаниям.

Много раз возникали ситуации, когда я думал: "Я бы использовал в своем коде логику программы X". Но если логика находилась в пакете main, это было невозможно.

os.Exit

Если вы заботитесь о создании программы, которая делает именно то, чего ожидает пользователь, то вам следует позаботится и о том, с каким кодом выхода она завершается. Единственный способ сделать это — вызвать os.Exit (или вызвать что-то, что вызовет os.Exit, например log.Fatal).

Тем не менее, вы не сможете протестировать функцию, которая вызывает os.Exit. Почему? Потому что вызов os.Exit во время выполнения теста приведет к завершению тестируемого приложения. Это довольно трудно обнаружить, если у вас это получилось случайно (это я знаю из личного опыта). Когда вы запускаете тестирование, никакого тестирования в действительности не происходит, эти тесты просто завершаются раньше, чем должны были, а вам остается только чесать в затылке.

Самое простое, что можно сделать — это не вызывать os.Exit. Большая часть вашего кода в любом случае не должна вызывать os.Exit… у кого-нибудь может реально "поехать крыша", если он импортирует вашу библиотеку, а она случайным образом, при некоторых условиях, будет останавливать его приложение.

Таким образом, вызывайте os.Exit ровно в одном месте, как можно ближе к "наружности" вашего приложения, с минимальным количеством точек входа. Кстати, поговорим и о них…

func main()

Это единственная функция, которая должна быть у любой программы, написанной на Go. Вы, наверное, думаете, что каждая функция main должна отличаться от программы к программе, поскольку все программы разные, так ведь? Что ж, оказывается, если вы действительно хотите сделать ваш код тестируемым и переиспользуемым, существует, по большому счету, только один правильный ответ на вопрос "что находится в вашей функции main?"

Забегая немного вперед, я думаю, что также есть только один правильный ответ на вопрос "что находится в вашем пакете main?" и этот ответ выглядит так:

// command main documentation here.
package main

import (
    "os"

    "github.com/you/proj/cli"
)

func main() {
    os.Exit(cli.Run())
}

Вот и все. Это самый минимальный код, который должен быть в вашем полезном пакете main. Мы почти не потратили никаких усилий на код, который другие не могут повторно использовать. При этом, мы изолировали os.Exit в однострочной функции, которая является самой внешней частью нашего проекта и, фактически, не нуждается в тестировании.

Схема проекта

Давайте взглянем на общую схему проекта:

/home/you/src/github.com/you/proj $ tree
.
├── cli
│   ├── parse.go
│   ├── parse*test.go
│   └── run.go
├── LICENSE
├── main.go
├── README.md
└── run
    ├── command.go
    └── command*test.go

Мы уже знаем, что находится у нас в main.go… и, фактически, main.go является единственным файлом go в пакете main. Файлы LICENSE и README.md не требуют пояснений. (Всегда указывайте лицензию! В противном случае многие люди не смогут использовать ваш код.)

Теперь мы переходим к двум подкаталогам — run и cli.

CLI

Пакет cli содержит логику синтаксического анализа командной строки. Здесь вы определяете интерфейс (UI) вашей программы. Пакет содержит анализ флагов, анализ аргументов, тексты справки и так далее.

Он также содержит исходный код, который возвращает код выхода для функции main (которая передает его в os.Exit). Таким образом, вы можете тестировать коды выхода, возвращаемые этими функциями, вместо того, чтобы пытаться тестировать коды выхода вашей программы в целом.

Run

Если пакеты main и cli — это "кости" логики вашей программы, то пакет run содержит ее "мясо". Вам следует писать этот пакет так, как если бы это была отдельная библиотека. Во время его разработки вы не должны думать о CLI, флагах и тому подобном. Пакет должен получать структурированные данные и возвращать ошибки. Представьте, что он может быть вызван другой библиотекой, или вебсервисом, или еще чьей-то программой. Делайте как можно меньше предположений о том, как его будут применять… в общем, это должна быть обычная библиотека.

Очевидно, что для больших проектов потребуется больше одного каталога. Фактически, вы можете разделить вашу логику на отдельные репозитории. Это зависит от того, насколько вероятным вы считаете, что другие люди захотят переиспользовать вашу логику. Если вы считаете это весьма вероятным, я рекомендую реализовать логику в отдельном каталоге (репозитории). На мой взгляд, отдельный каталог для логики обязывает к большему качеству и стабильности, чем некоторый случайный каталог, запрятанный где-то глубоко в репозитории.

Собираем все вместе

Пакет cli формирует интерфейс командной строки для логики, реализованной в пакете run. Если кто-нибудь увидит вашу программу и захочет использовать ее логику для веб API, ему нужно будет просто импортировать пакет run и использовать эту логику напрямую. Аналогично, если кому-то не нравятся ваши параметры командной строки, он может просто написать свой собственный парсер аргументов и использовать его как интерфейс к пакету run.

Это я и имею в виду, говоря о повторном использовании кода. Я не хочу, чтобы кому-нибудь пришлось "хакать" куски моего кода, чтобы побольше его использовать. И лучший способ облегчить переиспользование кода — отделить интерфейс от логики. Это ключевая часть. Не позволяйте идеям из ваших интерфейсов (UI/CLI) просачиваться в логику. Это лучший способ сохранить логику в общем виде, а интерфейс — управляемым.

Более крупные проекты

Этот схема хороша для малых и средних проектов. Здесь есть единственная программа, которая находится в корне репозитория, поэтому ее легче получить (go-get'нуть), чем если бы она была в нескольких подкаталогах. В больших проектах все может быть совсем по другому. Там может быть несколько исполняемых файлов, а они не могут все вместе лежать в корне репозитория. Однако такие проекты обычно имеют настраиваемые этапы сборки и требуют больше, чем просто go-get (об этом я расскажу позже).

Подробности скоро будут.

18 Октября 2016 г.

Серия: Разработка приложений на Go.

Прим.пер.: серии как таковой не случилось, это единственная статья в "серии" с момента публикации. Тем не менее, статья довольно интересная.

Автор: Сергей Соломеин

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js