Безкоштовна онлайн-конференцiя з Python від fwdays. 14 грудня. Реєструйся!
×Закрыть

Визначаємо вартість декоратора в Golang

Привіт, мене звати Ярослав, займаюсь розробкою сервісу для збереження активів у криптовалюті в компанії ITAdviser, розробляємо на Go. У цій статті розглянемо декоратор, його вартість і чи варто використовувати його в розробці нових сервісів.

Коротко про мене

Кілька років тому почав цікавитись Go, подарував другу на день народження книжку «The Go Programming Language», сам грався задачами з LeetCode, облишив, через півроку продовжив, вийшов професійний курс від «Техносфери», передивився і цього було достатньо, щоб почати працювати як Junior Go.

Go зацікавив тестами та бенчмарками з коробки, можливістю розбиратись в коді стандартних бібліотек, які теж написані на Go. А ще в Києві хороше Go ком’юніті. В деяких мовах рішення певних задач лаконічніше та красивіше, ніж в інших. Уже вкотре зустрічаю теми, де автори описують, як бачать ідеальну мову програмування, а інші ж створюють такі мови, прикладу Ruby.

Що таке декоратор

Так, в Go зручно реалізувати патерн декоратор. Це відомий патерн, вже описаний в книжці Gang of Four «Design Patterns: Elements of Reusable Object-Oriented Software» (та початківцям краще починати з «Head First Design Patterns»).

Декоратор зручний, коли треба розширити функціональність без змін компонентів. Мені він нагадує матрьошку, якій треба розмалювати іншим кольором руки. Беремо матрьошку, обертаємо її в прозору плівку, розмальовуємо руки, плівка та малюнок і будуть декоратором. Шрек приводив у приклад цибулю.

В основному проекті ми використовуємо декорацію для запису в журнал взаємодії через API клієнти та для синхронізації.

Дуже просто покрити тестами основну логіку, а всі додаткові обгортки винести в декорацію. Але перед тим як так структурувати частину проекта через декоратори, треба довести, що його вартість мала.

Реалізація

В Go реалізувати декоратор простіше, ніж через ООП. Візьмемо штучний приклад класу на PHP з двома методами. Один треба змінити, а інший залишити, як є:

interface GeneratorInterface
{
    public function increment(int $step): int;

    public function stats(): Stats;
}

class GeneratorIncrementDecorator implements GeneratorInterface
{
    private $source;

    private $coefficient;

    public function __construct(GeneratorInterface $source, int $coefficient)
    {
        $this->source = $source;
        $this->coefficient = $coefficient;
    }

    public function increment(int $step): int
    {
        // decorated
        return $this->source->increment($step * $this->coefficient);
    }

    public function stats(): Stats
    {
        // as is
        return $this->source->stats();
    }
}

class Stats{}

А тепер на Go:

type Generator interface {
	Increment(step int) int
	Stats() Stats
}

type GeneratorIncrementDecorator struct {
	Generator
	coefficient int
}

func NewGeneratorIncrementDecorator(source Generator, coefficient int) Generator {
	return GeneratorIncrementDecorator{
		Generator:   source,
		coefficient: coefficient,
	}
}

func (d GeneratorIncrementDecorator) Increment(step int) int {
	return d.Generator.Increment(step * d.coefficient)
}

type Stats struct{}

В Go декоруємо тільки потрібний метод, а метод Stats вбудовується. В офіційній документацій це називається Embedding. В PHP, як і в Java та C#, треба буде обгортати усі методи.

А тепер приклад, щоб визначити вартість. Візьмемо структуру з однаковими функціями.

    type (
        source interface {
            increment(int) int
            wrap(int) int
            proxy(int) int
            same(int) int
        }

        handler struct {
        }
    )

    func (handler) increment(s int) int {
        return s + 1
    }

    func (handler) wrap(s int) int {
        return s + 1
    }

    func (handler) proxy(s int) int {
        return s + 1
    }

    func (handler) same(s int) int {
        return s + 1
    }

