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

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

27 января 2023 г.

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

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

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

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


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

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


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

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

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

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

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


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

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

Итак, приступим.


Пример

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

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


Photo by Mikael Seegen on Unsplash

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

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

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

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

Image by Ahmed Tarek

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

Image by Ahmed Tarek

А это:

Image by Ahmed Tarek


Набросок Fluent API

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

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

Image by Ahmed Tarek

Примечания:

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

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

Теперь давайте начнем работать над кодом.

Шаги

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

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

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

  1. Конструктор
  2. BuilderDtos
  3. Дескрипторы BuilderDtos
  4. Реализация конструктора
  5. BuilderInterfaces

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


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

.

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

namespace FluentApi.Builder.Interfaces
{
    public interface IMemberBuilder
    {
        IHuman New { get; }
    }
}

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

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


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

namespace FluentApi.Builder.Interfaces
{
    public interface IHuman
    {
        IHaveAgeAndCanHaveName WithAge(int age);
        IHaveNameAndCanHaveAge WithName(string name);
    }
}

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

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

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


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

namespace FluentApi.Builder.Interfaces
{
    public interface IHaveAgeAndCanHaveName
    {
        IHasRole WithName(string name);
    }
}

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


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

namespace FluentApi.Builder.Interfaces
{
    public interface IHaveNameAndCanHaveAge
    {
        IHasRole WithAge(int age);
    }
}

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


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

namespace FluentApi.Builder.Interfaces
{
    public interface IHasRole
    {
        IAmTeaching AsTeacher { get; }
        IAmStudying AsStudent { get; }
    }
}

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


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

using FluentApi.Builder.Dtos;

namespace FluentApi.Builder.Interfaces
{
    public interface IAmStudying
    {
        IHasStudyingSchedule Studying(params Subject[] subjects);
    }
}

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

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

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

using System;
using System.Collections.Generic;
using System.Linq;

namespace FluentApi.Builder.Dtos
{
    public sealed class Subject : IEquatable<Subject>
    {
        public Subject(string name)
        {
            Name = name;
        }

        public Subject(Subject other)
        {
            if (other != null)
            {
                Name = other.Name;
            }
        }

        public string Name { get; }

        public bool Equals(Subject other)
        {
            if (ReferenceEquals(null, other)) return false;
            if (ReferenceEquals(this, other)) return true;

            return Name == other.Name;
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != this.GetType()) return false;

            return Equals((Subject)obj);
        }

        public override int GetHashCode()
        {
            return (Name != null ? Name.GetHashCode() : 0);
        }

        public static bool operator ==(Subject left, Subject right)
        {
            return Equals(left, right);
        }

        public static bool operator !=(Subject left, Subject right)
        {
            return !Equals(left, right);
        }
    }

    public static class SubjectExtensions
    {
        public static IEnumerable<Subject> Clone(this IEnumerable<Subject> subjects)
        {
            return (subjects != null)
                ? subjects
                  .Where(s => s != null)
                  .Select(s => new Subject(s))
                : new List<Subject>();
        }
    }
}

На что здесь обратить внимание:

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

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

using FluentApi.Builder.Dtos;

namespace FluentApi.Builder.Interfaces
{
    public interface IAmTeaching
    {
        IHasTeachingSchedule Teaching(Subject subject);
    }
}

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

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


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

using FluentApi.Builder.Dtos;

namespace FluentApi.Builder.Interfaces
{
    public interface IHasStudyingSchedule
    {
        ICanBeBuilt WithSchedule(params SubjectSchedule[] subjectsSchedules);
    }
}

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

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

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

using System;
using System.Collections.Generic;
using System.Linq;

namespace FluentApi.Builder.Dtos
{
    public class Schedule
    {
        public Schedule(DateTime from, DateTime to)
        {
            From = from;
            To = to;
        }

        public Schedule(Schedule other)
        {
            if (other != null)
            {
                From = other.From;
                To = other.To;
            }
        }

        public DateTime From { get; }
        public DateTime To { get; }
    }

    public static class ScheduleExtensions
    {
        public static IEnumerable<Schedule> Clone(this IEnumerable<Schedule> schedules)
        {
            return (schedules != null)
                ? schedules
                  .Where(s => s != null)
                  .Select(s => new Schedule(s))
                : new List<Schedule>();
        }
    }
}

using System.Collections.Generic;
using System.Linq;

namespace FluentApi.Builder.Dtos
{
    public class SubjectSchedule
    {
        public SubjectSchedule(Subject subject, Schedule schedule)
        {
            Subject = subject;
            Schedule = schedule;
        }

        public SubjectSchedule(SubjectSchedule other)
        {
            if (other != null)
            {
                Subject = new Subject(other.Subject);
                Schedule = new Schedule(other.Schedule);
            }
        }

