Найти в Дзене
Сделай игру

Последовательная разработка: краткое руководство

Оглавление

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

Зверь-разработчик
Зверь-разработчик

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

Об этом я и хочу рассказать.

Начальные условия

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

Поэтапное развитие

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

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

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

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

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

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

Первый подход: разделяй и властвуй

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

  1. Диалоговое окно;
  2. Таблица;
  3. Кнопки окна;
  4. Строка таблицы;
  5. Ячейка таблицы;
  6. Кнопка внутри ячеек.

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

Второй подход: входные данные

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

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

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

Третий подход: разработка наугад

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

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

Но! Как только я это сделаю - весь дальнейший код сильно усложнится. А мне, вполне возможно, и не понадобится вовсе эти три слоя одновременно. Или же, когда придёт пора эти слои реализовывать, выяснится, что намного проще сделать просто параллельный уровень, на котором есть и земля, и подземелье (как было сделано в HOMM III).

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

Четвёртый подход: инструкция или обработчик

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

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

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

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

Пятый подход: уровень изоляции

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

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

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

И вот, теперь компонент таблицы уже знает про работу с запросами. Изоляция слабнет, связность крепнет. Плохо!

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

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

Работа с кодом

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

Я намеренно пропущу правила форматирования: они имеют смысл, но это слишком объёмная тема. Если есть желание узнать про это побольше - милости прошу в PEP8: там можно научиться хорошему.

Размер и структура файлов

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

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

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

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

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

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

Разделение функционала

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

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

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

Полагаю, общий принцип понятен.

Итоги

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

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

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