Продекоруємо її різними методами:

    type (
        decorator struct {
            source
        }
    )

    func newDecorator(source source) source {
        return decorator{source}
    }

    func (d decorator) increment(s int) int {
        return d.source.increment(s) + 1
    }

    func (d decorator) wrap(s int) int {
        return d.source.wrap(s + 1)
    }

    func (d decorator) proxy(s int) int {
        return d.source.proxy(s)
    }

    // embedding
    //func (d decorator) same(s int) int {
    //	return d.source.same(s)
    //}

Додамо benchmark на кожну функцію інтерфейсу та допоміжну тестову функцію, щоб декорувати N разів:

    import "testing"

    const N = 127

    func BenchmarkSource(b *testing.B) {
        handler := handler{}

        for i := 0; i < b.N; i++ {
            handler.increment(i)
        }
    }

    func BenchmarkDecoratorIncrement(b *testing.B) {
        handler := createNTimesDecoratedHandler(handler{}, N)

        for i := 0; i < b.N; i++ {
            handler.increment(i)
        }
    }

    func BenchmarkDecoratorWrap(b *testing.B) {
        handler := createNTimesDecoratedHandler(handler{}, N)

        for i := 0; i < b.N; i++ {
            handler.wrap(i)
        }
    }

    func BenchmarkDecoratorProxy(b *testing.B) {
        handler := createNTimesDecoratedHandler(handler{}, N)

        for i := 0; i < b.N; i++ {
            handler.proxy(i)
        }
    }

    func BenchmarkDecoratorSame(b *testing.B) {
        handler := createNTimesDecoratedHandler(handler{}, N)

        for i := 0; i < b.N; i++ {
            handler.same(i)
        }
    }

    func createNTimesDecoratedHandler(source source, times int) source {
        result := source

        for i := 0; i < times; i++ {
            result = newDecorator(result)
        }

        return result
    }

І запустимо:

go test ./... -bench=. -benchmem

Результати (середовище: go version go1.11.1 linux/amd64):

Для N = 0:

Назва тестуКількість ітераційСередній час ітераціїВиділення пам’яті
Source20000000000.38 ns/op0 B/op 0 allocs/op
Increment3000000004.72 ns/op0 B/op 0 allocs/op
Wrap3000000004.99 ns/op0 B/op 0 allocs/op
Proxy3000000004.97 ns/op0 B/op 0 allocs/op
Same3000000004.78 ns/op0 B/op 0 allocs/op

Для N = 127:

Назва тестуКількість ітераційСередній час ітераціїВиділення пам’яті
Increment10000001299 ns/op0 B/op 0 allocs/op
Wrap10000001257 ns/op0 B/op 0 allocs/op
Proxy10000001245 ns/op0 B/op 0 allocs/op
Same2000000725 ns/op0 B/op 0 allocs/op

Висновки

Операція додавання дуже швидка ~ 0.4 наносекунди, а от обгортка інтерфейсу ~ 4.5 наносекунди. Декорація має свою вартість ~ 10 наносекунд, навіть через embedding ~ 5-6 наносекунд.

Якщо зробити загальний висновок — після впровадження декорації стало простіше розробляти нові сервіси.

LinkedIn

57 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Класна стаття (бо спровокувала класні коменти). Приємно було почитати. Дякую усім, хто долучився.

Спасибо Ярослав. Отличный пример, который показывает, как всего лишь 50 строчками кода на Go написать программу, которая ничего не делает.

Чем больше смотрю на статьи голанга, тем больше убеждаюсь что язык-бойлерплейт — очень много писанины, а суть размазывается.

Для того что бы не было бойлерплейта — нужно использовать кодогенерацию, а так как у нас 99% полу-джуны, до go generate почти никто не доползает...

Так и имеем по 3К LоC бойлерплейта где нужно 100 LoC шаблон.
Про то что bazel’ем можно собирать, и делать горячее обновление на плагинах история вообще умалчивает. Про offheap и zero alloc тем более.