        public Subject Subject { get; }
        public Schedule Schedule { get; }
    }

    public static class SubjectScheduleExtensions
    {
        public static IEnumerable<SubjectSchedule> Clone(this IEnumerable<SubjectSchedule> subjectsSchedules)
        {
            return (subjectsSchedules != null)
                ? subjectsSchedules
                  .Where(s => s != null)
                  .Select(s => new SubjectSchedule(s))
                : new List<SubjectSchedule>();
        }
    }
}

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


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

using FluentApi.Builder.Dtos;

namespace FluentApi.Builder.Interfaces
{
    public interface IHasTeachingSchedule
    {
        ICanBeBuilt WithSchedule(params Schedule[] schedules);
    }
}

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

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


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

using FluentApi.Builder.Dtos.Descriptors;

namespace FluentApi.Builder.Interfaces
{
    public interface ICanBeBuilt
    {
        MemberDescriptor Build();
    }
}

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

Итак, мы создаем файл SubjectSchedule.cs в папке Dtos>Descriptors.

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


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

namespace FluentApi.Builder.Dtos
{
    public enum MemberRole
    {
        Teacher = 1,
        Student = 2
    }
}

namespace FluentApi.Builder.Dtos.Descriptors
{
    public class MemberDescriptor
    {
        public MemberDescriptor(MemberDescriptor other = null)
        {
            if (other != null)
            {
                Name = other.Name;
                Age = other.Age;
                Role = other.Role;
            }
        }

        public string Name { get; set; }
        public int Age { get; set; }
        public MemberRole Role { get; set; }

        public virtual MemberDescriptor Clone()
        {
            return new MemberDescriptor(this);
        }
    }
}

На что здесь обратить внимание:

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

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

Image by Ahmed Tarek

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

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

using System.Collections.Generic;
using System.Linq;

namespace FluentApi.Builder.Dtos.Descriptors
{
    public class TeacherDescriptor : MemberDescriptor
    {
        public TeacherDescriptor(MemberDescriptor member = null) : base(member)
        {
            if (member is TeacherDescriptor teacher)
            {
                Subject = teacher.Subject != null ? new Subject(teacher.Subject) : null;

                Schedules = teacher.Schedules != null
                    ? teacher.Schedules.Clone().ToList()
                    : new List<Schedule>();
            }
        }

        public Subject Subject { get; set; }
        public List<Schedule> Schedules { get; set; } = new List<Schedule>();

        public override MemberDescriptor Clone()
        {
            return new TeacherDescriptor(this);
        }
    }
}

На что здесь обратить внимание:

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

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

using System.Collections.Generic;
using System.Linq;

namespace FluentApi.Builder.Dtos.Descriptors
{
    public class StudentDescriptor : MemberDescriptor
    {
        public StudentDescriptor(MemberDescriptor member = null) : base(member)
        {
            if (member is StudentDescriptor student)
            {
                Subjects = student.Subjects != null
                    ? student.Subjects.Clone().ToList()
                    : new List<Subject>();

                SubjectsSchedules =
                    student.SubjectsSchedules != null
                        ? student.SubjectsSchedules.Clone().ToList()
                        : new List<SubjectSchedule>();
            }
        }

        public List<Subject> Subjects { get; set; } = new List<Subject>();
        public List<SubjectSchedule> SubjectsSchedules { get; set; } = new List<SubjectSchedule>();

        public override MemberDescriptor Clone()
        {
            return new StudentDescriptor(this);
        }
    }
}

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


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

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

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

using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    public class MemberBuilder : IMemberBuilder
    {
        public IHuman New => new Human(new MemberDescriptor());
    }
}

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

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


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

using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class Human : IHuman
    {
        private readonly MemberDescriptor m_Descriptor;

        public Human(MemberDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public IHaveNameAndCanHaveAge WithName(string name)
        {
            var clone = new MemberDescriptor(m_Descriptor) { Name = name };
            return new HaveNameAndCanHaveAge(clone);
        }

        public IHaveAgeAndCanHaveName WithAge(int age)
        {
            var clone = new MemberDescriptor(m_Descriptor) { Age = age };
            return new HaveAgeAndCanHaveName(clone);
        }
    }
}

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

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

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


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

using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class HaveAgeAndCanHaveName : IHaveAgeAndCanHaveName
    {
        private readonly MemberDescriptor m_Descriptor;

        public HaveAgeAndCanHaveName(MemberDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public IHasRole WithName(string name)
        {
            var clone = new MemberDescriptor(m_Descriptor) { Name = name };
            return new HasRole(clone);
        }
    }
}

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


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

using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class HaveNameAndCanHaveAge : IHaveNameAndCanHaveAge
    {
        private readonly MemberDescriptor m_Descriptor;

        public HaveNameAndCanHaveAge(MemberDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public IHasRole WithAge(int age)
        {
            var clone = new MemberDescriptor(m_Descriptor) { Age = age };
            return new HasRole(clone);
        }
    }
}

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


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

