Как написать структуру приложения

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

К моему удивлению оказалось, что на вроде бы актуальный вопрос: «Как построить хорошую/красивую архитектуру ПО?» — не так легко найти ответ. Не смотря на то, что есть много книг и статей, посвященных и шаблонам проектирования и принципам проектирования, например, принципам SOLID (кратко описаны тут, подробно и с примерами можно посмотреть тут, тут и тут) и тому, как правильно оформлять код, все равно оставалось чувство, что чего-то важного не хватает. Это было похоже на то, как если бы вам дали множество замечательных и полезных инструментов, но забыли главное — объяснить, а как же «проектировать табуретку».

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

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

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

Критерии хорошей архитектуры

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

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

Гибкость системы. Любое приложение приходится менять со временем — изменяются требования, добавляются новые. Чем быстрее и удобнее можно внести изменения в существующий функционал, чем меньше проблем и ошибок это вызовет — тем гибче и конкурентоспособнее система. Поэтому в процессе разработки старайтесь оценивать то, что получается, на предмет того, как вам это потом, возможно, придется менять. Спросите у себя: «А что будет, если текущее архитектурное решение окажется неверным?», «Какое количество кода подвергнется при этом изменениям?». Изменение одного фрагмента системы не должно влиять на ее другие фрагменты. По возможности, архитектурные решения не должны «вырубаться в камне», и последствия архитектурных ошибок должны быть в разумной степени ограничены. «Хорошая архитектура позволяет ОТКЛАДЫВАТЬ принятие ключевых решений» (Боб Мартин) и минимизирует «цену» ошибок.

Расширяемость системы. Возможность добавлять в систему новые сущности и функции, не нарушая ее основной структуры. На начальном этапе в систему имеет смысл закладывать лишь основной и самый необходимый функционал (принцип YAGNI — you ain’t gonna need it, «Вам это не понадобится») Но при этом архитектура должна позволять легко наращивать дополнительный функционал по мере необходимости. Причем так, чтобы внесение наиболее вероятных изменений требовало наименьших усилии.

Требование, чтобы архитектура системы обладала гибкостью и расширяемостью (то есть была способна к изменениям и эволюции) является настолько важным, что оно даже сформулировано в виде отдельного принципа — «Принципа открытости/закрытости» (Open-Closed Principle — второй из пяти принципов SOLID): Программные сущности (классы, модули, функции и т.п.) должны быть открытыми для расширения, но закрытыми для модификации.

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

Это означает, что приложение следует проектировать так, чтобы изменение его поведения и добавление новой функциональности достигалось бы за счет написания нового кода (расширения), и при этом не приходилось бы менять уже существующий код. В таком случае появление новых требований не повлечет за собой модификацию существующей логики, а сможет быть реализовано прежде всего за счет ее расширения. Именно этот принцип является основой «плагинной архитектуры» (Plugin Architecture). О том, за счет каких техник это может быть достигнуто, будет рассказано дальше.

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

Тестируемость. Код, который легче тестировать, будет содержать меньше ошибок и надежнее работать. Но тесты не только улучшают качество кода. Многие разработчики приходят к выводу, что требование «хорошей тестируемости» является также направляющей силой, автоматически ведущей к хорошему дизайну, и одновременно одним из важнейших критериев, позволяющих оценить его качество: «Используйте принцип «тестируемости» класса в качестве «лакмусовой бумажки» хорошего дизайна класса. Даже если вы не напишите ни строчки тестового кода, ответ на этот вопрос в 90% случаев поможет понять, насколько все «хорошо» или «плохо» с его дизайном» (Идеальная архитектура).

Существует целая методология разработки программ на основе тестов, которая так и называется — Разработка через тестирование (Test-Driven Development, TDD).

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

Хорошо структурированный, читаемый и понятный код. Сопровождаемость. Над программой, как правило, работает множество людей — одни уходят, приходят новые. После написания сопровождать программу тоже, как правило, приходится людям, не участвовавшем в ее разработке. Поэтому хорошая архитектура должна давать возможность относительно легко и быстро разобраться в системе новым людям. Проект должен быть хорошо структурирован, не содержать дублирования, иметь хорошо оформленный код и желательно документацию. И по возможности в системе лучше применять стандартные, общепринятые решения привычные для программистов. Чем экзотичнее система, тем сложнее ее понять другим (Принцип наименьшего удивления — Principle of least astonishment. Обычно, он используется в отношении пользовательского интерфейса, но применим и к написанию кода).

Ну и для полноты критерии плохого дизайна:

  1. Его тяжело изменить, поскольку любое изменение влияет на слишком большое количество других частей системы. (Жесткость, Rigidity).
  2. При внесении изменений неожиданно ломаются другие части системы. (Хрупкость, Fragility).
  3. Код тяжело использовать повторно в другом приложении, поскольку его слишком тяжело «выпутать» из текущего приложения. (Неподвижность, Immobility).

Модульная архитектура. Декомпозиция как основа

software architecture

Не смотря на разнообразие критериев, все же главной при разработке больших систем считается задача снижения сложности. А для снижения сложности ничего, кроме деления на части, пока не придумано. Иногда это называют принципом «разделяй и властвуй» (divide et impera), но по сути речь идет об иерархической декомпозиции. Сложная система должна строится из небольшого количества более простых подсистем, каждая из которых, в свою очередь, строится из частей меньшего размера, и т.д., до тех пор, пока самые небольшие части не будут достаточно просты для непосредственного понимания и создания.

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

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

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

  • Масштабируемость (Scalability)
    возможность расширять систему и увеличивать ее производительность, за счет добавления новых модулей.
  • Ремонтопригодность (Maintainability)
    изменение одного модуля не требует изменения других модулей
  • Заменимость модулей (Swappability)
    модуль легко заменить на другой
  • Возможность тестирования (Unit Testing)
    модуль можно отсоединить от всех остальных и протестировать / починить
  • Переиспользование (Reusability)
    модуль может быть переиспользован в других программах и другом окружении
  • Сопровождаемость (Maintenance)
    разбитую на модули программу легче понимать и сопровождать

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

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

«Архитектура — это организация системы, воплощенная в ее компонентах, их отношениях между собой и с окружением.
Система — это набор компонентов, объединенных для выполнения определенной функции.«

Таким образом, хорошая архитектура это, прежде всего, модульная/блочная архитектура. Чтобы получить хорошую архитектуру надо знать, как правильно делать декомпозицию системы. А значит, необходимо понимать — какая декомпозиция считается «правильной» и каким образом ее лучше проводить?

«Правильная» декомпозиция

1. Иерархическая

Не стоит сходу рубить приложение на сотни классов. Как уже говорилось, декомпозицию надо проводить иерархически — сначала систему разбивают на крупные функциональные модули/подсистемы, описывающие ее работу в самом общем виде. Затем, полученные модули, анализируются более детально и, в свою очередь, делятся на под-модули либо на объекты.

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

software decomposition

Эта мысль, при всей своей очевидности, не так банальна как кажется. Например, в чем заключается суть такого распространенного «архитектурного шаблона» как Модель-Вид-Контроллер (MVC)? Всего навсего в отделении представления от бизнес-логики, то есть в том, что любое пользовательское приложение вначале делится на два модуля — один из которых отвечает за реализацию собственно самой бизнес логики (Модель), а второй — за взаимодействие с пользователем (Пользовательский Интерфейс или Представление). Затем, для того чтобы эти модули могли разрабатываться независимо, связь между ними ослабляется с помощью паттерна «Наблюдатель» (подробно о способах ослабления связей будет рассказано дальше) и мы фактически получаем один из самых мощных и востребованных «шаблонов», которые используются в настоящее время.

Типичными модулями первого уровня (полученными в результате первого деления системы на наиболее крупные составные части) как раз и являются — «бизнес-логика», «пользовательский интерфейс», «доступ к БД», «связь с конкретным оборудованием или ОС».

Для обозримости на каждом иерархическом уровне рекомендуют выделять от 2 до 7 модулей.

2. Функциональная

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

Модуль = Функция + Данные, необходимые для ее выполнения.

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

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

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

3. High Cohesion + Low Coupling
Самым же главным критерием качества декомпозиции является то, насколько модули сфокусированы на решение своих задач и независимы. Обычно это формулируют следующим образом: «Модули, полученные в результате декомпозиции, должны быть максимально сопряженны внутри (high internal cohesion) и минимально связанны друг с другом (low external coupling).«

  • High Cohesion, высокая сопряженность или «сплоченность» внутри модуля, говорит о том, модуль сфокусирован на решении одной узкой проблемы, а не занимается выполнением разнородных функций или несвязанных между собой обязанностей. (Сопряженностьcohesion, характеризует степень, в которой задачи, выполняемые модулем, связаны друг с другом )

    Следствием High Cohesion является принцип единственной ответственности (Single Responsibility Principle — первый из пяти принципов SOLID), согласно которому любой объект/модуль должен иметь лишь одну обязанность и соответственно не должно быть больше одной причины для его изменения.

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

    Это значит, что при правильном проектировании, при изменении одного модуля, не придется править другие или эти изменения будут минимальными. Чем слабее связанность, тем легче писать/понимать/расширять/чинить программу.

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

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

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

Делая декомпозицию системы желательно проверять ее качество задавая себе вопросы: «Какую функцию выполняет каждый модуль?«, “Насколько модули легко тестировать?”, “Возможно ли использовать модули самостоятельно или в другом окружении?”, “Как сильно изменения в одном модуле отразятся на остальных?

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

Как ослаблять связанность между модулями

Для наглядности, картинка из неплохой статьи «Decoupling of Object-Oriented Systems», иллюстрирующая основные моменты, о которых будет идти речь.

Decoupling architecture

1. Интерфейсы. Фасад

Главным, что позволяет уменьшать связанность системы, являются конечно же Интерфейсы (и стоящий за ними принцип Инкапсуляция + Абстракция + Полиморфизм):

  • Модули должны быть друг для друга «черными ящиками» (инкапсуляция). Это означает, что один модуль не должен «лезть» внутрь другого модуля и что либо знать о его внутренней структуре. Объекты одной подсистемы не должны обращаться напрямую к объектам другой подсистемы
  • Модули/подсистемы должны взаимодействовать друг с другом лишь посредством интерфейсов (то есть, абстракций, не зависящих от деталей реализации) Соответственно каждый модуль должен иметь четко определенный интерфейс или интерфейсы для взаимодействия с другими модулями.

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

Благодаря интерфейсам и полиморфизму, как раз и достигается возможность модифицировать и расширять код, без изменения того, что уже написано (Open-Closed Principle). До тех пор, пока взаимодействие модулей описано исключительно в виде интерфейсов, и не завязано на конкретные реализации, мы имеем возможность абсолютно «безболезненно» для системы заменить один модуль на любой другой, реализующий тот же самый интерфейс, а также добавить новый и тем самым расширить функциональность. Это как в конструкторе или «плагинной архитектуре» (plugin architecture) — интерфейс служит своего рода коннектором, куда может быть подключен любой модуль с подходящим разъемом. Гибкость конструктора обеспечивается тем, что мы можем просто заменить одни модули/«детали» на другие, с такими же разъемами (с тем же интерфейсом), а также добавить сколько угодно новых деталей (при этом уже существующие детали никак не изменяются и не переделываются). Подробнее про Open-Closed Principle и про то, как он может быть реализован можно почитать тут + хорошая статья на английском.

Интерфейсы позволяют строить систему более высокого уровня, рассматривая каждую подсистему как единое целое и игнорируя ее внутреннее устройство. Они дают возможность модулям взаимодействовать и при этом ничего не знать о внутренней структуре друг друга, тем самым в полной мере реализуя принцип минимального знания, являющейся основой слабой связанности. Причем, чем в более общей/абстрактной форме определены интерфейсы и чем меньше ограничений они накладывают на взаимодействие, тем гибче система. Отсюда фактически следует еще один из принципов SOLID — Принцип разделения интерфейса (Interface Segregation Principle), который выступает против «толстых интерфейсов» и говорит, что большие, объемные интерфейсы надо разбивать на более маленькие и специфические, чтобы клиенты маленьких интерфейсов (зависящие модули) знали только о методах, которые необходимы им в работе. Формулируется он следующим образом: «Клиенты не должны зависеть от методов (знать о методах), которые они не используют» или “Много специализированных интерфейсов лучше, чем один универсальный”.

Итак, когда взаимодействие и зависимости модулей описываются лишь с помощью интерфейсов, те есть абстракций, без использования знаний об их внутреннем устройстве и структуре, то фактически тем самым реализуется инкапсуляция, плюс мы имеем возможность расширять/изменять поведения системы за счет добавления и использования различных реализаций, то есть за счет полиморфизма. Из этого следует, что концепция интерфейсов включает в себя и в некотором смысле обобщает почти все основные принципы ООП — Инкапсуляцию, Абстракцию, Полиморфизм. Но тут возникает один вопрос. Когда проектирование идет не на уровне объектов, которые сами же и реализуют соответствующие интерфейсы, а на уровне модулей, то что является реализацией интерфейса модуля? Ответ: если говорить языком шаблонов, то как вариант, за реализацию интерфейса модуля может отвечать специальный объект — Фасад.

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

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

Facade Decoupling architecture

Замечание: Хотя большинство программистов понимают важность интерфейсов при проектировании классов (объектов), складывается впечатление, что идея необходимости использовать интерфейсы также и на уровне модулей только зарождается. Мне встретилось очень мало статей и проектов, где интерфейсы бы применялись для ослабления связанности между модулями/слоями и соответственно использовался бы паттерн «Фасад». Кто, например, видел «Фасад» на схемах уже упоминавшегося «архитектурного шаблона» Модель-Вид-Контроллер, или хотя бы слышал его упоминание среди паттернов, входящих в состав MVC (наряду с Observer и Composite)? А ведь он там должен быть, поскольку Модель это не класс, это модуль, причем центральный. И у создателя MVC Трюгве Реенскауга он, конечно же, был (смотрим «The Model-View-Controller (MVC ). Its Past and Present», только учитываем, что это писалось в 1973 году и то, что мы сейчас называем Представлением — Presentaition/UI тогда называлось Editior). Странным образом «Фасад» потерялся на многие годы и вновь обнаружить его мне удалось лишь недавно, в основном, в обобщенном варианте MVC от Microsoft («Microsoft Application Architecture Guide»). Вот соответствующие слайды:

Facade MVC Model-View-Controller Decoupling

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

Facade MVC Model-View-Controller Decoupling

2. Dependency Inversion. Корректное создание и получение зависимостей