Кодогенерация _не_решение_ проблемы бойлерплейта. Это костыль, который не уменьшает количества одинакового кода, а увеличивает и осложняет контроль за ним. В нормальных языках для этих целей сделаны шаблоны и макросы(между макросами и кодогенераторами есть разница), а кодогенератор — крайняя мера. Соответственно, эта фраза только подтверждает что go — язык бойлерплейт.

увеличивает и осложняет контроль за ним

Например как ?

Наличием гораздо более сложного и замороченного инструмента, чем «простой язык без шаблонов» для кодогенерации. Разрыв этапа компиляции в два раздельных шага — генерация кода и компиляция. Кроме того, кодогенераторы могут порождать разные интересные баги, связанные с привязкой к иднетификаторам к ещё не сгенерированных участкам кода. Я молчу о том как приятно такой код рефакторить (ручками и очень внимательно по 3 раза перечитывая и осмысливая написанное).
У недалёких и неопытных возникает желание править сгенерированный код, дописывать туда свои классы и т.д., и потом думать, а куда делся мой код.
Так или иначе, можно решать эти проблемы — но они уже решены в виде шаблонов/дженериков, макросов/рефлексии.

Недостаток квалификации разработчиков не делает инструмент плохим.

Не бывает плохих инструментов. Бывают подходящие и не подходящие. Так вот, если инструмент(или его разработчик) гордо заявляет, что он — не хрен собачий, и простые холопы не могут его использовать левой пяткой, в то же время он ничего особеного такого и не делает, а требует много скила для произведения самых элементарных действий, он явно не будет подходящим для кого-то кроме гуру либо больших поклонников/вендор-залоченных. В большинстве языков дженерики не требуют каких — то особенных умственных усилий и при том неплохо так подстраховывают от выстрелов себе в ногу (кодогенераторы — нет). А кое в каких — макросы тоже сделаны по человечески(Nemerele, Haskell, Scala) и не являются кодогенераторами.

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

Не стоит забывать о кадровом вопросе — сколько будет стоить разработчик который надёжно владеет кодогенераторами и сколько будет стоить тот который не владеет? Эта величина тоже имеет значение и напрямую влияет на то подходит ли кому-то этот инструмент или нет.

В частности, не существует разработчиков с квалификацией, достаточной для того чтобы писать веб-сервисы на ассемблере с той же продуктивностью и надёжностю что и на других ЯП высокого уровня ориентированых на их написание.

Та штука которую ты предлагаешь — это и есть в каком то роде ассемблер.

У Тебя просто не хватает опыта что бы полноценно понимать риски и особенности использования кодогенерации в проектах.

Кодогенерация повсеместно используется в Qt / GTK / WPF / Android UI / iOS UIKit / Jooq ... и что-то никто пока не пострадал «от багов недокодогенеренного».

Мне жаль что упоминаешь о «стоимости разработчика владеющего...» — кто-то может просто сесть, прочитать 2-3 статьи на медиуме и идти работать, вместо того что бы строчить здесь 3ех километровые посты с целью личной психологической компенсации.

Это называется сложность из ниоткуда

Сложность компиляции — возможно, но если руки с правильного места — настроить инкрементальную компиляцию не проблема. Сгенерированный код никто из разработчиков руками вообще не должен трогать и в глаза видеть... так что не знаю что там «забыли / не досмотрели / потёрлость» — бред дилетантский...

Так вот, если инструмент(или его разработчик) гордо заявляет

Это «официальный» подход... я лично к нему не имею отношения blog.golang.org/generate

Не вижу смысла продолжать этот цирк.
Поди почитай чего-то — не позорься лишний раз.

О, какой тон ведения дискуссии, переход на личности. Видно профессионала.
Ты же обо мне ровным счётом ничего не знаешь, почему ты сразу делаешь неопровержимые выводы?

Это лишний раз подвержадет, что С++, С# и прочие с-подобины обладают унылой системой типов и в силу своей оброслости лигасями с этим ничего поделать не могут, вот и нуждаются в костылях.