using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class HasRole : IHasRole
    {
        private readonly MemberDescriptor m_Descriptor;

        public HasRole(MemberDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public IAmTeaching AsTeacher =>
            new AmTeaching(new TeacherDescriptor(m_Descriptor) { Role = MemberRole.Teacher });

        public IAmStudying AsStudent =>
            new AmStudying(new StudentDescriptor(m_Descriptor) { Role = MemberRole.Student });
    }
}

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


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

using System.Linq;
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class AmStudying : IAmStudying
    {
        private readonly StudentDescriptor m_Descriptor;

        public AmStudying(StudentDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public IHasStudyingSchedule Studying(params Subject[] subjects)
        {
            var clone = new StudentDescriptor(m_Descriptor) { Subjects = subjects.AsEnumerable().Clone().ToList() };
            return new HasStudyingSchedule(clone);
        }
    }
}

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

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

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

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


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

using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class AmTeaching : IAmTeaching
    {
        private readonly TeacherDescriptor m_Descriptor;

        public AmTeaching(TeacherDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public IHasTeachingSchedule Teaching(Subject subject)
        {
            var clone = new TeacherDescriptor(m_Descriptor) { Subject = new Subject(subject) };
            return new HasTeachingSchedule(clone);
        }
    }
}

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

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

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


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

using System;
using System.Linq;
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class HasStudyingSchedule : IHasStudyingSchedule
    {
        private readonly StudentDescriptor m_Descriptor;

        public HasStudyingSchedule(StudentDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public ICanBeBuilt WithSchedule(params SubjectSchedule[] subjectsSchedules)
        {
            if (m_Descriptor.Subjects.Any(s => !subjectsSchedules.Select(ss => ss.Subject).Contains(s)))
            {
                throw new ArgumentException("Some of the registered subjects are not scheduled.");
            }

            if (subjectsSchedules.Select(ss => ss.Subject).Any(s => !m_Descriptor.Subjects.Contains(s)))
            {
                throw new ArgumentException("Some of the scheduled subjects are not registered.");
            }

            var clone = new StudentDescriptor(m_Descriptor)
            {
                SubjectsSchedules = subjectsSchedules.AsEnumerable().Clone().ToList()
            };

            return new CanBeBuilt(clone);
        }
    }
}

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

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


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

using System.Linq;
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class HasTeachingSchedule : IHasTeachingSchedule
    {
        private readonly TeacherDescriptor m_Descriptor;

        public HasTeachingSchedule(TeacherDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public ICanBeBuilt WithSchedule(params Schedule[] schedules)
        {
            var clone = new TeacherDescriptor(m_Descriptor)
            {
                Schedules = schedules.AsEnumerable().Clone().ToList()
            };

            return new CanBeBuilt(clone);
        }
    }
}

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


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

using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class CanBeBuilt : ICanBeBuilt
    {
        private readonly MemberDescriptor m_Descriptor;

        public CanBeBuilt(MemberDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public MemberDescriptor Build()
        {
            return m_Descriptor.Clone();
        }
    }
}

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

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

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


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

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

using System;
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Implementations;

namespace FluentApi
{
    class Program
    {
        static void Main(string[] args)
        {
            var memberBuilder = new MemberBuilder();

            var ahmed =
                memberBuilder
                    .New
                    .WithName("Ahmed")
                    .WithAge(36)
                    .AsTeacher
                    .Teaching(new Subject("Software Engineering"))
                    .WithSchedule(
                        new Schedule
                        (
                            new DateTime(2021, 11, 20),
                            new DateTime(2021, 12, 20)
                        ), new Schedule
                        (
                            new DateTime(2022, 1, 5),
                            new DateTime(2021, 3, 5)
                        ))
                    .Build();

            var subjectsToStudy = new Subject[]
            {
                new Subject("Software Engineering"),
                new Subject("Physics")
            };

            var mohamed =
                memberBuilder
                    .New
                    .WithAge(15)
                    .WithName("Mohamed")
                    .AsStudent
                    .Studying(subjectsToStudy)
                    .WithSchedule
                    (
                        new SubjectSchedule
                        (
                            subjectsToStudy[0],
                            new Schedule
                            (
                                new DateTime(2021, 11, 20),
                                new DateTime(2021, 12, 20)
                            )
                        ),
                        new SubjectSchedule
                        (
                            subjectsToStudy[1],
                            new Schedule
                            (
                                new DateTime(2021, 11, 20),
                                new DateTime(2021, 12, 20)
                            )
                        )
                    )
                    .Build();

            Console.ReadLine();
        }
    }
}

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

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

Photo by Ray Hennessy on Unsplash

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

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

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


Также опубликовано здесь


Оригинал