Login

Визначаємо вартість декоратора в 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 наносекунд.

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

Похожие статьи:
“Человек есть общественное животное”, Аристотель (384–322 до н. э.) “Вместе весело шагать по просторам, По просторам, по просторам И,...
Міністр оборони України Денис Шмигаль підписав наказ про впровадження бойової цифрової системи DELTA на всіх рівнях Сил оборони....
Преподаватель НТУУ «КПИ им. И. Сикорского» и Киевской школы экономики, идеолог, организатор и архитектор программистского...
Оператор мобильной связи «Билайн» представил новое пакетное предложение, включающее, как он отмечает, современный...
У свіжому випуску новинного дайджесту DOU News розповідаємо про думки президента України Володимира Зеленського...
Switch to Desktop Version