Паттерны проектирования в автоматизации тестирования / Хабр

Паттерны проектирования в автоматизации тестирования / Хабр Женщине

Что такое дизайн-паттерн (design pattern), зачем эта штука существует?

Паттерны проектирования в автоматизации тестирования / Хабр

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

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

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

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

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

Большая часть этих факторов находится под влиянием разделения концепции: в любом вашем тесте – функциональном, интеграционном, unit-тесте — всегда присутствует три компонента: тестовая логика, тестовые данные и application driver, или technical details, technical parts – часть, отвечающая за непосредственное взаимодействие с вашим приложением, вашим кодом (вызов функций, клики на экран и т. п.).

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

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

Assert object/matchers

Следующий паттерн, о котором говорят все, но применяют его очень мало людей – это Assert Object, или «матчеры». У нас есть классический подход – мы вытащили пользователей и хотим сделать на них какие-то проверки. Классический подход отличается тем, что мы делаем множество различных проверок, и все они относятся к какой-то одной доменной сущности.

Паттерны проектирования в автоматизации тестирования / Хабр

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

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

Factory/page factory

Следующий паттерн – это Page Factory, или просто Factory, потому что он может применяться не только к страницам. Возник этот паттерн потому, что иногда, чтобы инициализировать вашу страницу, необходимо сделать больше действий, чем просто сказать «new page» или open, или еще что-то.

В этот случае вам бы хотелось, чтобы эта информация была скрыта от того, кто создает эту страницу, чтобы она была спрятана – это техническая информация, которая никому не важна. Именно здесь применяется подход Factory. В данном случае у меня действует такой подход: я говорю «new MainPage», передаю туда драйвер и потом говорю «страница, откройся».

Если бы я хотел сделать что-то дополнительное на этом открытии, мне нужно было бы либо занести это в метод open, который стал бы factory-методом, потому что он открывал бы эту страницу, инициализируя ее и делая ее новой, либо мне нужно было бы внести это в конструктор, что может быть тоже не очень хорошо.

Поэтому есть альтернативный подход – когда вы просто указываете вашу фабрику (я для примера привел здесь классическую Page Factory, которая есть в Java для web-драйвера), вы можете просто заказать Page Factory, Init elements, и у вас на выходе получится экземпляр класса этой страницы со всеми инициализированными элементами, которые есть в этой странице.

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

«Фабрики» есть разные. Вы можете написать свои, можете использовать фабричные методы – важно понимать суть, для чего это делается.

Fluent/chain of invocations

Следующая проблематика, которую вы хотите решить – когда вы вызываете что-нибудь, например, на login page. Можете ли вы далее вызвать что-нибудь на login page или нет? Вы не знаете.

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

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

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

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

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

Реализуется это очень просто – в своих page objects или других местах, где можно применить этот паттерн, вы используете возвращаемое значение, которое может быть this или любой другой объект, с которым вы дальше хотите продолжить работу.

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

Loadable component

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

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

Зачастую такого wait не делают, потому что многие рассчитывают, что за вас это сделает фреймер. Например, в web-драйвере есть понятие implicit wait, и за вас – если что, неявно дождутся того элемента со стороны браузера, с которым вы будете работать.

Некоторые говорят «ОК, мне этого достаточно», но если implicit wait маленький и это не работает для вас, появляется так называемый explicit wait, когда вы основательно ждете чего-то, и получается, что теперь в вашу тестовую логику после каждого такого хорошего действия, которое меняет логическую страницу, дополняется еще wait: что-то сделали – wait, что-то сделали – wait.

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

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

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

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

Как быть Леди:  Статистика ожирения в России: чем опасен лишний вес и как питаются россияне

В данном конкретном случае – на слайде выше есть стратегия по регистрации пользователя. Первая реализация этой стратегии использует для этого браузер – она открывает страничку и выводит поля, кнопку «зарегистрировать», получая ID на выходе из URL и возвращая нам объект юзера, а вторая идет через API.

Как вы думаете, какая быстрее?

Наверняка вторая: хочу ли я везде прописывать этот код руками, если я потом решил поменять его на 97%? Наверно, нет.

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

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

Object pool/flyweight

Следующий паттерн знают и используют ещё меньше людей. Flyweight – это название паттерна, которое пришло из классических паттернов, и решает проблемы с объектами или наборами объектов, которые тяжеловесны в создании. Вместо того, чтобы создавать их каждый раз, мы берём их, пользуемся, и после этого они возвращаются в тот же Pool, откуда мы их взяли.

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

Браузеры не обязательно создавать прямо в тесте, вместо этого можно использовать Background Pool, в котором настроено необходимое вам количество браузеров, и в этом пуле, когда браузер в него возвращается – вы его очищаете, делаете еще что-то, но это все происходит в бэкграунде, в параллельных с выполнением ваших тестов потоках.

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

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

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

Steps

Концепция последнего на сегодня паттерна – Steps, интересна вне зависимости от того, используете ли вы Behavior Driven Development, Keyword Driven или Behavior Specification. Когда вы используете логически сценарий, он состоит из шагов. Когда вы реализуете его в коде, зачастую шаги теряются – появляются вызовы каких-то технических деталей, подготовка данных, еще что-то, и очень непросто среди этого вычленить шаги.

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

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

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

У вас есть Page Object или любая другая техническая реализация ваших тестов, и их нужно соединить, для чего и существуют Steps, реализующие логические составляющие ваших тестов.

Если вы задумаетесь над web-драйвер тестом в верхней части слайда, который написан в концепции Steps, в котором каждый шаг вызывается — он логический, его легко прочитать.

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

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

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

Графический паттерн «бриллиант»

Паттерн «Бриллиант» — редкий разворотный паттерн в виде ромба. Формируется на локальных максимумах и минимумах графика цены в ходе восходящего или нисходящего тренда. Указывает на то, что действующая тенденция ослабла и ожидается либо коррекция, либо разворот и смена на противоположный тренд.

Если паттерн «Бриллиант» сформирован на максимумах графика в ходе восходящего тренда – рекомендуются продажи после закрытия цены ниже линии поддержки технической фигуры, цель отработки – высота паттерна (Н) в пунктах. Если «Бриллиант» сформирован на минимумах графика в ходе нисходящего тренда – рекомендуются покупки после закрытия цены выше линии сопротивления паттерна, цель отработки – высота паттерна (Н) в пунктах.

Графический паттерн "Бриллиант"
Графический паттерн «Бриллиант»

Графический паттерн «клин»

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

Если паттерн «Клин» сформирован на максимумах графика в ходе восходящего тренда – рекомендуются продажи после закрытия цены ниже линии поддержки, цель отработки – величина основания паттерна (Н) в пунктах. Если паттерн «Клин» сформирован на минимумах графика в ходе нисходящего тренда – рекомендуются покупки после закрытия цены выше линии сопротивления, цель отработки – величина основания паттерна (Н) в пунктах.

Графический паттерн "Клин"
Графический паттерн «Клин»

Графический паттерн «прямоугольник»

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

Графический паттерн "Прямоугольник"
Графический паттерн «Прямоугольник»

Графический паттерн «флаг»

Паттерн «Флаг» — фигура продолжения действующей тенденции. Выглядит в виде флага, когда после сильного ценового движения («древко флага»), формируется зона коррекции («полотнище флага»), которая расположена либо горизонтально, либо имеет наклон против «древка».

Зона коррекции паттерна «Флаг» может иметь вид «Прямоугольника», «Треугольника», «Клина». После завершения коррекции и закрытия цены выше линии сопротивления «полотнища флага» рекомендуются покупки, величина отработки – высота «древка флага» (Н).

Графический паттерн "Флаг"
Графический паттерн «Флаг»

Итератор (iterator)

Википедия гласит:

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

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

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

Простыми словами: Представляет способ доступа к элементам объекта без показа базового представления.

Обратимся к примерам в коде. В PHP очень просто реализовать это, используя SPL (Standard PHP Library). Приводя наш пример с радиостанциями, изначально у нас есть Radiostation:

class RadioStation
{
    protected $frequency;

    public function __construct(float $frequency)
    {
        $this->frequency = $frequency;
    }

    public function getFrequency(): float
    {
        return $this->frequency;
    }
}

Затем у нас есть итератор:

use Countable;
use Iterator;

class StationList implements Countable, Iterator
{
    /** @var RadioStation[] $stations */
    protected $stations = [];

    /** @var int $counter */
    protected $counter;

    public function addStation(RadioStation $station)
    {
        $this->stations[] = $station;
    }

    public function removeStation(RadioStation $toRemove)
    {
        $toRemoveFrequency = $toRemove->getFrequency();
        $this->stations = array_filter($this->stations, function (RadioStation $station) use ($toRemoveFrequency) {
            return $station->getFrequency() !== $toRemoveFrequency;
        });
    }

    public function count(): int
    {
        return count($this->stations);
    }

    public function current(): RadioStation
    {
        return $this->stations[$this->counter];
    }

    public function key()
    {
        return $this->counter;
    }

    public function next()
    {
        $this->counter  ;
    }

    public function rewind()
    {
        $this->counter = 0;
    }

    public function valid(): bool
    {
        return isset($this->stations[$this->counter]);
    }
}

Пример использования:

Команда (command)

Википедия гласит:

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

Пример из жизни: Типичный пример: вы заказываете еду в ресторане. Вы (т.е. Client) просите официанта (например, Invoker) принести еду (то есть Command), а официант просто переправляет запрос шеф-повару (то есть Receiver), который знает, что и как готовить.

Простыми словами: Позволяет вам инкапсулировать действия в объекты. Основная идея, стоящая за шаблоном — это предоставление средств, для разделения клиента и получателя.

Как быть Леди:  Стокгольмский синдром. Парадоксы сознания жертвы | Психологические тренинги и курсы он-лайн. Системно-векторная психология | Юрий Бурлан

Обратимся к коду. Изначально у нас есть получатель Bulb, в котором есть реализация каждого действия, которое может быть выполнено:

Паттерн «декоратор»

Паттерн «Декоратор» (Decorator) используется для расширения функционала объектов без модификации существующих классов или функций-конструкторов. Этот паттерн можно использовать для добавления к объектам неких возможностей без модификации кода, который ответственен за их создание.

Вот простой пример использования этого паттерна:

function Car(name) {
  this.name = name;
  // Значение по умолчанию
  this.color = 'White';
}
// Создание нового объекта, который планируется декорировать
const tesla= new Car('Tesla Model 3');
// Декорирование объекта - добавление нового функционала
tesla.setColor = function(color) {
  this.color = color;
}
tesla.setPrice = function(price) {
  this.price = price;
}
tesla.setColor('black');
tesla.setPrice(49000);
// Выводит black
console.log(tesla.color);

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

class Car() {
}
class CarWithAC() {
}
class CarWithAutoTransmission {
}
class CarWithPowerLocks {
}
class CarWithACandPowerLocks {
}

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

Car

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

class Car {
  constructor() {
  // Базовая стоимость
  this.cost = function() {
  return 20000;
  }
}
}
// Функция-декоратор
function carWithAC(car) {
  car.hasAC = true;
  const prevCost = car.cost();
  car.cost = function() {
    return prevCost   500;
  }
}
// Функция-декоратор
function carWithAutoTransmission(car) {
  car.hasAutoTransmission = true;
   const prevCost = car.cost();
  car.cost = function() {
    return prevCost   2000;
  }
}
// Функция-декоратор
function carWithPowerLocks(car) {
  car.hasPowerLocks = true;
  const prevCost = car.cost();
  car.cost = function() {
    return prevCost   500;
  }
}


Здесь мы сначала создаём базовый класс

Car

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

Car

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

cost

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

const car = new Car();
console.log(car.cost());
carWithAC(car);
carWithAutoTransmission(car);
carWithPowerLocks(car);


После этого можно узнать стоимость автомобиля в улучшенной комплектации:

// Нахождение стоимости автомобиля с учётом улучшений
console.log(car.cost());

Паттерн «модуль»

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

Модули — это составная часть любого современного JavaScript-приложения. Они помогают поддерживать чистоту кода, способствуют разделению кода на осмысленные фрагменты и помогают его организовывать. В JavaScript существует множество способов создания модулей, одним из которых является паттерн «Модуль» (Module).

В отличие от других языков программирования, JavaScript не имеет модификаторов доступа. То есть, переменные нельзя объявлять как приватные или публичные. В результате паттерн «Модуль» используется ещё и для эмуляции концепции инкапсуляции.

Этот паттерн использует IIFE (Immediately-Invoked Functional Expression, немедленно вызываемое функциональное выражение), замыкания и области видимости функций для имитации этой концепции. Например:

const myModule = (function() {
  
  const privateVariable = 'Hello World';
  
  function privateMethod() {
    console.log(privateVariable);
  }
  return {
    publicMethod: function() {
      privateMethod();
    }
  }
})();
myModule.publicMethod();

Так как перед нами IIFE, код выполняется немедленно и возвращаемый выражением объект назначается константе

myModule

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

В результате переменные и функции, объявленные внутри IIFE, скрыты от механизмов, находящихся во внешней по отношению к ним области видимости. Они оказываются приватными сущностями константы myModule.

После того, как этот код будет выполнен, myModule будет выглядеть следующим образом:

const myModule = {
  publicMethod: function() {
    privateMethod();
  }};

То есть, обращаясь к этой константе, можно вызвать общедоступный метод объекта

publicMethod()

, который, в свою очередь, вызовет приватный метод

privateMethod()

. Например:

// Выводит 'Hello World'
module.publicMethod();

Паттерн «открытый модуль»

Паттерн «Открытый модуль» (Revealing Module) представляет собой немного улучшенную версию паттерна «Модуль», которую предложил Кристиан Хейльманн. Проблема паттерна «Модуль» заключается в том, что нам приходится создавать публичные функции только для того, чтобы обращаться к приватным функциям и переменным.

В рассматриваемом паттерне мы назначаем свойствам возвращаемого объекта приватные функции, которые хотим сделать общедоступными. Именно поэтому данный паттерн и называют «Открытый модуль». Рассмотрим пример:

const myRevealingModule = (function() {
  
  let privateVar = 'Peter';
  const publicVar  = 'Hello World';
  function privateFunction() {
    console.log('Name: '  privateVar);
  }
  
  function publicSetName(name) {
    privateVar = name;
  }
  function publicGetName() {
    privateFunction();
  }
  /** открываем функции и переменные, назначая их свойствам объекта */
return {
    setName: publicSetName,
    greeting: publicVar,
    getName: publicGetName
  };
})();
myRevealingModule.setName('Mark');
// Выводит Name: Mark
myRevealingModule.getName();

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

После выполнения IIFE myRevealingModule выглядит так:

const myRevealingModule = {
  setName: publicSetName,
  greeting: publicVar,
  getName: publicGetName
};

Мы можем, например, вызвать метод

myRevealingModule.setName('Mark')

, который представляет собой ссылку на внутреннюю функцию

publicSetName

. Метод

myRevealingModule.getName()

ссылается на внутреннюю функцию

publicGetName

. Например:

myRevealingModule.setName('Mark');
// выводит Name: Mark
myRevealingModule.getName();


Рассмотрим преимущества паттерна «Открытый модуль» перед паттерном «Модуль»:

Паттерн «фабрика»

Паттерн «Фабрика» (Factory) использует для создания объектов так называемые «фабричные методы». При этом не требуется указывать классы или функции-конструкторы, которые применяются для создания объектов.

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

class Car{
  constructor(options) {
    this.doors = options.doors || 4;
    this.state = options.state || 'brand new';
    this.color = options.color || 'white';
  }
}
class Truck {
  constructor(options) {
    this.doors = options.doors || 4;
    this.state = options.state || 'used';
    this.color = options.color || 'black';
  }
}
class VehicleFactory {
  createVehicle(options) {
    if(options.vehicleType === 'car') {
      return new Car(options);
    } else if(options.vehicleType === 'truck') {
      return new Truck(options);
      }
  }
}

Здесь созданы классы

CarTruck

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

cartruck

. Также здесь объявлен класс

VehicleFactory

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

vehicleType

, передаваемого соответствующему методу возвращаемого им объекта в объекте с параметрами

options

. Вот как со всем этим работать:

const factory = new VehicleFactory();
const car = factory.createVehicle({
  vehicleType: 'car',
  doors: 4,
  color: 'silver',
  state: 'Brand New'
});
const truck= factory.createVehicle({
  vehicleType: 'truck',
  doors: 2,
  color: 'white',
  state: 'used'
});
// Выводит Car {doors: 4, state: "Brand New", color: "silver"}
console.log(car);
// Выводит Truck {doors: 2, state: "used", color: "white"}
console.log(truck);


Здесь создан объект

factory

класса

VehicleFactory

. После этого можно создавать объекты классов

Car

или

Truck

, вызывая метод

factory.createVehicle()

и передавая ему объект

options

со свойством

vehicleType

, установленным в значение

car

или

truck

Посетитель (visitor)

Википедия гласит:

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

Пример из жизни: Туристы собрались в Дубай. Сначала им нужен способ попасть туда (виза). После прибытия они будут посещать любую часть города, не спрашивая разрешения ходить где вздумается. Просто скажите им о каком-нибудь месте — и туристы могут там побывать. Шаблон посетитель помогает добавлять места для посещения.

Простыми словами: Шаблон посетитель позволяет добавлять будущие операции для объектов без их модифицирования.

Перейдем к примерам в коде. Возьмём зоопарк: у нас есть несколько видов Animal, и нам нужно послушать издаваемые ими звуки.

// Посещаемый
interface Animal
{
    public function accept(AnimalOperation $operation);
}

// Посетитель
interface AnimalOperation
{
    public function visitMonkey(Monkey $monkey);
    public function visitLion(Lion $lion);
    public function visitDolphin(Dolphin $dolphin);
}

Затем у нас есть реализация для животных:

Равносторонний треугольник

Универсальный паттерн «Треугольник», может предвещать как разворот, так и продолжение действующего тренда. Образуется между двумя сходящимися линиями поддержки и сопротивления. Рекомендуется вести торговлю в сторону пробития паттерна – если цена закрывается выше линии сопротивления – покупаем, если цена закрывается ниже линии поддержки – продаем. Цель отработки – величина основания паттерна (Н) в пунктах.

Графический паттерн "Равносторонний треугольник"
Графический паттерн «Равносторонний Треугольник»

Стратегия (strategy)

Википедия гласит:

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

Пример из жизни: Возьмём пример с пузырьковой сортировкой. Мы её реализовали, но с ростом объёмов данных сортировка работа стала выполняться очень медленно. Тогда мы сделали быструю сортировку. Алгоритм работает быстрее на больших объёмах, но на маленьких он очень медленный.

Простыми словами: Шаблон стратегия позволяет переключаться между алгоритмами или стратегиями в зависимости от ситуации.

Перейдем к коду. Возьмем наш пример. Изначально у нас есть наша SortStrategy и разные её реализации:

Структурные паттерны – structural patterns

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

Первая группа таких паттернов – это Page Object. Зачем он нужен, какова проблематика?Первая часть проблематики: у нас есть логическая структура нашего приложения, и когда мы пишем тесты в коде, мы не совсем понимаем, где именно мы сейчас находимся – мы же не видим UI непосредственно со своим тестом.

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

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

Как быть Леди:  ВОРЧУН - что такое в Толковом словаре русского языка

Вот три проблемы, которые помогает решать Page Object. Если у вас только пара тестов, у вас нет этой проблемы – просто нет масштаба, на который вы можете это применить. Если у вас пять – десять – пятнадцать тестов, у вас эта проблема может быть и может не быть. Поэтому вы должны понимать, соответствует ли этот паттерн тому, что вы делаете.

Давайте быстро «пробежимся» по Page Object. У нас есть какая-то страница с какими-то элементами, неважно, чем вы их помечаете (в данном примере с помощью аннотаций @FindBy). Вы можете использовать любые аннотации, которые вам нравятся. Я вынес все элементы этой логической страницы в одно отдельное место, снабдив ее дополнительно доменными методами, которые теперь выглядят таким образом:

Типы шаблонов

Шаблоны бывают следующих трех видов:

  1. Порождающие.
  2. Структурные.
  3. Поведенческие о них мы рассказываем в этой статье.

Простыми словами: Поведенческие шаблоны связаны с распределением обязанностей между объектами. Их отличие от структурных шаблонов заключается в том, что они не просто описывают структуру, но также описывают шаблоны для передачи сообщений / связи между ними. Или, другими словами, они помогают ответить на вопрос «Как запустить поведение в программном компоненте?»

Википедия гласит:

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

Поведенческие шаблоны:

Фигура «голова и плечи»

Техническая Фигура «Голова и плечи» — формируется на максимумах графика цены в ходе восходящей тенденции. Через точки 1 и 2 проводится линия основания фигуры (линия шеи). Фигура считается полностью сформированной только после закрытия цены ниже линии основания фигуры.

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

Графический паттерн "Голова и плечи"
Графический паттерн «Голова и плечи»

Фигура «двойная вершина»

Паттерн «Двойная вершина» формируется после восходящего движения на локальном максимуме графика. Состоит из двух последовательных вершин на графике цены, через минимум между ними (точка 1) проводится горизонтальный уровень поддержки – линия основания фигуры.

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

Графический паттерн "Двойная вершина"
Графический паттерн «Двойная вершина»

Фигура «двойное дно»

Паттерн «Двойное дно» формируется после нисходящего движения на локальном минимуме графика. Состоит из двух последовательных впадин на графике цены, через максимум между ними (точка 1) проводится горизонтальный уровень сопротивления – линия основания фигуры.

После пробития ценой линии основания техническая фигура «Двойное дно» считается сформированной, рекомендуются покупки. Цель отработки – рост цены от линии основания фигуры на величину Н – высоту фигуры в пунктах.

Графический паттерн "Двойное дно"
Графический паттерн «Двойное дно»

Фигура «перевернутая голова и плечи»

Паттерн «Перевернутая голова и плечи» — формируется на минимумах графика цены в ходе нисходящей тенденции. Через точки 1 и 2 проводится линия основания фигуры (линия шеи). Данная фигура технического анализа считается полностью сформированной только после закрытия цены выше линии основания фигуры.

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

Графический паттерн "Перевернутая голова и плечи"
Графический паттерн «Перевернутая голова и плечи»

Фигура «тройная вершина»

Техническая фигура «Тройная вершина» — паттерн образуется на максимумах графика цены после предшествующей восходящей тенденции. Представляет из себя три последовательные вершины на графике цены, находящиеся примерно на одном уровне. Через два находящихся между ними минимума (точки 1 и 2) проводится линия основания фигуры, которая может быть горизонтальной, либо иметь небольшой наклон вверх или вниз.

Графический паттерн "Тройная вершина"
Графический паттерн «Тройная вершина»

Фигура «тройное дно»

Паттерн «Тройное дно» — образуется на минимумах графика цены после предшествующей нисходящей тенденции. Представляет из себя три последовательные впадины на графике цены, находящиеся примерно на одном уровне. Через два находящихся между ними максимума (точки 1 и 2) проводится линия основания фигуры, которая может быть горизонтальной, либо иметь небольшой наклон вверх или вниз.

Графический паттерн "Тройное дно"
Графический паттерн «Тройное дно»

Хранитель (memento)

Википедия гласит:

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

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

Простыми словами: Шаблон хранитель фиксирует и хранит текущее состояние объекта, чтобы оно легко восстанавливалось.

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

Изначально у нас есть наш объект EditorMemento, который может содержать состояние редактора:

class EditorMemento
{
    protected $content;

    public function __construct(string $content)
    {
        $this->content = $content;
    }

    public function getContent()
    {
        return $this->content;
    }
}

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

class Editor
{
    protected $content = '';

    public function type(string $words)
    {
        $this->content = $this->content . ' ' . $words;
    }

    public function getContent()
    {
        return $this->content;
    }

    public function save()
    {
        return new EditorMemento($this->content);
    }

    public function restore(EditorMemento $memento)
    {
        $this->content = $memento->getContent();
    }
}

Пример использования:

Цепочка обязанностей (chain of responsibility)

Википедия гласит:

Цепочка обязанностей — поведенческий шаблон проектирования предназначенный для организации в системе уровней ответственности.

Пример из жизни: например, у вас есть три платежных метода (A, B и C), настроенных на вашем банковском счёте. На каждом лежит разное количество денег. На A есть 100 долларов, на B есть 300 долларов и на C — 1000 долларов. Предпочтение отдается в следующем порядке: A, B и C.

Вы пытаетесь заказать что-то, что стоит 210 долларов. Используя цепочку обязанностей, первым на возможность оплаты будет проверен метод А, и в случае успеха пройдет оплата и цепь разорвется. Если нет, то запрос перейдет к методу B для аналогичной проверки. Здесь A, B и C — это звенья цепи, а все явление — цепочка обязанностей.

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

Обратимся к коду. Приведем пример с банковскими счетами. Изначально у нас есть базовый Account с логикой для соединения счетов цепью и некоторые счета:

Шаблонный метод

Когда приходится спрашивать человека, какие паттерны проектирования ему доводилось использовать, почему-то мало кто называет паттерн «Шаблонный метод» (Template Method). Вероятно, это связано с пробелом в знании номенклатуры паттернов, ибо лично я с трудом представляю себе, чтобы более-менее опытный программист ни разу не использовал такой удобный и полезный паттерн. Предлагаю ещё раз взглянуть на него поближе.

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

Допустим, у мы пишем класс Crypt, который предназначен для шифрования некоторой строки текста. В классе определена функция шифрования:

void encrypt() {
    // Установка начальных параметров
    setupRnd();
    setupAlgorithm();

    // Получаем строку
    std::string fContent = getString();
    // Применяем шифрование
    std::string enc = applyEncryption(fContent);
    // Сохраняем строку
    saveString(fContent);

    // Подчищаем следы работы алгоритма
    wipeSpace();
}

С помощью паттерна «Шаблонный метод» мы можем использовать алгоритм, представленный в функции encrypt(), чтобы работать со строками, полученными из разных источников — с клавиатуры, прочитанные с диска, полученные по сети. При этом сама структура алгоритма и неизменные шаги (установка начальных параметров, подчистка следов работы, и при желании применение шифрования) остаются неизменными. Это позволяет нам:

  1. Повторно использовать код, который не изменяется для различных подклассов;
  2. Определить общее поведение семейства подклассов, используя единожды определённый код;
  3. Разграничить права доступа — при реализации изменяемых шагов алгоритма мы будем использовать закрытые виртуальные функции. Это гарантирует, что такие операции будут вызываться только в качестве шагов модифицируемого алгоритма (или, скорее, не будут вызываться производными классами в неподходящих для этого местах).

Итак, дополним класс Crypt необходимыми членами:

private:
    void setupRnd() {
        // Некая инициализация алгоритма случайных чисел
        std::cout << "setup rndn";
    };
    void setupAlgorithm() {
        // Начальные установки алгоритма шифрования
        std::cout << "setup algorithmn";
    };
    void wipeSpace() {
        // Удаление следов работы
        std::cout << "wipen";
    };
    
    virtual std::string applyEncryption(const std::string& content) {
        // Шифрование
        std::string result = someStrongEncryption(content);
        return result;
    }
    virtual std::string getString() = 0;
    virtual void saveString(const std::string& content) = 0;


Обратите внимание, что функции закрытые. Это намеренное ограничение на их вызов, которое не мешает переопределить их в производном классе.

И, собственно, производный класс — шифрующий файл на диске:

class DiskFileCrypt : public Crypt {
public:
    DiskFileCrypt(const std::string& fName)
        : fileName(fName) {};
private:
    std::string fileName;

    virtual std::string getString() {
        std::cout << "get disk file named "" << fileName << ""n";
        // Прочитать файл с диска и вернуть содержимое
        return fileContent;
    }
    virtual void saveString(const std::string& content) {
        std::cout << "save disk file named "" << fileName << ""n";
        // Записать файл на диск
    }
};

Уже понятно, что при вызове

DiskFileCrypt d("foo.txt");
d.encrypt();

Будет выполнен алгоритм функции encrypt() и в консоли будет следующее:

setup rnd
setup algorithm
get disk file named "foo.txt"
save disk file named "foo.txt"
wipe

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

Шаблонный метод (template method)

Википедия гласит:

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

Пример из жизни: Допустим, вы собрались строить дома. Этапы будут такими:

  1. Подготовка фундамента.
  2. Возведение стен.
  3. Настил крыши.
  4. Настил перекрытий.

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

Простыми словами: Шаблонный метод определяет каркас выполнения определённого алгоритма, но реализацию самих этапов делегирует дочерним классам.

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

Изначально у нас есть наш Builder, который описывает скелет для построения алгоритма:

abstract class Builder
{

    // Шаблонный метод
    final public function build()
    {
        $this->test();
        $this->lint();
        $this->assemble();
        $this->deploy();
    }

    abstract public function test();
    abstract public function lint();
    abstract public function assemble();
    abstract public function deploy();
}

Затем у нас есть его реализации:

Оцените статью
Ты Леди!
Добавить комментарий