Формально, требование, чтобы модули не содержали ссылок на конкретные реализации, а все зависимости и взаимодействие между ними строились исключительно на основе абстракций, то есть интерфейсов, выражается принципом Инвертирования зависимостей (Dependency Inversion — последний из пяти принципов SOLID):

  • Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Реализация должна зависеть от абстракции.

У этого принципа не самая очевидная формулировка, но суть его, как и было сказано, выражается правилом: «Все зависимости должны быть в виде интерфейсов». Подробно и очень хорошо принцип инвертирования зависимостей разбирается в статье Модульный дизайн или «что такое DIP, SRP, IoC, DI и т.п.». Статья из разряда must-read, лучшее, что доводилось читать по архитектуре ПО.

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

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

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

Ну а теперь разберем подробнее, как это делается на практике и каким образом модули могут корректно создавать и получать свои «зависимости», не нарушая принципа Dependency Inversion.

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

  • что модуль делает, какую функцию выполняет
  • что модулю нужно от его окружения, то есть с какими объектами/модулями ему придется иметь дело и
  • как он это будет получать

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

  1. Модуль сам создает объекты необходимые ему для работы.

    Но, как и было сказано, модуль не может это сделать напрямую — для создания необходимо вызвать конструктор конкретного типа, и в результате модуль будет зависеть не от интерфейса, а от конкретной реализации. Решить проблему в данном случае позволяет шаблон Фабричный Метод (Factory Method).

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

    В случаях, когда нужно создавать группы или семейства взаимосвязанных объектов, вместо Фабричного Метода используется Абстрактная Фабрика (Abstract factory).

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

    Этот подход реализуется шаблоном Локатор Сервисов (Service Locator), основная идея которого заключается в том, что в программе имеется объект, знающий, как получить все зависимости (сервисы), которые могут потребоваться.

    Главное отличие от фабрик в том, что Service Locator не создаёт объекты, а фактически уже содержит в себе инстанцированные объекты (или знает где/как их получить, а если и создает, то только один раз при первом обращении). Фабрика при каждом обращении создает новый объект, который вы получаете в полную собственность и можете делать с ним что хотите. Локатор же сервисов выдает ссылки на одни и те же, уже существующие объекты. Поэтому с объектами, выданными Service Locator, нужно быть очень осторожным, так как одновременно с вами ими может пользоваться кто-то еще.

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

    Вообще говоря, Service Locator иногда называют антипаттерном и не рекомендуют использовать (главным образом потому, что он создает неявные связности и дает лишь видимость хорошего дизайна). Подробно можно почитать у Марка Симана:
    Service Locator is an Anti-Pattern
    Abstract Factory or Service Locator?

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

    Это так и называется — Внедрение Зависимостей (Dependency Injection). Обычно требуемые зависимости передаются либо в качестве параметров конструктора (Constructor Injection), либо через методы класса (Setter injection).

    Такой подход инвертирует процесс создания зависимости — вместо самого модуля создание зависимостей контролирует кто-то извне. Модуль из активного элемента, становится пассивным — не он делает, а для него делают. Такое изменение направления действия называется Инверсия Контроля (Inversion of Control), или Принцип Голливуда — «Не звоните нам, мы сами вам позвоним».

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

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

Более подробно и с примерами о способах создания и получения зависимостей можно почитать, например, в этой статье (только надо иметь ввиду, что хотя автор пишет о Dependency Inversion, он использует термин Inversion of Control; возможно потому, что в русской википедии содержится ошибка и этим терминам даны одинаковые определения). А принцип Inversion of Control (вместе с Dependency Injection и Service Locator) детально разбирается Мартином Фаулером и есть переводы обеих его статей: «Inversion of Control Containers and the Dependency Injection pattern» и “Inversion of Control”.

Не будет преувеличением сказать, что использование интерфейсов для описания зависимостей между модулями (Dependency Inversion) + корректное создание и внедрение этих зависимостей (прежде всего Dependency Injection) являются центральными/базовыми техниками для снижения связанности. Они служат тем фундаментом, на котором вообще держится слабая связанность кода, его гибкость, устойчивость к изменениям, переиспользование, и без которого все остальные техники имеют мало смысла. Но, если с фундаментом все в порядке, то знание дополнительных приемов может быть очень даже полезным. Поэтому продолжим.

3. Замена прямых зависимостей на обмен сообщениями

Иногда модулю нужно всего лишь известить других о том, что в нем произошли какие-то события/изменения и ему не важно, что с этой информацией будет происходить потом. В этом случае модулям вовсе нет необходимости «знать друг о друге», то есть содержать прямые ссылки и взаимодействовать непосредственно, а достаточно всего лишь обмениваться сообщениями (messages) или событиями (events).

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

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

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

  • Посредник (Mediator). Применяется, когда между модулями имеется зависимость «многие ко многим. Медиатор выступает в качестве посредника в общении между модулями, действуя как центр связи и избавляет модули от необходимости явно ссылаться друг на друга. В результате взаимодействие модулей друг с другом («все со всеми») заменяется взаимодействием модулей лишь с посредником («один со всеми»). Говорят, что посредник инкапсулирует взаимодействие между множеством модулей.

    Mediator Посредник

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

Дополнение: Модули могут пересылать друг другу не только «простые сообщения, но и объекты-команды. Такое взаимодействие описывается шаблоном Команда (Command). Суть заключается в инкапсулировании запроса на выполнение определенного действия в виде отдельного объекта (фактически этот объект содержит один единственный метод execute()), что позволяет затем передавать это действие другим модулям на выполнение в качестве параметра, и вообще производить с объектом-командой любые операции, какие могут быть произведены над обычными объектами. Кратко рассмотрен тут, соответствующая глава из книги банды четырех тут, есть также статья на хабре.

4. Замена прямых зависимостей на синхронизацию через общее ядро

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

Ядро-посредник может как знать о модулях-клиентах и управлять ими (пример — архитектура apache ), так и может быть полностью, или почти полностью, независимым и ничего о клиентах не знать. В сущности именно этот подход реализован в «шаблоне» Модель-Вид-Контроллер (MVC), где с одной Моделью (являющейся ядром приложение и общим хранилищем данных) могут взаимодействовать множество Пользовательских Интерфейсов, которые работают синхронно и при этом не знают друг о друге, а Модель не знает о них. Ничто не мешает подключить к общей модели и синхронизировать таким образом не только интерфейсы, но и другие вспомогательные модули.

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

5. Закон Деметры (law of Demeter)

Закон Деметры запрещает использование неявных зависимостей: «Объект A не должен иметь возможность получить непосредственный доступ к объекту C, если у объекта A есть доступ к объекту B и у объекта B есть доступ к объекту C«. Java-пример.

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

Закон Деметры реализует уже упоминавшийся «принцип минимального знания», являющейся основой слабой связанности и заключающийся в том, что объект/модуль должен знать как можно меньше деталей о структуре и свойствах других объектов/модулей и вообще чего угодно, включая собственные подкомпоненты. Аналогия из жизни: Если Вы хотите, чтобы собака побежала, глупо командовать ее лапами, лучше отдать команду собаке, а она уже разберётся со своими лапами сама.

6. Композиция вместо наследования

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

Могу только посоветовать в данном контексте обратить внимание на шаблон Делегат (Delegation/Delegate) и пришедший из игр шаблон Компонет (Component), который подробно описан в книге «Game Programming Patterns» (соответствующая глава из этой книги на английском и ее перевод).

Что почитать

Статьи в интернете:

  • Design patterns for decoupling (небольшая полезная глава из UML Tutorial);
  • Немного про архитектуру;
  • Patterns For Large-Scale JavaScript Application Architecture;
  • Слабое связывание компонентов в JavaScript;
  • Как писать тестируемый код — статья из которой хорошо видно что критерии тестируемости кода и хорошего дизайна совпадают;
  • сайт Мартина Фаулера.

Замечательный ресурс — Архитектура приложений с открытым исходным кодом, где «авторы четырех дюжин приложений с открытым исходным кодом рассказывают о структуре созданных ими программ и о том, как эти программы создавались. Каковы их основные компоненты? Как они взаимодействуют? И что открыли для себя их создатели в процессе разработки? В ответах на эти вопросы авторы статей, собранных в данных книгах, дают вам уникальную возможность проникнуть в то, как они творят«. Одна из статей полностью была опубликована на хабре — «Масштабируемая веб-архитектура и распределенные системы».

Интересные решения и идеи можно найти в материалах, посвященных разработке игр. Game Programming Patterns — большой сайт с подробным описанием многих шаблонов и примерами их применения к задаче создания игр (оказывается, есть уже его перевод — «Шаблоны игрового программирования», спасибо strannik_k за ссылку). Возможно будет полезна также статья «Гибкая и масштабируемая архитектура для компьютерных игр» (и ее оригинал. Нужно только иметь ввиду что автор почему-то композицию называет шаблоном «Наблюдатель»).

По поводу паттернов проектирования:

  • Интересная «Мысль про паттерны проектирования»;
  • Удобный сайт с краткими описаниями и схемами всех шаблонов проектирования — «Обзор паттернов проектирования»;
  • сайт на базе книги банды четырех, где все шаблоны описаны очень подробно — «Шаблоны [Patterns] проектирования».

Есть еще принципы/паттерны GRASP, описанные Крэгом Лэрманом в книге «Применение UML 2.0 и шаблонов проектирования», но они больше запутывают чем проясняют. Краткий обзор и обсуждение на хабре (самое ценное в комментариях).

Ну и конечно же книги:

  • Мартин Фаулер «Архитектура корпоративных программных приложений»;
  • Стив Макконнелл «Совершенный код»;
  • Шаблоны проектирования от банды четырех (Gang of Four, GoF) — «Приемы объектно-ориентированного проектирования. Паттерны проектирования».

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

  • база данных;

  • процесс;

  • подсистема;

  • вычислительный узел;

  • библиотека;

  • и др.

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

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

Архитектура приложений

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

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

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

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

  • СЕО-специалиста и маркетолога в их интересах, чтобы приложение обладало уникальными функциями и было конкурентоспособным;

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

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

Архитектура приложений: выбор и виды

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

  • заинтересованных лицах: заказчиках, пользователях, разработчиках и др.;

  • миссии приложения отсюда вытекают требования к приложению;

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

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

  • архитектура «клиент-сервер»;

  • монолитная архитектура;

  • микропроцессорная архитектура;

  • событийная;

  • структурированная;

  • многослойная;

  • трехуровневая;

  • сервис-ориентированная;

  • поиск-ориентированная;

  • неявная;

  • и др.

Архитектура приложений: распространенные виды

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

Многослойная архитектура

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

Многослойная архитектура разделяет программу на следующие слои:

  • слой представления отвечает за интерфейс пользователя;

  • слой бизнес-логики отвечает за функционал и логику приложения и не отвечает за интерфейс;

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

Каждый элемент в приложении «проходит» через все слои. Такая архитектура считается:

  • простой в реализации;

  • абстрактной;

  • защищенной за счет изолированности каждого слоя;

  • легко управляемой и масштабируемой.

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

Многоуровневая архитектура приложений

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

  • одноуровневая,

  • двухуровневая,

  • трехуровневая.

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

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

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

  • сервер отвечает за работу хранилищ и баз данных, а также за обработку информации.

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

  • клиент,

  • сервер для обработки,

  • базы данных для хранения.

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

Микросервисная архитектура приложений 

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

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

  • изолированность каждого отдельного компонента;

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

  • удобно масштабировать и внедрять обновления;

  • и др.

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

Заключение

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

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

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

mobile app download growth statisticsС недавним ростом спроса на мобильные приложения, связанные с бизнесом, увеличился и ассортимент предлагаемых мобильных приложений. Сегодня предприятия могут использовать традиционный путь создания мобильного приложения с помощью команды разработчиков программного обеспечения. Однако на выбор также предлагается множество готовых для платформы мобильных приложений «с низким кодом и без кода». Эти варианты разработки мобильных приложений еще больше упростили бизнес-процессы. Это помогает им быть более отзывчивыми к своим клиентам без необходимости быть экспертами в области кодирования. Предприятия должны иметь базовое понимание архитектуры мобильных приложений, чтобы привлекать и поддерживать интерес пользователей своих мобильных приложений.

Что такое архитектура мобильных приложений?

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

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

3 архитектуры дизайна

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

  • SOLID архитектура мобильного приложения
  • KISS архитектура мобильных приложений
  • DRY архитектура мобильных приложений

SOLID-архитектура

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

KISS-архитектура

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

DRY-архитектура

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

ДОПОЛНИТЕЛЬНЫЙ ПРИНЦИП МОБИЛЬНОЙ АРХИТЕКТУРЫ

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

ЧИСТАЯ АРХИТЕКТУРА

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

Каковы основные элементы мобильной архитектуры?

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

Пользовательский опыт или UX-дизайн

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

UI and UX designИсточник: Dribbble

Пропускная способность сети или сетевая стратегия

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

Стратегия навигации

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

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

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

Используемое устройство

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

Как мобильные приложения выбирают архитектуру?

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

Логичная и четко определенная

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

Универсальное использование на всех платформах

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

Масштабируемый технологический стек

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

Полностью функциональная

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

Низкая стоимость обслуживания

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

Каковы 3 уровня веб-приложений?

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

  • Слой 1 — Презентация
  • Слой 2 — бизнес
  • Уровень 3 — данные

Презентация

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

Бизнес

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

Данные

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

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

Итак, какая архитектура считается лучшей для мобильных приложений? И почему? Чтобы выбрать лучший тип архитектуры мобильного приложения для ваших приложений, разработчикам необходимо учитывать платформу, предполагаемых конечных пользователей, процессы обработки данных, ключевые функции приложения и бюджет проекта. Обычно команды разработчиков и разработчики имеют возможность выбрать архитектуру мобильного приложения по своему вкусу. Для начала работы они могут выбрать один из трех вариантов, а именно: нативную, веб-ориентированную и гибридную мобильную архитектуру и технологические стеки. Выбор зависит от их личных предпочтений и стиля, который им удобен. Однако если вы ищете вариант разработки мобильных приложений без кода или с низким кодом, App Master — отличный и удобный способ начать работу без лишних хлопот.

no code mobile builder

Нативные мобильные приложения

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

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

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

Мобильные веб-приложения

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

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

Гибридные мобильные приложения

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

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

ПРЕИМУЩЕСТВА

  • Более широкая целевая аудитория
  • Простота и быстрота разработки
  • Низкая стоимость создания
  • Низкая стоимость обслуживания
  • Широкая интеграция

Что такое диаграмма архитектуры мобильных приложений?

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

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

  • Идентифицировать системный процесс
  • Позволяет обратную связь
  • Дает визуальный контекст

Идентифицировать системные процессы

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

Позволяет обратную связь

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

Дает визуальный контекст

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

Каковы этапы создания базовых мобильных приложений?

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

  • Определение реалистичного бюджета
  • Фаза обнаружения архитектуры приложения
  • Самые необходимые функции приложения
  • Выбор подходящей платформы
  • Создание MVP приложения
  • Тестирование приложения перед запуском
  • Запуск готового приложения
  • Регулярное обслуживание приложения
  • Отслеживание показателей приложения

Составьте реалистичный бюджет

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

Фаза обнаружения

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

Выберите функции приложения

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

Выберите платформу

Пользовательский интерфейс (UI) и пользовательский опыт или UX дизайн архитектуры мобильного приложения будет зависеть от выбранной платформы. UI и UX должны быть способны взаимодействовать с платформой мобильного приложения. Архитектура мобильного приложения должна способствовать бесшовному взаимодействию, независимо от того, является ли мобильная платформа Android, веб-платформой или iOS,

Создайте MVP

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

Тестирование мобильного приложения

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

User experienceИсточник: Dribbble

Запуск мобильного приложения

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

Обслуживание приложения

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

Отслеживание показателей приложения

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

Итог

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

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

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

Мотивация

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

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

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

А что есть?

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

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

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

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

Требования к нотации

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

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

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

Структура диаграммы

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

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

Компоненты

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

Это может быть:

  • Контроллер, которые обрабатывает входящие запросы.
  • Сервис, отвечающий за бизнес логику работы с платежами.
  • Репозиторий, взаимодействующий с базой данных.
  • Обработчик event событий при использовании event sourcing.
  • Бизнес entity пользователя, содержащие поля для него и методы работы.

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

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

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

Примеры:

Компоненты

Внутренние связи

Неразрывной стрелкой показываются связи между компонентами системы. При этом, направление стрелки указывает направление зависимости (вызова методов). Если комплект UserContoller требует вызова метода из UserService:

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

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

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

Внутренние связи

Внешние связи

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

В описании можно дать:

  • Название команды
  • Тип запроса
  • Название внешнего сервиса.

Примеры:

Внешние связи

Модули

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

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

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

Модульности может меняться от одного архитектурного подхода к другому. Например, для Nest.js или Angular уже есть такое понятие как модуль, а для React модулем может выступать папка, в которой группированы те или иные компоненты. Для С# это может быть Namespace или логически выделенный кусок приложения.

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

Модули

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

Приложения

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

Приложения

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

Передаваемые объекты

При передаче данных между компонентами системы, может возникнуть необходимость описать их структуру. Обычно такие объекты носят название DTO (data transfer object). Они обозначаются прямоугольниками с закруглёнными на 50% углами. Можно добавить не только название DTO, но и его тип: event, query, command.

Передаваемые объекты

Детализация

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

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

При этом если для вашего обсуждения детализация не нужна, не тратьте на неё время.

Детализация

Пример

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

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

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

Пример

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

Использование

Вы можете использовать эту нотацию в любом удобном для вас инструменте, например draw.io или visio. Но для удобства я подготовил для вас готовую библиотеку в Figma, которая позволит быстро рисовать диаграммы, добавляя компоненты из библиотеки. Вы можете скачать её тут. После открытия ссылки нажмите на Duplicate и получите готовую библиотеку у себя в figma. Перейдя на вкладку Assets вы получите готовые компоненты, которые можете перетаскивать и изменять по своему желанию.

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

Надеюсь эта нотация пригодиться и вам!

Привет, Хабр! Представляю вашему вниманию вольный перевод «Руководство по архитектуре приложения (Guide to app architecture)» из JetPack. Все замечания по переводу прошу оставлять в комментариях, и они будут исправлены. Так же для всех будут полезны комментарии от тех кто использовал представленную архитектуру с рекомендациями её использования.

Это руководство охватывает лучшие практики и рекомендуемую архитектуру для создания надежных приложений. Эта страница предполагает базовое знакомство с Android Framework. Если вы новичок в разработке приложений для Android, ознакомьтесь с нашими руководствами для разработчиков, чтобы начать работу и узнать больше о концепциях, упомянутых в этом руководстве. Если вы интересуетесь архитектурой приложений и хотели бы ознакомиться с материалами этого руководства с точки зрения программирования на Kotlin, ознакомьтесь с курсом Udacity «Разработка приложений для Android с помощью Kotlin».

Опыт пользователя мобильного приложения

В большинстве случаев настольные приложения имеют единую точку входа с рабочего стола или программы запуска, а затем запускаются как единый монолитный процесс. Приложения на Android имеют гораздо более сложную структуру. Типичное приложение для Android содержит несколько компонентов приложения, включая Activities, Fragments, Services, ContentProviders и BroadcastReceivers.

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

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

  1. Приложение вызывает намерение (Intent) камеры. Android запускает приложение камеры для обработки запроса. На данный момент пользователь покинул приложение для социальных сетей, и его опыт как пользователя безупречен.
  2. Приложение камеры может вызывать другие намерения, например запуск средства выбора файлов, которое может запустить еще одно приложение.
  3. В конце концов, пользователь возвращается в приложение социальной сети и делится фотографией.

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

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

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

Общие архитектурные принципы

Если вы не должны использовать компоненты приложения для хранения данных и состояния приложения, как вы должны разрабатывать свое приложение?

Разделение ответственности

Самый важный принцип, которому нужно следовать, — это разделение ответственности. Распространена ошибка, когда вы пишете весь свой код в Activity или Fragment. Это классы пользовательского интерфейса которые должны содержать только логику обрабатывающую взаимодействие пользовательского интерфейса и операционной системы. Как можно больше разделяя ответственность в этих классах (SRP), вы можете избежать многих проблем, связанных с жизненным циклом приложения.

Управление пользовательским интерфейсом из модели

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

Постоянная модель идеально подходит по следующим причинам:

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

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

Рекомендуемая архитектура приложения

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

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

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

Обзор

Для начала рассмотрим схему взаимодействия модулей архитектуры готового приложения:

Обратите внимание, что каждый компонент зависит только от компонента на один уровень ниже его. Например, Activity и Fragments зависят только от модели представления. Repository является единственным классом, который зависит от множества других классов; в этом примере хранилище зависит от постоянной модели данных и удаленного внутреннего источника данных.

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

Создаём пользовательский интерфейс

Пользовательский интерфейс состоит из фрагмента UserProfileFragment и соответствующего ему файла макета user_profile_layout.xml.

Для управления пользовательским интерфейсом наша модель данных должна содержать следующие элементы данных:

  • User ID: идентификатор пользователя. Лучшим решением является передача этой информации во фрагмент, используя аргументы фрагмента. Если ОС Android разрушает наш процесс, эта информация сохраняется, поэтому идентификатор будет доступен при следующем запуске нашего приложения.
  • User object: класс данных, который содержит сведения о пользователе.

Мы используем UserProfileViewModel, основанный на компоненте архитектуры ViewModel, чтобы сохранить эту информацию.

Объект ViewModel предоставляет данные для определенного компонента пользовательского интерфейса, таких как fragment или Activity, и содержит бизнес-логику обработки данных для взаимодействия с моделью. Например, ViewModel может вызывать другие компоненты для загрузки данных и может пересылать запросы пользователей на изменение данных. ViewModel не знает о компонентах пользовательского интерфейса, поэтому на него не влияют изменения конфигурации, такие как воссоздание Activity при повороте устройства.

Теперь мы определили следующие файлы:

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

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

class UserProfileViewModel : ViewModel() {
   val userId : String = TODO()
   val user : User = TODO()
}

class UserProfileFragment : Fragment() {
   private val viewModel: UserProfileViewModel by viewModels()

   override fun onCreateView(
       inflater: LayoutInflater, container: ViewGroup?,
       savedInstanceState: Bundle?
   ): View {
       return inflater.inflate(R.layout.main_fragment, container, false)
   }
}

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

Примечание. SavedStateHandle позволяет ViewModel получить доступ к сохраненному состоянию и аргументам связанного фрагмента или действия.

// UserProfileViewModel
class UserProfileViewModel(
   savedStateHandle: SavedStateHandle
) : ViewModel() {
   val userId : String = savedStateHandle["uid"] ?:
          throw IllegalArgumentException("missing user id")
   val user : User = TODO()
}

// UserProfileFragment
private val viewModel: UserProfileViewModel by viewModels(
   factoryProducer = { SavedStateVMFactory(this) }
   ...
)

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

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

Примечание. Если вы уже используете такие библиотеки, как RxJava или Agera, вы можете продолжать использовать их вместо LiveData. Однако при использовании библиотек и подобных подходов убедитесь, что вы правильно обрабатываете жизненный цикл своего приложения. В частности, убедитесь, что вы приостановили свои потоки данных, когда связанный LifecycleOwner остановлен, и уничтожили эти потоки, когда связанный LifecycleOwner был уничтожен. Вы также можете добавить артефакт android.arch.lifecycle: реактивные потоки, чтобы использовать LiveData с другой библиотекой реактивных потоков, такой как RxJava2.

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

class UserProfileViewModel(
   savedStateHandle: SavedStateHandle
) : ViewModel() {
   val userId : String = savedStateHandle["uid"] ?:
          throw IllegalArgumentException("missing user id")
   val user : LiveData<User> = TODO()
}

Теперь модифицируем UserProfileFragment для наблюдения за данными во ViewModel и для обновления пользовательского интерфейса в соответствии с изменениями:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   viewModel.user.observe(viewLifecycleOwner) {
       // обновляем UI
   }
}

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

Если вы знакомы с другими библиотеками, в которых используются наблюдаемые обратные вызовы, возможно, вы поняли, что мы не переопределили метод onStop() фрагмента, чтобы прекратить наблюдать за данными. Этот шаг не является обязательным для LiveData, поскольку он поддерживает жизненный цикл, это означает, что он не вызовет обратный вызов onChanged(), если фрагмент находится в неактивном состоянии; то есть он получил вызовonStart(), но еще не получил onStop()). LiveData также автоматически удаляет наблюдателя при вызове метода onDestroy() у фрагмента.

Мы не добавили никакой логики для обработки изменений конфигурации, таких как поворот экрана устройства пользователем. UserProfileViewModel автоматически восстанавливается при изменении конфигурации, поэтому, как только создается новый фрагмент, он получает тот же экземпляр ViewModel, и обратный вызов вызывается немедленно с использованием текущих данных. Учитывая, что объекты ViewModel предназначены для того, чтобы пережить соответствующие объекты View, которые они обновляют, вы не должны включать прямые ссылки на объекты View в вашу реализацию ViewModel. Для получения дополнительной информации о сроке службы ViewModel соответствует жизненному циклу компонентов пользовательского интерфейса, см. Жизненный цикл ViewModel.

Получение данных

Теперь, когда мы использовали LiveData для подключения UserProfileViewModel к UserProfileFragment, как мы можем получить данные профиля пользователя?

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

Вот наше определение Webservice , который связывается с нашим backend-ом:

interface Webservice {
   /**
    * @GET declares an HTTP GET request
    * @Path("user") annotation on the userId parameter marks it as a
    * replacement for the {user} placeholder in the @GET path
    */
   @GET("/users/{user}")
   fun getUser(@Path("user") userId: String): Call<User>
}

Первая идея для реализации ViewModel может включать прямой вызов Webservice для извлечения данных и назначения этих данных нашему объекту LiveData. Этот дизайн работает, но с его использованием наше приложение становится все сложнее поддерживать по мере роста. Это дает слишком большую ответственность классу UserProfileViewModel, что нарушает принцип разделения интересов. Кроме того, область действия ViewModel связана с жизненным циклом Activity или Fragment, что означает, что данные из Webservice теряются, когда заканчивается жизненный цикл связанного объекта пользовательского интерфейса. Такое поведение создает нежелательный пользовательский опыт.

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

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

Наш класс UserRepository, показанный в следующем фрагменте кода, использует экземпляр WebService для извлечения данных пользователя:

class UserRepository {
   private val webservice: Webservice = TODO()
   // ...
   fun getUser(userId: String): LiveData<User> {
       // Это не оптимальная реализация. Мы исправим это позже.
       val data = MutableLiveData<User>()
       webservice.getUser(userId).enqueue(object : Callback<User> {
           override fun onResponse(call: Call<User>, response: Response<User>) {
               data.value = response.body()
           }
           // Случай ошибки опущен для краткости.
           override fun onFailure(call: Call<User>, t: Throwable) {
               TODO()
           }
       })
       return data
   }
}

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

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

Управление зависимостями между компонентами

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

Для решения этой проблемы вы можете использовать следующие шаблоны проектирования:

  • Внедрение зависимостей (DI). Внедрение зависимостей позволяет классам определять свои зависимости, не создавая их. Во время выполнения, другой класс отвечает за предоставление этих зависимостей. Мы рекомендуем библиотеку Dagger 2 для реализации внедрения зависимостей в приложениях Android. Dagger 2 автоматически создает объекты, обходя дерево зависимостей, и обеспечивает гарантии времени компиляции для зависимостей.
  • (Service location) Локатор службы: шаблон локатора службы предоставляет реестр, в котором классы могут получать свои зависимости вместо их построения.

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

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

В нашем примере приложения используется Dagger 2 для управления зависимостями объекта Webservice.

Подключите ViewModel и хранилище

Теперь мы модифицируем наш UserProfileViewModel для использования объекта UserRepository:

class UserProfileViewModel @Inject constructor(
   savedStateHandle: SavedStateHandle,
   userRepository: UserRepository
) : ViewModel() {
   val userId : String = savedStateHandle["uid"] ?:
          throw IllegalArgumentException("missing user id")
   val user : LiveData<User> = userRepository.getUser(userId)
}

Кеширование

Реализация UserRepository абстрагирует вызов объекта Webservice, но поскольку он опирается только на один источник данных, он не очень гибок.

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

Эта конструкция является неоптимальной по следующим причинам:

  • Это тратит ценные ресурсы трафика.
  • Это заставляет пользователя ожидать завершения нового запроса.

Чтобы устранить эти недостатки, мы добавляем новый источник данных в наш UserRepository, который кэширует объекты User в памяти:

//Информируем Dagger, что этот класс должен быть создан только единожды.
@Singleton
class UserRepository @Inject constructor(
   private val webservice: Webservice,
   // Простой кэш в памяти. Детали опущены для краткости.
   private val userCache: UserCache
) {
   fun getUser(userId: String): LiveData<User> {
       val cached = userCache.get(userId)
       if (cached != null) {
           return cached
       }
       val data = MutableLiveData<User>()
       userCache.put(userId, data)
       // Эта реализация все еще неоптимальная, но лучше, чем раньше.
       // Полная реализация также обрабатывает случаи ошибок.
       webservice.getUser(userId).enqueue(object : Callback<User> {
           override fun onResponse(call: Call<User>, response: Response<User>) {
               data.value = response.body()
           }

           // Случай ошибки опущен для краткости.
           override fun onFailure(call: Call<User>, t: Throwable) {
               TODO()
           }
       })
       return data
   }
}