Весь мой посыл заключается в том, что в хороших языках макросы сделаны как надо так что ничего с бубном настраивать не надо, а результат даёт просто замечательный.
Вот например как сделано в хаскеле:
wiki.haskell.org/...​Template_Haskell_Tutorial

или в скале:
github.com/...​fortherestofus_slides.pdf

Весь этот ваш кодоген с рисками и особенностями меркнет перед скаловым и хаскельным вариантами.

Отличный материал, большое спасибо !

Почему вы выбрали обьекты-декораторы а не например функции-декораторы?

В мережі вже багато прикладів функцій-декораторів Go Web Examples, Middleware (Advanced)
Приклад зі статті — це простий варіант того, що використовуємо в проекті

В вашем коде с пхп примером есть infinite loop.

Дякую, відправив в редакцію правки

Не ображайтесь, але чим більше я дивлюсь на ваш голанг, тим більше бачу там пехапе...

До цього 4 роки використовував PHP, і бачив код тих хто перейшов з C++, бо назви змінних включали в себе назву типу $aUsers чи $iCount
Що ж в моїх прикладах на Go має окрасу PHP?

Вам не сподобається моя відповідь. Прочитайте хоч це golang.org/doc/effective_go.html — на рахунок стилю.
А ще — голанг це про мінімум максимально зрозумілого і ефективного коду. Про типи, а не про об’єкти. Не про те, що ви реалізували.
Порівняйте ваш код з кодом здорових пекейджів (та хоч з ядра) — знайдіть різницю.

Відкриваю Effective Go, знаходжу там секцію Embedding а в ній приклад

type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}
з поясненням
By embedding the structs directly, we avoid this bookkeeping.

І? Ви б хоч конвецію неймінгу осилили перед тим як писати статті. Бачите як вище бомбить у людей, бо вони ваш код за типовий голанг сприймають?

По тимпам вже відповів вище, вкажіть конкретно що бентежить

type source interface {
increment(int) int
wrap(int) int
proxy(int) int
same(int) int
}

1. Неймінг — golang.org/...​e_go.html#interface-names
2. Кількість методів — golang.org/...​ective_go.html#interfaces.
3. Для яких цілей вам потрібен приватний інтерфейс?
4. Для чого вам 4 ідентичних метода?

Я не хочу робити код ревью, але там у вас весь код «червоний».

Взагалі мене турбує не ваш «сахар», а те що ви обрали тему, яку в принципі слід оминати на голанг. Таке знущання над чужим типом — це збочення (хоча й гарно і професійно на об’єктах, ООП і пехапе). Якщо вам хочется таке писати — сядьте, подумайте кілька днів. — і не пишіть. Якщо вам таке треба писати — теж подумайте і не пишіть. Якщо вже вибору немає — пишіть, але обнесіть коментами як парканом, щоб всім було зрозуміло навіщо вам таке знущання. Якщо вам треба робити таке знущання над своїм типом — не використовуйте голанг (або хоч не виносьте це на публіку).

P.S.: Я розумію, це код для статті. Ви засунули купу непотребу, щоб проілюструвати декоратор. Але голанг — не про це.

Наприклад, приватний інтерфейс — який сенс? Розрулювати проблеми в своєму пекейджі — вітаю, у вас більші проблеми (код пекейджу і його розробники). 4 однакових метода в інтерфейсі — взагалі де логіка? В реалізації? А при чому інтерфейси до їх реалізації?

У мене, мабуть, все.

3. Для яких цілей вам потрібен приватний інтерфейс?

Краще розробляти приватні методи і відкривати коли потрібно, це як локальні змінні

Інтерфейс, метод і змінна — це три різні речі. Причому ваші приватні методи до приватного інтерфейсу?

там усе приватне, окрім тестів

Якщо у вас проект, де все приватне... Що ви самі про це думаєте?

Ви засунули купу непотребу, щоб проілюструвати декоратор. Але голанг — не про це.

Clean code using decorators
Користувачі Reddit вважають декоратор хорошим патерном, навіть для Golang

Там скоріше про/як Middleware. Можете ще порівняти код звідти і ваш.

У статтї декорують тільки одну функцію з інтерйесу в якому тільки одна функція

func main() {
  a = WrapCache(&sync.Map{})(WrapLogger(log.New(os.Stdout, "test", 1))(a))
  a.Add(10, 20)
}
В моєму випадку 4 функції, а замінити треба лише одну-дві
В моєму випадку 4 функції, а замінити треба лише одну-дві

Почему бы не сделать 4 инстанса (4 класса с 1 методом), по одному для каждого случая?

Бо ці 4 методи обробляють різні точки входу, і обернути їх одним декоратором простіше ніж під кожну писати свій.
Ось приклад максимально схожий на реальний проект [Go] Trader decorator — Pastebin.com

(копію додам сюди)

package main

type CreateAttempt struct {
	// ...
}

type CancelAttempt struct {
	// ...
}

type MoveAttempt struct {
	// ...
}

type Stats struct {
}

type Trader interface {
	CreateOrder(CreateAttempt)
	CancelOrder(CancelAttempt)
	MoveOrder(MoveAttempt)
	Stats() Stats
}

type Queue interface {
	Add(CreateAttempt)
	Iterator() <-chan CreateAttempt
}

type loggerTraderDecorator struct {
	Trader
}

func (l *loggerTraderDecorator) CreateOrder(attempt CreateAttempt) {
	// logging receive create

	l.Trader.CreateOrder(attempt)
}

func (l *loggerTraderDecorator) CancelOrder(attempt CancelAttempt) {
	// logging receive cancel

	l.Trader.CancelOrder(attempt)
}

func (l *loggerTraderDecorator) MoveOrder(attempt MoveAttempt) {
	// logging receive move

	l.Trader.MoveOrder(attempt)
}

func WrapLoggerDecorator(source Trader) Trader {
	return &loggerTraderDecorator{source}
}

type queueTraderDecorator struct {
	Trader
	queue Queue
}

func (q *queueTraderDecorator) CreateOrder(attempt CreateAttempt) {
	q.queue.Add(attempt)
}

func (q *queueTraderDecorator) run() {
	for attempt := range q.queue.Iterator() {
		q.Trader.CreateOrder(attempt)
	}
}

func WrapQueueDecorator(source Trader) Trader {
	queueDecorated := &queueTraderDecorator{
		source,
		nil, //new(Queue),
	}

	go queueDecorated.run()

	return queueDecorated
}

type hitTrader struct {
}

func (hitTrader) CreateOrder(CreateAttempt) {
	panic("implement me")
}

func (hitTrader) CancelOrder(CancelAttempt) {
	panic("implement me")
}

func (hitTrader) MoveOrder(MoveAttempt) {
	panic("implement me")
}

func (hitTrader) Stats() Stats {
	panic("implement me")
}

func newTrader(
// ... client connection
) *hitTrader {
	return &hitTrader{}
}

func main() {
	trader := WrapLoggerDecorator(WrapQueueDecorator(newTrader(
	// ...
	)))

	_ = trader
}

Тут всі чотири методи різні

Щодо цого коду? Ну, імхо це пехапе. Для того щоб зрозуміти як працює

Trader.CreateOrder()

треба розкурити вагон непотрібного коду в непотрібних місцях. Код працює неявно. Голанг створений з ідеєєю такого не робити. Чому не використовувати логер явно? Чому явно не використовувати черги? Від кого написана ця вся магія?

І ще, ви знаете різницю між «panic» та «fatal»? Для чого їх дві?

Чому використання «panic» та «fatal» добре тільки наверху ієрархії, в main()?

go queueDecorated.run()

- а sync.WaitGroup? Чи у вас норм, що головний тред уже всьо, процес на процесорі тоже всьо — а рутина ні сном ні духом. Чи ви не хочете її лякати перед смертю? Чуваки, це пехапе.

_ = trader

 — це невірна декларація оголошення змінної — просто не збілдиться. Якби у вас вище вже була правильна декларація «_» - то б збілдилося, але я не розумію для чого вам безіменна декларація чого-небудь. Таке роблять щоб щось проігнорувати, а тут для чого?

func newTrader(
// ... client connection
) *hitTrader {
return &hitTrader{}
}

- без коментів. Ой, таки з коментом.

І ще, я не знаю реалізації логера і черги, але передавати типи (накшталт CreateAttempt) по посиланню таки швидше аніж кожного разу іх копіювання.

Лінь далі колупатися.

Є багато мікросервісів Trader які взаємодіють з зовнішніми криптобіржами, для кожної біржі окрема реалізація і

hitTrader

це одна з абстракцій, і щоб уникнути дублювання коду в кожному Trader-і і зробили обгортки

Черга використовується бо у деяких бірж є обмеження по rps і якщо більше запитів то блокує

Вам треба throttling, а не queue. Точніше throttling поверх queue.

Зрозумів коли щойно знайшов RateLimiting, під капотом Queue в проекті схожа реалізація

throttling

зустрічаю вперше, тепер буду використовувати це поняття, дякую

Ярослав, ще одне. Ви приймаєте інтерфейс, а потім робите мутацію результата роботи цього інтерфейсу. Так, це буде працювати. Але є один момент — вашому коду побарабану на реалізацію цього інтерфейсу. Тобто, — воно не буде працювати так як треба, бо просунути можна будь яку реалізацію. Ви вже або робіть перевірку на тип, і працюйте з типом. Або не мутуйте результат роботи чужої реалізації інтерфейсу (яку ви не знаєте і знати не можете). Знову, гляньте не те як зроблено в тій статті, і подумайте чому так.

І на коменти гляньте і неймінг. Обнесено парканом з коментів, по назві і коментам зразу зрозуміло що то знущання над чужим типом.

І ще, що робить ваш інтерфейс Generator, інкрементує? Тоді якого він Generator?

Ідея була про вбудовування і його вартість, також додав спрощену версію того як в реальному проекті коментар зверху

Але ж код таки поганий...

відкритий до конструктивної критики

Лінь, я й так надто багато часу витратив

Сударь, ну что вы травите?
Код ужос, и интерфейсы тут наименьшее из зол. Я минут 10-30 потратил чтобы понять общее флоу тестов, а еще было бы хорошо код на гитхаб залить (а то пришлось читать соглажения по именованию тестовых файлов :) ).

Статья банальная попытка сделать микробенчмарки «цены вызова метода», отсюда и «не кошерные интерфейсы». Хорошо бы добавить дисклеймер к статье, что она про микробенчмарки, а не про то как писать на го.

Інтерфейси тут саме просте ))

Так стаття навмисно дуже проста, і назва схожа на

«цены вызова метода»

Цікаво співпало що схожа стаття вийшла на Habr-і Цена композиции в JavaScript-мире

Використовуєш прийом багато років, він здається простим і логічним. Потім виявляється що це якийсь-там паттерн.
І не перший раз же.

Є таке, я привів у приклад матрьошку, а її вже використовували для прикладу патерну Декоратор, та ще й визначення краще

Декоратор — це структурний патерн проектування, що дає змогу динамічно додавати об’єктам нову функціональність, загортаючи їх у корисні «обгортки».

ну так зручно для комунікації, нє? :) замість того, щоб говорити пояснювати на матрьошках, показувати на пальцях, говорити про якийсь логічний прийом, можна просто коротко сказати: патерн декоратор.

Добре що не антипаттерн;)

В PHP, як і в Java та C#, треба буде обгортати усі методи.

як то кажуть: «explicit is better than implicit» :)

Якщо правильно тебе зрозумів то explicit:

func (d decorator) proxy(s int) int {
        return d.source.proxy(s)
}
а implicit:
// embedding
// func (d decorator) same(s int) int {
// 	return d.source.same(s)
// }
так через embedding працює швидше і меньше коду

Ні. Якщо правильно, то: «Згадуйте спочатку найпопулярніші мови» )))

Подписаться на комментарии