Как системы мониторинга и прогноза встраиваются в бизнес-процессы ТОиР: сценарии на примере F5 PMM и F5 EAM

Привет, Хабр! Мы – Factory5, российский разработчик ПО для промышленных предприятий. Создаём решения для управления производственными активами и интеллектуального анализа больших данных на базе технологий машинного обучения. Сегодня расскажем о том, как наши системы встраиваются в бизнес-процессы и помогают оптимизировать ресурсы.

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

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

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

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

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

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

F5 PMM – бизнес-приложение для мониторинга и прогноза технического состояния активов. Оно позволяет выявлять аномалии в работе оборудования, прогнозировать отказы и определять их причины, а также прогнозировать остаточный ресурс или индекс здоровья оборудования.

F5 EAM – система управления производственными активами, которая автоматизирует процессы ведения базы активов, мониторинга их состояния, планирования и контроля исполнения работ ТОиР, а также процессы управления МТР.

Рассмотрим четыре сценария применения продуктов F5 для управления производственными активами.

Сценарий применения №1 (базовый): Мониторинг (данные разрознены, не хранятся)

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

В качестве базы данных используем ClickHouse, ее преимущества в скорости обработки и извлечения данных и быстрой аналитике. При необходимости наше решение можно интегрировать с 1C:ТОиР и SAP.

Сценарий применения №2 (базовый): Обслуживание по наработке

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

В ЕАМ-системах часто предусмотрена возможность прямого подключения к базам данных и считывания наработки. Преимущество F5 PMM в том, что есть возможность собирать данные с нескольких разрозненных систем и подключаться не только к базам данных, но и к протоколам более низкого уровня и различным IoT-протоколам.

Сценарий применения №3 (стандартный): Назначение внеплановых работ

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

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

Диагностика с помощью экспертных правил реализуется на базе трех источников данных, объединенных в общий модуль экспертных правил: документация, методики и опыт специалистов. Это условия, описывающие сразу несколько показателей на протяжении определенного времени. Например, если при частоте вращения дизеля более 295 оборотов в минуту давление наддува превышает значение 0,95 атмосфер более чем в 2 раза, то это помпаж турбокомпрессора.

Сценарий применения №4 (стандартный): Обслуживание по состоянию

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

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

F5 PMM обеспечивает гибкость выбора решений для прогнозной аналитики на любом уровне зрелости. Детектор аномалий, основанный на типовых алгоритмах, способен без привлечения Data scientist`а сделать модель в пару кликов на основе исторических данных. Важно понимать, что эта модель подсвечивает слабые места в виде дефектов или аномалий, выдает уведомления о них и составляет отчеты. Для поиска аномалий мы используем метод isolation forest, линейную регрессию, автоэнкодер и другие.

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

Интеграция предиктивной аналитики в систему управления активами

Как мы видим, возможны различные сценарии совместного использования системы предиктивной аналитики F5 PMM и системы управления активами F5 EAM. Эти сценарии зависят от уровня зрелости предприятия в управлении своими производственными активами, и этот уровень зрелости необходимо учитывать. Однако у использования как комплексного решения, так и продуктов по отдельности есть преимущество – вы можете начать с простых сценариев и плавно переходить к сложным, совершенствуя процессы управления активами и накапливая экспертизу. При использовании комплексного решения экономится время создания каталога активов, происходит обогащение ЕАМ-системы данными о состоянии оборудования, сокращается время на диагностику и регистрацию инцидентов.


ссылка на оригинал статьи https://habr.com/ru/company/factory5/blog/674410/

Decorrelating Subqueries

По материалам статьи Craig Freedman: Decorrelating Subqueries

В статье про скалярные подзапросы было несколько примеров, в которых оптимизатор мог переписать запрос с коррелированным подзапросом как запрос с соединением. Например, можно было видеть, как представленный ниже простой подзапрос с «in»:

create table T1 (a int, b int) create table T2 (a int, b int, c int)  select * from T1 where T1.a in (select T2.a from T2 where T2.b = T1.b)

Мог быть переписан как левое полу-соединение:

  |--Nested Loops(Left Semi Join, WHERE:([T2].[b]=[T1].[b] AND [T1].[a]=[T2].[a]))        |--Table Scan(OBJECT:([T1]))        |--Table Scan(OBJECT:([T2]))

Обратите внимание, что между двумя просмотрами таблицы отсутствует какая-либо корреляция. Просмотр любой таблицы может происходить независимо и будет использоваться такой алгоритм соединения, какой наиболее удобен.  Эту стратегию оптимизации мы называем декорреляцией. Невозможно удалить корреляцию для всех подзапросов. Например, как мы видели в предыдущих статьях, простая замена подзапроса «in» скалярным подзапросом «=» (при отсутствии уникального индекса) вынуждает оптимизатор добавить в план оператор Assert, и это не позволит избавиться от корреляции подзапроса. Даже когда мы можем декоррелировать подзапрос, в результате необязательно получаем лучший план, но появляется возможность проанализировать больше вариантов плана и найти из них лучший.

Декорреляция подзапросов с агрегацией

Вспомним представленный ниже запрос из предыдущей статьи:

select * from T1 where T1.a > (select max(T2.a) from T2 where T2.b < T1.b)
  |--Filter(WHERE:([T1].[a]>[Expr1008]))        |--Nested Loops(Inner Join, OUTER REFERENCES:([T1].[b]))             |--Table Scan(OBJECT:([T1]))             |--Stream Aggregate(DEFINE:([Expr1008]=MAX([T2].[a])))                  |--Table Scan(OBJECT:([T2]), WHERE:([T2].[b]<[T1].[b]))

Мы не можем декоррелировать этот подзапрос. Теперь рассмотрим почти идентичный запрос, но заменим в предложении where подзапроса «<» на «=»:

select * from T1 where T1.a > (select max(T2.a) from T2 where T2.b = T1.b)

Это, казалось бы, незначительное изменение позволяет нам убрать корреляцию и написать этот запрос как обычное соединение:

select T1.* from T1, (select T2.b, max(T2.a) max_a from T2 group by T2.b) S where T1.b = S.b and T1.a > S.max_a

Оба запроса выбирают такой план:

  |--Nested Loops(Inner Join, OUTER REFERENCES:([T2].[b], [Expr1008]))        |--Stream Aggregate(GROUP BY:([T2].[b]) DEFINE:([Expr1008]=MAX([T2].[a])))        |    |--Sort(ORDER BY:([T2].[b] ASC))        |         |--Table Scan(OBJECT:([T2]))        |--Index Spool(SEEK:([T1].[b]=[T2].[b] AND [T1].[a] > [Expr1008]))             |--Table Scan(OBJECT:([T1]))

Исходный план выполняет подзапрос для каждой строки T1, он работает вполне эффективно за счет предварительного вычисления всех возможных результатов агрегации для подзапроса и объединения этого результата с T2. Чтобы вычислить все возможные результаты подзапроса, к подзапросу добавляется предложение «group by» и это учитывается в ключе соединения. Оператор буферизации индекса «Index Spool» создает индекс на лету, это позволяет повысить производительность соединения. Вместо соединения вложенных циклов «Nested Loops» с просмотром таблицы на внутренней стороне у нас есть соединение «Nested Loops» индекса с поиском по индексу на внутренней стороне.

Давайте рассмотрим, когда этот новый план будет лучше изначального. Если в T2.b хранится всего несколько уникальных значений и, следовательно, будет всего несколько групп для вычисления, а также если в T1 будет много строк, новый план может оказаться очень эффективным. С другой стороны, если в T2.b много уникальных значений, а в T1 всего несколько строк, может оказаться выгоднее вычислять только те агрегаты, которые нужны:

set nocount on declare @i int set @i = 0 while @i < 10000   begin     insert T2 values(@i, @i, @i)     set @i = @i + 1   end  select * from T1 where T1.a > (select max(T2.a) from T2 where T2.b = T1.b)
   |--Filter(WHERE:([T1].[a]>[Expr1008]))        |--Stream Aggregate(GROUP BY:([Bmk1000]) DEFINE:([Expr1008]=MAX([T2].[a]), [T1].[a]=ANY([T1].[a]), [T1].[b]=ANY([T1].[b])))             |--Nested Loops(Inner Join, WHERE:([T1].[b]=[T2].[b]))                  |--Table Scan(OBJECT:([T1]))                  |--Table Scan(OBJECT:([T2]))

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

Обратите внимание, что результаты агрегации после соединения мы группируем по [Bmk1000]. Это уникальный реляционный ключ для T1. Вы можете убедиться в том, что [Bmk1000] является ключом для T1, исследовав столбец для этих значений в результатах выполнения showplan_all. Группировка по ключу T1 необходима, так как одна строка T1 может соединяться с несколькими строками T2, но запрос предполагает вычисление MAX(T2.a) значений для каждой строки T1.

Нет необходимости в сортировке потока после агрегации, поскольку соединение «Nested Loops» выбирает все строки T2, которые подлежат соединению со строкой из T1, прежде чем перейти к следующей строке T1. Другими словами, результаты соединения уже «сгруппированы» по T1, даже если они и не отсортированы по T1.

Если у T1 есть кластерный или любой уникальный индекс, мы будем использовать этот ключ вместо столбца закладки [Bmk1000]. На самом деле, если мы создадим уникальный ключ для T1, мы можем переписать запрос иначе:

create unique clustered index T1a on T1(a)  select T1.a, min(T1.b) as T1b from T1 join T2 on T1.b = T2.b group by T1.a having T1.a > max(T2.a)

План в принципе тот же:

 |--Filter(WHERE:([T1].[a]>[Expr1007]))     |--Stream Aggregate(GROUP BY:([T1].[a]) DEFINE:([Expr1007]=MAX([T2].[a]), [Expr1008]=MIN([T1].[b])])))         |--Nested Loops(Inner Join, WHERE:([T2].[b]=[T1].[b]))            |--Clustered Index Scan(OBJECT:([T1].[T1a]))            |--Table Scan(OBJECT:([T2]))

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

set nocount on declare @i int set @i = 0 while @i < 10000   begin     insert T1 values(@i, @i)     set @i = @i + 1   end    select * from T1 where T1.a > (select max(T2.a) from T2 where T2.b = T1.b)
   |--Hash Match(Inner Join, HASH:([T1].[b])=([T2].[b]), RESIDUAL:([T1].[b]=[T2].[b] AND [T1].[a]>[Expr1007]))        |--Clustered Index Scan(OBJECT:([T1].[T1a]))        |--Hash Match(Aggregate, HASH:([T2].[b]), RESIDUAL:([T2].[b] = [T2].[b]) DEFINE:([Expr1007]=MAX([T2].[a])))             |--Table Scan(OBJECT:([T2]))

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

Как видите, декорреляция подзапроса может открыть возможность получения множества альтернативных планов.


ссылка на оригинал статьи https://habr.com/ru/post/674414/

Как сделать сервис реактивным в одну строку в Vue.js + Typescript

С выходом Composition API в Vue появилось новые возможности повторного использования кода. Больше нет необходимости в миксинах, компонентах высшего порядка и прочих “хаках”, если вам нужно вынести общую логику для нескольких компонентов. Но что если у вас есть нереактивный сервис, инкапсулирующий бизнес-логику, а переписывать все на composition api не хочется? 

К примеру возьмем простой класс с состоянием:

export class MyService {   foo: Object = {}    setFoo (foo: Object) {     this.foo = foo   } }

В нем все прекрасно, но что если мы хотим сделать его свойство реактивным? Согласно документации, мы можем сделать, например, вот так: 

import { Ref, ref } from 'vue'  export class MyService {   foo: Ref<Object> = ref({})    setFoo (foo: Object) {     this.foo.value = foo   } }

Вроде бы тоже неплохо, но если ваш сервис большой и иерархичный, переводить каждое свойство в ref будет трудно и больно. Тут на помощь приходят декораторы Typescript:

export class MyService {   @Reactive foo: Object = {}    setFoo (foo: Object) {     this.foo = foo   } }

Успех! Наше свойство реактивно, но теперь уже без манипуляций c ref-ами. Как это работает? Рассмотрим сам декоратор:

import { shallowRef } from 'vue'  function initRefs (target: any, key: string, value ?: any) {   target.__refs = target.__refs ?? {}    if (!target.__refs[key]) {     target.__refs[key] = shallowRef(value)   } }  export function Reactive (target: any, key: string): void {   Object.defineProperty(target, key, {     configurable: true,     enumerable: true,     get () {       initRefs(this, key)       return this.__refs[key].value     },     set (value) {       initRefs(this, value, key)       this.__refs[key].value = value     }   }) }

Благодаря Object.defineProperty, свойство попросту заменяется геттером и сеттером, обращающимся к ref-у, который хранится в том же классе. Его инициализация происходит при первом чтении либо записи.

Заключение

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

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

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

  3. Это не работает с SSR из коробки, хотя это преодолимая проблема, например, с помощью Nuxt и его ssrRef

Тем не менее, лично мне такой путь нравится, он красив и элегантен и не накладывает драконовских ограничений. А как вы считаете? Комментарии и предложения к подходу охотно принимаются.


ссылка на оригинал статьи https://habr.com/ru/post/674432/

Устраняем популярные изъяны в коде — чек-лист ошибок junior-разработчиков

Источник картинки

Работающий код может иметь изъяны — например, быть недостаточно простым, лаконичным и понятным. Это может быть признаком более глубоких проблем в коде, а избавиться от них можно с помощью рефакторинга. В этой статье разберем наиболее популярные недостатки кода. Материал адаптирован на русский язык вместе с Глебом Михеевым, директором по технологиям Skillbox и спикером профессии «Frontend-разработчик PRO» и преподавателем курса Frontend-разработки в Skillbox Борзуновым Игорем.

Непонятный выбор имени

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

Общие принципы выбора имени:

  • Стратегия выбора последовательна.

  • Из имени понятно назначение переменной.

  • Имена легко различимы между собой.

  • Имя переменной можно произнести вслух.

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

const a = 'Frontend course';  const b = 1;  const c = '100$'

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

const courseName = 'Frontend course';  const courseId = 1;  const coursePrice = '100$'

Длинный метод

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

async function makeAnOrder() {    const form = document.querySelector<HTMLFormElement>('form#basket');    const basket = new FormData(form);    if (!basket.get('name')) {        throw new Error('Вы не указали имя');    }    if (!basket.get('address')) {        throw new Error('Вы не указали адрес доставки');    }    const user = await fetch('/api/user').then(results => results.json())    basket.set('user_id', user.id);    basket.set('discount', user.discount);    fetch('/api/order', { method: 'POST', body: basket })        .then(results => results.json())        .then(() => {            alert('Заказ удачно отправлен')        })  }

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

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

async function makeAnOrder() {    const basket = getBasket();    vaildateBasket(basket);    await applyDiscount(basket);    createOrder(basket).then(() => {      alert('Заказ удачно отправлен')    })  }  function getBasket() {    const form = document.querySelector('form#basket');    return new FormData(form);  }  function vaildateBasket(basket) {    if (!basket.get('name')) {      throw new Error('Вы не указали имя');    }    if (!basket.get('address')) {      throw new Error('Вы не указали адрес доставки');    }  }  function createOrder(basket) {    fetch('/api/order', { method: 'POST', body: basket })      .then(results => results.json())  }  async function applyDiscount() {    const user = await fetch('/api/user').then(results => results.json())    basket.set('user_id', user.id);    basket.set('discount', user.discount);  }

Объект бога

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

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

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

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

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

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

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

let button = document.getElementById("button");  let anotherButton = document.getElementById("anotherButton");  button.onclick = function() {      modal.style.display = "block";      modal.style.height = "100%";      modal.style.width = "50%";      modal.style.position = "fixed";  }  anotherButton.onclick = function() {      modal.style.display = "block";      modal.style.height = "100%";      modal.style.width = "50%";      modal.style.position = "fixed";  }  Для того, чтобы избегать подобных казусов, следует пользоваться обобщенными конструкциями:  function showModal() {    modal.style.display = "block";    modal.style.display = "block";    modal.style.height = "100%";    modal.style.width = "50%";    modal.style.position = "fixed";  }  button.onclick = showModal  anotherButton.onclick = showModal

Слишком много параметров

Большое число аргументов функции затрудняет чтение и тестирование кода. Разработчик Роберт Мартин в книге «Чистый код. Создание анализ и рефакторинг» назвал ноль идеальным числом параметров:

Глеб Михеев: «В идеальном случае количество аргументов функции равно нулю. Далее следуют функции с одним аргументом и с двумя аргументами. Функций с тремя аргументами следует по возможности избегать. Необходимость функций с большим количеством аргументов должна быть подкреплена очень вескими доводами».

Надуманная сложность

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

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

Стрельба дробью

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

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

Мутации переменной

Это специфическая проблема JavaScript, которая указывает на изменения переменной. Но мутация отличается от изменения тем, что происходит непредсказуемо. Из-за изменения одной переменной в другом месте меняется и другая, связанная с ней.

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

Хорошая практика в JavaScript — использование так называемого иммутабельного подхода: запрета мутирования переменных. Он позволяет избавиться от этой проблемы. Пример реализации –– библиотека ImmutableJs.

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

let age = 25;  age = "Двадцать пять";

В случае с const, будет выведена ошибка, так как ключевое слово const неизменно в данном примере 

const age = 25;  age = 'Ягодка опять';  > Uncaught TypeError: Assignment to constant variable.


ссылка на оригинал статьи https://habr.com/ru/company/skillbox/blog/674434/

Взгляд тестировщика на SOLID

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

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

Соответственно, этот материал состоит из двух частей:

  • в первой возьмем простое приложение на Java и улучшим его с помощью SOLID принципов — от программы с парой классов, которые делают все подряд, дойдем до приложения, разбитого на несколько модулей с конкретными функциями (да, это еще одно объяснение SOLID, смело пропускайте, если уже и так все знаете);

  • во второй части посмотрим, где во фреймворках автоматизированного тестирования может использоваться SOLID.

SOLID: базовые факты

SOLID — это пять основных принципов объектно-ориентированного программирования и проектирования кода. 

Вот что стоит за каждой из букв в обозначении:

S — SRP, Single Responsibility Principle (Принцип единственной ответственности)

O — OCP, Open/ Closed Principle (Принцип открытости/ закрытости)

L — LSP, Liskov Substitution Principle (Принцип подстановки Барбары Лисков)

I — ISP, Interface Segregation Principle (Принцип разделения интерфейсов)

D — DIP, Dependency Inversion Principle (Принцип инверсии зависимостей) 

Принципы SOLID не были искусственно придуманы теоретиками, а скорее обобщают коллективные знания и опыт разработчиков. После нескольких десятилетий работы Роберт «Дядюшка Боб» Мартин объединил их в единую концепцию. А звучный акроним SOLID предложил Майкл Физерс. 

Основными бонусами от использования SOLID принципов должны стать:

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

  • стабильный код, в который можно максимально безболезненно встраивать новые фичи, запрошенные заказчиком;

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

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

SOLID принципы используются на уровне модулей и классов в программах, построенных в парадигме ООП. 

Существуют и другие подходы к проектированию кода. Это методологии GRASP, DRY, KISS, YAGNI и др. Каждый из подходов имеет свои особенности, но суть одна — сделать код более простым и удобным для активной разработки и поддержки.

Тестовое приложение

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

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

Так выглядит структура проекта в самом начале (ветка SRP-1):

В проекте есть пакет models, где лежит enum Figure. Тут перечислены все типы фигур, которые поддерживаются приложением на данный момент. 

Здесь же, в пакете models, лежат объекты самих фигур. К примеру, треугольник выглядит так:

@Data public class Triangle {         private Double baseLength;      private Double height; }

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

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

В UserInteraction – методы для общения с пользователем:

public class UserInteraction {     //...       public Figure readFigureFromInput() {        //ask user to enter figure in console and return figure    }     public String readAreaTypeFromInput(Figure figure) {             //ask user to enter area type in console and return area type      }     public void printAreaInConsole(Figure figure, String areaType, Double area) {             //print area in console       } }

Класс CalculateArea – точка входа для вычисления площади. Именно его метод calculateArea(Figure figure, String areaType) вызывается в исполняемом классе Main. В этом методе в зависимости от типа фигуры и типа площади вызываются методы для вычислений. Если нужно посчитать чистую площадь без коэффициентов, используем методы из этого же класса, а если нужна площадь под покраску или для плитки, используем инстанс класса CalculateDecorationArea и его методы для фигур. 

public class CalculateArea {         private CalculateDecorationArea calculateDecorationArea = new CalculateDecorationArea();         public Double calculateArea(Figure figure, String areaType) {          Double area = null;             if (areaType == "simple") {                  if (figure == Figure.CIRCLE) {                        area = calculateCircleArea();                  } else if (figure == Figure.SQUARE) {                        area = calculateSquareArea();                  } else if (figure == Figure.TRIANGLE) {                        area = calculateTriangleArea();                  }            } else if (areaType == "painting") {                  area = calculateDecorationArea.calculateDecorationArea(figure);            } else if (areaType == "tile") {                  area = calculateDecorationArea.calculateDecorationArea(figure);            }       return area;      }         public Double calculateSquareArea() {     //user input and calculations for square      }         public double calculateCircleArea() {           //user input and calculations for circle       }         public double calculateTriangleArea() {           //user input and calculations for triangle   } }

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

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

Что здесь не так? Ведь код компилируется, и программа выполняет свои функции. Посмотрим на приложение с точки зрения SOLID принципов.

Окунемся в SOLID на живом примере

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

S – SRP, Single Responsibility Principle 

Первый из принципов. Роберт Мартин в своей книге «Чистая архитектура» расшифровывает его так:

Модуль должен иметь одну и только одну причину для изменения

ПО меняется в ответ на запросы пользователей → Модуль должен отвечать за одну и только одну группу пользователей или заинтересованных лиц, то есть за одного актора → Модуль должен отвечать за одного и только одного актора.

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

Спустимся на уровень классов. Если класс делает вычисления, что-то куда-то отправляет, выводит, описывает логику логирования – это нарушение SRP и антипаттерн. Такой объект можно назвать «божественный объект» или God object.

Чтобы лучше понять этот принцип, обратимся к тестовому приложению. 

Пример из приложения 

Первый и явный пример – конфликт интересов маляров и плиточников. Класс CalculateDecorationArea содержит код, которым пользуются эти две группы акторов. К каким проблемам это может привести? Например, маляры поймут, что в программе заложен слишком большой коэффициент. Допустим, они захотят его поменять, чтобы при закупке краски не оставалось излишков. Но что если для кафельной плитки такие коэффициенты вполне подходят? Получается, с одной стороны у нас есть маляры, а с другой — укладчики плитки, и обе эти группы пользуются одним и тем же кодом для расчета площади. Если код поменяется с учетом новых требований от маляров, плитки будет закуплено слишком мало. Соответственно, в приложении появится дефект с точки зрения плиточников.

Лучше в этом случае разделить вычисления и создать два разных класса. В ветке SRP-1 как раз появляются два отдельных класса CalculatePaintingArea — для маляров с их коэффициентом и CalculateTileArea – для плиточников. 

Пример для маляров с обновленным коэффициентом:

public class CalculatePaintingArea {          private static final Double PAINTING_COEFFICIENT = 1.1;          public Double calculatePaintingArea(Figure figure) {                  Double paintingArea = null;                  if (figure == Figure.CIRCLE) {                   paintingArea = calculateCirclePaintingArea();     } else if (figure == Figure.SQUARE) {       paintingArea = calculateSquarePaintingArea();     } else if (figure == Figure.TRIANGLE) {       paintingArea = calculateTrianglePaintingArea();     }     return paintingArea;       }          public double calculateSquarePaintingArea() {     //user input and calculations for square with coefficient   }      public double calculateCirclePaintingArea() {     //user input and calculations for square with coefficient   }      public double calculateTrianglePaintingArea() {     Double triangleArea;          Scanner sn = new Scanner(System.in);     System.out.println("Enter the length of the triangle base: ");     Double length = sn.nextDouble();     System.out.println("Enter the length of the triangle height: ");     Double height = sn.nextDouble();          triangleArea = length * height / 2 * PAINTING_COEFFICIENT;     return triangleArea;   } }

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

Поменяем методы с вычислениями. Теперь они принимают на вход названия фигур, делают вычисления и возвращают площадь.

public double calculateTriangleArea(Triangle triangle) {      return triangle.getBaseLength() * triangle.getHeight() / 2 * PAINTING_COEFFICIENT; }

Взаимодействие с пользователем было вынесено в UserInteraction класс. Теперь там есть методы для создания фигур по измерениям, которые вводит пользователь. Например, для квадрата:

public Square createSquareWithUserInput() {     Square square = new Square();     System.out.println("Enter length of the square side: ");    Double length = myObj.nextDouble();     square.setSideLength(length);     return square; }

В ветке SRP-2 уже есть улучшения в соответствии с принципом SRP. Здесь я разделила код для маляров и плиточников и вынесла из метода для вычисления площади код для создания фигуры с измерениями, заданными пользователем. В итоге в программе появилось два небольших модуля: один для взаимодействия с пользователем, а второй – для вычислений. Соответственно, в структуре кода теперь два разных пакета: calculations и user.interactions

В чем преимущества:

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

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

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

O – OCP, Open/ Closed Principle 

Второй принцип из пятерки SOLID – принцип открытости/ закрытости. Для консистентности снова обратимся к формулировке Роберта Мартина из книги «Чистая архитектура»:

Программные сущности должны быть открыты для расширения и закрыты для изменения

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

OCP помогает выстроить иерархию компонентов в коде. Наиболее высокоуровневые компоненты и классы должны быть максимально защищены. Это проявляется в том, что они ничего не знают о классах более низкого уровня (не имеют зависимостей на них), а, значит, никак не реагируют на изменения в них. Таким образом, при соблюдении принципа открытости/ закрытости запрос на создание новой функциональности в приложении приведет к тому, что мы будем добавлять новые классы или модули и оставим неизменной логику существующей системы. А это в свою очередь сократит или сведет к нулю появление регрессионных багов.

Пример из приложения

Давайте попробуем найти нарушение OCP в текущем состоянии приложения для дизайнеров и отделочников. Для этого перейдем на ветку OCP-1. Что будет, если мы решим поменять формулу для расчета площади треугольника и, соответственно, измерения, которые нужны для ее вычисления? К примеру, пользователям нашего приложения сложно измерить высоту треугольника, но измерить длину сторон и угол между ними они могут. Поэтому поменяем нашу модель треугольника и изменим формулу в классе CalculateSimpleArea. К чему это приведет? Код будет содержать ошибки, так как другие классы для расчетов (например, CalculatePaintingArea) все еще будут указывать на несуществующие параметры треугольника: высоту и длину основания.

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

В ветке OCP-2 формулы остались только в одном классе CalculateArea (в нем уже есть новая формула для вычисления площади треугольника).

public double calculateTriangleArea(Triangle triangle) {     return triangle.getFirstSideLength() * triangle.getSecondSideLength() / 2 * Math.sin(Math.toRadians(triangle.getFirstAndSecondSidesAngle())); }

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

public class CalculatePaintingArea extends CalculateArea {     private static final Double PAINTING_COEFFICIENT = 1.1;     private static UserInteraction userInteraction = new UserInteraction();     public Double calculatePaintingArea(Figure figure) {         Double paintingArea = null;         if (figure == Figure.CIRCLE) {            Circle circle = userInteraction.createCircleWithUserInput();            paintingArea = calculateCircleArea(circle) * PAINTING_COEFFICIENT;        } else if (figure == Figure.SQUARE) {            Square square = userInteraction.createSquareWithUserInput();            paintingArea = calculateSquareArea(square) * PAINTING_COEFFICIENT;        } else if (figure == Figure.TRIANGLE) {            Triangle triangle = userInteraction.createTriangleWithUserInput();            paintingArea = calculateTriangleArea(triangle) * PAINTING_COEFFICIENT;        }        return paintingArea;    } }

С изменением формулы пришлось изменить и структуру объекта Triangle, а именно — его поля. Метод для расчета площади фигуры напрямую зависит от ее измерений. Логичнее будет держать этот метод в классе, который описывает фигуру. В ветке OCP-3 я внесла изменения в классы, описывающие фигуры. Так, например, теперь выглядит треугольник:

@Data public class Triangle extends Figure {     private Double firstSideLength;    private Double secondSideLength;    private Double firstAndSecondSidesAngle;     public Double getArea() {         return firstSideLength * secondSideLength / 2                * Math.sin(Math.toRadians(firstAndSecondSidesAngle));    } }

На этом этапе также создадим интерфейс CalculateArea, через который клиентский код (класс Main) будет вызывать вычисления площадей. Если теперь нам понадобится добавить вычисления с другой логикой (то есть с другими коэффициентами), мы создадим новый класс. Он будет имплементировать интерфейс, а клиентский код будет по-прежнему вызывать интерфейс. 

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

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

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

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

Результат:

  • проще вносить изменения, они не ломают все приложение;

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

L – LSP, Liskov Substitution Principle 

Принцип подстановки Барбары Лисков – еще один из принципов SOLID. С его пониманием у меня возникло больше всего проблем. На сегодняшний день существует множество вариантов и переосмыслений этого принципа. В своей работе Data Abstraction and Hierarchy 1988 года Барбара Лисков сформулировала определение подтипов:

Если для каждого объекта o1 типа S существует такой объект o2 типа Т, что для всех программ Р, определенных в терминах T, поведение Р не изменится при подстановке o2 вместо o1, то S является подтипом T

Более доступная формулировка звучит так:

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

Еще одна формулировка для лучшего понимания (Герберт и Александреску «Стандарты программирование на С++»): 

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

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

Пример из приложения

Для того, чтобы понять принцип LSP, посмотрим на его нарушение на классическом примере.

Предположим, мы захотим расширить функционал программы и считать площадь еще и для прямоугольника. Добавим Rectangle в пакет models. В школе нам говорили, что квадрат – это частный случай прямоугольника. Значит, класс Square в нашей модели может наследоваться от класса Rectangle

В ветке LSP-1 новый класс Rectangle наследуется от Figure

@Data public class Rectangle extends Figure {     private Double width;    private Double height;     public Double getArea() {         return width * height;    } }

А Square теперь наследуется от Rectangle и фактически в нем ничего не меняется по сравнению с Rectangle

@Data public class Square extends Rectangle {}

Во всех классах с вычислениями теперь тоже появился Rectangle

В соответствии с принципом подстановки Лисков программы должны иметь возможность использовать экземпляр класса-наследника вместо базового класса без каких-либо дополнительных изменений. Поэкспериментируем с методом UserInteraction.readFigureFromInput(), который вызывает создание фигуры в зависимости от ввода пользователя. Попробуем использовать Square вместо Rectangle

public Figure readFigureFromInput() {     Figure figure = null;     System.out.println("Enter figure type from the list. Enter the number and press Enter: " +            "\n 1 - Square \n 2 - Circle \n 3 - Triangle \n 4 - Rectangle");     Integer figureNumber = Integer.parseInt(myObj.nextLine());       if (figureNumber == 1) figure = new Square();    else if (figureNumber == 2) figure = new Circle();    else if (figureNumber == 3) figure = new Triangle();    else if (figureNumber == 4) figure = new Square();    return figure; }

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

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Double.doubleValue()" because "this.height" is null

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

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

@Data public class Square extends Rectangle {     @Override    public void setHeight(Double height) {        super.setHeight(height);        super.setWidth(height);    }     @Override    public void setWidth(Double width) {        super.setWidth(width);        super.setHeight(width);    } }

При запуске программы получим правильные вычисления для квадрата. Но использовать Square как подтип Rectangle по-прежнему не можем: вместо площади прямоугольника получаем площадь квадрата. То есть в программе есть части, которые не могут работать с подтипом прямоугольника как с самим прямоугольником. А значит, тут нарушен принцип подстановки Барбары Лисков и неправильно используется наследование.

В ветке LSP-2 и Square, и Rectangle наследуются от Figure и имеют свои кейсы использования.

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

I – ISP, Interface Segregation Principle 

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

Программные сущности не должны зависеть от методов, которые они не используют

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

Пример из приложения

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

В ветке ISP-1 уже добавлен новый пакет output, в котором хранится интерфейс ProcessResult. Интерфейс содержит 5 методов. Два – для подготовки, предварительной обработки данных, и три – для вывода в разные места.

public interface ProcessResult {     List<String[]> prepareDataAsList(Figure figure, String areaType, Double area);     Map<String, Object> prepareDataAsMap(Figure figure, String areaType, Double area);     void writePreparedListDataToFile(List<String[]> preparedListData);     void writePreparedMapDataToDb(Map<String, Object> preparedMapData);     void writePlainDataToConsole(Figure figure, String areaType, Double area); }

Тут же в пакете output содержится три класса, реализующих вывод результата в консоль, файл и в базу данных. В каждом классе есть всего 1-2 метода интерфейса, которые действительно нужны данному классу и реализуются в нем, остальные – пустые. Пример для вывода в файл:

public class SaveResultToCsvFile implements ProcessResult {     private List<String[]> dataLines = new ArrayList<>();      @Override    public List<String[]> prepareDataAsList(Figure figure, String areaType, Double area) {        dataLines.add(new String[] {"Figure", "Type of area", "Area"});        dataLines.add(new String[]{figure.getClass().getSimpleName(), areaType, area.toString()});         return dataLines;    }     @Override    public Map<String, Object> prepareDataAsMap(Figure figure, String areaType, Double area) {        // Not needed here        return null;    }     @Override    public void writePreparedListDataToFile(List<String[]> preparedListData) {         File csvOutputFile = new File("C:\\Users\\o_kus\\Documents\\solid-example\\new_file-"                + Arrays.stream(preparedListData.get(1)).findFirst().get() + new Random().nextInt() + ".csv");         try (PrintWriter pw = new PrintWriter(csvOutputFile)) {            dataLines.stream()                    .map(this::convertLineToCSV)                    .forEach(pw::println);        } catch (FileNotFoundException e) {            e.printStackTrace();        }    }     @Override    public void writePreparedMapDataToDb(Map<String, Object> preparedMapData) {        // Not needed here    }     @Override    public void writePlainDataToConsole(Figure figure, String areaType, Double area) {        // Not needed here    }     String convertLineToCSV(String[] dataLines) {        return Stream.of(dataLines)                .collect(Collectors.joining(","));    } }

В Main используем два класса из output: SendResultToConsole для вывода в консоль и SaveResultToCsvFile для записи в файл. Реализацию для БД в рамках этого примера я прописывать не стала.

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

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

Посмотрим, как это исправить в ветке ISP-2. Тут вместо одного «толстого» интерфейса есть три разных, каждый из которых содержит методы, нужные для этого типа вывода результата. Посмотрим пример для вывода в файл:

public interface FileOutput {     List<String[]> prepareDataAsList(Figure figure, String areaType, Double area);     void writePreparedListDataToFile(List<String[]> preparedListData); }

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

public class StringConsoleAndCsvFileOutput implements ConsoleOutput, FileOutput {     private List<String[]> dataLines = new ArrayList<>();     @Override    public void writePlainDataToConsole(Figure figure, String areaType, Double area) {              //some code to write to console      }     @Override    public List<String[]> prepareDataAsList(Figure figure, String areaType, Double area) {        //some code to convert input data to list    }     @Override    public void writePreparedListDataToFile(List<String[]> preparedListData) {        //some code to write to file     } … }

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

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

D – DIP, Dependency Inversion Principle 

Разберем заключительный принцип из пятерки SOLID. Мы уже затрагивали организацию зависимостей в разделе про принцип открытости/ закрытости, здесь посмотрим на зависимости пристальнее. DIP декларирует: 

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

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

DIP позволяет бороться с признаками плохого дизайна кода. Роберт Мартин определяет эти признаки так:

  • rigidity — дизайн можно назвать жестким, когда его тяжело изменить, любое изменение в одной части системы затрагивает большое количество других частей;

  • fragility — хрупкий дизайн тот, при котором изменения в одной части системы приводят к неожиданным нарушениям в других частях;

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

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

Пример из приложения

Давайте посмотрим на ветку DIP-1 (фактически это то же состояние кода, что и в предыдущей ветке, где мы применяли ISP). Пришло время улучшить вычисления и продемонстрировать силу принципа инверсии зависимостей. Каждый класс с вычислениями сейчас проверяет инстанс фигуры, полученной на вход, а затем вызывает метод getArea() у конкретного объекта. Все это выглядит громоздко и получается, что в вычислениях все равно завязываемся на детали. Вот пример для вычисления площади под покраску:

public class CalculatePaintingArea implements CalculateArea {     private static final Double PAINTING_COEFFICIENT = 1.1;     private static UserInteraction userInteraction = new UserInteraction();      @Override    public Double calculateArea(Figure figure) {         Double area = null;         if (figure instanceof Square) {             Square square = userInteraction.createSquareWithUserInput();            area = square.getArea() * PAINTING_COEFFICIENT;         } else if (figure instanceof Circle) {         //some code for other figures        return area;    } }

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

Попробуем это исправить в ветке DIP-2. Что тут изменилось? Теперь Figure — это абстрактный класс, у которого есть метод getArea(). Метод абстрактного класса мы можем реализовать, а можем оставить реализацию классам, которые будут его имплементировать. Выберем второй вариант. 

public abstract class Figure {     public abstract Double getArea(); }

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

public class CalculatePaintingArea implements CalculateArea {     private static final Double PAINTING_COEFFICIENT = 1.1;     @Override    public Double calculateArea(Figure figure) {         return figure.getArea() * PAINTING_COEFFICIENT;    } }

В этой ветке DIP можно увидеть еще в одном месте. Я разбила пакет input на варианты с введением данных через консоль и созданием фигур из файлов. И создала интерфейс CreateInput, который предоставляет два метода — для получения фигуры и типа вычислений. Клиентскому коду достаточно использовать нужную имплементацию интерфейса, сам код при этом не меняется.

Результат: код в вычислениях стал проще и появилась возможность получения входных параметров разными способами за счет зависимости от абстракций вместо завязки на детали.

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

SOLID в автоматизированном тестировании

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

SRP

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

Наше приложение – дашборд, на котором можно посмотреть метрики и атрибуты сущностей в разных измерениях — от самых общих отчетов до детальных (назовем их Dimension1, Dimension2 для примера). Интеграционные тесты для API заключались в том, что мы сравнивали ответ API с данными из баз данных для каждого измерения. Базы данных было две: в MySQL хранились атрибуты сущностей, а в ClickHouse – метрики. При этом структура классов, отвечающих за получение данных из баз, выглядела следующим образом:

В коде JdbcRepository описано подключение к абстрактной БД и создание namedParameterJdbcTemplate для работы с данными из баз.

public abstract class JdbcRepository {    protected final NamedParameterJdbcTemplate namedParameterJdbcTemplate;     protected JdbcRepository(String driverClassName, String url, String username, String password) {        var dataSource = new DriverManagerDataSource();        dataSource.setDriverClassName(driverClassName);        dataSource.setUrl(url);        dataSource.setUsername(username);        dataSource.setPassword(password);         this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);    } }

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

public class Dimension1MySqlRepository extends JdbcRepository {     public Dimension1MySqlRepository(String url, String username, String password) {        super("com.mysql.cj.jdbc.Driver", url, username, password);    }     public Integer findDimension1CountByEntityId(Integer entityId) {        var query = readFile("path_to_sql_file");        var paramMap = Map.of("entityId", entityId);         return namedParameterJdbcTemplate.queryForObject(query, paramMap, (rs, rowNum) -> rs.getInt("COUNT"));    }     ... }

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

OCP 

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

Пример организации сущностей во фреймворке для тестирования интернет-магазина:

public abstract class Customer {...}  public class OrdinaryCustomer extends Customer {...}  public class SilverCustomer extends Customer {...}  public class GoldCustomer extends Customer {...}

LSP 

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

Если вы используете Java и JUnit5 в своем тестовом фреймворке, то привести систему в нужное состояние перед началом теста можно при помощи методов, помеченных аннотациями @BeforeEach и @BeforeAll, а добиться первоначального состояния можно при помощи @AfterEach и @AfterAll. Если одни и те же пред- и постусловия нужны практически для всех тестов или для группы тестов, их можно вынести в класс BaseTest. В тестовом классе нужно просто наследоваться от базового. 

Пример BaseTest…

public class BaseTest {     @BeforeAll    void openDatabaseConnection() {        //some code    }       @AfterAll    void closeDatabaseConnection() {        //some other code    } }

и тестового класса:

public class ApiTest extends BaseTest {     @Test    public void getResponse_shouldReturnDataFromDb_whenSendValidInput() {        //some test code    } }

Перед выполнением всех тестов тестового класса отработает метод openDatabaseConnection() из базового класса, а после — closeDatabaseConnection(). Нарушением LSP тут будет наследование от базового класса в тесте, где нужны другие пред- или постусловия.

ISP 

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

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

public interface Discount {       int getDiscountAmount(); }  public interface FreeDelivery {     void getFreeDelivery(); }  public class OrdinaryCustomer extends Customer implements Discount {    @Override    public int getDiscountAmount() {        return 5;    }  }  public class GoldCustomer extends Customer implements FreeDelivery, Discount {    @Override    public int getDiscountAmount() {        return 25;    }     @Override    public void getFreeDelivery() {        //some code    }      //some other code }

DIP 

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

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

public class HomePage {     private WebDriver driver;      public HomePage(WebDriver driver) {         this.driver = driver;     }      public void navigateToUserProfile() {         driver.findElement(By.id("profile")).click();     }     //some other code }

Выводы

За время изучения принципов SOLID я усвоила несколько важных вещей: 

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

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

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

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

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


ссылка на оригинал статьи https://habr.com/ru/company/lineate/blog/674144/