ШАБЛОНЫ ПРОЕКТИРОВАНИЯ

Шаблон проектирования Builder в .NET C#

Пошаговое руководство по разработке Fluent API с нуля в .NET C# с использованием шаблона проектирования Builder.

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

В этой статье мы рассмотрим весь процесс разработки Fluent API с использованием шаблона проектирования Builder, от первых шагов до последнего этапа тестирования. это.

Поэтому пристегните ремни, и начнем наше путешествие.



Что такое шаблон проектирования Builder?

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

Каковы преимущества шаблона проектирования Builder?

Некоторые из хорошо известных преимуществ шаблона проектирования Builder:

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

Каковы недостатки шаблона проектирования Builder?

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

Как реализовать шаблон проектирования Builder?

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

Поэтому приступим.

Пример

Во-первых, давайте придумаем пример для использования through или trip. Я выбрал простой пример процесса регистрации школы.

Проще говоря, в какой-то момент всего решения вам нужно будет определить некоторых учителей и некоторых учеников. Предположим, что эти объекты «Учитель» и «Студент» настолько сложны, что нам нужно разработать Fluent API для их создания.

Отказ от ответственности

  1. Некоторые передовые практики будут проигнорированы/отброшены, чтобы сосредоточить основное внимание на других передовых практиках, рассматриваемых в этой статье.
  2. Пример, используемый в этой статье, предназначен только для демонстрационных целей. Это не лучший кандидат для применения шаблона проектирования Builder.
  3. Мы можем интегрировать различные методы с шаблоном проектирования Builder, такие как использование дженериков и других вещей, но все они исключены, чтобы сделать пример максимально простым.
  4. Существуют разумные различия в способах реализации шаблона проектирования Builder, поэтому вы можете найти несколько других реализаций, отличных от той, которую мы собираемся использовать в этой статье.
  5. Старайтесь использовать шаблон проектирования Builder только тогда, когда это действительно необходимо, поскольку он усложняет решение в целом.

Загляните в будущее

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

И вы могли бы написать такой код:

И это:

Набросок Fluent API

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

Итак, наш эскиз будет примерно таким:

Примечания:

  1. Builder – это основная точка входа. Оттуда мы перейдем к Новый.
  2. Тогда у нас может быть два варианта; WithName(имя) и WithAge(возраст).
  3. Однако на следующем шаге, если вы уже пришли из WithName(name), мы разрешаем только WithAge(age). И, следуя той же концепции, если вы уже пришли из WithAge(age), мы разрешаем только WithName(name).
  4. Тогда бы мы сливались в одну общую точку.
  5. Исходя из этой общей точки, у нас есть два варианта; Учитель и Ученик.
  6. Для как учителя поток будет следующим: Обучение(предмет) ›› С расписанием(расписание).
  7. И из AsStudent поток будет Изучение(предметы) ›› WithSchedule(stydingSchedule).
  8. Наконец, все они объединяются в команду Build().

Определение интерфейсов

Теперь приступим к работе над кодом.

Шаги

Откройте VS или предпочтительную IDE.

Создайте новую библиотеку классов или консольное приложение. Я назвал свой проект FluentApi.

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

  1. Строитель
  2. Строитель\Dtos
  3. Строитель\Dtos\Дескрипторы
  4. Строитель\Реализации
  5. Конструктор\Интерфейсы

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

Теперь давайте начнем с нашего первого интерфейса, IMemberBuilder. Вот важная хитрость. Я создал файл в папке Interfaces и назвал его 01.IMemberBuilder.cs.

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

Из Sketch мы знаем, что наш Builder должен предоставлять свойство New, и это свойство должно привести нас к чему-то, что предоставляет два метода; WithName(name) и WithAge(age).

Итак, свойство New должно возвращать, скажем, новый интерфейс с именем IHuman.

Переходя к следующему шагу, давайте определим интерфейс IHuman. Итак, создайте файл 02.IHuman.cs и определите интерфейс следующим образом:

Из Sketch мы знаем, что интерфейс IHuman должен иметь два метода WithName(name) и WithAge(age). Однако эти два метода должны иметь разные типы возвращаемых значений. Почему???

Потому что мы хотим, чтобы после вызова WithName(name) единственным доступным вариантом был вызов WithAge(age), а не другого WithName(name). И то же самое относится к WithAge(age).

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

Переходя к следующему шагу, давайте определим интерфейс IHaveAgeAndCanHaveName. Итак, создайте файл 03.IHaveAgeAndCanHaveName.cs и определите интерфейс следующим образом:

Из Sketch мы знаем, что интерфейс IHaveAgeAndCanHaveName должен иметь метод WithName(name). И этот метод должен возвращать что-то, что раскрывает свойства AsTeacher и AsStudent.

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

Из Sketch мы знаем, что интерфейс IHaveNameAndCanHaveAge должен иметь метод WithAge(age). И этот метод должен возвращать что-то, что раскрывает свойства AsTeacher и AsStudent, как и IHaveAgeAndCanHaveName.WithName(name).

Переходя к следующему шагу, давайте определим интерфейс IHasRole. Итак, создайте файл 04.IHasRole.cs и определите интерфейс следующим образом:

Из Sketch мы знаем, что интерфейс IHasRole должен иметь два свойства AsTeacher и AsStudent. И каждое из этих свойств должно возвращать что-то другое в соответствии со следующим шагом скетча.

Переходя к следующему шагу, давайте определим интерфейс IAmStudying. Итак, создайте файл 05.IAmStudying.cs и определите интерфейс следующим образом:

Из Sketch мы знаем, что интерфейс IAmStudying должен иметь метод Studying(subjects). Этот метод должен ожидать ввод массива типов Subject. Итак, нам нужно определить класс Subject.

Кроме того, Studying(subjects) должен возвращать что-то, раскрывающее WithSchedule(subjectsSechedules).

Итак, мы создаем файл Subject.cs внутри папки Dtos, и код будет следующим:

Что здесь заметить:

  1. Он имеет только одно свойство Name.
  2. Это неизменно.
  3. Он наследует интерфейс IEquatable<Subject>, и мы сгенерировали все необходимые члены.
  4. Мы определили конструктор public Subject(Subject other), чтобы обеспечить способ клонирования субъекта из другого субъекта. Возможность клонирования в шаблоне Builder настолько важна, потому что на каждом этапе вам нужно иметь дело с совершенно отдельным объектом (с другой ссылкой), чем на предыдущем и следующем шагах.
  5. Мы также определили метод расширения с Clone по IEnumerable<Subject>, чтобы избежать повторения одного и того же кода в разных местах.
  6. Внутри метода расширения мы используем конструктор public Subject(Subject other), который мы определили в классе Subject.

Переходя к следующему шагу, давайте определим интерфейс IAmTeaching. Итак, создайте файл 05.IAmTeaching.cs и определите интерфейс следующим образом:

Из Sketch мы знаем, что интерфейс IAmTeaching должен иметь метод Teaching(subject). Этот метод должен ожидать ввода типа Subject.

Кроме того, Teaching(subject) должен возвращать что-то, раскрывающее WithSchedule(sechedules).

Переходя к следующему шагу, давайте определим интерфейс IHasStudyingSchedule. Итак, создайте файл 06.IHasStudyingSchedule.cs и определите интерфейс следующим образом:

Из Sketch мы знаем, что интерфейс IHasStudyingSchedule должен иметь метод WithSchedule(subjectsSchedules). Этот метод должен ожидать ввод массива типов SubjectSchedule.

Кроме того, WithSchedule(subjectsSchedules) должен возвращать что-то, раскрывающее метод Build().

Итак, мы создаем файлы Schedule.cs и SubjectSchedule.cs внутри папки Dtos, и код будет следующим:

Здесь мы следуем тем же правилам, что и в классе Subject.

Переходя к следующему шагу, давайте определим интерфейс IHasTeachingSchedule. Итак, создайте файл 06.IHasTeachingSchedule.cs и определите интерфейс следующим образом:

Из Sketch мы знаем, что интерфейс IHasTeachingSchedule должен иметь метод WithSchedule(schedules). Этот метод должен ожидать ввод массива типов SubjectSchedule.

Кроме того, WithSchedule(schedules) должен возвращать что-то, раскрывающее метод Build().

Переходя к следующему шагу, давайте определим интерфейс ICanBeBuilt. Итак, создайте файл 07.ICanBeBuilt.cs и определите интерфейс следующим образом:

Из Sketch мы знаем, что интерфейс ICanBeBuilt должен иметь метод Build(), возвращающий окончательный состав MemberDescriptor.

Итак, мы создаем файл SubjectSchedule.cs внутри папки Dtos›Descriptors.

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

дескриптор участника

Что здесь заметить:

  1. Класс MemberDescriptor предоставляет основную информацию о члене. Более конкретная информация об Учителя или Ученик будет находиться в двух других классах Учитель и Ученик.
  2. Класс не является неизменным, потому что на каждом этапе процесса создания вы будете добавлять к объекту небольшую деталь. Таким образом, у вас нет всех деталей сразу. Однако вы по-прежнему можете сделать его неизменяемым, но вам потребуется предоставить более одного конструктора, который соответствует вашим потребностям для каждого шага.
  3. Тем не менее, мы предоставляем конструктор public MemberDescriptor(MemberDescriptor other = null) для целей клонирования, как объяснялось ранее.
  4. И мы добавили метод public virtual MemberDescriptor Clone() по важной причине. На некоторых этапах процесса вы будете переходить от более конкретного случая к более общему. В таких случаях ваши реализации интерфейсов должны иметь дело с родительским классом MemberDescriptor, а не с его дочерними элементами. Кроме того, ему потребуется клонировать объект, не зная, что он изначально Учитель или Ученик.

Например, на этом этапе слияния

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

Описание учителя

Что здесь заметить:

  1. Внутри конструктора клонирования нам нужно проверить наличие нулевых свойств, потому что, как объяснялось ранее, детали добавляются по частям более чем на одном шаге.
  2. Мы также используем метод расширения IEnumerable<Schedule> для клонирования.
  3. Мы определили переопределение метода Clone и теперь используем наш конструктор клонов для конкретного типа.

Описание учащегося

Следуя той же концепции, что и в TeacherDescriptor.

Реализации интерфейсов

Теперь переходим к реализации наших интерфейсов.

Давайте определим класс MemberBuilder, реализующий IMemberBuilder interface. Итак, создайте файл 01.MemberBuilder.cs и определите класс следующим образом:

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

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

Переходим к определению класса Human, реализующего интерфейс IHuman. Итак, создайте файл 02.Human.cs и определите класс следующим образом:

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

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

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

Переходим к определению класса HaveAgeAndCanHaveName, реализующего интерфейс IHaveAgeAndCanHaveName. Итак, создайте файл 03.HaveAgeAndCanHaveName.cs и определите класс следующим образом:

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

Переходим к определению класса HaveNameAndCanHaveAge, реализующего интерфейс IHaveNameAndCanHaveAge. Итак, создайте файл 03.HaveNameAndCanHaveAge.cs и определите класс следующим образом:

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

Переходим к определению класса HasRole, реализующего интерфейс IHasRole. Итак, создайте файл 04.HasRole.cs и определите класс следующим образом:

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

Переходим к определению класса AmStudying, реализующего интерфейс IAmStudying. Итак, создайте файл 05.AmStudying.cs и определите класс следующим образом:

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

Здесь следует отметить, что конструктор ожидает StudentDescriptor, а не MemberDescriptor, и это потому, что в момент построения AmStudying это ясно.

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

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

Переходим к определению класса AmTeaching, реализующего интерфейс IAmTeaching. Итак, создайте файл 05.AmTeaching.cs и определите класс следующим образом:

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

Здесь следует отметить, что конструктор ожидает TeacherDescriptor, а не MemberDescriptor, и это потому, что в момент построения AmTeaching это ясно.

Кроме того, здесь мы не передаем тот же самый Subject, переданный конечным пользователем, мы передаем клон.

Переходим к определению класса HasStudyingSchedule, реализующего интерфейс IHasStudyingSchedule. Итак, создайте файл 06.HasStudyingSchedule.cs и определите класс следующим образом:

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

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

Переходим к определению класса HasTeachingSchedule, реализующего интерфейс IHasTeachingSchedule. Итак, создайте файл 06.HasTeachingSchedule.cs и определите класс следующим образом:

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

Переходим к определению класса CanBeBuilt, реализующего интерфейс ICanBeBuilt. Итак, создайте файл 07.CanBeBuilt.cs и определите класс следующим образом:

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

Здесь следует отметить, что конструктор ожидает MemberDescriptor, так как в этот момент переданное MemberDescriptor может быть TeacherDescriptor или StudentDescriptor.

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

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

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

Что вы можете заметить здесь:

  1. Наш Fluent API работает как надо.
  2. Возможно, для этого примера вам может показаться излишним создавать Fluent API для таких простых объектов, однако мы используем этот простой пример только в демонстрационных целях.

Заключительные слова

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

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

Надеюсь, вы нашли этот контент полезным. Если вы хотите поддержать:

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

Другие источники

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