Привіт, мене звати Ярослав, займаюсь розробкою сервісу для збереження активів у криптовалюті в компанії 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->increment($step * $this->coefficient); } public function stats(): Stats { // as is return $this->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:
Назва тесту | Кількість ітерацій | Середній час ітерації | Виділення пам’яті |
Source | 2000000000 | 0.38 ns/op | 0 B/op 0 allocs/op |
Increment | 300000000 | 4.72 ns/op | 0 B/op 0 allocs/op |
Wrap | 300000000 | 4.99 ns/op | 0 B/op 0 allocs/op |
Proxy | 300000000 | 4.97 ns/op | 0 B/op 0 allocs/op |
Same | 300000000 | 4.78 ns/op | 0 B/op 0 allocs/op |
Для N = 127:
Назва тесту | Кількість ітерацій | Середній час ітерації | Виділення пам’яті |
Increment | 1000000 | 1299 ns/op | 0 B/op 0 allocs/op |
Wrap | 1000000 | 1257 ns/op | 0 B/op 0 allocs/op |
Proxy | 1000000 | 1245 ns/op | 0 B/op 0 allocs/op |
Same | 2000000 | 725 ns/op | 0 B/op 0 allocs/op |
Висновки
Операція додавання дуже швидка ~ 0.4 наносекунди, а от обгортка інтерфейсу ~ 4.5 наносекунди. Декорація має свою вартість ~ 10 наносекунд, навіть через embedding ~
Якщо зробити загальний висновок — після впровадження декорації стало простіше розробляти нові сервіси.