Как язык Go решает связанность кода

Go

Что такое муфта?

В программном обеспечении измерение степени, в которой любые две части объекта, пакета или функции зависят друг от друга, называется сцеплением. Например следующий код:

type Config struct {
    DSN            string
    MaxConnections int
    Timeout        time.Duration
}

type PersonLoader struct {
    Config *Config
}

Без какой-либо стороны эти два объекта не могут существовать, и компиляция сообщит об ошибке. Поэтому они считаются тесно связанными.

Почему тесно связанный код является проблемой?

Тесно связанный код имеет много побочных эффектов, но наиболее важным является то, что он может привести к изменениям кода. Shotgun Surgery относится к изменениям в части кода, которые приводят к изменениям в других частях кода, которые необходимо соответствующим образом модифицировать. Рассмотрим следующий код:

func GetUserEndpoint(resp http.ResponseWriter, req *http.Request) {
    // get and check inputs
    ID, err := getRequestedID(req)
    if err != nil {
        resp.WriteHeader(http.StatusBadRequest)
        return
    }

    // load requested data
    user, err := loadUser(ID)
    if err != nil {
        // technical error
        resp.WriteHeader(http.StatusInternalServerError)
        return
    }
    if user == nil {
        // user not found
        resp.WriteHeader(http.StatusNoContent)
        return
    }
    
    // prepare output
    switch req.Header.Get("Accept") {
    case "text/csv":
        outputAsCSV(resp, user)

    case "application/xml":
        outputAsXML(resp, user)

    case "application/json":
        fallthrough

    default:
        outputAsJSON(resp, user)
    }
}

Теперь представьте, что произойдет, если мы добавим поле пароля к объекту User. Предположим, мы не хотим, чтобы это поле выводилось как часть ответа API. Затем мы должны ввести дополнительный код в функции outputAsCSV(), outputAsXML() и outputAsJSON().
Все это кажется разумным, но что произойдет, если у нас есть другая запись, которая также содержит тип пользователя как часть своего вывода, например запись «Получить всех пользователей»? Это заставит нас внести аналогичные изменения и там. Это связано с тем, что запись «Получить всех пользователей» тесно связана с выходным рендерингом типа пользователя.
С другой стороны, если мы переместим логику рендеринга из GetUserHandler() в тип User, у нас будет только одно место для внесения изменений. Возможно, что более важно, это место очевидно и его легко найти, потому что оно находится прямо рядом с тем местом, где мы добавляем новые поля, улучшая удобство сопровождения всего кода.

Принцип шаблона проектирования — принцип инверсии зависимостей (DIP)

Принцип инверсии зависимостей — это термин, введенный Робертом С. Мартином в его статье 1996 года под названием «Принцип инверсии зависимостей» в C++ Report. Он определяет это так: модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракции. Абстракция не должна зависеть от деталей. Детали должны зависеть от абстракции.
Несколько слов Роберта К. Мартина очень мудры, и нижеследующее является соответствующим заключением моего перевода на язык Go:
1) Пакеты высокого уровня не должны зависеть от пакетов низкого уровня. Когда мы пишем приложение на языке Go, некоторые пакеты вызываются из main(), их можно рассматривать как высокоуровневые пакеты. Наоборот, некоторые пакеты, которые взаимодействуют с внешними ресурсами, такими как базы данных, обычно вызываются не из main(), а из уровня бизнес-логики, который находится на 1-2 уровня ниже. В связи с этим высокоуровневые пакеты не должны зависеть от низкоуровневых пакетов. Пакеты высокого уровня зависят от абстракций, а не от реализации этих базовых деталей. тем самым разделяя их.
2) Структуры не должны зависеть от структур. Когда структура использует другую структуру в качестве входных данных метода или переменной-члена:

type PizzaMaker struct{}

func (p *PizzaMaker) MakePizza(oven *SuperPizaOven5000) {
    pizza := p.buildPizza()
    oven.Bake(pizza)
}

Эти две структуры невозможно разделить, эти объекты тесно связаны и поэтому не очень гибки. Рассмотрим следующий пример из реальной жизни: предположим, я захожу в туристическое агентство и спрашиваю, могу ли я забронировать место 15D на рейс Qantas в Сидней в четверг в 15:30? Туристическому агентству будет трудно удовлетворить мою просьбу. Но что, если я ослаблю требования и вместо этого спрошу, могу ли я забронировать рейс в Сидней на четверг? Таким образом, жизнь турагента становится более гибкой, и у меня больше шансов занять свое место. Обновите наш код следующим образом:

type PizzaMaker struct{}

func (p *PizzaMaker) MakePizza(oven Oven) {
    pizza := p.buildPizza()
    oven.Bake(pizza)
}

type Oven interface {
    Bake(pizza Pizza)
}

Теперь мы можем использовать любой объект, реализующий метод Bake().
3) Интерфейсы не должны зависеть от структур. Как и в предыдущем пункте, речь идет о специфике потребностей. Если мы определим наш интерфейс как:

type Config struct {
    DSN            string
    MaxConnections int
    Timeout        time.Duration
}

type PersonLoader interface {
    Load(cfg *Config, ID int) *Person
}

Затем мы отделяем PersonLoader от указанной структуры Config.

type PersonLoaderConfig interface {
    DSN() string
    MaxConnections() int
    Timeout() time.Duration
}

type PersonLoader interface {
    Load(cfg PersonLoaderConfig, ID int) *Person
}

Теперь мы можем повторно использовать PersonLoader без каких-либо изменений.
(Вышеуказанные структуры следует рассматривать как относящиеся к структурам, которые обеспечивают логику и/или реализуют интерфейс, и не включают структуры, которые используются в качестве объектов передачи данных)

Исправление сильно связанного кода

Оставив все предыстории в стороне, давайте углубимся в то, как решить тесно связанный код с более богатыми примерами. Наш пример начинается с двух объектов, Person и BlueShoes, в двух разных пакетах, как показано ниже:

Как видите, они тесно связаны, структура Person не может существовать без BlueShoes. Если у вас есть опыт работы с Java/C++ или другим кодом, как у исходного автора, первым делом для разделения объектов будет определение интерфейса в пакете Shoes. Результат будет следующим:
Во многих языках это будет его конечным результатом. Но с Go мы можем еще больше отделить эти объекты.
Перед этим следует обратить внимание на другую проблему. Вы могли заметить, что структура Person реализует только один метод Walk(), тогда как Footwear реализует оба метода Walk() и Run(). Это различие делает отношения между Person и Footwear несколько неясными и нарушает другой принцип разделения интерфейса, называемый Принципом разделения интерфейса (ISP), предложенный Робертом С. Мартином, который гласит: Клиенты не должны быть вынуждены зависеть от методов, которые они не используют. К счастью, мы можем решить обе эти проблемы, определив интерфейс в пакете People вместо определения интерфейса в пакете Shoes, как на изображении выше:
Эта мелочь может не стоить вашего драгоценного времени, но она имеет большое значение. В этом примере наши два пакета теперь полностью разделены. Людям не нужно зависеть или использовать пакет обуви.
Это изменение делает требования к интерфейсу пакета «Люди» четкими, краткими и легкими для поиска, поскольку они находятся в примере пакета, и, наконец, изменения в пакете «Обувь» вряд ли повлияют на пакет «Люди».

Суммировать

Философия Unix — одна из самых популярных концепций в Go, как написал первоначальный автор в книге «Практики внедрения зависимостей в Go», в которой говорится: «Пишите программы, которые делают одну вещь и делают ее хорошо. Пишите программы, которые будут работать вместе».
Это означает различать разные требования, чтобы каждый из ваших кодов делал только одну вещь и делал ее хорошо, чтобы они работали вместе друг с другом.
Эти концепции широко распространены в стандартной библиотеке Go и даже появляются в решениях по проектированию языка. Как неявная реализация интерфейса (т.е. без ключевого слова «реализует»). Такие решения позволяют нам (пользователям языка) реализовать несвязанный код, который служит одной цели и прост в написании.
Слегка связанный код облегчает понимание, поскольку вся необходимая информация находится в одном месте, что упрощает тестирование и масштабирование.
Поэтому в следующий раз, когда вы увидите конкретный объект в качестве параметра функции или переменной-члена, спросите себя, так ли это необходимо? Будет ли он более гибким, более простым для понимания или более простым в обслуживании, если я изменю его на интерфейс?

govip cn Daily News Рекомендуемые статьиhow-to-fix-tightly-coupled-go-code