Почему «нижним» уровням приложений лучше не знать о «более высоких» уровнях?


66

В типичном (хорошо спроектированном) веб-приложении MVC база данных не знает о коде модели, код модели не знает о коде контроллера, а код контроллера не знает о коде представления. (Я полагаю, что вы могли бы даже начать с аппаратного обеспечения или даже дальше, и схема могла бы быть такой же.)

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

Я могу интуитивно понять, почему это хорошая идея, но я не могу сформулировать ее. Почему этот однонаправленный стиль наложения является хорошей идеей?


10
Может быть, это потому, что данные поступают из базы данных в представление. Он «запускается» в базе данных и «прибывает» в представление. Уровень осведомленности движется в противоположном направлении, так как данные «путешествуют». Мне нравится использовать "цитаты".
Джейсон Светт

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

2
На самом деле, я не думаю, что в большинстве случаев рекомендуется, чтобы View знал о контроллере. Поскольку контроллеры почти всегда осведомлены о представлении, наличие представления о контроллере создает круговую ссылку
Эми Бланкеншип

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

1
Очевидно, представление знает о строго типизированной модели представления.
DazManCat

Ответы:


121

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

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

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


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

12
Это обязательное чтение для тех, кто считает, что полное разделение - идеал, к которому нужно стремиться, но не может понять, почему оно не работает.
Роберт Харви

6
Ну, @ Андреас, всегда есть Мел .
TRiG

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

1
@Peri: такой закон существует, см. En.wikipedia.org/wiki/Law_of_Demeter . Согласны ли вы с этим или нет, это другой вопрос.
Майк Чемберлен

61

Фундаментальная мотивация заключается в следующем: вы хотите иметь возможность вырывать целый слой и заменять его совершенно другим (переписанным), и НИКТО НЕ ДОЛЖЕН (Уметь) УКАЗАТЬ РАЗНИЦУ.

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

Следующий пример - когда вы вырываете средний слой и подставляете другой средний слой. Рассмотрим приложение, которое использует протокол, работающий через RS-232. Однажды вы должны полностью изменить кодировку протокола, потому что «что-то еще изменилось». (Пример: переход от прямого кодирования ASCII к кодированию Рида-Соломона потоков ASCII, потому что вы работали по радиоканалу от центра Лос-Анджелеса до Марина-дель-Рей, и теперь вы работаете по радиоканалу из центра Лос-Анджелеса до зонда, вращающегося вокруг Европы одна из спутников Юпитера, и эта связь нуждается в гораздо лучшем прямом исправлении ошибок.)

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

Теперь, это не совсем тот случай, когда нижние уровни НИЧЕГО не знают о верхних слоях. Скорее, нижний уровень знает, что уровень, находящийся непосредственно над ним, будет работать точно в соответствии с определенным интерфейсом. Он может больше ничего не знать, потому что по определению все, что не находится в определенном интерфейсе, может быть изменено БЕЗ УВЕДОМЛЕНИЯ.

Уровень RS-232 не знает, работает ли он ASCII, Reed-Solomon, Unicode (арабская кодовая страница, японская кодовая страница, кодовая страница Rigellian Beta) или что-то еще. Он просто знает, что получает последовательность байтов и записывает эти байты в порт. На следующей неделе он может получить совершенно другую последовательность байтов из чего-то совершенно другого. Ему все равно. Он просто перемещает байты.

Первым (и лучшим) объяснением многоуровневого дизайна является классическая статья Дейкстры «Структура мультипрограммирующей системы» . Требуется чтение в этом деле.


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

+1 за отличные примеры. Мне нравится объяснение, данное JRS
ViSu

@JasonSwett: Если бы я перевернул монету, я бы перевернул ее, пока она не обозначит этот ответ! ^^ +1 Джону.
Оливье Дюлак

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

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

8

Потому что более высокие уровни могут измениться.

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


4

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

Вот выдержка:

Недостатки

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

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


4

ИМО, это очень просто. Вы не можете повторно использовать то, что продолжает ссылаться на контекст, в котором оно используется.


4

Слои не должны иметь двусторонних зависимостей

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

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

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

Направление зависимости должно следовать командному направлению

Причина, по которой мы предпочитаем структуру зависимостей сверху вниз, заключается в том, что верхние объекты создают и используют нижние объекты . Зависимость - это, в основном, отношение, которое означает «А зависит от В, если А не может работать без В». Так что, если объекты в A используют объекты в B, вот так должны развиваться зависимости.

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

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

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

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

3

Я хотел бы добавить свои два цента к тому, что Мэтт Фенвик и Килиан Фот уже объяснили.

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

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


3

Просто для удовольствия.

Подумайте о пирамиде болельщиков. Нижний ряд поддерживает ряды над ними.

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

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

Не совсем техническая, но я подумал, что это может помочь.


3

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

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

Чтобы продолжить пример, теперь предположим, что A зависит от B, а B зависит от A. IOW, круговая зависимость. Теперь, когда бы ни вносилось изменение, оно могло бы сломать другой модуль. Изменение B может все еще сломать A, но теперь изменение A может также сломать B.

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

Это ухудшается в большом проекте, где есть много разработчиков (например, те, кто работает только на уровне A, некоторый уровень B и некоторый уровень C). Поскольку становится вероятным, что каждое изменение должно быть рассмотрено / обсуждено с участниками на других уровнях, чтобы убедиться, что ваши изменения не нарушаются и не заставляют переделывать то, над чем они работают. Если ваши изменения навязывают изменения другим, то вы должны убедить их, что они должны внести изменения, потому что они не захотят брать на себя больше работы только потому, что у вас есть этот замечательный новый способ работы в вашем модуле. IOW, бюрократический кошмар.

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


+1 У моего работодателя у нас фактически есть внутренняя диаграмма, описывающая суть вашего последнего абзаца, поскольку он относится к продукту, над которым я работаю, т.е. чем дальше вы идете вниз по стеку, тем ниже скорость изменения (и должна быть).
RobV

1

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

Обратите внимание, что «нижние слои» на самом деле являются средними слоями.

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

Операционная система не зависит ни от деталей внутри приложений, ни от драйверов устройств. Существует много видов драйверов устройств одного типа, и они используют одну и ту же структуру драйверов устройств. Иногда хакерам ядра приходится помещать некоторую специальную обработку в среду для конкретного оборудования или устройства (недавний пример, с которым я сталкивался: специфичный для PL2303 код в последовательной среде usb Linux). Когда это случается, они обычно помещают в комментарии о том, сколько это отстой и должно быть удалено. Несмотря на то, что ОС вызывает функции в драйверах, вызовы проходят через хуки, которые заставляют драйверы выглядеть одинаково, тогда как когда драйверы вызывают ОС, они часто используют определенные функции непосредственно по имени.

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


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

1

Разделение проблем и подходы «разделяй / властвуй» могут стать еще одним объяснением этих вопросов. Разделение проблем дает возможность переносимости, а в некоторых более сложных архитектурах это дает платформе независимое масштабирование и преимущества в производительности.

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

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


1

В продолжение ответа Килиана Фота, это направление наслоения соответствует направлению, в котором человек исследует систему.

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

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

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

Ну, у вас есть три сценария того, как эта ошибка может произойти.

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

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

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

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

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


-1

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

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

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

Скажем, вам нужно написать 5 драйверов для устройств Modbus. Один из них использует Modbus TCP, два используют Modbus на RS485, а остальные - через RS232. Вы не собираетесь переопределять Modbus 5 раз, потому что вы пишете 5 драйверов. Также вы не собираетесь переопределять Modbus 3 раза, потому что у вас есть 3 различных физических уровня ниже вас.

Что вы делаете, вы пишете TCP Media Access, RS485 Media Access и, возможно, RS232 Media Access. Разумно ли знать, что на этом этапе будет слой Modbus выше? Возможно нет. Следующий драйвер, который вы собираетесь реализовать, может также использовать Ethernet, но использовать HTTP-REST. Было бы обидно, если бы вам пришлось переопределить Ethernet Media Access для связи через HTTP.

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

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


возможность повторного использования уже была явно упомянута в ответе, опубликованном более полугода назад
gnat
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.