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

Блог компании ozon tech

Конец вечного противостоянияsnake_keysVScamelKeys наводим порядок встилях написания переменных

31.05.2021 20:20:52 | Автор: admin

Привет,Хабр!Меня зовут Владимир, работаю в Ozon, занимаюсьфронтендом.

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

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

Бекенд отдает и принимает данные в виде:

{ user_name: "user1", main_title: "Title", } 

Фронтенд:

{ userName: "user1", mainTitle: "Title", } 

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

В этой статье мы попробуемрешитьэтупроблемупреобразовать все данныебэкендав данныефронтендаи наоборот.Воспользуемся для этогоJavaScript.

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

Шаг 1. Преобразование строки

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

# Преобразованиеsnake_keysстроки вcamelKeys:

const snakeToCamel = str => {     return str.replace(/([_][a-z])/g, letter => {         return letter                 .toUpperCase()                 .replace('_', '')     }) } 

# ПреобразованиеcamelKeysстроки вsnake_keys:

const camelToSnake = str => {     return str.replace(/[A-Z]/g, letter => {         return '_' + letter.toLowerCase()     }) } 

Шаг 2. Работа с объектами

# Возьмем пример с начала статьи { user_name: "user1", main_title: "Title", } 

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

constsimpleKeysTransform=value=>{returnObject.entries(value).reduce((acc, [key,value]) => {constnewKey=snakeToCamel(key)        return{...acc, [newKey]:value}}, {})}

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

const keysTransform1 = (value, isInitialSnake = true) => {     const chooseStyle = isInitialSnake ? snakeToCamel : camelToSnake     return Object.entries(value).reduce((acc, [key, value]) => {         const newKey = chooseStyle(key)         return {...acc, [newKey]: value}     }, {}) } 

Шаг 3. Что делать с вложенными объектами

# Например {   user_info: {     first_name: "User",     last_name: "Userin   } } 

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

const keysTransform2 = (input, isInitialSnake = true) => {     const chooseStyle = isInitialSnake ? snakeToCamel : camelToSnake     const recursiveTransform = value => {         if (value && typeof value === 'object') {             return Object.entries(value).reduce((acc, [key, value]) => {                 const newKey = chooseStyle(key)                 const newValue = recursiveTransform(value)                 return {...acc, [newKey]: newValue}             }, {})         }         return value     }     return recursiveTransform(input) } 

Шаг 4. Что делать с массивами

# Например {   users: [     {       first_name: "user1",       phone_number: 8996923     },     {       first_name: "user2",       phone_number: 12312312     }   ]   } 

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

const keysTransform = (input, isInitialSnake = true) => {     const chooseStyle = isInitialSnake ? snakeToCamel : camelToSnake     const recursiveTransform = value => {         if (Array.isArray(value)) {             return value.map(recursiveTransform)         }         if (value && typeof value === 'object') {             return Object.entries(value).reduce((acc, [key, value]) => {                 const newKey = chooseStyle(key)                 const newValue = recursiveTransform(value)                 return {...acc, [newKey]: newValue}             }, {})         }         return value     }     return recursiveTransform(input) } 

Перемирие

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

Существуютидругиестили написания составных слов(PascalKeys,kebab-keys, UPPER_SNAKE_KEYS).При надобности, вы уже сами сможете с ними справиться.

Подробнее..

Майним еще больше данных настраиваем сбор рекламной статистики TikTok за день

11.06.2021 08:06:47 | Автор: admin

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

Медийная реклама Ozon представлена на разных площадках: Facebook, Google, MyTarget, TikTok и другие. Для эффективной работы любой рекламной кампании необходима оперативная аналитика. В данной статье речь пойдет о моём опыте сбора рекламных данных с площадки TikTok без посредников и лишних заморочек.

Задача на сбор статистики: вводные

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

У нас в базах уже были данные о заказах по кампаниям из TikTok, для эффективной аналитики не хватало данных о расходах.

Итак, весь процесс от "нам нужны данные по расходам из TikTok" до "у нас есть данные по расходам из TikTok" разделился для нас на следующие этапы:

  1. регистрация аккаунта разработчика,

  2. создание приложения,

  3. авторизация бизнес-аккаунта в приложении,

  4. запрос, получение, обработка и загрузка данных.

Рассмотрим каждый из этапов подробнее.

Регистрация разработчика

Мы зарегестрировали аккаунт разработчика на нашего бизнес-менеджера. Перешли на портал TikTok Marketing API, нажали на "My Apps", далее кликнули на "Become a Developer", и началась череда заполнения форм.

TikTok не Facebook, у нас ничего ни разу не отклонял, но всё равно мы были очень внимательны при заполнении полей и не добавляли то, что нам не нужно прямо сейчас. Например, в поле "What services do you provide?" добавили только "Reporting".

Последним пунктом был "Create App". Процесс создания аккаунта разработчика и приложения в первый раз происходит вместе.

Создание приложения

Заполняем имя и описание приложение, callback-address. Далее нужно выбрать разрешения, которые приложение будет запрашивать у авторизирующегося в нем аккаунта. Так же, как и при заполнении полей для аккаунта разработчика, выбрали только пункт "Reporting". Указали ID рекламного аккаунта. После этого отправили приложение на проверку.

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

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

Авторизация бизнес-аккаунта в приложении

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

Итак, по порядку, что мы делали не имея сайта, который бы отлавливал callback с авторизационным кодом.

  1. Зашли в приложение и указали Callback Address https://www.ozon.ru.

  2. Скопировали Authorized URL, перешли по нему, авторизовались под аккаунтом бизнес-менеджера.

  3. Согласились на предоставление разрешений для приложения, нажали "Confirm".

  4. Далее нас перекинуло на сайт Ozon, но с дополнительными аргументами в url. Получилось наподобие такого https://www.ozon.ru/?auth_code=XXXXXXXXXXX.

  5. Скопировали значение auth_code, в приложении скопировали secret и app_id и отправили запрос к TikTok на получение long-term Access Token.

curl -H "Content-Type:application/json" -X POST \-d '{    "secret": "SECRET",     "app_id": "APP_ID",     "auth_code": "AUTH_CODE"}' \https://ads.tiktok.com/open_api/v1.2/oauth2/access_token

Получили ответ такого вида:

{    "message": "OK",     "code": 0,     "data": {        "access_token": "XXXXXXXXXXXXXXXXXXXX",         "scope": [4],         "advertiser_ids": [            1111111111111111111,             2222222222222222222]    },     "request_id": "XXXXXXXXXXXXXXX"}

Важно было успеть отправить запрос на получение long-term Access Token как можно быстрее, после редиректа на сайт Ozon. Связано это с временем жизни auth_code 10 минут.

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

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

Всё, мы готовы писать запросы!

Получение статистики

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

Итак, у нас есть всё необходимое для получения данных, а именно:

  • access_token,

  • список advertiser_ids.

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

media source -> campaign -> adset -> ad_name

Значение media source всегда неизменно, так как источник один TikTok. По остальным параметрам можно запросить данные из API TikTok.

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

В новом методе получения данных добавили фильтр по типу размещения рекламы: AUCTION и RESERVATION. Ozon использует только AUCTION в своей стратегии ведения кампаний.

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

METRICS = [    "campaign_name", # название кампании    "adgroup_name", # название группы объявлений    "ad_name", # название объявления    "spend", # потраченные деньги (валюта задаётся в рекламном кабинете)    "impressions", # просмотры    "clicks", # клики    "reach", # количество уникальных пользователей, смотревших рекламу    "video_views_p25", # количество просмотров 25% видео    "video_views_p50", # количество просмотров 50% видео    "video_views_p75", # количество просмотров 75% видео    "video_views_p100", # количество просмотров 100% видео    "frequency" # среднее количество просмотра рекламы каждым пользователем]

В документации TikTok для каждого метода API описан пример на языках Java, Python, PHP и также curl-запрос. Я использовала пример на Python с небольшими изменениями.

В примерах из документации TikTok используются две дополнительные библиотеки:

pip install requestspip install six

Библиотека requests необходима для удобной отправки get-запросов. Библиотека six используется для генерации url-адреса запроса.

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

pip install pandaspip install sqlalchemy

В нашей компании для хранения данных используются SQL-подобные хранилища, поэтому я использую pandas для преобразования данных в DataFrame и sqlalchemy для записи DataFrame в базу.

Я использовала функции из примера в документации TikTok для генерации url и отправки запроса.

# генерирует url на основе словаря args с аргументами запросаdef build_url(args: dict) -> str:    query_string = urlencode({k: v if isinstance(v, string_types) else json.dumps(v) for k, v in args.items()})    scheme = "https"    netloc = "ads.tiktok.com"    path = "/open_api/v1.1/reports/integrated/get/"    return urlunparse((scheme, netloc, path, "", query_string, ""))# отправляет запрос к TikTok Marketing API,# возвращает результат в виде преобразованного json в словарьdef get(args: dict, access_token: str) -> dict:    url = build_url(args)    headers = {        "Access-Token": access_token,    }    rsp = requests.get(url, headers=headers)    return rsp.json()

На вход функции get нужно передать список аргументов и access token. Список аргументов под наши цели выглядит следующим образом:

args = {    "metrics": METRICS, # список метрик, описанный выше    "data_level": "AUCTION_AD", # тип рекламы    "start_date": 'YYYY-MM-DD', # начальный день запроса    "end_date": 'YYYY-MM-DD', # конечный день запроса    "page_size": 1000, # размер страницы - количество объектов, которое возвращается за один запрос     "page": 1, # порядковый номер страницы (если данные не поместились в один запрос, аргумент инкрементируется)    "advertiser_id": advertiser_id, # один из ID из advertiser_ids, который мы получили при генерации access token    "report_type": "BASIC", # тип отчета    "dimensions": ["ad_id", "stat_time_day"] # аргументы группировки, вплоть до объявления и за целый день} 

Подробнее про page_size: ответ на запрос может содержать большое количество информации и загружать всё это за один раз не эффективно. Поэтому у TikTok есть ограничение на максимальное количество объектов в ответе 1000. Чтобы получить следующую порцию данных, нужно отправить запрос с теми же входными аргументами на следующую страницу. Подробнее о постраничных запросах ниже.

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

{       # маркер успешности ответа    "message": "OK",    "code": 0,    "data": {        # информация о странице данных        "page_info": {            # общее количество объектов            "total_number": 3000,            # текущая страница            "page": 1,            # количество объектов на одной странице ответа            "page_size": 1000,            # общее количество страниц            "total_page": 3        },        # массив объектов        "list": [            # первый объект            {                # метрики                "metrics": {                    "video_views_p25": "0",                    "video_views_p100": "0",                    "adgroup_name": "adgroup_name",                    "reach": "0",                    "spend": "0.0",                    "frequency": "0.0",                    "video_views_p75": "0",                    "video_views_p50": "0",                    "ad_name": "ad_name",                    "campaign_name": "campaign_name",                    "impressions": "0",                    "clicks": "0"                },                # измерения (по каким параметрам группируем результаты)                "dimensions": {                    "stat_time_day": "YYYY-MM-DD HH: mm: ss",                    "ad_id": 111111111111111                }            },...        ]    },    # id ответа    "request_id": "11111111111111111111111"}

Как я описывала выше, если в ответе получается более 1000 объектов, ответ будет разбит на несколько страниц. В данном случае поле total_page говорит о том, что для получения полного набора данных по указанным параметрам, нужны будут три страницы. Следовательно, запускаем и коллекционируем ответы пока не выгрузим все страницы.

page = 1 # сначала всегда получаем данные по первой страницеresult_dict = {} # словарь, в который будем записывать ответыresult = get(args, access_token) # первый запросresult_dict[advertiser_id] = result['data']['list'] # сохраняем ответ на запрос к первой странице# пока текущая полученная страница page меньше # чем общее количество страниц в последнем ответе resultwhile page < result['data']['page_info']['total_page']:    # увеличиваем значение страницы на 1    page += 1    # обновляем значение текущей страницы в словаре аргументов запроса    args['page'] = page    # запрашиваем ответ по текущей странице page    result = get(args, access_token)    # накапливаем ответ    result_dict[advertiser_id] += result['data']['list']

Такое необходимо повторить для каждого рекламного аккаунта из списка advertiser_ids.

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

# результирующий DataFrame, который будем записывать в базуdata_df = pd.DataFrame()# для каждого рекламного аккаунта выполнить преобразованиеfor adv_id in advertiser_ids:    # получаем накопленные разультаты для аккаунта из словаря    adv_input_list = result_dict[adv_id]    # временный список    adv_result_list = []    # для каждого объекта    for adv_input_row in adv_input_list:        # берём словарь метрик        metrics = adv_input_row['metrics']        # насыщаем этот словарь словарём измерений        metrics.update(adv_input_row['dimensions'])        # добавляем полученный объект во временный список        adv_result_list.append(metrics)    # преобразуем временный словарь в DataFrame     result_df = pd.DataFrame(adv_result_list)    # добавляем колонку со значением id аккаунта    result_df['account'] = adv_id    # добавляем получившийся DataFrame в результирующий    data_df = data_df.append(        result_df,         ignore_index=True    )## здесь пропущены некоторые манипуляции # по преобразованию строк в числа## запись данных из результирующего DataFrame в базуdata_df.to_sql(    schema=schema,     name=table,     con=connection,    if_exists = 'append',    index = False)

TikTok утверждает, что исторические данные по статистике не меняеются, а если и меняются, то это должна быть экстроординарная ситуации, наподобие аварии в ЦОД. Но на основе опыта получения данных от Facebook, я решила что всё равно буду перезаписывать семь последних дней (цифра семь появилась эмпирически).

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

Полный текст скрипта.
# импорт библиотекimport jsonfrom datetime import datetimefrom datetime import timedeltaimport requestsfrom six import string_typesfrom six.moves.urllib.parse import urlencodefrom six.moves.urllib.parse import urlunparseimport pandas as pdimport sqlalchemy# генерирует url на основе словаря args с аргументами запросаdef build_url(args: dict) -> str:    query_string = urlencode({k: v if isinstance(v, string_types) else json.dumps(v) for k, v in args.items()})    scheme = "https"    netloc = "ads.tiktok.com"    path = "/open_api/v1.1/reports/integrated/get/"    return urlunparse((scheme, netloc, path, "", query_string, ""))# отправляет запрос к TikTok Marketing API,# возвращает результат в виде преобразованного json в словарьdef get(args: dict, access_token: str) -> dict:    url = build_url(args)    headers = {        "Access-Token": access_token,    }    rsp = requests.get(url, headers=headers)    return rsp.json()# обновляет данные в базе за последние семь дней# (или, если указаны start_date и end_date, для периода [start_date, end_date])def update_tiktik_data(    # словарь с доступами к API TikTok    tiktok_conn: dict,    # словарь с доступами к базе данных    db_conn: dict,    # список id рекламных кабинетов    advertiser_ids: list,    # необязательное поле: начало периода    start_date:datetime=None,    # необязательное поле: окончание периода    end_date:datetime=None):    access_token = tiktok_conn['password']    start_date = datetime.now() - timedelta(7) if start_date is None else start_date    end_date = datetime.now() - timedelta(1) if end_date is None else end_date    START_DATE = datetime.strftime(start_date, '%Y-%m-%d')    END_DATE = datetime.strftime(end_date, '%Y-%m-%d')    SCHEMA = "schema"    TABLE = "table"    PAGE_SIZE = 1000    METRICS = [        "campaign_name", # название кампании        "adgroup_name", # название группы объявлений        "ad_name", # название объявления        "spend", # потраченные деньги (валюта задаётся в рекламном кабинете)        "impressions", # просмотры        "clicks", # клики        "reach", # количество уникальных пользователей, смотревших рекламу        "video_views_p25", # количество просмотров 25% видео        "video_views_p50", # количество просмотров 50% видео        "video_views_p75", # количество просмотров 75% видео        "video_views_p100", # количество просмотров 100% видео        "frequency" # среднее количество просмотра рекламы каждым пользователем    ]    result_dict = {} # словарь, в который будем записывать ответы    for advertiser_id in advertiser_ids:        page = 1 # сначала всегда получаем данные по первой странице        args = {            "metrics": METRICS, # список метрик, описанный выше            "data_level": "AUCTION_AD", # тип рекламы            "start_date": START_DATE, # начальный день запроса            "end_date": END_DATE, # конечный день запроса            "page_size": PAGE_SIZE, # размер страницы - количество объектов, которое возвращается за один запрос             "page": 1, # порядковый номер страницы (если данные не поместились в один запрос, аргумент инкрементируется)            "advertiser_id": advertiser_id, # один из ID из advertiser_ids, который мы получили при генерации access token            "report_type": "BASIC", # тип отчета            "dimensions": ["ad_id", "stat_time_day"] # аргументы группировки, вплоть до объявления и за целый день        }        result = get(args, access_token) # первый запрос        result_dict[advertiser_id] = result['data']['list'] # сохраняем ответ на запрос к первой странице        # пока текущая полученная страница page меньше,         # чем общее количество страниц в последнем ответе result        while page < result['data']['page_info']['total_page']:            # увеличиваем значение страницы на 1            page += 1            # обновляем значение текущей страницы в словаре аргументов запроса            args['page'] = page            # запрашиваем ответ по текущей странице page            result = get(args, access_token)            # накапливаем ответ            result_dict[advertiser_id] += result['data']['list']    # результирующий DataFrame, который будем записывать в базу    data_df = pd.DataFrame()    # для каждого рекламного аккаунта выполнить преобразование    for adv_id in advertiser_ids:        # получаем накопленные разультаты для аккаунта из словаря        adv_input_list = result_dict[adv_id]        # временный список        adv_result_list = []        # для каждого объекта        for adv_input_row in adv_input_list:            # берем словарь метрик            metrics = adv_input_row['metrics']            # насыщаем этот словарь словарём измерений            metrics.update(adv_input_row['dimensions'])            # добавляем полученный объект во временный список            adv_result_list.append(metrics)        # преобразуем временный словарь в DataFrame         result_df = pd.DataFrame(adv_result_list)        # добавляем колонку со значением id аккаунта        result_df['account'] = adv_id        # добавляем получившийся DataFrame в результирующий        data_df = data_df.append(            result_df,             ignore_index=True        )    #    # здесь пропущены некоторые манипуляции     # по преобразованию строк в числа    #        # создание подключения к базе    connection = sqlalchemy.create_engine(        '{db_type}://{user}:{pswd}@{host}:{port}/{path}'.format(            db_type=db_conn['db_type'],             user=db_conn['user'],             pswd=db_conn['password'],            host=db_conn['host'],            port=db_conn['port'],            path=db_conn['path']         )    )    # удаление последних семи дней из базы    with connection.connect() as conn:        conn.execute(f"""delete from {SCHEMA}.{TABLE}         where date >= '{START_DATE}' and date <= '{END_DATE}'""")    # запись данных из результирующего DataFrame в базу    data_df.to_sql(        schema=SCHEMA,         name=TABLE,         con=connection,        if_exists = 'append',        index = False    )

Миссия выполнена!

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

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

К слову о лабиринтах, в Facebook тот же самый один рабочий день уходит на то, чтобы создать аккаунт разработчика, протыкать все галочки о политике конфидециальности и условий использования, создать приложение, настроить его и т.д. И в итоге к концу дня у тебя не работающий ETL по сбору данных, а очередной Permission Denied и распухшая голова, в которой крутится только одна мысль "что я делаю не так".

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

Полезные ссылки

Подробнее..

Figma плагины для продуктового дизайна. Локальный топчик с видео-инструкцией

25.05.2021 16:13:10 | Автор: admin

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

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

В конце статьи будет ссылка на видео-инструкцию.

Иерархия библиотек вOzon

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

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

Сейчас унас вOzon есть несколько типов библиотек:

  • Атомарные токены дизайн-системы.

  • Молекулярные библиотеки сэлементами интерфейса. Изних собираются экраны сценариев.

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

Библиотечная иерархияБиблиотечная иерархия

Рабочие лошадки иудобные пони

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

Master

Community

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

Тут унас типичная ситуация. Форма собрана измолекулярных библиотечных компонентов. Спроектированы разные её состояния. Становится понятно, что она достойна быть добавленной всценарную библиотеку. Если делать это штатными средствами, придётся перенастраивать кучу экранов. Ноунас есть Master.

Копируем одну изформ ивставляем её всценарную библиотеку. Делаем компонент ипубликуем. Запускаем команду Pick Target Component изарсенала плагина Master. Возвращаемся вфайл сценария ивыделяем все наши формы. Запускаем команду Link Objects toTarget Component.

Дапребудет стобой сила, Master!

Design System Organizer

Community

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

Тут унас локальные цветовые итипографские стили. Надо привязать ихктакимже библиотечным. Идём вбиблиотеку изапускаемDSO. Заходим вцветовые стили, выделяем нужную группу ивыбираем извыпадающего меню Set AsTarget. Возвращаемся внаш файл, запускаем DSO ивыбираем Relink Styles. Бум! Стили теперь тянуться изкомандной библиотеки. Такимже образом перелинковываем другие стили или компоненты.

Style Organizer

Community

Этот штепсель помогает неударять вгрязь лицом перед разработчиками. Проверяем, откуда тянутся стили. Ловим нестильные шейпы итексты. Чиним вручном или автоматическом режиме. Соответствуем высоким стандартам разработки Ozon иублажаем личного беса-перфекциониста.

Вот тут унас типичная ситуация. Наглазок всё впорядке, нотакли это? Стартуем Style Organizer. Ивидим, что один изчёрных квадратов имеет локальный стиль, второй библиотечный, атретий вообще без стиля. Можно пробежаться посписку ошибок вручном режиме, сшивая стили, аможно нажать Auto Fix Color иStyle Organizer будет действоватьсам.

Similayer

Community

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

Давайте выделим слово Zen синего цвета, вне зависимости отстиля текста. Выделяем один синий Zen изапускаем Similayer. Выбираем параметры Fill Style иText Characters.

Удобненько!

Instance Finder

Community

Ищет ивыделяет только инстансы. Зато делает это шустрее Similayer иработает повсему документу, анепоодной странице.

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

Select Layers

Community

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

Вот тут унас мега-вариант инпута. Выделяем поля пароля изапускаем плагин. Пишем название нужного слоя ивуаля! Можем поменять иконку, например.

Sorter

Community

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

Если попытаться пронумеровать экраны, нерасставив ихвпанели слоёв, получим хаос. Нуок. Выделяем все экраны, запускаем команду Sort Position ипосле этого уже штатный Rename Selection.

Порядочек.

Quantizer

Community

Расставляет элементы всетку сзаданным количеством колонок иотступами. Сильно помогает вработе над вариантами.

Выделяем, что нужно, истартуем Quantizer. Сделаем колонки сотступами по40px.

Кстати, вызнали, что можно таскать объекты всетке закруглые маркеры вцентре? Аменять отступ, хватаясь замаркеры между? Если делать это сShift, шаг будет кратен вашему Nudge Amount.

Swap

Community

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

Выделяем два объекта, запускаем команду Swap и, собственно, всё.

Layer Counter

Community

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

Если снять галку Include Nested Layers, увидим только количество выделенных фреймов.

Retextifier

Community

Может массово заменять текст ввыделенных блоках. Ноесть досадный недочёт при работе вWindows лишний перевод строки, скоторым можно бороться внешними средствами, заменив символ \n на\r или прогнав текст через сам плагин.

Замечательный инструмент, если умеешь импользоваться иутебя macOS.

НаmacOS: копируем текст, например, колонку изGoogle-таблицы. Выделяем целевые текстовые слои изапускаем плагин. Вставляем текст комбинацией Cmd+Shift+V.

Если увас Windows, действуем немного сложнее. Сначала вставляем скопированный текст вфайл Figma. Выделяем его, запускаем Retextifier, копируем итутже вставляем текст, жмём Change. Копируем изменённый текст, далее действуем как наmacOS. Надеюсь, автор плагина что-то придумает поповоду этого недоразумения.

Copy and Paste Text

Community

Вотличие отпредыдущего, этот плагин вставляет вовсе выделенные слои одинаковый текст избуфера. Безусловно полезно.

Копируем нужный текст, выделяем целевые слои, запускаем команду Paste Text

Find & Replace

Community

Поиск изамена потекстовым слоям споддержкой регулярных выражений (Regex). Для тех, кто умеет врегулярки ипонимает, как это круто.

Сейчас мыпоменяем местами буквы ицифры. Запускаем плагин, включаем поддержку Regex ипишем регулярное выражение. Найти (.*)-(.*) изаменить на$2-$1.

Мистика!

Nisa Text Splitter

Community

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

Давайте расставим фильмы похронологии. Запускаем Nisa Text Splitter ирежем наш блок настрочки. Сортируем поалфавиту Sort byalphabet исшиваем обратно водин блок Join text. Внутри плагина ещё много другой годноты. Например, сразу делать Auto Layout изнарезанного текста.

Change Text to Layer Name

Community

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

Выделяем текстовые блоки, применяем Rename Selection, оставляем старое название, добавляем кнему дефис иномер. Запускаем команду Change Text toLayer Name.

Математично!

Data Roulette

Community

Когда работаешь над продуктом, утебя есть постоянно используемые наборы данных. Эти наборы накапливаются икочуют изпродукта впродукт. Data Roulette позволяет хранить ихвGoogle-таблицах ирандомно заполнять ими макеты. Итекстами, ифотками.

Делаем Google-таблицу. Линк нанеё добавляем вData Roulette. Называем целевые слои всоответствии сназваниями колонок таблицы поставив вначало решётку. Можно сделать это вмастер-компоненте. Помере необходимости добавляем вGoogle-таблицу новые колонки.

Рулетка рулит!

Content Reel

Community

Для того, кому лень возиться сGoogle-таблицами, Microsoft наплагинил Content Reel. Только нужно залогиниться. Тогда можно будет создавать свои наборы данных.

Тут всё просто. Выделяем целевые слои, выбираем свой или чужой набор данных, вставляем.

Copypasta

Community

Очень часто нужно что-то добавить навсе экраны сценария. Водно итоже место. ИCopypasta прекрасно решает эту задачку.

Выделяем нужную деталь, запускаем Copypasta, жмём Save selection, выбираем целевые экраны, Duplicate Layers.

Шикарно.

Safely Delete Components

Community

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

Safely Delete Components удаляет неиспользуемые мастер-компоненты. Ноэтот плагин нужно использовать состорожностью. Нестоит запускать его вбиблиотеках.

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

SBOL-Typograph

Community

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

Божечки-Ёжечки!

Хорошо, ногдеже тот самый плагин?

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

Всем удачи напродуктовом фронте!

P.S.: Видео-версию статьи можете посмотреть намоём YouTube-канале.

P.P.S.: Ясерьёзно про комментарии.

Подробнее..

Чиним проблемы нагрузок в Go с помощью настройки пула HTTP-соединений

09.06.2021 20:16:20 | Автор: admin

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

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

В упрощенном виде его работу можно представить так:

  1. Проверить аутентификацию и авторизацию с помощью HTTP-запроса в сервис аутентификации

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

Иллюстрация работы API GatewayИллюстрация работы API Gateway

Конец декабря время роста нагрузок и числа ошибок

Настал конец декабря. Вместе с ним к нам в поддержку стали приходить вот такие обращения:

При работе в ЛК возникают постоянные ошибки системные по 10-20 раз на дню и больше. Просьба исправить и наладить работу площадки.

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

{err_type: context.deadlineExceededError, err: context deadline exceeded}{err_type: *errors.errorString, err: context canceled}

Трейсы в Jaeger показали ровно такую же картину мы не дожидались ответа от сервиса аутентификации за 2 секунды. Поэтому между нами и разработчиками сервиса аутентификации произошёл примерно такой диалог:

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

- Ничего подобного, у нас все норм мы за 200 миллисекунд отвечаем в 99% запросов. А вот вы по какой-то причине часто преждевременно обрываете соединение.

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

Скриншот с множеством ошибок Cancelled by clientСкриншот с множеством ошибок Cancelled by client

Итого, мы имеем:

  1. Используемый нами сервис аутентификации стабильно отрабатывает за 200 миллисекунд.

  2. Многие наши обращения к этому сервису таймаутят за 2 секунды.

Причина проблемы: дефолтные настройки в Go

В это время один из авторов нашего API Gateway отметил, что очень давно заметил аномалию: сервис открывает неожиданно много соединений к удаленным портам. При запуске команды из-под контейнера видно:

$ ss -natp state time-wait | awk '{print $4}' | sort -nr | uniq -c | sort -nr | head1053 10.20.49.117:801030 10.20.49.92:801016 10.20.49.91:801014 10.20.54.129:801013 10.20.53.213:801008 10.20.53.173:80969 10.20.53.172:80

Эта команда показывает количество TCP-сокетов в состоянии TIME_WAIT до разных удалённых портов. Если коротко, то состояние TIME_WAIT это де-факто закрытое клиентом соединение. Linux по возможности предотвращает повторное использование этих пар на протяжении 60 секунд, чтобы защититься от того, что старые пакеты помешают вновь установленному TCP-соединению.

Но для нас важно другое. Само существование TCP-соединения означает, что соединение установилось и закрылось. Если такая ситуация происходит массово, то мы имеем дело с накладными расходами на DNS-резолвинг и установку соединения. В результате этого время HTTP-запроса может увеличиваться. Избежать эту проблему помогают пулы соединении. В Go для этои цели используется абстракция http.Transport.

Здесь мы вплотную приближаемся к истокам проблемы. Мы для всех клиентских запросов использовали http.DefaultTransport. Он обладает следующими параметрами:

var DefaultTransport RoundTripper = &Transport{    Proxy: ProxyFromEnvironment,    DialContext: (&net.Dialer{        Timeout:   30 * time.Second,        KeepAlive: 30 * time.Second,    }).DialContext,    ForceAttemptHTTP2:     true,    MaxIdleConns:          100,    IdleConnTimeout:       90 * time.Second,    TLSHandshakeTimeout:   10 * time.Second,    ExpectContinueTimeout: 1 * time.Second,}

Среди перечисленных выше параметров к настройке пула соединений имеют отношения два:

  • MaxIdleConns число соединений, которое разрешается иметь в состоянии Idle (т.е. открытых TCP-соединений, которые в данный момент не используются);

  • IdleConnTimeout время, через которое закрываются такие неактивные соединения.

Однако в DefaultTransport совершенно не указан другой ключевой параметр MaxIdleConnsPerHost. Он отвечает за то, сколько неактивных TCP-соединений допускается устанавливать на один хост.

При этом если MaxIdleConnsPerHost не указан, тогда используется значение по умолчанию:

const DefaultMaxIdleConnsPerHost = 2

Поскольку мы использовали именно http.DefaultTransport для всех запросов, мы получили следующую проблему.

Представим, что нам одновременно понадобилось установить 10 соединений до сервиса аутентификации. Тогда хотя бы для 8 из них будут открыты и вскоре тут же закрыты TCP-соединения, из-за ограничения MaxIdleConnsPerHost. Если такая ситуация будет повторяться часто, у нас будет больше накладных расходов на один HTTP-запрос, поскольку для него понадобится новое соединение. Из-за этого вероятность таймаутов возрастает.

Решение: отдельный транспорт с особенными настройками

Чтобы решить проблему, мы сделали следующее:

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

  • Утилизировали выделенный пул на полную сделали так, чтобы значение MaxIdleConnsPerHost соответствовало значению MaxIdleConns:

func createOneHostTransport() *http.Transport {    result := http.DefaultTransport.(*http.Transport).Clone()    result.MaxIdleConnsPerHost = result.MaxIdleConns    return result}
График response time обращения к сервису аутентификацииГрафик response time обращения к сервису аутентификации

Тут видно значительное уменьшение 0.99-квантиля по времени обращения графиков (голубой цвет) с 2-3 секунд до менее 300 миллисекунд. Должен признать, даже после этого мы изредка видели таймауты при обращении к сервису аутентификации. Но теперь мы хотя бы видели эти же таймауты на графиках другого сервиса.

Но почему в Go такие настройки по умолчанию?

Возможно, у вас сейчас возник вопрос: зачем делать такие настройки, которые потом приходится исправлять? Неужели разработчики языка Go и библиотек к нему не подумали о том, как это будет использоваться на практике?

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

Если бы приложения могли говорить...

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

Приложение: http.Transport, привет! Нам тут сотня пользователей пришла одновременно и им всем вдруг срочно понадобилось посмотреть инфу о своём профиле. Установи, пожалуйста, сотню соединений к user-service

http.Transport: Конечно, вот получи сотню установленных соединений под запросы! Но я их не буду ещё полторы минуты закрывать, вдруг пригодятся .

Приложение: Всё сработало, спасибо! Только вот теперь им всем вдруг захотелось посмотреть информацию о товаре. Пожалуйста, установи соединение с product-service.

http.Transport: Да, не вопрос держи. Но только я их у себя в пуле соединений хранить не буду, поскольку он уже полностью забит соединениями до user-service, которые тебе больше не нужны.

Приложение: (_)

Разбираемся с непонятными таймаутами в Go: чеклист

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

  1. Проверьте метрики по скорости выполнения запросов к сторонним сервисам по HTTP. Если таких метрик нет, заведите пригодятся.

  2. Если видите расхождения в таймаутах клиента и сервера, проверьте количество соединений TIME_WAIT.

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

  4. Для хоста с неоправданно большим числом запросов к нему рассмотрите вариант заведения отдельного транспорта.

Что ещё почитать по теме

Подробнее..

КакстроитьдиаграммуГанттапоJira-тикетам

04.06.2021 08:06:10 | Автор: admin

Время чтения: 5 минут

Статья для менеджеров, которым необходимо вести управление проектами и ставить сроки в изменчивом мире Agile. Поделюсь опытом использования двух приложений Jira Roadmap и Structure Gantt.

Пример Диаграммы Гантта Пример Диаграммы Гантта

Ганттдля(ленивых) экономящих свое время

Для построения диаграммыГантта существует множество программ: Ganttpro, Monday, Teamgantt,старый добрыйExcelи другие.

Нам с коллегами в Ozonони не подходили,потому чтоежедневная работа завязана наJira-тикеты.Чтобы пользоваться сторонними сервисами, приходилосьпереписывать всю информацию изJiraещёраз (название задачи, исполнитель, связи, статус).Этотребовало много времении терпениянамонотонную механическую работу.

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

  1. JiraRoadmap

  2. StructureGantt

JiraRoadmap

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

Jira RoadmapJira Roadmap

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

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

StructureGanttк функционалуStructureдобавляется возможность видетьJira-тикетыи связи между задачами на временной ленте.

Structure GanttStructure Gantt

В конце 2020 годаруководители Ozon приобрелиStructureGanttдля удобного и наглядного ведения проектов. Однаков менеджерском раю оказаться сразу не получилась.Причины: сложная функциональностьи отсутствиенаглядногообучающего материала.

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

  • В чате Slack#jira-structureможнобылооперативно получить ответ на вопрос по работе приложение или поделиться своими находками.

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

  • Созванивались и пытались коллективным разумом найтивыход из тупиковых ситуаций.

Слабые стороныStructureGantt

Сильные стороныStructureGantt

Сложно разобраться с инструментом

Богатаяфункциональность

Невозможно увидеть время переходаJira-тикетыиз одного статуса в другой

Вся информация автоматически дублируется междуJira-тикетамииStructureGantt

В мануалах нет примеров использованияна практике

На одном экране помещается большое количество данных

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

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

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

Есть несколько режимовStructure Gantt, которые я меняю в зависимости отконтекста, чтобы минимизировать объём работы.

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

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

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

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

Описание статуса проектатолько структура без диаграммыГантта

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

Планирование спринта

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

Мои любимыефичиStructureGantt

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

Связи между задачами

История декомпозирована,Jira-тикетына задачи заведены. На следующем этапе при растягиванииколбасокповременной ленте я расставляю связи междутикетами. Делаю так,чтобычётковыделить,какая задача заблокирована иликакиезадачи должны быть закончены одновременно (классические зависимостив управлении проектами:finishtostart,finishtofinish).

Цвета

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

Переход между режимами

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

Выбор за вами

Нам с коллегами Structure Gantt позволяет собирать всю нужную информацию из Jira в одном месте. Получается больше порядка и контроля над проектами.

При этом у Structure Gantt есть свои слабые стороны. Буду рада, если продакты ALM Works (создатели Structure) обратят внимание на них и сделают программу более простой и понятной в освоении.

Во время подготовки статьи я обнаружила, что функциональность Jira Roadmap заметно изменилась c лета прошлого года. Набор возможностей стал похож на то, что предлагает Structure Gantt.

Если выбираете приложение к Jira для ведения проектов и создания диаграмм Гантта, попробуйте оба варианта. Главное старайтесь оптимизировать затяжную механическуюработу.У насменеджеров есть много других важных задач, помимо рутины!:)

Ссылки

  1. Описание функционала на сайте Atlassian JiraRoadmap и StructureGantt

  2. Рекламное видео "What is Structure.Gantt?"

Подробнее..

Какв Ozon пришли к релизам мобильных приложений раз в неделю

23.05.2021 16:04:26 | Автор: admin

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

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

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

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

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

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

  5. Невыносимо долго проводить регрессионное тестирование. Всё,что было переделаноза несколько месяцев,мы будем проверять, чинить, перепроверять, чинить, перепроверятьНу, вы поняли

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

Проблеммного,и они все значимые. Когда бизнеспришёл к нам и сказал, что надо релизить быстрее мы уже ожидали этого. Но не ожидали, что они скажут: Релизьте раз в неделю.

Мы в шоке, как это реализоватьнепонятно.Мы в шоке, как это реализоватьнепонятно.

Тут у насслучиласьстадия отрицания:Мы не успеем всёпроверить, никаких фичей в релиз не попадет, Apple ревьюит о-го-госколько.Зачем, почему, может,не надо?.В ответ мы услышали: Релизы раз в неделю.

Сокращаем время выпуска релиза: первая попытка

Решили, что надодвигаться к целиитерационно.

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

Ок, дата есть. Прикинули, что неделя уходит на регресс. На разработку остается три недели. Запланировали работына весь срок девелопмента.

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

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

Вторая попытка: нужно что-то менять

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

Тем временемтестировщикипродолжают писатьавтотесты

Пробуем всёравно каждый раз не успеваем.

Стали анализировать,из-за чегоне выходит уложиться в срок:

  1. Неправильно оцениваем время на разработку.

  2. Блочимсябекендом от этого тормозится и разработка, и тестирование.

  3. Продактыпоздно вносятпоследниеправки в ТЗ.

  4. Не учитываем время на починкубагов.

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

И тут пришлиQA.Сказали, что при двухнедельных релизахскоупнеособосократился.

Разработчиков много, они разделены по разномуфункционалуи задвенедели успеваютнакомититьпрактически во все компоненты. Стало понятно, что несмотря на то, что мыНИ РАЗУне успели в срок ни при месячном релизе, ни при двухнедельных пора двигаться дальше.

Недельные релизымыидём к вам!

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

Понедельник

Релизная ветка

Вторник

Фичи

Среда

Фичи

Четверг

Фичи

Пятница

Фиксы багов

Фичиуходятвсвои фича-ветки. И когда она полностью сделана, проверена, принятапродактом, тогда уже попадает вdevelop.

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

Получается, что где-то в среду-четверг должен начаться процентныйроллаутприложения.

Меняемся в тестировании

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

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

Чтобы разработка не зависала на начальных этапах, мы выработали требования кфиче, которуюберём в работу у неёдолжныбыть в наличии

  1. Дизайн;

  2. Бекенд;

  3. Контракт.

Если чего-то из этого нет смысла братьфичув разработку на мобильной платформе тоже нет.

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

  1. Бекендфичидолжен быть напродакшене.

  2. К началурелизноготестирования нет критичныхбагов(ни на фронте, ни набекенде).

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

Меняемся в разработке

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

Ещёмы постоянно автоматизируем процессы, чтобы разработчикам надо было меньше отвлекатьсяна какие-то рутинные вещи,и они могли сконцентрироваться на разработке.

Тикетавтоматом двигается по статусам. Запушилкоммит перешел вinprogress.Создалmergerequest перешел вcodereview.ПрошелreviewпопалвQA.

Втикетеавтоматически проставляется версия релиза, куда он попал и номербилда.

По каждой сборке запускаютсяUI-автотестыпозатронутомуфункционалу.Это тоже определяется самопо измененным файлам вmergerequest.В результате репорт попадаетвкомментарийтикета вJira.

Дажеmergerequestна влитие эпика вdevсоздаётсяпросто по принятию продактом фичивJira.Если нет конфликтов, то и вливается сам.Релиз сам закрывается, а новый самсоздаётся.

QA Notes

Ещёмы ввели требования к разработчикам писатьQANotes.Там указывается:

  1. Что было сделано.

  2. Что могло быть задето.

  3. Приложены скриншоты или видео.

  4. На какой среде удалось проверить.

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

QANotesпозволили значительно ускоритьтестированиеиревьюкода. А ещёдали нам скрытый бонус:пропалиреопеныотQAиз-закрешейна старте.

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

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

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

А еще добавиласьротируемаяроль QAза релиз.Этоттестировщикгде-то раз вдватримесяцаделаетповторяющиесявещи:

  1. Составляет наборрелизныхтестов.

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

  3. Напоминаеттестировщикампосмотретьотчёты поавтотестамнарелизномбилде.

  4. Пушитпересборкурелиз-кандидатов, если что-то добавилось.

  5. После релиза неделюмониторитпаденияи отвечает на запросы поддержки.

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

Автоматизация помогает контролировать стабильность среды с помощью выполнения тестов после изменения кода приложения:

Мы также проверяем, что новыеавтотестыне ломают существующие:

Новая схема релиза мобильных приложений

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

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

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

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

Какиеестьсложности

В первую очередь этонепростойрефакторинг. Когдавмерживодинкоммит, вам надо перепроверить всёприложение.Сейчас мы прежде всеговливаемтакое по понедельникам-вторникам, чтобырефакторингмаксимально настоялся вdev, накрутилсяавтотестамии был потроган при тестированиифичей.

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

Что ещёможно улучшить

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

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

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

  1. Строгие требования к готовностифичи.

  2. Приоритет напереоткрытыхтикетах.

  3. Весь функционалзакрытфичефлагами.

  4. Строгое расписание релизов.

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

Подробнее..

За что банит Apple(и Google)

27.05.2021 02:21:05 | Автор: admin

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

Рассмотрим некоторые из них.

Покупки не через сервисы Google&Apple

Начнем с одной из самых известных сейчас блокировок - удаление игры Fortnite от Epic Games из мобильных сторов. Издатель решил, что отдавать 30 процентов комиссии с каждой покупки слишком много и сделал оплату в обход стандартного механизма In-app payment. Что, конечно, запрещено. И ни Apple, ни Google не захотели терять свой доход(хотя на некоторые послабления уже пошли Apple Google).

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

COVID-19

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

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

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

Apple ссылались на пункт 5.2.1 Apps should be submitted by the person or legal entity that owns or has licensed the intellectual property and other relevant rights.

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

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

Хранение данных

Когда-то я работал в компании, которой прилетел реджект за то, что мы храним данные приложения в iCloud'е пользователя. Делали мы это не специально, а по не знанию) При этом, как оказалось, какие-то данные хранить в iCloud можно, но это должен быть сгенерированный пользователем контент.

Это самый первый пункт из iOS Data Storage Guidelines: "Only documents and other data that is user-generated, or that cannot otherwise be recreated by your application, should be stored in the <Application_Home>/Documents directory and will be automatically backed up by iCloud. "

Убрали хранение данных в локальный кеш и смогли пройти ревью.

Пермишены

В iOS 14.5 стал обязательным запрос нового пермишена - про трекинг данных пользователя. Компании принялись рассказывать юзерам на предварительных экранах почему же надо разрешить трекинг и... некоторые столкнулись с блокировками как раз из-за онбординг экрана для пермишена. Дело было в добавлении двух кнопок на этот экран. По нажатию одной - запрашивалось разрешение, второй - нет. В гайдланах это строчка "If you display a custom screen that precedes a privacy-related permission request, it must offer onlyoneaction, which must display the system alert."

При этом, например, у facebook'а получалось проходить ревью с двумя кнопками При этом, например, у facebook'а получалось проходить ревью с двумя кнопками Но позже и facebook, и instagram заменили эти экраны на однокнопочныеНо Но позже и facebook, и instagram заменили эти экраны на однокнопочныеНо

Ссылки на другие приложения

Когда я начинал разрабатывать свои приложения, то пользовался кросслинками из одного в другое. И так мои новые игры набирали лояльную аудиторию из старых. Довольно неплохо работало. Делал я это максимально примитивно - ставил иконку нового приложения в угол экрана меню старых. А еще на экране подтверждения закрытия приложения третьей кнопкой был переход в новую игру. Но через какое-то время Google начал поочередно блокировать одно приложение за другим, т.к. "Ads must not simulate the user interface of any app". Оказалось, что к подобным переходам надо явно писать, что они являются рекламой.

Слишком взрослый рейтинг

Напоследок совсем забавная для меня формулировка. Первая от Google. Приложения не пропускали в стор из-за того, что google play решил, что поставлен слишком высокий возрастной рейтинг. Сделано это, чтобы не заморачиваться с контентом, который мог где-нибудь(например, в рекламе) появиться, а детям его показывать не стоит. Но google сказал, что "We determine that some elements of your store listing may appeal to children under 13: Animated characters in app icon, young characters". Пришлось понижать возрастной рейтинг и фильтровать "опасные" категории рекламы.

А с какими блокировками приходилось сталкиваться вам?

Подробнее..

Архитектура кода программного обеспечения декорируем стратегией. Рассказ в 10 эпизодах, основанный на реальных событиях

21.05.2021 08:10:50 | Автор: admin

Встречаются два эксперта-консультанта по конструированию программного обеспечения:
- Как написать сложное корпоративное приложение, поддерживать которое будет всегда легко и дешево.
- Могу рассказать...
- Рассказать и я могу! Написать-то как?..

Время чтения: 25 мин.

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

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

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

Введение в предметную область

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

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

В этой статье я хочу предложить технику написания программ, в основе которой лежит два паттерна проектирования ООП: декоратор и стратегия. Я уверен, что основная часть читающих статью наверняка не раз сталкивалась с этими паттернами (возможно, даже на практике). Но чтобы все чувствовали себя "в своей тарелке", обращусь к определениям из "Паттернов проектирования" Эриха Гаммы, Ричарда Хелма, Ральфа Джонсона и Джона Влиссидеса (Банда четырех, Gang of Four, GoF):

  • Декоратор (Decorator, Wrapper) паттерн проектирования, позволяющий динамически добавлять объекту новые обязанности. Является гибкой альтернативой порождению подклассов с целью расширения функциональности.

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

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

Декорирование стратегией, на мой взгляд, даёт великую пользу при поддержке приложений на очень большом жизненном цикле программного продукта. Компоненты в коде, написанные с применением данного подхода, соответствуют всем принципам дизайна SOLID из "Чистой архитектуры" Роберта Мартина. Каждый компонент, который мы напишем далее, будет отвечать только за одно действие; после написания нового компонента мы ни разу не модифицируем логику его методов, а лишь будем расширять ее в декорирующих компонентах; в силу паттерна "Декоратор" все расширяемые и расширяющие компоненты соответствуют одному контракту, следовательно их можно заменять друг другом; интерфейсы компонентов не содержат зависимостей, которые не используются; компоненты бизнес-логики ни в коей мере не зависят от деталей.

Я не раз сталкивался в обсуждениях с опытными разработчиками, которые говорят: "А вот всё, что связано с применением принципов SOLID, паттернов ООП на практике это миф!". Любезно обращаясь к скептически настроенным к применению теории разработки в реальных больших корпоративных проектах, хочу сказать: "А вот посмотрим!"

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

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

  • Должная обработка ошибок. В коде мы ограничимся оборачиванием ошибок дополнительным сообщением с помощью пакета "github.com/pkg/errors".

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

  • Комментарии и документирование кода.

  • Всё, что связано, с конкурентным выполнением задач и синхронизацией.

  • Структура файлов и директорий проекта.

  • Стили, линтеры и статический анализ.

  • Покрытие кода тестами.

  • Сквозь методы компонентов рекомендуется с первых этапов разработки "тянуть" context.Context, даже если он в тот момент не будет использоваться. Для упрощения повествования в примерах далее контекст также использоваться не будет.

Перейдём же наконец от скучной теории к занимательной практике!

Пролог. Закладываем фундамент

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

Итак, начнём. Здесь мы с вами высококвалифицированные разработчики программных продуктов. К нам приходит наш первый заказчик от бизнеса и говорит что-то вроде: "Нам нужна функциональность обновления такой-то информации о пользователях нашей платформы". Мы обрабатываем требования, продумываем архитектуру и переходим к конструированию кода.

Первое, что нужно сделать определить интерфейс нашего первого компонента службы, которая будет представлять желаемый use-case SavePersonService. Но для этого нам нужно определить объекты нашей предметной области, а именно структуру данных, содержащую информацию о человеке PersonDetails. Создадим в корне проекта пакет app, далее создадим файл app/person.go, и оставим в нём нашу структуру:

// app/person.gotype PersonDetails struct {    Name string    Age  int}

Данный файл завершён, больше мы к нему в этой статье возвращаться не будем. Далее создаем файл app/save-person.go, и определяем в нём интерфейс нашего use-case:

// app/save-person.gotype SavePersonService interface {    SavePerson(id int, details PersonDetails) error}

Оставим сразу рядом с определением интерфейса его первую реализацию компонент noSavePersonService, который ничего не делает в теле интерфейсного метода:

// app/save-person.go// ... предыдущий код ...type noSavePersonService struct{}func (noSavePersonService) SavePerson(_ int, _ PersonDetails) error { return nil }

Поскольку объекты noSavePersonService не содержат состояния, можно гарантировать, что данный "класс" может иметь только один экземпляр. Напоминает паттерн проектирования Синглтон (Singleton ещё его называют Одиночка, но мне это название по ряду причин не нравится). Предоставим глобальную точку доступа к нему. В Golang легче всего это сделать, определив глобальную переменную:

/ app/save-person.go// ... предыдущий код ...var NoSavePersonService = noSavePersonService{}

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

Эпизод 1. Будем знакомы, Декоратор Стратегией

Перейдём непосредственно к реализации бизнес-логики нашей задачи. Нам нужно в конечном счёте иметь хранилище, в котором содержатся данные о пользователях. С точки зрения выбора технологии мы сразу себе представляем, что будем использовать PostgreSQL, но правильно ли завязываться в коде нашей бизнес-логики на конкретную технологию. Вы правы конечно нет. Определить компонент нашего хранилища нам позволит паттерн Репозиторий (Repository). Создадим пакет с реализациями интерфейса нашего use-case save-person внутри app, и в нём создадим файл app/save-person/saving_into_repository.go реализации нашего use-case, которая обновляет данные в репозитории:

// app/save-person/saving_into_repository.gotype PersonRepository interface {    UpdatePerson(id int, details app.PersonDetails) error}type SavePersonIntoRepositoryService struct {    base app.SavePersonService    repo PersonRepository}func WithSavingPersonIntoRepository(base app.SavePersonService, repo PersonRepository) SavePersonIntoRepositoryService {    return SavePersonIntoRepositoryService{base: base, repo: repo}}func (s SavePersonIntoRepositoryService) SavePerson(id int, details app.PersonDetails) error {    err := s.base.SavePerson(id, details)    if err != nil {        return errors.Wrap(err, "save person in base in save person into repository service")    }    err = s.repo.UpdatePerson(id, details)    if err != nil {        return errors.Wrap(err, "update person in repo")    }    return nil}

В коде выше впервые появляется компонент, который выражает наш подход "Декорирование стратегией". Сам компонент представляет собой декоратор, реализующий интерфейс нашего use-case, который оборачивает любой компонент с таким же интерфейсом. В реализации метода изначально вызывается метод декорируемого объекта s.base; после этого происходит вызов стратегии обновления данных о человеке в хранилище s.repo. По сути, весь подход это конструирование компонентов-декораторов, которые содержат два объекта:

  1. Непосредственно декорируемый объект с таким же интерфейсом.

  2. Стратегия, логику которой мы добавляем в довесок к логике декорируемого объекта.

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

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

Напомню, что бизнес-логика не должна содержать ненужные зависимости, зависимости от деталей и т.п. Другими словами, бизнес-логика должна быть "чистая, как слеза". Где тогда должны находиться зависимости от конкретных реализаций, зависимости от используемых технологий? Ответ в файле main.go. Следуя замечаниям Роберта Мартина, можно сделать умозаключение, что код компонентов файла, содержащего точку входа в программу, является самым "грязным" с точки зрения зависимостей от всего. Обозначим в main.go метод, который нам возвращает клиент к базе данных PostgreSQL. И собственно сборку объекта службы нашего use-case и вызов его метода на условных входных данных:

// main.gofunc NewPostgreSQLDatabaseClient(dsn string) savePerson.PersonRepository {    _ = dsn // TODO implement    panic("not implemented")}func run() error {    userService := savePerson.WithSavingPersonIntoRepository(        app.NoSavePersonService,        NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/users?sslmode=disable"))    err := userService.SavePerson(5, app.PersonDetails{        Name: "Mary",        Age:  17,    })    if err != nil {        return errors.Wrap(err, "save user Mary")    }    return nil}

В коде выше мы можем заметить, что в качестве стратегии репозитория выступает обозначенный конкретный компонент клиента к PostgreSQL. В качестве же декорируемого объекта выступает наша "фиктивная" реализация use-case app.NoSavePersonService, которая по сути ничего не делает. Зачем она нужна? Она ничего полезного ведь не делает? Не легче ли просто вызвать метод клиента к базе данных? Спокойно, звёздный час этой реализации сейчас настанет.

Ссылка на полный код эпизода

Эпизод 2. Магия начинается!

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

// main.go// ... предыдущий код ...func NewMemoryCache() savePerson.PersonRepository {    // TODO implement    panic("not implemented")}// ... последующий код ...

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

// main.go// внутри run()userService := savePerson.WithSavingPersonIntoRepository(    savePerson.WithSavingPersonIntoRepository(        app.NoSavePersonService,        NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/users?sslmode=disable")),    NewMemoryCache(),)err := userService.SavePerson(5, app.PersonDetails{    Name: "Mary",    Age:  17,})if err != nil {    return errors.Wrap(err, "save user Mary")}

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

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 3. Рефакторинг для здоровья

В предыдущем листинге кода создание сервиса выглядит достаточно громоздко. Нетрудно догадаться, применяя наш подход, мы продолжим и далее всё больше и больше оборачивать компонент, добавляя к логике новые стратегии. Поэтому мы, как опытные разработчики, замечаем эту потенциальную трудность и производим небольшой рефакторинг когда. Нам поможет паттерн Билдер (Builder опять же мне не очень нравится ещё одно его название Строитель). Это будет отдельный компонент, зона ответственности которого предоставить возможность сборки объекта службы нашего use-case. Файл app/save-person/builder.go:

// app/save-person/builder.gotype Builder struct {    service app.SavePersonService}func BuildIdleService() *Builder {    return &Builder{        service: app.NoSavePersonService,    }}func (b Builder) SavePerson(id int, details app.PersonDetails) error {    return b.service.SavePerson(id, details)}

Компонент Builder должен обязательно реализовывать интерфейс службы нашего use-case, так как именно он будет использоваться в конечном счёте. Поэтому мы добавляем метод SavePerson, который вызывает одноименный метод объекта в приватном поле service. Конструктор данного компонента называется BuildIdleService, потому что создаёт объект, который ничего не будет делать при вызове SavePerson (нетрудно заметить инициализацию поля service объектом app.NoSavePersonService). Зачем нам нужен этот бесполезный компонент? Чтобы получить всю истинную пользу, необходимо обогатить его другими методами. Эти методы будут принимать в параметрах стратегию и декорировать ею объект службы в поле service. Но вначале сделаем конструктор WithSavingPersonIntoRepository в app/save-person/saving_into_repository.go приватным, так как для создания службы мы теперь будем использовать только Builder:

// app/save-person/saving_into_repository.go// ... предыдущий код ...func withSavingPersonIntoRepository(base app.SavePersonService, repo PersonRepository) SavePersonIntoRepositoryService {    return SavePersonIntoRepositoryService{base: base, repo: repo}}// ... последующий код ...

Добавляем соответствующий метод для Builder:

// app/save-person/builder.go// ... предыдущий код ...func (b *Builder) WithSavingPersonIntoRepository(repo PersonRepository) *Builder {    b.service = withSavingPersonIntoRepository(b.service, repo)    return b}

И наконец производим рефакторинг в main.go:

// main.go// ... предыдущий код ...userService := savePerson.BuildIdleService().        WithSavingPersonIntoRepository(NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/platform?sslmode=disable")).        WithSavingPersonIntoRepository(NewMemoryCache())// ... последующий код ...

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 4. Больше заказчиков!

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

// main.go// ... предыдущий код ...func NewMongoDBClient(dsn string) savePerson.PersonRepository {    _ = dsn // TODO implement    panic("not implemented")}// ... последующий код ...

Воспользуемся нашим билдером и просто добавим новый код в main.go под имеющийся фрагмент с userService:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithSavingPersonIntoRepository(NewMemoryCache())err = taxpayerService.SavePerson(1326423, app.PersonDetails{    Name: "Jack",    Age:  37,})if err != nil {    return errors.Wrap(err, "save taxpayer Jack")}

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

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 5. Путь в никуда

Проходит ещё время. Заказчик 2 ставит нам такую задачу. Так как все налогоплательщики должны быть совершеннолетними, необходимо в бизнес-логику добавить функциональность проверки возраста человека перед сохранением в хранилище. С этого момента начинаются интересные вещи. Мы можем добавить эту валидацию в метод SavePersonIntoRepositoryService.SavePerson в файле app/save-person/saving_into_repository.go. Но тогда при нескольких декорированиях стратегией сохранения информации в репозиторий эта валидация будет вызываться столько раз, сколько производилось таких декораций. Хотя и все проверки помимо первой никак не влияют на результат напрямую, всё-таки не хочется лишний раз вызывать один и тот же метод.

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

// app/save-person/builder.gotype Builder struct {    service           app.SavePersonService    withAgeValidation bool}func BuildIdleService(withAgeValidation bool) *Builder {    return &Builder{        service:           app.NoSavePersonService,        withAgeValidation: withAgeValidation,    }}func (b Builder) SavePerson(id int, details app.PersonDetails) error {    if b.withAgeValidation && details.Age < 18 {        return errors.New("invalid age")    }    return b.service.SavePerson(id, details)}// ... последующий код ...

И тогда в main.go нужно вызывать конструкторы билдера с разными значениями флага withAgeValidation:

// main.go// ... предыдущий код ... userService := savePerson.BuildIdleService(false).// ... код ...taxpayerService := savePerson.BuildIdleService(true).// ... последующий код ...

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

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 6. Путь истины

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

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

// app/save-person/validating.gotype PersonValidator interface {    ValidatePerson(details app.PersonDetails) error}type PreValidatePersonService struct {    base      app.SavePersonService    validator PersonValidator}func withPreValidatingPerson(base app.SavePersonService, validator PersonValidator) PreValidatePersonService {    return PreValidatePersonService{base: base, validator: validator}}func (s PreValidatePersonService) SavePerson(id int, details app.PersonDetails) error {    err := s.validator.ValidatePerson(details)    if err != nil {        return errors.Wrap(err, "validate person")    }    err = s.base.SavePerson(id, details)    if err != nil {        return errors.Wrap(err, "save person in base in pre validate person service")    }    return nil}

Опять ничего нового. PreValidatePersonService это очередной декоратор стратегией валидации перед последующим вызовом декорируемого метода.

Добавим соответствующий метод в Builder:

// app/save-person/builder.go// ... предыдущий код ...func (b *Builder) WithPreValidatingPerson(validator PersonValidator) *Builder {    b.service = withPreValidatingPerson(b.service, validator)    return b}

Добавление каждого нового декоратора стратегией требует добавление нового метода в наш билдер.

Добавим реализацию валидатора, проверяющую возраст человека:

// main.go// ... предыдущий код ...type personAgeValidator struct{}func (personAgeValidator) ValidatePerson(details app.PersonDetails) error {    if details.Age < 18 {        return errors.New("invalid age")    }    return nil}var PersonAgeValidator = personAgeValidator{}// ... последующий код ...

Так как personAgeValidator не имеет состояния, можем сделать для компонента единую точку доступа PersonAgeValidator. Далее просто вызываем новый метод в main.go только для taxpayerService:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithSavingPersonIntoRepository(NewMemoryCache()).    WithPreValidatingPerson(PersonAgeValidator)// ... последующий код ...

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 7. А ну-ка закрепим

Уверен, к данному эпизоду вы поняли смысл подхода "Декорирование стратегией". Чтобы закрепить, давайте добавим ещё один такой компонент. Представим, технический руководитель требует от нас покрыть метриками время выполнения сохранения данных в хранилище. Мы могли бы замерить это время, просто добавив пару строчек кода в SavePersonIntoRepositoryService. Но как бы не так! Мы же не изменяем уже работающий в продакшне код, а можем его только расширить. Давайте же так и сделаем. Добавим новый декоратор стратегией отправки метрики времени:

// app/save-person/sending_metric.gotype MetricSender interface {    SendDurationMetric(metricName string, d time.Duration)}type SendMetricService struct {    base         app.SavePersonService    metricSender MetricSender    metricName   string}func withMetricSending(base app.SavePersonService, metricSender MetricSender, metricName string) SendMetricService {    return SendMetricService{base: base, metricSender: metricSender, metricName: metricName}}func (s SendMetricService) SavePerson(id int, details app.PersonDetails) error {    startTime := time.Now()    err := s.base.SavePerson(id, details)    s.metricSender.SendDurationMetric(s.metricName, time.Since(startTime))    if err != nil {        return errors.Wrap(err, "save person in base in sending metric service")    }    return nil}

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

// app/save-person/builder.go// ... предыдущий код ...func (b *Builder) WithMetricSending(metricSender MetricSender, metricName string) *Builder {    b.service = withMetricSending(b.service, metricSender, metricName)    return b}

И наконец обозначаем в main.go функцию, возвращающую savePerson.MetricSender и добавляем вызов нового метода Builder в сборку наших сервисов:

// main.go// ... предыдущий код ...func MetricSender() savePerson.MetricSender {    // TODO implement    panic("not implemented")}// ... код ...userService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/platform?sslmode=disable")).    WithMetricSending(MetricSender(), "save-into-postgresql-duration").    WithSavingPersonIntoRepository(NewMemoryCache())// ... код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithMetricSending(MetricSender(), "save-into-mongodb-duration").    WithSavingPersonIntoRepository(NewMemoryCache()).    WithPreValidatingPerson(PersonAgeValidator)// ... последующий код ...

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

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 8. Результаты ясновидения

Проходит время. Заказчик 2 ставит новую задачу. Он желает знать, как долго выполняется сохранение данных о налогоплательщике, но с небольшой оговоркой: учитывать нужно всё, кроме валидации. Похоже на замер времени, который мы недавно реализовали для своих целей, не правда ли? Чтобы решить задачу, всё что нам требуется это добавить вызов метода для новой метрики в main.go:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithMetricSending(MetricSender(), "save-into-mongodb-duration").    WithSavingPersonIntoRepository(NewMemoryCache()).    WithMetricSending(MetricSender(), "save-taxpayer-duration").    WithPreValidatingPerson(PersonAgeValidator)

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 9. Укрощение капризов

Мы вот только недавно произвели релиз последней задачи от заказчика 2, но он захотел изменить начальные требования. Такие изменения часто возникают на стороне заказчика, которые заставляют нас "перелопатить" весь код. Знакомо? На этот раз заказчик желает отказаться от оговорки из предыдущего эпизода и производить замер полного цикла сохранения данных о налогоплательщике вместе с валидацией. Если бы мы конструировали нашу бизнес-логику в виде сценария транзакции (transaction script), то это повлекло бы за собой непосредственное вмешательство в тело метода, copy-paste кода, что требует приложить силы, в том числе в процессе ревью, тестирования и т.п. В нашем же случае нам достаточно просто подвинуть вызов метода WithMetricSending в цепочке методов создания объекта службы в main.go:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithMetricSending(MetricSender(), "save-into-mongodb-duration").    WithSavingPersonIntoRepository(NewMemoryCache()).    WithPreValidatingPerson(PersonAgeValidator).    WithMetricSending(MetricSender(), "save-taxpayer-duration")

В коде выше мы поменяли местами второй WithMetricSending и WithPreValidatingPerson.

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

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 10. Взгляд в будущее

Этот заключительный эпизод всего лишь подчеркивает потенциал дальнейших доработок логики данного кода. Что ещё может пожелать заказчик от бизнеса или с технической стороны? Вариантов более чем достаточно. Может потребоваться функциональность отправки асинхронных событий об изменении информации о человеке (полезно при ведении журнала аудита, коммуникации с другими сервисами и т.д.). Может понадобиться введение механизма гомогенных и даже гетерогенных транзакций. Возможно, потребуется добавить запрос данных к соседнему микросервису. По техническим соображениям возможно будет нужен предохранитель (circuit-breaker) для таких запросов к другим сервисам. Наверняка нужно будет добавлять механизм трассировки (tracing). И многое-многое другое.

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

Эпилог. Подводим итоги

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

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

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

Что есть что на этом графике? Почему на осях нет чисел? Всё потому что график абстрактный. Он отражает качественный смысл содержимого, не количественный. По горизонтальной оси у нас время, прошедшее с момента начала разработки продукта. Или если желаете, количество добавлений новой функциональности в изначально разработанный продукт. Меру по вертикальной оси тоже можно выразить различными способами. Это может быть цена добавления новой строчки кода функционала в денежном эквиваленте; может быть время добавления новой функциональности; может быть количество потраченных нервных клеток разработчиком, ревьювером или тестировщиком. Красный график демонстрирует зависимость этих величин для подхода разработки, который называется сценарием транзакции (Transaction Script) последовательно следующие друг за другом инструкции. Синий график показывает эту зависимость для подхода модели предметной области (Domain Model).

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

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

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

Литература

  1. Макконнелл С. Совершенный код. Мастер-класс., 2020.

  2. Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Приемы объектно-ориентированного проектирования. Паттерны проектирования., 2020.

  3. Мартин Р. Чистая архитектура. Искусство разработки программного обеспечения., 2020

  4. Фаулер, Мартин. Шаблоны корпоративных приложений., 2020.

Подробнее..

Категории

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

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