Постоянные данные

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

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

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

Правильный способ справиться с этой ситуацией — использовать постоянную модель. Нам на помощь приходит библиотека сохранения постоянных данных (БД) Room.

Room — это библиотека объектно-реляционного отображения (object-mapping), которая обеспечивает локальное сохранение данных с минимальным стандартным кодом. Во время компиляции он проверяет каждый запрос на соответствие вашей схеме данных, поэтому неработающие запросы SQL приводят к ошибкам во время компиляции, а не к сбоям во время выполнения. Room абстрагируется от некоторых базовых деталей реализации работы с необработанными таблицами SQL и запросами. Это также позволяет вам наблюдать за изменениями в данных БД, включая коллекции и запросы на соединение, выставляя такие изменения с помощью объектов LiveData. Он даже явно определяет ограничения выполнения, которые решают общие проблемы с потоками, такие как доступ к хранилищу в основном потоке.

Примечание. Если ваше приложение уже использует другое решение, такое как объектно-реляционное отображение SQLite (ORM), вам не нужно заменять существующее решение на Room. Однако, если вы пишете новое приложение или реорганизуете существующее приложение, мы рекомендуем использовать Room для сохранения данных вашего приложения. Таким образом, вы можете воспользоваться возможностями абстракции библиотеки и проверки запросов.

Чтобы использовать Room, нам нужно определить нашу локальную схему. Сначала мы добавляем аннотацию @Entity в наш класс модели данных User и аннотацию @PrimaryKey в поле id класса. Эти аннотации помечают User как таблицу в нашей базе данных, а id — как первичный ключ таблицы:

@Entity
data class User(
   @PrimaryKey private val id: String,
   private val name: String,
   private val lastName: String
)

Затем мы создаем класс базы данных, реализуя RoomDatabase для нашего приложения:

@Database(entities = [User::class], version = 1)
abstract class UserDatabase : RoomDatabase()

Обратите внимание, что UserDatabase является абстрактной. Библиотека Room автоматически обеспечивает реализацию этого. Подробности смотрите в документации по Room.

Теперь нам нужен способ вставки пользовательских данных в базу данных. Для этой задачи мы создаем объект доступа к данным (DAO).

@Dao
interface UserDao {
   @Insert(onConflict = REPLACE)
   fun save(user: User)

   @Query("SELECT * FROM user WHERE id = :userId")
   fun load(userId: String): LiveData<User>
}

Обратите внимание, что метод load возвращает объект типа LiveData. Room знает, когда база данных изменена, и автоматически уведомляет всех активных наблюдателей об изменении данных. Поскольку Room использует LiveData, эта операция эффективна; он обновляет данные только при наличии хотя бы одного активного наблюдателя.

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

Определив наш класс UserDao, мы затем ссылаемся на DAO из нашего класса базы данных:

@Database(entities = [User::class], version = 1)
abstract class UserDatabase : RoomDatabase() {
   abstract fun userDao(): UserDao
}

Теперь мы можем изменить наш UserRepository, чтобы включить источник данных Room:

// Информирует Dagger, что этот класс должен быть создан только один раз.
@Singleton
class UserRepository @Inject constructor(
   private val webservice: Webservice,
   // Простой кэш в памяти. Детали опущены для краткости.
   private val executor: Executor,
   private val userDao: UserDao
) {
   fun getUser(userId: String): LiveData<User> {
       refreshUser(userId)
       // Возвращает объект LiveData непосредственно из базы данных.
       return userDao.load(userId)
   }

   private fun refreshUser(userId: String) {
       // Работает в фоновом потоке.
       executor.execute {
           // Проверьте, если пользовательские данные были получены недавно.
           val userExists = userDao.hasUser(FRESH_TIMEOUT)
           if (!userExists) {
               // Обновляем данные.
               val response = webservice.getUser(userId).execute()

               // Проверьте на ошибки здесь.

               // Обновляем базу данных. Объект LiveData автоматически обновляется,
               // поэтому нам здесь больше ничего не нужно делать.
               userDao.save(response.body()!!)
           }
       }
   }

   companion object {
       val FRESH_TIMEOUT = TimeUnit.DAYS.toMillis(1)
   }
}

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

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

Единственный источник правды

Обычно разные конечные точки REST API возвращают одни и те же данные. Например, если у нашего бэкэнда есть другая конечная точка, которая возвращает список друзей, один и тот же пользовательский объект может исходить из двух разных конечных точек API, возможно, даже с использованием разных уровней детализации. Если бы UserRepository возвращал ответ от запроса Webservice как есть, без проверки согласованности, наши пользовательские интерфейсы могли бы показывать запутанную информацию, потому что версия и формат данных из хранилища зависели бы от последней вызванной конечной точки.

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

Показывать прогресс операции

В некоторых случаях использования, таких как pull-to-refresh, важно, чтобы пользовательский интерфейс показывал пользователю, что в данный момент выполняется сетевая операция. Рекомендуется отделять действие пользовательского интерфейса от фактических данных, поскольку данные могут обновляться по разным причинам. Например, если мы получили список друзей, тот же пользователь может быть снова выбран программным образом, что приведет к обновлению LiveData. С точки зрения пользовательского интерфейса, факт наличия запроса в полете — это просто еще одна точка данных, аналогичная любой другой части данных в самом объекте User.

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

  • Измените getUser (), чтобы он возвращал объект типа LiveData. Этот объект будет включать в себя статус работы сети. Для примера, смотрите реализацию NetworkBoundResource в проекте GitHub android-Architecture-components.
  • Предоставьте другую общедоступную функцию в классе UserRepository, которая может возвращать состояние обновления пользователя. Этот вариант лучше использовать, если вы хотите отображать состояние сети в вашем пользовательском интерфейсе только в том случае, если процесс извлечения данных возник из явного действия пользователя, такого как pull-to-refresh.

Протестируйте каждый компонент

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

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

  • Пользовательский интерфейс и взаимодействие: используйте инструментарий Android UI тест. Лучший способ создать этот тест — использовать библиотеку Espresso. Вы можете создать фрагмент и предоставить ему макет UserProfileViewModel. Поскольку фрагмент связывается только с UserProfileViewModel, насмешка над этим одним классом достаточна для полного тестирования пользовательского интерфейса вашего приложения.
  • ViewModel: вы можете протестировать класс UserProfileViewModel с помощью теста JUnit. Вам нужно только смоделировать один класс, UserRepository.
  • UserRepository: вы также можете протестировать UserRepository с помощью теста JUnit. Вам нужно испытывать Webservice и UserDao. В этих тестах проверьте следующее поведение:
    • Хранилище делает правильные вызовы веб-службы.
    • Репозиторий сохраняет результаты в базе данных.
    • Хранилище не делает ненужных запросов, если данные кэшируются и обновляются.

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

    Внимание: Room позволяет указать реализацию базы данных, поэтому можно протестировать DAO, предоставив реализацию JSQL для SupportSQLiteOpenHelper. Однако такой подход не рекомендуется, поскольку работающая на устройстве версия SQLite может отличаться от версии SQLite на компьютере разработчика.

  • Веб-сервис: в этих тестах избегайте сетевых вызовов на ваш сервер. Для всех тестов, особенно веб-, важно быть независимым от внешнего мира. Несколько библиотек, включая MockWebServer, могут помочь вам создать поддельный локальный сервер для этих тестов.
  • Тестирование артефактов: Компоненты архитектуры предоставляют артефакт maven для управления фоновыми потоками. Артефакт тестирования ядра androidx.arch.core: содержит следующие правила JUnit:
    • InstantTaskExecutorRule: Используйте это правило для мгновенного выполнения любой фоновой операции в вызывающем потоке.
    • CountingTaskExecutorRule: Используйте это правило для ожидания фоновых операций компонентов архитектуры. Вы также можете связать это правило с Espresso в качестве ресурса в режиме ожидания.

