Создание программного обеспечения со слоями - обычное дело - и ломается. Он не работает по двум причинам:

  • Слои предполагают некоторую форму абстракции; но очень фундаментально наслоение - это не абстракция.
  • Уровни полагаются на функциональные зависимости, которые трудно тестировать, и которые затрудняют понимание и развитие программного обеспечения.

Без абстракции со слоями

Возьмем этот многоуровневый дизайн:

и сравните его с этими слоями:

Вы видите разницу?

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

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

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

Рецепт представляет собой целое «Сделать бутерброд» на высоком уровне абстракции, а попросить маму сделать это самому - это все на низком уровне абстракции.

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

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

Абстракция со стратами

Что же тогда делать с термином «слой»? Я предлагаю оставить его, но использовать только в одной модели. Оставим это для многоуровневого шаблона дизайна. Сегодня это более широко известно, чем модель уровня OSI.

Но как назвать слои OSI? Я предлагаю называть их стратами (единственное число: страта).

Я взял этот термин от Абельсона / Сассмана, которые использовали его в своей статье Лисп: язык для стратифицированного дизайна «.

Если уровни - это уровни одинаковой абстракции в программном обеспечении, то страты - это уровни различной абстракции. «7 уровней OSI» затем становятся «7 слоями OSI».

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

И не только Абельсон / Сассман говорят о силе абстракции. Алан Кей, отец термина объектная ориентация, тоже. Однако он делает это несколько иначе. Вместо страта он использует термин« язык , язык предметной области или проблемно-ориентированный язык ».

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

Функциональные зависимости сложно проверить

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

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

Звучит нормально. Но также звучит сложно развить и испытать. Вот почему были изобретены такие принципы, как инверсия управления (IoC) и инверсия зависимостей (DI), а также такие инструменты, как контейнеры для внедрения зависимостей и имитирующие фреймворки.

Однако, несмотря на то, что более высокий уровень теперь зависит только от абстракции, а не от реализации во время разработки, он нуждается в реализации во время выполнения. Зависимость может быть несколько ослаблена, но, по сути, она все еще существует. Тестирование сейчас может быть проще, но не очень-то просто. И рассуждать о коде по-прежнему сложно, потому что нигде не видно целого. Глядя на один слой, можно увидеть лишь некоторую связь с другим слоем. Что было сделано ранее в рамках задачи, что будет сделано после следующего уровня…? Чтобы ответить на эти вопросы, вы должны прыгать в коде или даже отлаживать его. Целое, состоящее из нескольких слоев, не имеет четкого представления.

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

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

Многослойный дизайн - пример

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

Пользователь вводит однострочный текст через консоль, и программа определяет количество слов в тексте. Но не все слова в счет! Некоторые стоп-слова, определенные в файле «stopwords.txt», следует игнорировать.

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

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

public static void Main(string[] args) {
    var data = new DataLayer();
    var business = new BusinessLayer(data);
    var presentation = new PresentationLayer(business);
    presentation.Show();
}

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

Вот пользовательский интерфейс делает свою работу:

class PresentationLayer {
    readonly BusinessLayer business;
    public PresentationLayer(BusinessLayer business) {
        this.business = business;
    }
    public void Show() {
        Console.Write("Text: ");
        var text = Console.ReadLine();
        var n = this.business.Count_words(text);
        Console.WriteLine($"Number of words: {n}");
    }
}

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

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

Как вы проверяете, если

Console.Write("Text: ");
var text = Console.ReadLine();

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

Как вы проверяете, если

Console.WriteLine($"Number of words: {n} ");

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

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

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

То же самое и с бизнес-уровнем:

class BusinessLayer {
    readonly DataLayer data;
    public BusinessLayer(DataLayer data) {
        this.data = data;
    }
    public int Count_words(string text) {
        var words = Extract_words(text);
        return words.Length;
    }
    private string[] Extract_words(string text) {
        var words = text.Split(new[] { ' ', '\t', '\r', '\n' }, 
                           StringSplitOptions.RemoveEmptyEntries);
        return Remove_stopwords(words);
    }
    private string[] Remove_stopwords(string[] words) {
        var stopwords = this.data.Load_stopwords();
        words = words.Except(stopwords).ToArray();
        return words;
    }
}

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

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

Зависимости классов могут быть простыми:

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

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

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

Стратифицированный дизайн - пример

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

public static void Main(string[] args) {
    var data = new Data();
    var business = new Business();
    var presentation = new Presentation();
    var app = new App(presentation, business, data);
    app.Run();
}

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

Во-вторых, появился новый класс App{}. Он представляет верхний слой приложения, обозначает все, что нужно сделать. Единственная цель App{} - интегрировать слои в это целое.

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

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

Если Run() обо всем, то то, что находится внутри функции, тоже обо всем - только на немного более низком уровне абстракции:

public void Run() {
    var text = presentation.Ask_for_text();
    var n = Count_words(text);
    presentation.Display_word_count(n);
}

А, теперь вы видите: «все» означает запрос текста, затем подсчет слов в тексте и, наконец, представление количества слов пользователю.

В многоуровневом дизайне нигде нет такого обзора всего процесса.

Но подождите, это еще не все! Что означает «подсчет слов»? Как это делается? Вы можете увидеть все это на высоком уровне абстракции, выполнив еще одну детализацию:

private int Count_words(string text) {
    var stopwords = data.Load_stopwords();
    return Business.Count_words(text, stopwords);
}

А, теперь вы видите: «все» в «подсчете слов» означает сначала загрузку стоп-слов, а затем выполнение фактического подсчета слов с учетом стоп-слов.

Поскольку Ask_for_text() и Display_word_count() содержат только логику

public string Ask_for_text() {
    Console.Write("Text: ");
    return Console.ReadLine();
}
public void Display_word_count(int n) {
    Console.WriteLine($"Number of words: {n} ");
}

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

А как насчет основного домена «подсчет слов» в Business{}? Давайте снова углубимся в детали:

class Business {
    public static int Count_words(string text, string[] stopwords) {
        var words = Extract_words(text);
        words = Remove_stopwords(words, stopwords);
        return words.Count();
    }
    private static string[] Extract_words(string text) {
        return text.Split(new[] { ' ', '\t', '\r', '\n' }, 
                          StringSplitOptions.RemoveEmptyEntries);
    }
    private static string[] Remove_stopwords(string[] words, 
                                             string[] stopwords) {
        return words.Except(stopwords).ToArray();
    }
}

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

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

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

Ознакомьтесь с дизайном класса:

Это еще более ясно показывает, насколько независимы функциональные аспекты решений. Только App{} знает классы рабочих лошадок, но App{} не содержит никакой логики. Он нужен только для того, чтобы объединить части в единое целое. Но это очень важная задача! Это отдельная ответственность за то, чтобы быть отделенными от таких вещей, как обработка или загрузка данных.

Но учтите: связь между App{} и Presentation{} и т. Д. Не работает! В App{} нет логики, которая использует логику в Presentation{} и т. Д. Таким образом, отношения являются чисто «интеграционными».

И еще есть иерархия функций:

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

Возможно, это станет более ясным, когда я подробно покажу вам страты:

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

Я вызываю эти функции с простой логикой в ​​них и без каких-либо зависимостей операций. Другие без логики, но с интеграционными зависимостями - это интеграции.

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

Run(), Count_words() - это интеграции, Ask_for_text() или Extract_words() и т. Д. - операции.

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

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

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

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

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

Заключение

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

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

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

Возьмем, к примеру, CSV Table-izer. Востребованная функция - это целое, она представляет собой верхний слой на высшем уровне абстракции. Теперь развернитесь и оставайтесь верными IOSP. Вот и все, что касается многослойного дизайна. Создайте свой собственный небольшой доменный язык. Никаких инструментов DSL не требуется. Просто придумайте много глаголов и несколько существительных на разных уровнях абстракции, которые вы расположите по слоям. Вы значительно улучшите читаемость и тестируемость своего кода - и, наконец, избежите ада зависимостей многоуровневого дизайна.

Если вам понравилась эта статья, нажмите в сердце и / или поделитесь ею. Спасибо! Или вы можете посмотреть мой личный блог здесь или (в основном на немецком языке) блог о чистом кодировании от Школы разработчиков чистого кода.