Pull to refresh

Делать хорошо, делая плохо: написание «злого» кода с помощью Go, часть 2

Reading time9 min
Views3.3K
Original author: Jon Bodner
Вредные советы для Go-программиста

image

В первой части публикации я объяснил, как стать «злобным» программистом Go. Зло приходит во многих формах, но в программировании оно заключается в намеренном затруднении понимания и поддержки кода. «Злые» программы игнорируют основные средства языка в пользу техник, которые обеспечивают краткосрочные выгоды, в обмен на долгосрочные проблемы. В качестве краткого напоминания, злые «практики» Go включают в себя:

  • Плохо названные и организованные пакеты
  • Неправильно организованные интерфейсы
  • Передача указателей на переменные в функции для заполнения их значений
  • Использование паники вместо ошибок
  • Использование init-функций и пустого импорта для настройки зависимостей
  • Загрузка файлов конфигурации с использованием init-функций
  • Использование фреймворков вместо библиотек

Большой клубок зла


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

И я сделал такой. Если вы зайдете на https://github.com/evil-go, вы увидите Fall (падение), — DI фреймворк, дающий возможность внедрять любые «злые» практики, которые только захотите. Я спаял Fall с крошечным веб-фреймворком Outboy, который следует тем же принципам.

Вы можете спросить, насколько они злодейские? Давайте посмотрим. Предлагаю пройтись по простой программе Go (написанной с использованием лучших практик), которая предоставляет http endpoint. А затем перепишем ее, используя Fall и Outboy.

Лучшие практики


Наша программа лежит в единственном пакете под названием greet, который использует все основные функции для реализации нашего endpoint-a. Поскольку это пример, мы используем работающий в памяти DAO, с тремя полями для значений, которые мы будем возвращать. У нас будет также метод, который, в зависимости от входных данных, подменяет вызов нашей БД и возвращает нужное приветствие.

package greet
type Dao struct {
      DefaultMessage string
      BobMessage string
      JuliaMessage string
}
func (sdi Dao) GreetingForName(name string) (string, error) {
      switch name {
      case "Bob":
            return sdi.BobMessage, nil
      case "Julia":
            return sdi.JuliaMessage, nil
      default:
            return sdi.DefaultMessage, nil
      }
}

Далее идет бизнес-логика. Для ее реализации мы определяем структуру хранения выходных данных, интерфейс GreetingFinder для описания того, что бизнес-логика ищет на уровне поиска данных, и структуру для хранения самой бизнес-логики с полем для GreetingFinder. Фактическая логика проста, — она просто вызывает GreetingFinder и обрабатывает любые ошибки, которые могут произойти.

type Response struct {
      Message string
}
type GreetingFinder interface {
      GreetingForName(name string) (string, error)
}
type Service struct {
      GreetingFinder GreetingFinder
}
func (ssi Service) Greeting(name string)(Response, error) {
      msg, err := ssi.GreetingFinder.GreetingForName(name)
      if err != nil {
            return Response{}, err
      }
      return Response{Message: msg}, nil
}

Затем идет веб-слой, и для этой части мы определяем интерфейс Greeter, который обеспечивает всю нужную нам бизнес-логику, а так же структуру, содержащую обработчик http, настроенный с помощью Greeter. Затем мы создаем метод для реализации интерфейса http.Handler, который разбивает http-запрос, вызывает greeter-a (приветствующего), обрабатывает ошибки и возвращает результаты.

type Greeter interface {
      Greeting(name string) (Response, error)
}
type Controller struct {
      Greeter Greeter
}
func (mc Controller) ServeHTTP(rw http.ResponseWriter,
                               req *http.Request) {
      result, err := mc.Greeter.Greeting(
                               req.URL.Query().Get("name"))
      if err != nil {
            rw.WriteHeader(http.StatusInternalServerError)
            rw.Write([]byte(err.Error()))
            return
      }
      rw.Write([]byte(result.Message))
}

Это конец пакета greet. Далее мы посмотрим, как «добрый» разработчик Go написал бы main для использования этого пакета. В пакете main мы определяем структуру с именем Config, которая содержит свойства нужные нам для запуска. Функция main затем делает 3 вещи.

  • Во-первых, она вызывает функцию loadProperties, которая использует простую библиотеку (https://github.com/evil-go/good-sample/blob/master/config/config.go) для загрузки свойств из конфиг-файла и помещает их в наш экземпляр конфига. Если загрузка конфигурации не удалась, функция main сообщает об ошибке и завершает работу.
  • Во-вторых, функция main связывает компоненты в пакете greet, явно присваивая им значения из конфига и настраивая зависимости.
  • В-третьих, она вызывает небольшую серверную библиотеку (https://github.com/evil-go/good-sample/blob/master/server/server.go) и передает в endpoint адрес, HTTP-метод и http.Handler для обработки запроса. Вызов библиотеки запускает веб-сервис. И это наше приложение целиком.

package main
type Config struct {
      DefaultMessage string
      BobMessage string
      JuliaMessage string
      Path string
}
func main() {
      c, err := loadProperties()
      if err != nil {
            fmt.Println(err)
            os.Exit(1)
      }
      dao := greet.Dao{
            DefaultMessage: c.DefaultMessage,
            BobMessage: c.BobMessage,
            JuliaMessage: c.JuliaMessage,
      }
      svc := greet.Service{GreetingFinder: dao}
      controller := greet.Controller{Greeter: svc}
      err = server.Start(server.Endpoint{c.Path, http.MethodGet, controller})
      if err != nil {
            fmt.Println(err)
            os.Exit(1)
      }
}

Пример получился довольно коротким, но он показывает, как классно написан Go; некоторые вещи неоднозначны, но в целом понятно, что происходит. Мы склеиваем маленькие библиотеки, которые определенно настроены для совместной работы. Ничто не скрыто; любой может взять этот код, понять, как его части соединяются вместе, и при необходимости переделать на новые.

Гиблое место


Теперь мы рассмотрим версию Fall и Outboy. Первое, что мы сделаем, — это разобьем пакет greet на несколько пакетов, каждый из которых содержит один слой приложения. Вот пакет DAO. Он импортирует Fall, наш DI-фреймворк, и поскольку мы являемся «злыми» и определяем отношения с интерфейсами наоборот, мы определим интерфейс под названием GreetDao. Обратите внимание, — мы удалили все ссылки на ошибки; если что-то не так, — мы просто паникуем. На этом этапе у нас уже есть плохая упаковка, плохие интерфейсы и плохие ошибки. Прекрасное начало!

Мы слегка переименовали нашу структуру из хорошего примера. Поля теперь имеют теги struct; они используются, чтобы Fall устанавливал зарегистрированное значение в поле. У нас также есть функция init для нашего пакета, с помощью которой мы накапливаем «злую силу». В функции init пакета мы вызывем Fall дважды:

  • Один раз, чтобы зарегистрировать конфиг-файл, который предоставляет значения для тегов структуры.
  • А другой, – чтобы зарегистрировать указатель на экземпляр структуры. Fall сможет заполнить эти поля для нас и сделать DAO доступным для использования другим кодом.

package dao
import (
      "github.com/evil-go/fall"
)
type GreetDao interface {
      GreetingForName(name string) string
}
type greetDaoImpl struct {
      DefaultMessage string `value:"message.default"`
      BobMessage string `value:"message.bob"`
      JuliaMessage string `value:"message.julia"`
}
func (gdi greetDaoImpl) GreetingForName(name string) string {
      switch name {
      case "Bob":
            return gdi.BobMessage
      case "Julia":
            return gdi.JuliaMessage
      default:
            return gdi.DefaultMessage
      }
}
func init() {
      fall.RegisterPropertiesFile("dao.properties")
      fall.Register(&greetDaoImpl{})
}

Давайте посмотрим пакет service. Он импортирует пакет DAO, потому что ему нужен доступ к определенному там интерфейсу. Пакет service также импортирует пакет model, который мы еще не рассматривали — мы будем хранить там наши типы данных. И мы импортируем Fall, потому что, как и все «хорошие» фреймворки, он проникает везде. Мы также определяем интерфейс для service, чтобы дать доступ к веб-слою. Опять же, без обработки ошибок.

Реализация нашего сервиса теперь имеет структурный тег с wire. Поле, помеченное wire, автоматически подключает свою зависимость, когда структура зарегистрирована в Fall. В нашем крошечном примере ясно, что будет назначено этому полю. Но в более крупной программе вы будете знать только то, что где-то реализован этот интерфейс GreetDao, и он зарегистрирован в Fall. Вы не можете контролировать поведение зависимости.

Далее идет метод нашего сервиса, который был немного изменен, чтобы получать структуру GreetResponse из пакета model, и который удаляет любую обработку ошибок. Наконец, у нас в пакете есть функция init, которая регистрирует экземпляр службы в Fall.

package service
import (
      "github.com/evil-go/fall"
      "github.com/evil-go/evil-sample/dao"
      "github.com/evil-go/evil-sample/model"
)
type GreetService interface {
      Greeting(string) model.GreetResponse
}
type greetServiceImpl struct {
      Dao dao.GreetDao `wire:""`
}
func (ssi greetServiceImpl) Greeting(name string)     model.GreetResponse {
      return model.GreetResponse{Message: ssi.Dao.GreetingForName(name)}
}
func init() {
      fall.Register(&greetServiceImpl{})
}

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

package model
type GreetResponse struct {
      Message string
}

В пакете web у нас лежит веб-интерфейс. Здесь мы импортируем как Fall, так и Outboy, а также импортируем пакет service, от которого зависит пакет web. Поскольку фреймворки хорошо сочетаются друг с другом только тогда, когда они интегрируются «за кулисами», у Fall есть специальный код, чтобы убедиться, что он и Outboy работают вместе. Мы также изменяем структуру, чтобы она стала контроллером для нашего веб-приложения. У нее два поля:

  • Первое связывается через Fall с реализацией интерфейса GreetService из пакета service.
  • Второе — это путь для нашего единственного web endpoint-a. Ему присваивается значение из конфиг-файла, зарегистрированного в функции init данного пакета.

Наш http-обработчик переименован в GetHello, и он теперь свободен от обработки ошибок. У нас также есть метод Init (с большой буквы), который не следует путать с функцией init. Init — это магический метод, который вызывается для структур, зарегистрированных в Fall, после заполнения всех полей. В Init мы вызываем Outboy, чтобы зарегистрировать наш контроллер и его endpoint в пути, который был задан с помощью Fall. Глядя на код, вы увидите путь и обработчик, но HTTP-метод не указан. В Outboy имя метода используется для определения того, на какой HTTP-метод отвечает обработчик. Поскольку наш метод называется GetHello, он отвечает на запросы GET. Если вы не знаете этих правил, вы не сможете понять, на какие запросы он отвечает. Правда, это очень по-злодейски?

Наконец, мы вызываем функцию init для регистрации конфиг-файла и контроллера в Fall.

package web
import (
      "github.com/evil-go/fall"
      "github.com/evil-go/outboy"
      "github.com/evil-go/evil-sample/service"
      "net/http"
)
type GreetController struct {
      Service service.GreetService `wire:""`
      Path string `value:"controller.path.hello"`
}
func (mc GreetController) GetHello(rw http.ResponseWriter, req *http.Request) {
      result := mc.Service.Greeting(req.URL.Query().Get("name"))
      rw.Write([]byte(result.Message))
}
func (mc GreetController) Init() {
      outboy.Register(mc, map[string]string{
            "GetHello": mc.Path,
      })
}
func init() {
      fall.RegisterPropertiesFile("web.properties")
      fall.Register(&GreetController{})
}

Осталось только показать, как мы запускаем программу. В пакете main мы используем пустой импорт для регистрации Outboy и пакета web. А функция main вызывает fall.Start () для запуска всего приложения.

package main
import (
    _ "github.com/evil-go/evil-sample/web"
    "github.com/evil-go/fall"
    _ "github.com/evil-go/outboy"
)
func main() {
      fall.Start()
}

Срыв покровов


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

И все же, вы должны признать, что в написании кода с Fall and Outboy есть что-то притягательное. Для крошечной программы можно даже сказать, что — это улучшение. Посмотрите, как просто задать конфигурацию! Я могу подключать зависимости практически без кода! Я зарегистрировал обработчик для метода, просто используя его имя! И без какой-либо обработки ошибок все выглядит так чисто!

Именно так и действует зло. На первый взгляд оно действительно привлекательно. Но по мере того, как ваша программа меняется и растет, вся эта магия начинает только мешать, усложняя понимание происходящего. Только когда вы полностью одержимы злом, вы оглядываетесь и понимаете, что попали в ловушку.

Для Java-разработчиков это может показаться знакомым. Эти техники можно найти во многих популярных Java-фреймворках. Как я уже упоминал ранее, я работал с Java более 20 лет, начиная с 1.0.2 в 1996 году. Во многих случаях Java-разработчики первыми сталкивались с проблемами написания крупномасштабного корпоративного программного обеспечения в эпоху Интернета. Я помню времена, когда сервлеты, EJB, Spring и Hibernate только появились. Решения, которые были приняты Java-разработчиками, в то время имели смысл. Но с годами эти техники проявляют свой возраст. Более новые языки, такие как Go, предназначены для устранения болевых точек, обнаруженных при использовании старых техник. Однако, поскольку разработчики Java начинают изучать Go и писать код с его помощью, им следует помнить, что попытки воспроизвести шаблоны из Java приведут к нехорошим результатам.

Go был разработан для серьезного программирования, — для проектов, которые охватывают сотни разработчиков и десятки команд. Но для того, чтобы Go это делал, его нужно использовать так, как он работает лучше всего. Мы можем выбирать — быть злыми или добрыми. Если мы выберем зло, мы можем побудить молодых Go-разработчиков изменить свой стиль и техники, прежде чем они разберутся в Go. Или мы можем выбрать добро. Часть нашей работы как Go-разработчиков состоит в том, чтобы наставлять молодых Гоферов (Gophers — суслики), — помогать им понять принципы, лежащие в основе наших лучших практик.

Единственный недостаток следования по пути добра в том, что вам придется искать иной способ выражать свое внутреннее зло. Может, попробуете езду со скоростью 30км/ч на федеральной трассе?
Tags:
Hubs:
+2
Comments1

Articles

Change theme settings