Лучшие практики

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

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

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

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

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

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

Выставляйте как можно меньше от каждого модуля.

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

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

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

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

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

Сохраняйте как можно больше актуальных и свежих данных.

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

Назначьте один источник данных единственным источником истинны.

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

Дополнение: раскрытие статуса сети

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

В этом разделе показано, как отобразить состояние сети с помощью класса Resource, который инкапсулирует как данные, так и их состояние.

Следующий фрагмент кода предоставляет пример реализации Resource:

// Общий класс, который содержит данные и статус о загрузке этих данных.
sealed class Resource<T>(
   val data: T? = null,
   val message: String? = null
) {
   class Success<T>(data: T) : Resource<T>(data)
   class Loading<T>(data: T? = null) : Resource<T>(data)
   class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
}

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

На следующей диаграмме показано дерево решений для NetworkBoundResource:

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

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

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

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

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

В следующем фрагменте кода показан открытый API, предоставленный классом NetworkBoundResource для его подклассов:

// ResultType: Введите данные ресурса.
// RequestType: Введите ответ API.
abstract class NetworkBoundResource<ResultType, RequestType> {
   // Вызывается для сохранения результата ответа API в базу данных.
   @WorkerThread
   protected abstract fun saveCallResult(item: RequestType)

   // Вызывается с данными в базе данных, чтобы решить, следует ли извлекать
   // потенциально обновленные данные из сети.
   @MainThread
   protected abstract fun shouldFetch(data: ResultType?): Boolean

   // Вызывается для получения кэшированных данных из базы данных.
   @MainThread
   protected abstract fun loadFromDb(): LiveData<ResultType>

   // Вызывается для создания вызова API.
   @MainThread
   protected abstract fun createCall(): LiveData<ApiResponse<RequestType>>

   // Вызывается, когда получение не удается. Дочерний класс
   // может захотеть сбросить компоненты, такие как ограничитель скорости.
   protected open fun onFetchFailed() {}

   // Возвращает объект LiveData, представляющий ресурс,
   // реализованный в базовом классе.
   fun asLiveData(): LiveData<ResultType> = TODO()
}

Обратите внимание на следующие важные детали определения класса:

  • Он определяет два параметра типа, ResultType и RequestType, поскольку тип данных, возвращаемый из API, может не соответствовать типу данных, используемому локально.
  • Он использует класс ApiResponse для сетевых запросов. ApiResponse — это простая оболочка для класса Retrofit2.Call, которая преобразует ответы в экземпляры LiveData.

Полная реализация класса NetworkBoundResource появляется как часть проекта GitHub android-Architecture-components.

После создания NetworkBoundResource мы можем использовать его для записи наших привязанных к диску и сети реализаций User в классе UserRepository:

// Информирует Dagger2, что этот класс должен быть создан только один раз.
@Singleton
class UserRepository @Inject constructor(
   private val webservice: Webservice,
   private val userDao: UserDao
) {
   fun getUser(userId: String): LiveData<User> {
       return object : NetworkBoundResource<User, User>() {
           override fun saveCallResult(item: User) {
               userDao.save(item)
           }

           override fun shouldFetch(data: User?): Boolean {
               return rateLimiter.canFetch(userId) && (data == null || !isFresh(data))
           }

           override fun loadFromDb(): LiveData<User> {
               return userDao.load(userId)
           }

           override fun createCall(): LiveData<ApiResponse<User>> {
               return webservice.getUser(userId)
           }
       }.asLiveData()
   }
}

Описание архитектуры приложения

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

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

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

График скорости разработкикрасный — без предпроектного планирования и разработки архитектуры проекта,
синий — с подготовкой архитектуры на предпроектном этапе

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

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

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

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

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

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

Существует несколько основных схем:

  • диаграмма прецедентов;
  • диаграмма классов;
  • диаграмма объектов;
  • диаграмма последовательностей;
  • диаграмма взаимодействия;
  • диаграмма состояний;
  • диаграмма активности;
  • диаграмма развертывания.

Рассмотрев все варианты, мы остановились на двух из них:

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

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

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

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

Заходим на сайт Slack, указываем название и workspace. Далее добавляем Incoming Webhooks, выбираем канал и получаем URL, который мы добавляем в конфигурацию fastlane.

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

Пример диаграммы активности

Далее разбиваем приложение на модули. И готовим диаграмму классов.

Диаграмма классов может использоваться:

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

Какие плюсы мы получили:

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

Выгода от продумывания проекта схематично очевидная. Не стоит пренебрегать этим этапом.

BytePace © Все права защищены

Содержание:

Для чего нужно проектирование?

Этапы проектирования приложения

Шаг 1. Исследования и аналитика

Шаг 2. Формирование структуры и прототипа

Шаг 3. Разработка дизайна мобильного приложения

Шаг 4. Написание технического задания

Заключение

Для чего нужно проектирование

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

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

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

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

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

Этапы проектирования приложения

Шаг 1. Исследование рынка и аналитика

Шаг 2. Формирование структуры и прототипа

Шаг 3. Разработка дизайна мобильного приложения

Шаг 4. Написание технического задания

Шаг 1. Исследования и аналитика

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

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

  • Анализ целевой аудитории приложения 

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

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

Для исследования ЦА могут использоваться такие методы как:

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

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

  • Пользовательские сценарии

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

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

  • Обзор рынка и конкурентов

Анализ конкурентов и подбор референсов дает возможность понять, чего не хватает приложению, что нравится и не нравится, какие решения кажутся удачными или наоборот. Какие фишки есть у конкурентов? Электронные библиотеки каталогов, 3D-модели, элементы доверия или видео-контент — стоит задуматься, возможно что-то из этого можно добавить в дизайн проект вашего приложения.

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

Шаг 2. Формирование структуры и прототипа

Проектирование мобильных приложений: создание прототипов

Прототип — это эскиз, схематичное изображение структуры мобильного приложения. 

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

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

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

Шаг 3. Разработка дизайн-макета web-приложения

проектирование: дизайн-макет мобильного приложения

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

Инструкции как правильно оформлять приложения содержатся в специальных публичных гайдах операционных систем. Для IOS это Human Interface, а для Android — Material Desing, там же описана и логика работы элементов.

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

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

макет мобильного приложения: проектная часть

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

Шаг 4. Написание технического задания на создание приложения

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

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

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

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

В заключение

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

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

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

Понравилась статья? Поделить с друзьями:

Не пропустите и эти статьи:

  • Как написать стрелкову гиркину
  • Как написать структурные формулы веществ
  • Как написать стрелки на клавиатуре
  • Как написать структурную формулу изомера
  • Как написать стрелки на глазах

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии