Русский
Русский
English
Статистика
Реклама

Design patterns

Front End Meetup от Facebook Developer Circle Moscow

07.10.2020 10:05:19 | Автор: admin

Второй год сообщество Facebook Developer Circle: Moscow активно развивается в области JavaScript и Front End'а. И я рад вам сообщить, что скоро в сообществе будет проводиться очередной митап. Но этот митап будет не один, а три дня! Вы сможете посмотреть гораздо больше интересных докладов)

Day 1 - 15 октября

7:00pm - 7:45pm - DSL approach with JS component libraries
Andrey Kobets / Head of Front-end Development at Yandex

How to write modern frontend application using React/Vue/Angular/..., and
1 Do it the way you like it, but not the way the library requires it.
2 Use only the best solutions for your task.
3 Not to fear major updates of dependencies.
4 Not to rewrite project every time with a new extra fast/robust/flexible library.


7:45pm - 8:30pm - Quo vadis, Frontend?
Evgeny Kot / Director of Development at Wrike

There is no industry that is more rapidly developing than the frontend. You can argue, but how many more ecosystems do you know where frameworks are emerging at this rate? Web standards are being implemented and immediately become obsolete. The most important question is: where are we going, and where will we come to? The question is not rhetorical: how not to stay out of business in this bubbling stream and capture only the most important, what will be the trend in 2021.


8:30pm - 9:15pm - Dive into effector
Yan Lavryushev / Frontend Developer

Mental health issues is the main epidemic of the 21st century, could we reduce it a bit? I guess so



Day 2 - 16 октября

7:00pm - 7:45pm - What's beyond CRA*?
Vitaly Kosenko / Head of Search Interfaces Architecture at Yandex

The Yandex Search page result doesnt look the same for every search request. A great amount of such requests transforms the search page into complex services like hotel booking services or streaming services. We optimized the performance of the interfaces for users and the performance for developers, and then, we decided to rewrite everything using a different technological stack. Ill tell you how the migration was technically done. Of course, well discuss every key step in detail. Be ready for a series of advice from real-world projects.

7:45pm - 8:30pm - Practical Serverless & Edge Computing
Aleksey Taktarov / resume.io

While serverless is way beyond its hype point, it is still an underestimated technology that could open up new approaches in how we build our apps. In this talk I'm going to give an overview of practical applications of serverless JS. We're going to cover serverless frontend microservices, authorization methods, Smart CDNs, caching (including stale-while-revalidate) with Vercel and Cloudflare Workers. I'll illustrate these methods with practical examples from the tools we build internally at [resume.io](http://personeltest.ru/away/resume.io): OG image generation, PDF/DOCX rendering, automatic Critical CSS injection and more.

Day 3 - 17 октября

10:45am - 11:30am - Pipelinekiller. Life after create-react-app
Aleksey Zolotykh / Lead Software Developer at EPAM

With the release of react-scripts, next and other parsel, it seems that here it's happiness left only to write code and enjoy life. But it's not so. Life doesn't stop at create-react-app. After all, front-end is not enough just to build, it also needs to be checked. My presentation will be about how to do it without noise and dust.


11:30am - 12:15asssm - Binary JS saga
Aleksandr Korotaev / Senior Software Developer at VK.com

I want to talk about parsing binary files using Node.js.
How to read and write binary data structures on JS. How to speedup mass creation files process.
And stories of parsing data chunks of formats for which there is no specification.


Событие будет проводиться на английском языке, а зарегистрироваться вы можете здесь.
До встречи на митапе!)

Подробнее..

Погружение во внедрение зависимостей (DI), или как взломать Матрицу

03.06.2021 16:16:28 | Автор: admin

Давным-давно в далекой Галактике, когда сестры Вачовски еще были братьями, искусственный разум в лице Архитектора поработил человечество и создал Матрицу Всем привет, это снова Максим Кравец из Holyweb, и сегодня я хочу поговорить про Dependency Injection, то есть про внедрение зависимостей, или просто DI. Зачем? Возможно, просто хочется почувствовать себя Морфеусом, произнеся сакраментальное: Я не могу объяснить тебе, что такое DI, я могу лишь показать тебе правду.

Постановка задачи

Вот. Взгляни на этих птиц. Существует программа, чтобы ими управлять. Другие программы управляют деревьями и ветром, рассветом и закатом. Программы совершенствуются. Все они выполняют свою собственную часть работы.

Пифия

Фабула, надеюсь, всем известна есть Матрица, к ней подключены люди. Люди пытаются освободиться, им мешают Агенты. Главный вопрос кто победит? Но это будет в конце фильма, а мы с вами пока в самом начале. Так что давайте поставим себя на место Архитектора и подумаем, как нам создать Матрицу?

Что есть программы? Те самые, которые управляют птицами, деревьями, ветром.

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

Что нам нужно обеспечить для функционирования Матрицы? Механизм внедрения, или (внимание, рояль в кустах), инжекции (Injection) функционала классов, отвечающих за всю вышеперечисленную флору, фауну и прочие природные явления, внутрь Матрицы.

Подождем, пока грузчики установят в кустах очередной музыкальный инструмент, и зададимся вопросом: а что произойдет с Матрицей после того, как мы в нее инжектируем нужный нам функционал? Все правильно у нее появятся зависимости (Dependency) от внешних по отношению к ней классов.

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

Первым делом, посмотрим на цитату в начале текста и обратим внимание на предложение: Программы совершенствуются. То есть переписываются. Изменяются. Что это означает для нас? Работа нашей Матрицы не должна зависеть от конкретной реализации класса зависимости.

Кажется, ерунда какая-то зависимость на то и зависимость, чтобы от нее зависеть!

А теперь следите за руками. Мы внедряем в Матрицу не конкретную реализацию зависимости, а абстрактный контракт, и реализуем механизм предоставления конкретной реализации, соответствующей этому контракту! Остались сущие пустяки понять, как же это все реализовать.

Внедрение зависимости в чистом виде

Оставим романтикам рассветы и закаты, птичек и цветочки. Мы, человеки, должны вырваться из под гнета ИИ вообще и Архитектора в частности. Так что будем разбираться с реализацией DI и параллельно освобождаться из Матрицы. Первая итерация. Создадим класс matrix, непосредственно в нем создадим агента по имени Смит, определим его силу. Там же, внутри Матрицы, создадим и претендента, задав его силу, после чего посмотрим, кто победит, вызвав метод whoWin():

class Matrix {  agent = {    name: 'Smith',    damage: 10000,  };  human = {    name: 'Cypher',    damage: 100,  };  whoWin(): string {    const result = this.agent.damage > this.human.damage      ? this.agent.name      : this.human.name;    return result;  }}const matrixV1 = new Matrix();console.log(Побеждает , matrixV1.whoWin());

Да, Сайфер не самый приятный персонаж, да еще и хотел вернуться в Матрицу, так что на роль первого проигравшего подходит идеально.

Побеждает  Smith

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

class Human {  name;  damage;  constructor(name, damage) {    this.name = name;    this.damage = damage;  }  get name(): string {    return this.name;  }  get damage(): number {    return this.damage;  }}class Matrix {  agent = {    name: 'Smith',    damage: 10000,  }; human;  constructor(challenger) {    this.human = challenger;  }  whoWin(): string {    const result = this.agent.damage > this.human.damage      ? this.agent.name      : this.human.name;    return result;  }

Мы добавили класс Human, принимающий на вход конструктора имя и силу человека, и возвращающий их в соответствующих методах. Также мы внесли изменения в наш класс Матрицы теперь информацию о претенденте на победу он получает через конструктор. Давайте проверим, сможет ли Тринити победить Агента Смита?

const Trinity = new Human('Trinity', 500);const matrixV1 = new Matrix(Trinity);console.log('Побеждает ', matrixV1.whoWin());

Увы, Тринити всего лишь человек (с), и ее сила по определению не может быть больше, чем у агента, так что итог закономерен.

Побеждает  Smith

Но стоп! Давайте посмотрим, что случилось с Матрицей? А случилось то, что класс Matrix и результаты его работы стал зависеть от класса Human! И нашему оператору, отправляющему Тринити в Матрицу, достаточно немного изменить код, чтобы обеспечить победу человечества!

class Human {   get damage(): number {    return this.damage * 1000;  }}

...

Пьем шампанское и расходимся по домам?

Чем плох подход выше? Тем, что класс Matrix ждет от зависимости challenger, передаваемой в конструктор, наличие метода damage, поскольку именно к нему мы обращаемся в коде. Но об этом знает Архитектор, создавший Матрицу, а не наш оператор! В примере мы можем угадать. А если не знать заранее название метода? Может быть, надо было написать не damage, а power? Или strength?

Инверсия зависимостей

Знакомьтесь! Dependency inversion principle, принцип инверсии зависимостей (DIP). Название, кстати, нередко сокращают, убирая слово принцип , и тогда остается только Dependency inversion (DI), что вносит путаницу в мысли новичков.

Принцип инверсии зависимостей имеет несколько трактовок, мы приведем лишь две:

  1. Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.

  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Зачем он нам понадобился? Для того, чтобы обеспечить механизм соблюдения контракта для зависимости, о необходимости которого мы говорили при постановке задачи, и отсутствие которого в нашей реализации не дало нам пока наполнить бокалы шампанским.

Давайте внедрим в наш класс Matrix некий абстрактный класс AbstractHuman, а конкретную реализацию в виде класса Human попросим имплементировать эту абстракцию:

abstract class AbstractHuman {  abstract get name(): string;  abstract get damage(): number;}class Human implements AbstractHuman{  name;  damage;  constructor(name, damage) {    this.name = name;    this.damage = damage;  }  get name(): string {    return this.name;  }  get damage(): number {    return this.damage;  }}class Matrix {  agent = {    name: 'Smith',    damage: 10000,  }; human;  constructor(challenger: AbstractHuman) {    this.human = challenger;  }  whoWin(): string {    const result = this.agent.damage > this.human.damage      ? this.agent.name      : this.human.name;    return result;  }}const Morpheus = new Human('Morpheus', 900);const matrixV2 = new Matrix(Morpheus);console.log('Побеждает ', matrixV2.whoWin());

Морфеуса жалко, но все же он не избранный.

Побеждает  Smith

Вторая версия Матрицы пока что выигрывает, но что получилось на текущий момент? Класс Matrix больше не зависит от конкретной реализации класса Human задачу номер один мы выполнили. Класс Human отныне точно знает, какие методы с какими именами в нем должны присутствовать пока контракт в виде абстрактного класса AbstractHuman не будет полностью реализован (имплементирован) в конкретной реализации, мы будем получать ошибку. Задача номер два также выполнена.

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

В бою с Морфеусом побеждает  SmithВ бою с Тринити побеждает  Smith

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

...class TheOne implements AbstractHuman{  name;  damage;  constructor(name, damage) {    this.name = name;    this.damage = damage;  }  get name(): string {    return this.name;  }  get damage(): number {    return this.damage * 1000;  }}const Neo = new TheOne('Neo, 500);const matrixV5 = new Matrix(Neo);

Свершилось!

В бою с Нео побеждает  Нео

Инверсия управления

Давайте посмотрим, кто управляет кодом? В нашем примере мы сами пишем и класс Matrix, и класс Human, сами создаем инстансы и задаем все параметры. Мы управляем нашим кодом. Захотели внесли изменения и обеспечили победу Тринити.

Увы, по условиям мира, придуманного Вачовски, мы можем лишь вклиниваться в работу Матрицы, добавляя свои кусочки программного кода. Матрица управляет не только фантомами внутри себя, но и тем, как с ней работать извне!

Возможно, авторы трилогии увлекались программированием, потому что ситуация целиком и полностью списана с реальности и даже имеет свое название Inversion of Control (IoC).

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

Кстати, уже использованный нами выше DIP (принцип инверсии зависимостей) одно из проявлений механизма IoC.

К-контейнер н-нада?

Последний шаг передача управления разрешением зависимостей. Кому и какой инстанс предоставить, использовать singleton или multiton также решается не программистом (оператором), а фреймворком (Матрицей).

Вариантов решения задачи множество, но все они сводятся к одной идее.

  • на верхнем уровне приложения создается глобальный объект,

  • в этом объекте регистрируется абстрактный интерфейс и класс, который его имплементирует,

  • модуль запрашивает необходимый ему интерфейс (абстрактный класс),

  • глобальный объект находит класс, имплементирующий данный интерфейс, при необходимости создает инстанс и передает его в модуль.

Конкретные реализации у каждого фреймворка свои: где-то используется Локатор сервисов/служб (Service Locator), где-то Контейнер DI, чаще называемый IoC Container. Но на уровне базовой функциональности отличия между подходами стираются до неразличимости.

У нас есть класс, который мы планируем внедрить (сервис). Мы сообщаем фреймворку о том, что этот класс нужно отправить в контейнер. Наиболее наглядно это происходит в Angular мы просто вешаем декоратор Injectable.

@Injectable()export class SomeService {}

Декоратор добавит к классу набор метаданных и зарегистрирует его в IoC контейнере.

Когда нам понадобится инстанс SomeService, фреймворк обратится к контейнеру, найдет уже существующий или создаст новый инстанс сервиса и вернет его нам.

Крестики-нолики, а точнее плюсы и минусы

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

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

Вместо заключения, или как это использовать практически?

Окей, если необходимость добавления промежуточного слоя в виде контракта более-менее очевидна, то где на практике нам может пригодиться IoC?

Кейс 1 тестирование.

  • У вас есть модуль, который отвечает за оформление покупки в интернет-магазине.

  • Функционал списания средств мы вынесем в отдельный сервис и внедрим его через DI. Этот сервис будет обращаться к реальному эквайрингу банка Х.

  • Нам нужно протестировать работу модуля в целом, но мы не готовы совершать реальную покупку при каждом тесте.

  • Решение напишем моковый сервис, имплементирующий тот же контракт, что и боевой, и для теста через IoC будем вызывать моковую реализацию.

Кейс 2 расширение функционала.

  • Модуль прежний, оформление покупки в интернет-магазине.

  • Поступает задача добавить возможность оплаты не только в банке Х, но и в банке Y.

  • Мы пишем еще один платежный сервис, реализующий взаимодействие с банком Y и имплементирующий тот же контракт, что и сервис банка X.

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

Кейс 3 управление на уровне инфраструктуры.

  • Модуль прежний.

  • Для production работаем с боевым сервисом платежей.

  • Для разработки подгружаем моковый вариант, симулирующий списание средств.

  • Для тестового окружения пишем третий сервис, который будет симулировать списание средств, а заодно вести расширенный лог состояния.

Надеюсь, этот краткий список примеров вас убедил в том, что вопроса, использовать или не использовать DI, в современной разработке не стоит. Однозначно использовать. А значит надо понимать, как это работает. Надеюсь, мне удалось не только помочь Нео в его битве со Смитом, но и вам в понимании, как устроен и работает DI.

Если есть вопросы или дополнения по теме, буду рад продолжить общение в комментариях. Напишите, о чем рассказать в следующей статье? А если хотите познакомиться с нами ближе, я всегда на связи в Телеграме @maximkravec.

Подробнее..

Перевод Lets Go! Три подхода к структурированию кода на Go

22.08.2020 12:20:38 | Автор: admin
Привет, Хабр! Не так давно у нас вышла новая книга по Golang, и успех ее настолько впечатляет, что мы решили опубликовать здесь очень важную статью о подходах к проектированию приложений на Go. Идеи, изложенные в статье, очевидно не устареют в обозримом будущем. Возможно, автору даже удалось предвосхитить некоторые гайдлайны по работе с Go, которые могут войти в широкую практику в ближайшем будущем.


Язык Go был впервые анонсирован в конце 2009 года, а официальный релиз состоялся в 2012 году, но лишь в последние несколько лет стал приобретать серьезное признание. Go был одним из наиболее быстрорастущих языков в 2018 году и третьим по востребованности языком программирования в 2019 году.

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

Всем известен пример с hello world http на Golang, и его можно сравнить с аналогичными примерами на других языках, например, на Java. Между первым и вторым не заметно существенной разницы ни в сложности, ни в количестве кода, который нужно написать для реализации примера. Но видна фундаментальная разница в подходе. Go стимулирует нас действовать по принципу пиши простой код, когда это только возможно. Если абстрагироваться от объектно-ориентированных аспектов Java, то, думаю, наиболее важный вывод из этих фрагментов кода заключается в следующем: Java требует создавать отдельный экземпляр для каждой операции (экземпляр HttpServer), тогда как Go стимулирует нас использовать глобальный синглтон.

Таким образом, вам придется поддерживать меньше кода, передавать в нем меньше ссылок. Если вы знаете, что вам придется создать всего один сервер (а так обычно и бывает), то зачем же утруждаться лишнего? Такая философия кажется все более веской по мере того, как растет ваша база кода. Тем не менее, жизнь иногда подбрасывает сюрпризы :(. Дело в том, что на выбор вам все равно остается несколько уровней абстрагирования, и, если неправильно их комбинировать, то можно самому себе понаставить серьезных капканов.

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

Мы собираемся реализовать HTTP-сервер, на котором содержится информация о пользователях (на следующем рисунке обозначен как Main DB), где каждому пользователю присвоена роль (допустим, базовый, модератор, администратор), а также реализовать дополнительную базу данных (на следующем рисунке обозначена как Configuration DB), где указаны совокупности прав доступа, отведенные для каждой из ролей (напр., чтение, запись, редактирование). Наш HTTP-сервер должен реализовывать конечную точку, возвращающую набор прав доступа, которыми обладает пользователь с заданным ID.



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

Весь код находится в репозитории к этой статье, расположенном на GitHub.

Подход I: Единственный пакет

В подходе с единственным пакетом используется одноуровневая иерархия, где весь сервер реализован в рамках одного пакета. Весь код.

Внимание: комментарии в коде информативны, важны для понимания принципов каждого подхода.


/main.gopackage mainimport ("net/http")// Как было указано выше, поскольку у нас планируется всего по одному экземпляру // на эти три сервиса, мы объявим экземпляры-синглтоны,// и убедимся, что пользуемся ими только для доступа к этим сервисам.var (userDBInstance   userDBconfigDBInstance configDBrolePermissions  map[string][]string)func main() {// Предполагается, что далее наши экземпляры синглтонов будут// инициализироваться, и отвечает за их инициализацию// инициатор.// Главная функция будет проделывать это над конкретной // реализацией, а тестовые кейсы, если мы планируем их иметь,// могут пользоваться сымитированной реализацией.userDBInstance = &someUserDB{}configDBInstance = &someConfigDB{}initPermissions()http.HandleFunc("/", UserPermissionsByID)http.ListenAndServe(":8080", nil)}// Таким образом права доступа, хранящиеся в памяти, будут оставаться актуальными.func initPermissions() {rolePermissions = configDBInstance.allPermissions()go func() {for {time.Sleep(time.Hour)rolePermissions = configDBInstance.allPermissions()}}()}/database.gopackage main// Мы используем интерфейсы в качестве типов экземпляров нашей базы данных,// чтобы можно было писать тесты и использовать имитационные реализации.type userDB interface {userRoleByID(id string) string}// Обратите внимание на именование `someConfigDB`. В конкретных случаях мы// используем некоторую реализацию БД и соответственно именуем наши структуры // Например, при использовании MongoDB, мы назовем нашу конкретную структуру// `mongoConfigDB`. При работе с тестовыми кейсами также может быть объявлена// имитационная реализация `mockConfigDB`.type someUserDB struct {}func (db *someUserDB) userRoleByID(id string) string {// Для ясности опускаем детали реализации...}type configDB interface {allPermissions() map[string][]string // отображается с роли на ее права доступа}type someConfigDB struct {}func (db *someConfigDB) allPermissions() map[string][]string {// реализация}/handler.gopackage mainimport ("fmt""net/http""strings")func UserPermissionsByID(w http.ResponseWriter, r *http.Request) {id := r.URL.Query()["id"][0]role := userDBInstance.userRoleByID(id)permissions := rolePermissions[role]fmt.Fprint(w, strings.Join(permissions, ", "))}


Обратите внимание: мы все равно используем разные файлы, это делается для разделения ответственности. Так код получается более удобочитаемым и более удобным в поддержке.

Подход II: Парные пакеты

В этом подходе давайте узнаем, что такое работа с пакетами. Пакет должен единолично отвечать за некоторое определенное поведение. Здесь мы позволяем пакетам взаимодействовать друг с другом таким образом, нам приходится поддерживать меньше кода. Тем не менее, необходимо убедиться, что мы не нарушаем принцип единственной ответственности, и поэтому гарантировать, что каждая часть логики полностью реализована в отдельном пакете. Еще одна важная рекомендация при данном подходе такова: поскольку в Go не допускаются кольцевые зависимости между пакетами, необходимо создать нейтральный пакет, в котором содержатся лишь голые определения интерфейсов и экземпляров синглтона. Так мы избавимся от кольцевых зависимостей. Весь код.

/main.gopackage main// Обратите внимание: пакет main  единственный, импортирующий // другие пакеты сверх пакета с определениями.import ("github.com/myproject/config""github.com/myproject/database""github.com/myproject/definition""github.com/myproject/handler""net/http")func main() {// В данном подходе также используются экземпляры синглтона, и,// опять же, инициатор отвечает за то, чтобы они// были инициализированы.definition.UserDBInstance = &database.SomeUserDB{}definition.ConfigDBInstance = &database.SomeConfigDB{}config.InitPermissions()http.HandleFunc("/", handler.UserPermissionsByID)http.ListenAndServe(":8080", nil)}/definition/database.gopackage definition// Обратите внимание, что при данном подходе и экземпляр синглтона, // и тип его интерфейса объявляются в пакете с определениями. // Убедитесь, что в этом пакете не содержится никакой логики; в// противном случае в него, возможно, потребуется импортировать другие пакеты,// и его нейтральная суть будет нарушена.var (UserDBInstance   UserDBConfigDBInstance ConfigDB)type UserDB interface {UserRoleByID(id string) string}type ConfigDB interface {AllPermissions() map[string][]string // отображение с роли на права доступа}/definition/config.gopackage definitionvar RolePermissions map[string][]string/database/user.gopackage databasetype SomeUserDB struct{}func (db *SomeUserDB) UserRoleByID(id string) string {// реализация}/database/config.gopackage databasetype SomeConfigDB struct{}func (db *SomeConfigDB) AllPermissions() map[string][]string {// реализация}/config/permissions.gopackage configimport ("github.com/myproject/definition""time")// Поскольку пакет с определениями не должен содержать никакой логики,// управление конфигурацией реализуется в пакете config.func InitPermissions() {definition.RolePermissions = definition.ConfigDBInstance.AllPermissions()go func() {for {time.Sleep(time.Hour)definition.RolePermissions = definition.ConfigDBInstance.AllPermissions()}}()}/handler/user_permissions_by_id.gopackage handlerimport ("fmt""github.com/myproject/definition""net/http""strings")func UserPermissionsByID(w http.ResponseWriter, r *http.Request) {id := r.URL.Query()["id"][0]role := definition.UserDBInstance.UserRoleByID(id)permissions := definition.RolePermissions[role]fmt.Fprint(w, strings.Join(permissions, ", "))}


Подход III: Независимые пакеты

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

/main.gopackage main// Обратите внимание: главный пакет  единственный, импортирующий // другие локальные пакеты.import ("github.com/myproject/config""github.com/myproject/database""github.com/myproject/handler""net/http")func main() {userDB := &database.SomeUserDB{}configDB := &database.SomeConfigDB{}permissionStorage := config.NewPermissionStorage(configDB)h := &handler.UserPermissionsByID{UserDB: userDB, PermissionsStorage: permissionStorage}http.Handle("/", h)http.ListenAndServe(":8080", nil)}/database/user.gopackage databasetype SomeUserDB struct{}func (db *SomeUserDB) UserRoleByID(id string) string {// реализация}/database/config.gopackage databasetype SomeConfigDB struct{}func (db *SomeConfigDB) AllPermissions() map[string][]string {// реализация}/config/permissions.gopackage configimport ("time")// Здесь мы определяем интерфейс, представляющий наши локальные потребности,// предъявляемые к конфигурационной БД, а именно,// метод `AllPermissions`.type PermissionDB interface {AllPermissions() map[string][]string // отображение роли на права доступа}// Затем мы импортируем сервис, который будет предоставлять// права доступа из памяти, и, чтобы использовать этот сервис, другому // пакету потребуется объявить локальный интерфейсtype PermissionStorage struct {permissions map[string][]string}func NewPermissionStorage(db PermissionDB) *PermissionStorage {s := &PermissionStorage{}s.permissions = db.AllPermissions()go func() {for {time.Sleep(time.Hour)s.permissions = db.AllPermissions()}}()return s}func (s *PermissionStorage) RolePermissions(role string) []string {return s.permissions[role]}/handler/user_permissions_by_id.gopackage handlerimport ("fmt""net/http""strings")// объявление наших локальных потребностей из пользовательского экземпляра бд type UserDB interface {UserRoleByID(id string) string}// ... и наших локальных потребностей из долговременного хранилища данных в памяти.type PermissionStorage interface {RolePermissions(role string) []string}// Наконец наш обработчик не может быть полностью функциональным,// поскольку требует ссылок на экземпляры, не являющиеся синглтонами.type UserPermissionsByID struct {UserDB             UserDBPermissionsStorage PermissionStorage}func (u *UserPermissionsByID) ServeHTTP(w http.ResponseWriter, r *http.Request) {id := r.URL.Query()["id"][0]role := u.UserDB.UserRoleByID(id)permissions := u.PermissionsStorage.RolePermissions(role)fmt.Fprint(w, strings.Join(permissions, ", "))}


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

За и Против

Подход I: Единственный пакет

За
  • Меньше кода, гораздо быстрее в реализации, меньше работы по поддержке
  • Нет пакетов, а, значит, не приходится волноваться и о кольцевых зависимостях
  • Легко тестировать, поскольку существуют интерфейсы сервисов. Чтобы протестировать элемент логики, можно задать для синглтона любую реализацию на ваш выбор (конкретную или сымитированную), а затем запустить логику теста.

Против
  • Единственный пакет также не предусматривает приватного доступа, все открыто отовсюду. В результате ответственность разработчика возрастает. Например, помните, что нельзя напрямую инстанцировать структуру, когда для выполнения некоторой логики инициализации требуется функция конструктора.
  • Глобальное состояние (экземпляры синглтона) могут создавать невыполняемые допущения, например, неинициализированный экземпляр синглтона может спровоцировать во время выполнения панику нулевого указателя.
  • Поскольку логика тесно связана, в этом проекте ничего нельзя с легкостью переиспользовать, и из него будет сложно извлечь какие-либо составляющие.
  • Когда у вас нет пакетов, независимо управляющих каждый своим элементом логики, разработчик должен быть очень внимателен и правильно расставлять все элементы кода иначе могут возникать неожиданные поведения.



Подход II: Спаренные пакеты

За
  • Упаковывая проект, удобнее гарантировать ответственность за конкретную логику в рамках пакета, причем, это может соблюдаться при помощи компилятора. Кроме того, мы сможем использовать приватный доступ и контролировать, какие элементы кода нам открывать.
  • Использование пакета с определениями позволяет работать с экземплярами синглтонов, и в то же время избегать кольцевых зависимостей. Таким образом, можно писать меньше кода, обойтись без передачи ссылок при управлении экземплярами и не тратить времени на проблемы, которые потенциально могут возникать при компиляции.
  • Этот подход также располагает к тестированию, ведь существуют сервисные интерфейсы. При таком подходе возможно внутреннее тестирование каждого пакета.


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



Подход III: Независимые пакеты

За
  • При использовании пакетов мы гарантируем, что конкретная логика реализуется в пределах одного пакета, и мы обладаем полным контролем доступа.
  • Потенциально не должно возникать кольцевых зависимостей, так как пакеты полностью автономны.
  • Все пакеты отлично извлекаемы и доступны для многократного использования. Во всех тех случаях, когда пакет нужен нам в другом проекте, мы просто переносим его в разделяемое пространство и используем, ничего в нем не меняя.
  • Нет глобального состояния значит, нет и непредусмотренных поведений.
  • Этот подход лучше всех подходит для тестирования. Каждый пакет можно полностью протестировать, не беспокоясь о том, что он, возможно, зависит от других пакетов через локальные интерфейсы.


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


Выводы и примеры использования

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

Итак, когда же должен использоваться каждый из подходов? Предлагаю такую расстановку:

Подход I: Подход с единственным пакетом, пожалуй, наиболее уместен при работе в небольших многоопытных командах, занятых на малых проектах, где требуется быстро достигать результата. Такой подход проще и надежнее для быстрого старта, хотя, требует серьезного внимания и координации на этапе поддержки проекта.

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

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

Шаблоны проектирования в Go Абстрактная Фабрика

26.11.2020 18:13:16 | Автор: admin

Привет, Хабр! Представляю вашему вниманию перевод очередной статьи Design Patterns: Abstract Factory Pattern автора Shubham Zanwar.

Абстрактная фабрика - это порождающий шаблон проектирования. Он используется, когда нам нужно создать семейство похожих продуктов. Давайте для понимания возьмем пример сети пиццерий.

Пиццерия

Представим, что вы глава бизнеса и открываете сеть пиццерий по всему городу. Одной из ваших обязанностей является производство всех основных продуктов (в нашем случае это пиццы и жареный чесночный хлеб), которые будут представлять такие бренды как Домино и Жаровня (назовем их так - прим. перев.).

Есть множество способов как это сделать. Самый простой - создать фабрику по производству пицц каждого бренда и еще похожую фабрику для жаренного хлеба.

Если вы еще не представляете, как работают фабрики, вы можете почитать здесь

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

Не волнуйтесь, есть простой способ.

Вместо создания фабрики для каждого продукта (пиццы или жаренного хлеба), вы сможете создавать фабрику для каждого бренда. Обе фабрики будут производить пиццу и жареный хлеб.

Открыв пиццерию, вы передаете менеджеру фабрику Домино или Жаровню и можете отдохнуть, потому что теперь никто ничего не перепутает.

Давайте посмотрим на код. Перед тем как мы напишем фабрики, создадим сами продукты:

Обычная пицца

type iPizza interface {    GetPrice() float64    GetName() string    GetToppings() []string}type pizza struct {    name     string    price    float64    toppings []string}func (p *pizza) GetName() string {    return p.name}func (p *pizza) GetPrice() float64 {    return p.price}func (p *pizza) GetToppings() []string {    return p.toppings}

Пиццы наших брендов

type pizzaHutPizza struct {    pizza}type dominosPizza struct {    pizza}

Жареный чесночный хлеб

type iGarlicBread interface {    GetPrice() float64    GetName() string}type garlicBread struct {    name  string    price float64}func (g *garlicBread) GetName() string {    return g.name}func (g *garlicBread) GetPrice() float64 {    return g.price}

И наших брендов

type pizzaHutGarlicBread struct {    garlicBread}type dominosGarlicBread struct {    garlicBread}

Мы создали оба наших продукта, которые реализуют общий интерфейс, облегчая конечному пользователю их потребление. Вот такой каламбур.

Теперь напишем сами фабрики, сначала общая

type iPizzaFactory interface {    createPizza() iPizza    createGarlicBread() iGarlicBread}

Теперь наших брендов: Жаровня-фабрика и Домино-фабрика с унифицированной функциональностью

type PizzaHutFactory struct {}func (p *PizzaHutFactory) createPizza(): iPizza {    return &pizzaHutPizza{        pizza{            name:     "pepperoni",            price:    230.3,            toppings: []string{"olives", "mozzarella", "pork"},        },    }}func (p *pizzaHutFactory) createGarlicBread() iGarlicBread {    return &pizzaHutGarlicBread{        garlicBread{            name:  "garlic bread",            price: 180.99,        },    }}
type dominosFactory struct{}func (d *dominosFactory) createPizza() iPizza {    return &dominosPizza{        pizza{            name:     "margherita",            price:    200.5,            toppings: []string{"tomatoes", "basil", "olive oil"},        },    }}func (d *dominosFactory) createGarlicBread() iGarlicBread {    return &dominosGarlicBread{        garlicBread{            name:  "cheesy bread sticks",            price: 150.00,        },    }}

Мы можем выбирать любую фабрику и продолжить приготовление пиццы или жаренного хлеба и быть абсолютно уверенными, что любой производный продукт будет соответствующего семейства/бренда.

Мы почти у цели. Давайте обернем это в фабрику, которая будет возвращать нам фабрику по нашему желанию. Сбиты с толку? Тогда еще раз прочитайте предыдущее предложение.

Думайте о наших фабриках как о другом объекте. Основываясь на типе или пиццерии, которые мы хотим открыть (Жаровня или Домино), мы создаем нужную фабрику (просто другой объект). Чтобы автоматически получать эти "объекты", у нас будет своя фабрика.

Этот код поможем вам - Фабрика фабрик

func getPizzaFactory(chain string) (iPizzaFactory, error) {    if chain == "P" {        return &pizzaHutFactory{}, nil    }    if chain == "D" {        return &dominosFactory{}, nil    }    return nil, fmt.Errorf("Enter a valid chain type next time")}

Надеюсь, стало понятнее.

Основное, что нужно запомнить: шаблон абстрактная фабрика реализует фабрику фабрик. Внутренние фабрики используются для создания продуктов нужного вида.

Вы можете найти этот код на github

Пока

Подробнее..

Strategy Design Pattern

13.04.2021 22:15:19 | Автор: admin

Всем привет, друзья. Меня зовут Alex, я профессиональный разработчик и создатель программных продуктов в веб индустрии. Много лет изучаю языки, делюсь опытом с другими.

Сегодня хочу с вами поговорить про шаблон проектирования Стратегия (Strategy). Постараюсь донести до вас принципы и суть шаблона без воды, и покажу как его применять на практике.

В чем суть?

Design patter Strategy или шаблон проектирования Стратегия относится к поведенческим шаблонам проектирования. Его задача - выделить схожие алгоритмы, решающие конкретную задачу. Реализация алгоритмов выносится в отдельные классы и предоставляется возможность выбирать алгоритмы во время выполнения программы.

Шаблон дает возможность в процессе выполнения выбрать стратегию (алгоритм, инструмент, подход) решения задачи.

В чем проблема?

Рассмотрим задачи, при решении которых можно применять такой подход.

Представьте, что перед вами стоит задача написать веб-портал по поиску недвижимости. MVP (Minimum ViableProduct) или минимально работающий продукт был спроектирован и приоритизирован вашей командой Product Managerов и на портале должен появиться функционал для покупателей квартир. То есть целевые пользователи вашего продукта в первую очередь - это те, кто ищет себе новое жилье для покупки. Одной из самых востребованных функций должна быть возможность:

  • Выбрать область на карте, где покупатель желает приобрести жилье

  • И указать ценовой диапазон цен на квартиры для фильтрации.

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

Но тут приходят к вам Product Manager'ы и говорят, что нужно добавить возможность искать и отображать недвижимость, которая сдается в аренду. У нас появляется еще один тип пользователя - арендаторы. Для арендаторов не так важно показывать фильтры по цене, им важно состояние квартиры, поэтому нужно отображать фотографии арендуемых квартир.

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

Конечно на этом все не закончилось. В ближайших планах добавить функционал работы юридических лиц, функционал оплаты и бронирования квартир сразу на сайте. Дальше-больше - добавить возможность просматривать историю недвижимости, запрашивать пакет документов для сделки и связь с владельцами, оформление кредита и так далее.

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

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

  • Основной алгоритм поиска квартир был реализован в одном супер-классе

  • Алгоритм выбора и отображения элементов интерфейса был реализован в одном супер-классе

  • Изменения в этих классах, сделанные разными программистами, приводили к конфликтам и необходимости регрессивного тестирования

  • Релизы продукта затягивались, время на разработку нового функционала увеличилась в несколько раз

  • Больше времени стало уходить на разработку, тестирование, появилось множество багов.

Супер-класс с единым методом реализации алгоритма.Супер-класс с единым методом реализации алгоритма.

Какое решение?

В данном примере мы имеем несколько алгоритмов для одной функции:

  • Поиск квартир с продажей

  • Поиск квартир в аренду

  • Отображение или нет различных наборов фильтров

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

Шаблон проектирования Стратегия - решает такую задачу. Онпредлагает выделить семейство похожих алгоритмов, вынести их в отдельные классы. Это позволит без проблем изменять нужный алгоритм, расширять его, сводя к минимум конфликты разработки, зависимости от других классов и функционала. Вместо того, чтобы реализовывать алгоритм в едином классе, наш класс будет работать с объектами классов-стратегиями через объект-контекста и в нужным момент делегировать работу нужному объекту. Для смены алгоритма достаточно в нужным момент подставить в контекст нужный объект-стратегию.

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

Диаграмма классов шаблона StrategyДиаграмма классов шаблона Strategy

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

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

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

Задача контроллера определить класс-стратегию и запросить у класса-контекста данные для отображения, передав ему известный набор фильтров. Класс-контекст в этой схеме - это класс, которые реализует метод поиска квартир по заданным фильтрам. На диаграмме классов выше мы видим, что класс контекста определяет метод getData, и принимает аргументы filters. У него должен быть конструктор, принимающий активный в данный момент объект-стратегии и сеттерsetStrategy, устанавливающий активную стратегию. Такой метод пригодится для случая, когда пользователь меняет тип искомого объекта, например, он ищет недвижимость на продажу и хочет снять квартиру.

Пример реализации

Ниже рассмотрим пример, как решается описанная задача на языке GOlang. Первое что сделаем - определим интерфейс с методом doSearch:

Strategy.goStrategy.go

Данный метод определяет общее поведение для конкретных алгоритмов, реализующих разные стратегии. Метод может принимать различные аргументы, позволяющие реализовать ветвления в ваших алгоритмах. В примера я передаю пользовательские фильтры с типом Map.

Для реализации конкретных алгоритмов создаем два файла. В каждом файле определяется свой определяемый тип с базовым типом struct, реализующие интерфейс Strategy. Соответственно, в методы, определяемые интерфейсом для каждого алгоритма, будут передаваться пользовательские фильтры. Реализации выглядит следующим образом:

FirstAlgorithm.goFirstAlgorithm.goSecondAlgorithm.goSecondAlgorithm.go

Посмотрим на нашу диаграмму классов. Нам осталось реализовать класс-контекста и клиентский код вызова конкретных алгоритмов в нужным момент. Как это сделать? Для создания слоя класса-контекста реализуем исходник, реализующий:

  • определяемый тип в базовым типом struct

  • функцию initStrategy, инициализирующий стратегию по-умолчанию и пользовательские фильтры

  • метод типа struct setStrategy, устанавливающий активную стратегию

  • и функция getData, вызывающий конкретную стратегию и возвращаемый данные для показа пользователю.

Context.goContext.go

Последний шаг - посмотрим на клиентский код. Сначала инициализируется стратегия по-умолчанию, получаем данные из этой стратегии. Затем выставляем новую активную стратегию и повторно вызываем функцию получения данных getData. Как видим, каждый раз вызывается одна и та же функция файла-контекста (или класса-контекста в диаграмме классов) для получения данных рендеринга, отображаемых в дальнейшем пользователю. Для простоты я вывожу отладочную информацию в консоль, чтобы убедиться, что работают разные алгоритмы при вызовах. Вот код:

Client.goClient.go

Вот вывод такого подхода:

First implements strategy map[role:1]

Second implements strategy map[role:2]

Как видите, мы можем управлять вызовом разных алгоритмов в зависимости от контекста и пользовательских фильтров. Алгоритмы могут создавать ветвления в зависимости от входных фильтров и других параметров, переданных из клиентского кода в методы, реализующие конкретные алгоритмы. Вот такой интересный пример.

Объектно-ориентированный подход можно посмотреть. например, в этом курсе. Там показан пример на PHP.

Когда применять?

Напоследок поговорим когда применяется шаблон Strategy?

  1. У вас есть множество похожих реализаций отличающихся незначительным поведением. Можно вынести отличающее поведение в классы-стратегии, а повторяющий код свести к единому классу-контекста.

  2. Ваш алгоритм реализован в супер-классе с множественными условными операторами. Выделите блоки условных операторов в отдельные классы-стратегии, а управление вызовов нужных доверьте классу-контекста.

  3. Конкретные стратегии позволяют инкапсулировать алгоритмы в своих конкретных классах. Используйте этот подход для снижения зависимостей от других классов.

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

Подведем итог

Друзья, мы познакомились с поведенческим шаблоном проектирования Strategy. Шаблон используется для выделения схожих алгоритмов, решающих конкретную задачу. Посмотрели с вами реализацию на языке GOlang, ознакомились в возможностями подхода и разобрали когда его лучше применять.

Рад был с вами пообщаться, Alex Versus. Успехов!

Подробнее..

Factory Method Pattern

09.05.2021 20:20:27 | Автор: admin

Привет, друзья. С вами Alex Versus.

Ранее мы говорили про шаблоны проектирования Одиночка и Стратегия, про тонкости реализации на языке Golang.

Сегодня расскажу про Фабричный метод.

В чем суть?

Фабричный метод (Factory method) так же известный как Виртуальный конструктор(Virtual Constructor) - пораждающий шаблон проектирования, определяющий общий интерфес создания объектов в родительском классе и позволяющий изменять создаваемые объекты в дочерних классах.

Шаблон позволяет классу делегировать создание объектов подклассам. Используется, когда:

  1. Классу заранее неизвестно, объекты каких подклассов ему нужно создать.

  2. Обязанности делегируются подклассу, а знания о том, какой подкласс принимает эти обязанности, локализованы.

  3. Создаваемые объекты родительского класса специализируются подклассами.

Какую задачу решает?

Представьте, что вы создали программу управления доставкой еды. В программе в качестве единственного средства доставки используется электро-самокат. Ваши курьеры на электро-самокатах развозят еду из пункта А в пункт Б. Все просто.

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

Вы обнаруживаете, что большая часть ваших сущностей в программе сильно связаны с объектом Самокат и чтобы заставить вашу программу работать с другими способами доставки, вам придется добавить связи в 80% вашей кодовой базы и так повторить для каждого нового транспорта. Знакомая ситуация?

В итоге вы получите ужасающий код, наполненный условными операторами, которые выполняют то или иное действие, в зависимости от транспорта.

И какое решение?

Фабричный метод предлагает создавать объекты транспорта через вызов специального метода. Подклассы класса, который содержит фабричный метод могут изменять создаваемые объектов конкретных создаваемых транспортов. На первый взгляд, это может показаться бессмысленным: мы просто переместили вызов конструктора из одного конца программы в другой. Но теперь вы сможете переопределять фабричный метод в подклассе, чтобы изменить тип создаваемого транспорта.

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

Для клиента фабричного метода нет разницы между создаваемыми объектами, так как он трактует их как некий абстрактный Транспорт. Для него важно, чтобы данный объект мог доставить еду из пункта А в пункта В, а как конкретно он это будет делать, не важно.

Посмотрим на диаграмму классов такого подхода.

Диаграмма классов Factory MethodДиаграмма классов Factory Method

Реализация на Golang

Пример реализации на PHP, можно изучить тут. Так как в Golang отсутствуют возможности ООП, такие как классы и наследование, то реализовать в классическом виде этот шаблон невозможно. Несмотря на это, мы можем реализовать базовую версию шаблона - Простая фабрика.

В нашем примере есть файл iTransport.go, который определяет методы создаваемых транспортных средств для доставки еды. Сущность транспорта будем хранить в структуре (struct), которая применяет интерфейс iTransport.

Так же реализуем файл Factory.go, который представляет фабрику создания нужных объектов. Клиентский код реализован в файле main.go. Вместо прямого создания конкретных объектов транспорта клиентский код будет использовать для этого метод фабрики getTransport(t string), передавая нужный тип объекта в виде аргумента функции.

Когда применять?

  1. Когда хотим дать возможность расширять нашу библиотеку. Используя подход, пользователи вашей библиотеки могут создавать новые конкретные реализации классов, а создание объектов данных классов будет отведено фабричному методу вашей библиотеки.

  2. Фабричный метод отделяет код создания объектов от остального кода. Код создания объектов можно расширять, не трогая основной код программы. Для создания нового объекта вашего продукта, достаточно создать новый подкласс и определить в нем фабричный метод, возвращающий нужный продукт в нужной конфигурации.

Какие преимущества?

  1. Избавляет слой создания объектов от конкретных классов продуктов. Выделяет код производства продуктов в одно место, упрощая поддержку кода.

  2. Упрощает добавление новых продуктов в программу.

  3. Реализует принцип открытости/закрытости (англ. openclosed principle, OCP) принцип ООП, устанавливающий следующее положение: программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для изменения

Какие недостатки?

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

Итог

Используйте шаблон Фабричный метод в случае, когда вы хотите без проблем внедрять в вашу программу новые объекты с новыми конфигурациям для взаимодействия с основной бизнес логикой.

Рад был поделиться материалом, Alex Versus. Публикация на английском.
Всем удачи!

Подробнее..

Prototype Design Pattern в Golang

24.05.2021 18:21:26 | Автор: admin

Привет друзья! С вами Алекс и я продолжаю серию статей, посвящённых применению шаблонов проектирования в языке Golang.

Интересно получать обратную связь от вас, понимать на сколько применима данная область знаний в мире языка Golang. Ранее уже рассмотрели шаблоны: Simple Factory, Singleton и Strategy. Сегодня хочу рассмотреть еще один шаблон проектирования - Prototype.

Для чего нужен?

Это порождающий шаблон проектирования, который позволяет копировать объекты, не вдаваясь в подробности их реализации.

Какую проблему решает?

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

Есть и другая проблема. Копирующий код станет зависим от классов копируемых объектов. Ведь, чтобы перебрать все поля объекта, нужно привязаться к его классу. Из-за этого вы не сможете копировать объекты, зная только их интерфейсы, а не конкретные классы.

Какое решение?

Шаблон Prototype поручает создание копий самим копируемым объектам. Он вводит общий интерфейс для всех объектов, поддерживающих клонирование. Это позволяет копировать объекты, не привязываясь к их конкретным классам. Обычно такой интерфейс имеет всего один метод clone.

Реализация этого метода в разных классах очень схожа. Метод создаёт новый объект текущего класса и копирует в него значения всех полей собственного объекта. Так получится скопировать даже приватные поля, так как большинство языков программирования разрешает доступ к приватным полям любого объекта текущего класса. Объект, который копируют, называется прототипом, отсюда и название шаблона. Когда объекты программы содержат сотни полей и тысячи возможных конфигураций, прототипы могут служить своеобразной альтернативой созданию подклассов. Шаблон прототип должен копировать объекты любой сложности без привязки к их конкретным классам.

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

Диаграмма классов

Prototype Class DiagramPrototype Class Diagram

На диаграмме видим Интерфейс прототипов, который описывает операции клонирования. В большинстве случаев - это единственный метод clone. Конкретный прототип реализует логику клонирования самого себя. Тут важно не допускать ошибку клонирования. Например, клонирование связанных объектов, распутывание рекурсивных зависимостей и прочее. Клиент создаёт копию объекта, обращаясь к нему через общий интерфейс прототипов. Все просто.

Как реализовать?

Подробно пошаговую реализацию данного шаблона, а также других шаблонов проектирования на языке PHP можно посмотреть тут. Наша задача реализовать шаблон Prototype на языке Golang.

Рассмотрим пример реализации рубрикатора каталога продуктов в интернет-магазине с большим количеством категорий товаров. В рубрикаторе есть разделы верхнего уровня, в которых содержатся конечные рубрики и разделы второго уровня. Разделы второго уровня также могут содержать рубрик и разделы третьего уровня и т.д. То есть это древовидная структура объектов, у них есть стволы и листья - конечные элементы дерева. Каждая рубрика и раздел имеет свой набор свойств, относительно товаров раздела. Например, рубашки - это цвет, размер, бренд и т.д.

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

Каждую рубрика, как конечный элемент рубрикатора, может быть представлен интерфейсом prototype, который объявляет функцию clone. За основу конкретных прототипов рубрики и раздела мы берем тип struct, которые реализуют функции show и clone интерфейса prototype.

Итак, реализуем интерфейс прототипа. Далее мы реализуем конкретный прототип directory, который реализует интерфейс prototype представляет раздел рубрикатора. И конкретный прототип для рубрики. Обе структуру реализуют две функции show, которая отвечает за отображение конкретного контента ноды и clone для копирования текущего объекта. Функция clone в качестве единственного параметра принимает аргумент, ссылающийся на тип указателя на структуру конкретного прототипа - это либо рубрика, либо директория. И возвращает указатель на поле структуры, добавляя к наименованию поля _clone.

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

Open directory 2  Directory 2    Directory 1        category 1    category 2    category 3Clone and open directory 2  Directory 2_clone    Directory 1_clone        category 1_clone    category 2_clone    category 3_clone

Когда применять?

  1. У вас много объектов с множеством свойств и вам нужно создавать их клоны быстро и эффективно. Шаблон предлагает использовать набор прототипов, вместо создания подклассов для описания популярных конфигураций объектов. Вместо порождения объектов из подклассов, вы копируете существующие объекты-прототипы, в которых уже настроено состояние. Тем самым избегая роста количества классов в программе и уменьшая её сложность.

  2. Код не должен зависеть от классов копируемых объектов. Если ваш код работает с объектами, переданными через общий интерфейс - вы не можете привязаться к их классам, даже если бы хотели, поскольку их конкретные классы неизвестны. Прототип предоставляет клиенту общий интерфейс для работы со всеми прототипами. Клиенту не нужно зависеть от классов копируемых объектов, а только от интерфейса клонирования.

Итог

Друзья, шаблон Prototype предлагает:

  • Удобную концепцию для создания копий объектов.

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

  • В объектных языках позволяет избежать наследования создателя объекта в клиентском приложении, как это делает паттерн abstract factory, например.

Кстати, друзья, вот тут можно посмотреть результаты опроса читателей хабра. 63% опрошенных считают, что применение шаблонов проектирования в Golang - это зло. Связано, скорее всего, с тем, что язык Golang процедурный и ему чужды подходы объектно-ориентированных языков. Но рассматривать реализации и применение шаблонов стоит, так как это позволяет больше их понимать и периодически применять для решения тех или иных задач. Каждый подход требует, конечно, дискуссии и разумного применения.

Друзья, рад был поделиться темой, Алекс. На английском статью можно найти тут.
Удачи!

Подробнее..

Job шаблон проектирования для новичков и опытных Go программистов

16.01.2021 02:07:06 | Автор: admin

Я начал программировать на Go после достаточно продолжительного периода программирования на PHP. Полагаю судя по последним тенденциям, мой случай далеко не единичный. Go в целом набирает популярность среди Web разработчиков.

Итак, я в мире сусликов. А что делает прожженный до мозга костей PHP программист оказавшись там? Правильно он продолжает "пыхтеть" - в силу своей профессиональной деформации но уже на Go, со всеми вытекающими отсюда последствиями.

Признаюсь я никогда не был на собеседовании, устраиваясь на вакансию Go разработчика, но подозреваю, что там не задают вопросов про SOLID, Dependency Injection и различные прелести ООП. Там другой мир, другая парадигма (в целом разумеется не экстраординарная), связанная с параллельным программированием, строгой типизацией и отсутствием полноценного ООП в строгом его смысле - к тому ООП к которому мы привыкли по PHP, С++ или Java.

Поэтому в начале, разумеется, был определенный дискомфорт и ломка шаблонов. Я не понимал как использовать тот же content.Context, зачем он? Мои шаблоны готовы уже были треснуть, но матерого PHP программиста без хрена не съешь. Пора что-то с этим делать и писать свой велосипед! А именно то решение, которое мне, как PHP программисту, казалось достаточно очевидным для решения задача связанных с параллельным программирование, краеугольным камнем Go. Усевшись за рабочий ноутбук я почти месяц корпел над работой. Я хотел не просто сделать реализацию сферического коня в вакууме, а показать на конкретном примере возможность использования данного подхода. Так сказать, proof of concept. Ну и заодно набить руку на Go, чего греха таить.

После того как велосипед был готов, я выкатил его, осмотрел и, выдохнув, сказал сам себе: "ну, блин, вроде неплохо получилось. Нужно срочно рассказать об этом сообществу, пускай оценят". Сказано сделано. На Reddit была опубликована небольшая заметка про новый шаблон проектирования Go и, судя по реакции я понял что люди ничего не поняли. "Зачем? Как это использовать? Так это же over-engineering" - в целом так можно охарактеризовать реакцию. Я не сдавался: "сейчас я вам нафотошоплю свой контраргумент. Ага, добавить это в README.md - пусть знают эти суслики".

Как видят решение программистыКак видят решение программисты

А если без шуток, то это, наверное, тема для отдельной статьи.

В целом о чем я? Много воды - ноль конкретики. Чтобы было реализовано:

  1. Шаблон проектирования Job - это поведенческий шаблон проектирования, который инкапсулирует всю необходимую информацию для параллельного выполнения задач (task).

  2. В качестве примера использования данного шаблона был реализован простой прокси-сервер, выполняющий роль балансировщика уровня L4; клиент, который сканируют указанную директорию на наличие изображений и отправляет все найденные изображения на backend сервер для изменения их размера; backend сервер, который обрабатывает запросы по изменению размера изображений. В основе всех трех приложений лежит компонент Job. Код так же доступен в репозитории на Github.

В целом сам шаблон Job - это аналог хорошо известного Command pattern, но с прицелом на параллельное выполнение. В той же статье есть и конкретный пример его использования и для нашего случая:

Parallel Processing Where the commands are written as tasks to a shared resource and executed by many threads in parallel (possibly on remote machines; this variant is often referred to as the Master/Worker pattern)

Выполнение задач зависит друг от друга, если одна задача прерывает свое выполнение из-за ошибки все остальные задачи останавливаются тоже. Это все это, безусловно, можно реализовать с помощью content.Context, но реализация на более высоком уровне позволяет избавиться от рутинных действий, связанных с организацией параллельного выполнения задач и сконцентрироваться на реализации самой бизнес логики. Ну а сами ошибки очень легко отлавливать вызовами специальных методов вроде task.Assert заменяя ими более емкие конструкции if err != nil { panic(err) }.

Задачи разделают данные и оркестрируют свое выполнение с помощью так называемой ping/pong синхронизации. Я приведу здесь лишь небольшой кускок кода, чтобы дать общее представление полностью библиотека доступна в репозитории на Github по ссылке.

// Saves resized image to the output dirfunc (s *ImageResizer) SaveResizedImageTask(j job.Job) (job.Init, job.Run, job.Finalize) {// Do some initialization hereinit := func(t job.Task) {if _, err := os.Stat(s.inputDir); os.IsNotExist(err) {t.Assert(err)}if _, err := os.Stat(s.outputDir); os.IsNotExist(err) {err := os.Mkdir(s.outputDir, 755)t.Assert(err)}}run := func(task job.Task) {stream := j.GetValue().(netmanager.Stream)select {case finishedTask := <- j.TaskDoneNotify(): // Wait for the scanner task to be doneif finishedTask.GetIndex() == s.scanneridx {s.scandone = true}task.Tick()case frame := <-stream.RecvDataFrame(): // Process response from the backend servertask.AssertNotNil(frame)res := &imgresize.Response{}err := frame.Decode(res)task.Assert(err)baseName := fmt.Sprintf("%s-%dx%d%s",res.OriginalName, res.ResizedWidth, res.ResizedHeight, res.Typ.ToFileExt())filename := s.outputDir + string(os.PathSeparator) + baseNameif ! s.dryRun {ioutil.WriteFile(filename, res.ImgData, 0775)}j.Log(1) <- fmt.Sprintf("file %s has been saved", filename)stream.RecvDataFrameSync() // Tell netmanager.ReadTask that we are done processing the frames.recvx++task.Tick()default:switch {case s.scandone && s.recvx == s.sentx: // Check if all found images were processedtask.FinishJob()default:task.Idle() // Do nothing}}}return init, run, nil}

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

Насколько это решение over-engineered и насколько его использование вместо content.Context оправдано - пусть это решает читатель, я своё мнение выразил в виде сарказма на изображении выше.

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

Подробнее..
Категории: Php , Go , Software , Design patterns

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru