BuilderWzór.16 Hej dawno nie pisałem coś na temat wzorców projektowych, a jeszcze trochę ich zostało . Wzorzec projektowy Budowniczy zajmuje się utworzeniem złożonego obiektu i jego zadaniem jest ten proces uprościć. 

Jest to jeden ze wzorców, który przetrwał próbę czasu i nadal można zobaczyć jego użycie w różnych językach programowania nie tylko w C#. 

Technicznie mógłbyś uznać, że "StringBuilder" jest przykładem wzorca "builder" jak sama jego nazwa wskazuje.

using System.Text;

var code = "Console.WriteLine(\"Hello World\");";
var sb = new StringBuilder();
sb.Append("<p><pre><code class=\"hljs cs\">");
sb.Append(code);
sb.Append("</code></pre></p><p>...</p>");
Console.WriteLine(sb);

Dla przypomnienia "StringBuilder" powstał, ponieważ każda operacja dodania nowego napis tworzy instancje, czyli zapycha pamięć. Dlatego C# ma takiego budowniczego, który scali Ci odpowiednio napisy bez tworzenia miliona obiektów, po których zaraz trzeba i tak posprzątać.

Zbiór klas także moim zdaniem możemy potraktować jako użycie tego wzorca. Przy użyciu "LINQ to XML" możesz utworzyć plik XML ze wszystkim procesami na twoim komputerze.

W tym przypadku obiekt XDocument spełnia funkcję budowniczego pliku XML.

GetProcesses().Save(@"D:\proces.xml");

static XDocument GetProcesses()
{
    return new XDocument(
        new XElement("Processes",
            from p in Process.GetProcesses()
            orderby p.ProcessName ascending
            select new XElement("Process",
                new XAttribute("Name", p.ProcessName),
                new XAttribute("ProcessID", p.Id))));
}

Jak nie lubisz takiej składni LINQ to masz tutaj tradycyjną. 

GetProcesses().Save(@"D:\proces1.xml");

static XDocument GetProcesses()
{
    return new XDocument(
        new XElement("Processes",
            Process.GetProcesses().OrderBy(p => p.ProcessName)
            .Select(p => 
                new XElement("Process",
                new XAttribute("Name", p.ProcessName),
                       new XAttribute("ProcessID", p.Id)))));
}

Twój własny budowniczy złożonego obiektu

A w jakich przypadkach byś napisałbyś swojego budowniczego? Pokaże Ci fragment przykładu, który musiałem napisać na potrzeby robienia testów do mojej aplikacji na webinar.

Spokojnie później w artykule pokaże Ci luźniejszy przykład, ale chce Ci pokazać coś konkretniejszego niż budowniczy obiektu ciasta czy zupy. 

W tamtej aplikacji miałem klasę reprezentującą zgłoszenia na mowy i był to bardzo złożony obiekt. Tak złożony, że ilością kodu zalałby ten wpis.

Dlatego wybrałem z niego tylko jedną pod właściwość , czyli klasę reprezentującą mówcę. Wygląda ona tak:

public class Speaker
{
    public DateTime Birthdate { get; init; }
    public Name Name { get; init; }
    public Address Address { get; init; }
    public SpeakerWebsites SpeakerWebsites { get; init; }

    public string Biography { get; init; }
    public Contact Contact { get; init; }

    public Speaker(Name name, DateTime birthdate,
        Address address, SpeakerWebsites speakerWebsites, string biography,
        Contact contact)
    {
        if (contact == null)
            throw new ArgumentException("Contact cannot be null");
        if (biography == null)
            throw new ArgumentException("biography cannot be null");
        if (name == null)
            throw new ArgumentException("Name cannot be null");
        if (address == null)
            throw new ArgumentException("Address cannot be null");
        if (birthdate == default)
            throw new ArgumentException("Birthdate cannot be empty");
        if (speakerWebsites == default)
            throw new ArgumentException("SpeakerWebsites cannot be empty");

        Name = name;
        Birthdate = birthdate;
        Address = address;
        SpeakerWebsites = speakerWebsites;
        Biography = biography;
        Contact = contact;
    }
}

Ten mówca ma swoje : imię i nazwisko, adres, strony internetowe, biografię i kontakt. Oprócz biografii każda z tych rzeczy jest osobną klasą.

public class Name
{
    public Name(string first, string last)
    {
        if (string.IsNullOrWhiteSpace(first))
            throw new ArgumentException("First name cannot be empty");
        if (string.IsNullOrWhiteSpace(last))
            throw new ArgumentException("First name cannot be empty");

        First = first;
        Last = last;
    }

    public string First { get; }
    public string Last { get; }
}

public class Address
{
    public string Country { get; }
    public string ZipCode { get; }
    public string City { get; }
    public string Street { get; }

    public Address(string country, string zipCode, string city, string street)
    {
        if (string.IsNullOrWhiteSpace(country))
            throw new ArgumentException("Country cannot be empty.");
        if (string.IsNullOrWhiteSpace(zipCode))
            throw new ArgumentException("Zip code cannot be empty.");
        if (string.IsNullOrWhiteSpace(city))
            throw new ArgumentException("City cannot be empty.");
        if (string.IsNullOrWhiteSpace(street))
            throw new ArgumentException("Street cannot be empty.");
        if (!new Regex("[0-9]{2}-[0-9]{3}").Match(zipCode).Success)
            throw new ArgumentException("Zip code must be NN-NNN format.");

        Country = country;
        ZipCode = zipCode;
        City = city;
        Street = street;
    }


    protected Address()
    {
    }
}

public class Contact
{
    public string Phone { get; }
    public string Email { get; }

    public Contact(string phone, string email)
    {
        if (phone == null)
            throw new ArgumentException("phone cannot be null");
        if (email == null)
            throw new ArgumentException("email cannot be null");

        Phone = phone;
        Email = email;
    }

    protected Contact()
    {

    }
}

Zwróć uwagę, że klasa obrazująca strony mówcy także jest złożona. Ile ona ma pól😱

public class SpeakerWebsites
{
    public string Facebook { get; init; }
    public string LinkedIN { get; init; }
    public string Twitter { get; init; }
    public string Instagram { get; init; }
    public string TikTok { get; init; }
    public string YouTube { get; init; }
    public string FanPageOnFacebook { get; init; }

    public string GitHub { get; init; }
    public string Blog { get; init; }

    public SpeakerWebsites(string facebook = null, string linkedIN = null,
        string twitter = null, string instagram = null, string tikTok = null,
        string gitHub = null, string blog = null)
    {
        Facebook = facebook;
        LinkedIN = linkedIN;
        Twitter = twitter;
        Instagram = instagram;
        TikTok = tikTok;
        GitHub = gitHub;
        Blog = blog;
    }

    public SpeakerWebsites()
    {

    }

    public bool HaveSocialMedia()
    {
        if (!string.IsNullOrWhiteSpace(Facebook)
            || !string.IsNullOrWhiteSpace(LinkedIN)
            || !string.IsNullOrWhiteSpace(Twitter)
            || !string.IsNullOrWhiteSpace(Instagram)
            || !string.IsNullOrWhiteSpace(TikTok)
            || !string.IsNullOrWhiteSpace(YouTube)
            || !string.IsNullOrWhiteSpace(FanPageOnFacebook))
            return true;

        return false;
    }

    public bool HaveGitHub()
    {
        if (!string.IsNullOrWhiteSpace(GitHub))
            return true;

        return false;
    }

    public bool HaveBlog()
    {
        if (!string.IsNullOrWhiteSpace(Blog))
            return true;

        return false;
    }

}

Oto nasz projekt gotowy do testów.

Przykład z mówcą i jego dużym obiektem klasy

Chciałbym teraz napisać osobny projekt testów jednostkowych, który zweryfikuje walidacje tworzenia mojego obiektu mówcy. Gdy jakieś pole jest puste powinien dostać wyjątek.

Pisanie takich testów mogło być koszmarem.

Na potrzeby testów jednostkowych per test taki obiekt mówcy musimy stworzyć i wypełnić go czasem bzdurnymi danymi, a czasem nie.

Jest to bardzo złożony obiekt, więc chciałbym mieć większą wygodę w jego tworzeniu.Zwłaszcza że ten kod będzie się powtarzał

Wzorzec "Builder" rozwiąże ten problem i dzięki niemu nie będę musiał pisać za każdym razem masy konstruktorów.

Obiekt mówcy jest tak złożony, że najpierw muszę utworzyć budowniczego do klasy "SpeakerWebsites", który określa nam linki mówcy.

public class SpeakerWebsitesBuilder
{
    public static SpeakerWebsitesBuilder GivenSpeakerWebsites()
        => new SpeakerWebsitesBuilder();

    private string facebbok = "https://www.facebook.com/cezary.walenciuk";
    private string twitter = "https://twitter.com/walenciukc";
    private string tiktok = "https://www.tiktok.com/@shanselman?";
    private string instagram = "https://www.instagram.com/cezarywalenciuk/";
    private string youTube = "https://www.youtube.com/channel/UCaryk7_lKRI1EldZ6saVjBQ";
    private string fanPageOnFacebook = "https://www.facebook.com/JakProgramowac?fref=nf";
    private string linkedin = "https://www.linkedin.com/in/cezary-walenciuk-35615644/";
    private string blog = "https://cezarywalenciuk.pl/";
    private string github = "https://github.com/PanNiebieski";

    public SpeakerWebsitesBuilder ClearWebsites()
    {
        facebbok = "";
        instagram = "";
        twitter = "";
        tiktok = "";
        youTube = "";
        fanPageOnFacebook = "";
        linkedin = "";
        blog = "";
        blog = "";

        return this;
    }


    public SpeakerWebsitesBuilder WithFacebbok(string newfacebook)
    {
        facebbok = newfacebook;
        return this;
    }

    public SpeakerWebsitesBuilder WithInstagram(string newinstagram)
    {
        instagram = newinstagram;
        return this;
    }

    public SpeakerWebsitesBuilder WithTwitter(string newtwitter)
    {
        twitter = newtwitter;
        return this;
    }

    public SpeakerWebsitesBuilder WithTikTok(string newtiktok)
    {
        tiktok = newtiktok;
        return this;
    }

    public SpeakerWebsitesBuilder WithYoutube(string newyoutube)
    {
        youTube = newyoutube;
        return this;
    }

    public SpeakerWebsitesBuilder WithFanPageOnFacebook(string newfanPageOnFacebook)
    {
        fanPageOnFacebook = newfanPageOnFacebook;
        return this;
    }

    public SpeakerWebsitesBuilder WithLinkedIn(string newlinkedin)
    {
        linkedin = newlinkedin;
        return this;
    }

    public SpeakerWebsitesBuilder WithBlog(string newblog)
    {
        blog = newblog;
        return this;
    }

    public SpeakerWebsitesBuilder WithGitHub(string newgithub)
    {
        github = newgithub;
        return this;
    }

    public SpeakerWebsites Build()
    {
        return new SpeakerWebsites
        ()
        {
            Facebook = facebbok,
            Blog = blog,
            FanPageOnFacebook = fanPageOnFacebook,
            GitHub = github,
            Instagram = instagram,
            LinkedIN = linkedin,
            TikTok = tiktok,
            Twitter = twitter,
            YouTube = youTube
        };
    }
}

Jak widzisz w nim mamy pola prywatne, które określają mi jakieś fałszywe linki do social media na potrzeby testów być może walidacji uzupełnionych pól. 

Każdą metodą w moim budowniczym zwraca swój obiekt, znaczy to, że będę mógł zrobić ciąg wywołań, aby zbudować swój obiekt. Ta technika nazywa się "Fluent Method" lub "Fluent Interface"

Do zatwierdzenia budowy mojego obiektu na końcu muszę skorzystać z metody "Build()".

Teraz spójrzmy na budowniczego mówcy, który skorzysta z budowniczego linków do stron dla danego mówcy.

public class SpeakerBuilder
{
    private Name name = new Name("Jan", "B");
    private Address address =
        new Address("PL", "00-002", "Warsaw", "Lemonowa 12");
    private DateTime birthDate = AppTime.Now().AddYears(-25);

    private SpeakerWebsites speakerWebsites =
        SpeakerWebsitesBuilder.GivenSpeakerWebsites().Build();

    private Contact contact = new Contact("655-555-555", "C@gmail.com");

    private string Bio = "asasa";

    public static SpeakerBuilder GivenSpeaker() => new SpeakerBuilder();

    public SpeakerBuilder WithAge(int age)
    {
        this.birthDate = AppTime.Now().AddYears(-1 * age);
        return this;
    }

    public SpeakerBuilder BornOn(DateTime birthDate)
    {
        this.birthDate = birthDate;
        return this;
    }

    public SpeakerBuilder WithNullBirthDate()
    {
        this.birthDate = default;
        return this;
    }

    public SpeakerBuilder WithContact(string Phone, string email)
    {
        this.contact = new Contact(Phone, email);
        return this;
    }

    public SpeakerBuilder WithNullContact()
    {
        this.contact = null;
        return this;
    }

    public SpeakerBuilder WithNullName()
    {
        this.name = null;
        return this;
    }

    public SpeakerBuilder WithAddress(string country, string zip, string city, string street)
    {
        this.address = new Address(country, zip, city, street);
        return this;
    }

    public SpeakerBuilder WithNullAddress()
    {
        this.address = null;
        return this;
    }

    public SpeakerBuilder WithSpeakerWebsites(
        Action<SpeakerWebsitesBuilder> speakerBuilderAction)
    {
        var speakerWebsiteBuilder = new SpeakerWebsitesBuilder();
        speakerBuilderAction(speakerWebsiteBuilder);
        speakerWebsites = speakerWebsiteBuilder.Build();
        return this;
    }

    public SpeakerBuilder WithSpeakerWebsites(
        SpeakerWebsites speakerWebSites)
    {
        speakerWebsites = speakerWebSites;
        return this;
    }

    public SpeakerBuilder WithNullSpeakerWebsites()
    {
        speakerWebsites = null;
        return this;
    }

    public Speaker Build()
    {
        return new Speaker
        (
            name,
            birthDate,
            address,
            speakerWebsites,
            Bio,
            contact
        );
    }

}

Jak widzisz w nim domyślnie metoda "GivenSpeaker" uzupełni pola bzdurnymi danymi. W naszym budowniczym ma także metody do czyszczenia uzupełnionych od górnie i umieszczanie w nich wartości NULL.

Dodatkowo mam szereg metod do uzupełnienia konkretnych właściwości mojego mówcy, jeśli z jakiegoś powodu dla testów te domyślne wartości miałby się zmienić.

Na potrzeby testów i nie tylko warto opakować sobie funkcję zwracają obecną datę i czas.

public class AppTime
{
    public static Func<DateTime> CurrentTimeProvider
    { get; set; } = () => DateTime.Now;

    public static DateTime Now() => CurrentTimeProvider();
}

Pozostało Ci pokazać użycie tych budowniczych w praktyce. Dla wygody pisania testów zainstalowałem paczkę NuGet "FluentAssertions".

FluentAssertions

Do wygodniejszego pisania testów warto dodać rejestracje przestrzeni nazw naszych statycznych metod w naszych budowniczych w taki sposób.

using static UnitTestSpeaker.SpeakerBuilder;

Dzięki temu będę mógł zacząć swoją budowę od napisania "GivenSpeaker()" niż "SpeakerBuilder.GivenSpeaker()".

Mamy już wszystko, co potrzebne, aby przetestować walidację naszych obiektów.

Układ projektu XUnit do testu walidacji

Oto jak wygląda testy jednostkowe napisane w XUnit.

Najpierw chce sprawdzić fakt, że mówca nie powinien się utworzyć bez imienia i nazwiska. Tak by wyglądał kod bez naszego budowniczego.

[Fact]
public void Speaker_CannotBeCreatedWithout_Name()
{
    Action act = () => new Speaker
    (
        null,
        new DateTime(1974, 6, 26),
        new Address("Poland", "00-001", "Warsaw", "Lemonowa 81"),
        new SpeakerWebsites(),
        ""
        , new Contact("555-555-555", "c@gmail.com")
    );

    act
        .Should()
        .Throw<ArgumentException>()
        .WithMessage("Name cannot be null");
}

A tak z:

[Fact]
public void Speaker_CannotBeCreatedWithout_Name()
{
    Action act = () => GivenSpeaker().WithNullName();

    act
        .Should()
        .Throw<ArgumentException>()
        .WithMessage("Name cannot be null");
}

Potem sprawdzę fakt, że mówca nie może się utworzyć bez adresu. Tak wygląda kod bez budowniczego.

[Fact]
public void Speaker_CannotBeCreatedWithout_Address()
{
    Action act = () => new Speaker
    (
        new Name("Cezary", "W"),
        new DateTime(1974, 6, 26),
        null,
        new SpeakerWebsites(),
         ""
        , new Contact("555-555-555", "c@gmail.com")
    );

    act
        .Should()
        .Throw<ArgumentException>()
        .WithMessage("Address cannot be null");
}

A tak z:

[Fact]
public void Speaker_CannotBeCreatedWithout_Address()
{
    Action act = () => GivenSpeaker().WithNullAddress();

    act
        .Should()
        .Throw<ArgumentException>()
        .WithMessage("Address cannot be null");
}

Potem sprawdzę fakt, że mówca nie powinien się utworzyć, jeśli nie ma daty urodzenia. Tak wygląda kod bez budowniczego.

[Fact]
public void Speaker_CannotBeCreatedWithout_Birthdate()
{
    Action act = () => new Speaker
    (
        new Name("Cezary", "W"),
        default,
        new Address("Poland", "00-001", "Warsaw", "Lemonowa 81"),
        new SpeakerWebsites(),
         ""
        , new Contact("555-555-555", "c@gmail.com")
    );

    act
        .Should()
        .Throw<ArgumentException>()
        .WithMessage("Birthdate cannot be empty");
}

A tak z:

[Fact]
public void Speaker_CannotBeCreatedWithout_Birthdate()
{
    Action act = () => GivenSpeaker().WithNullBirthDate();

    act
        .Should()
        .Throw<ArgumentException>()
        .WithMessage("Birthdate cannot be empty");
}

A na końcu sprawdzę fakt, że mówca nie powinien się utworzyć, jeśli nie ma linków do stron internetowych. Tak wygląda kod bez wzorca builder.

[Fact]
public void Speaker_CannotBeCreatedWithout_SpeakerWebsities()
{
    Action act = () => new Speaker
    (
        new Name("Cezary", "W"),
        new DateTime(1974, 6, 26),
        new Address("Poland", "00-001", "Warsaw", "Lemonowa 81"),
        null,
         ""
        , new Contact("555-555-555", "c@gmail.com")
    );

    act
        .Should()
        .Throw<ArgumentException>()
        .WithMessage("SpeakerWebsites cannot be empty");
}

A tak z:

[Fact]
public void Speaker_CannotBeCreatedWithout_SpeakerWebsities()
{
    Action act = () => GivenSpeaker().WithNullSpeakerWebsites();

    act
        .Should()
        .Throw<ArgumentException>()
        .WithMessage("SpeakerWebsites cannot be empty");
}

Mam nadzieje, że ten przykład łatwo pokazuje, dlaczego korzystamy ze wzorca Builder. Pomyśl, że ten obiekt "mówcy" potem zawiera się w obiekcie "zgłoszenia na mowy", a ten także będzie miał swoje testy.

Jeśli więc masz bardzo złożone obiekty to nawet do testów jednostkowych wydajnie jest napisać takiego budowniczego raz, aby potem skorcić sobie kod wszędzie indziej.

Budowniczy składni HTML

Pamiętasz te pierwsze przykłady, które omówiliśmy na początku wpisu. To fakt, że w .NET mamy szereg gotowych klas do budowania tekstów XML, JSON i nawet string.

Czasem jednak nadejdzie okazja, aby napisać swojego budowniczego, który np. ma wydrukować Ci kod HTML. Ja na potrzeby tego właśnie bloga raz napisałem budowniczego, który drukował mi tabelki HTML z odpowiednimi klasami CSS. Jest on użyty na stronie archiwum

Na potrzeby wpisu spójrzmy na coś konkretnego.

Oto nasza cegła, którą będziemy budować elementy tekstu HTML.

public  class HtmlElement
{
    public HtmlElement(string name)
    {
        Name = name;
    }

    public HtmlElement(string name, string text)
    {
        Name = name;
        Text = text;
    }

    public List<HtmlElement> Elements { get; set; } = new List<HtmlElement>();

    public string Name { get; set; }

    public string Text { get; }

    public override string ToString()
    {
        StringBuilder sb = new StringBuilder();

        Build(sb,this);

        return sb.ToString();
    }

    public StringBuilder Build(StringBuilder sb,HtmlElement htmlElement)
    {
        sb.AppendLine($"<{htmlElement.Name}>");

        if (!string.IsNullOrWhiteSpace(htmlElement.Text))
            sb.AppendLine(htmlElement.Text);

        foreach (var item in htmlElement.Elements)
        {
            Build(sb, item);
        }

        sb.AppendLine($"</{htmlElement.Name}>");

        return sb;
    }

}

Korzystając z rekurencji będziemy drukować po kolei elementy HTML, które są zagnieżdżone w sobie.

Oto nasz budowniczy

class HtmlBuilder
{
    protected HtmlElement root;

    public HtmlBuilder(string rootName)
    {
        root= new HtmlElement(rootName);
    }

    public void AddChild(string childName, string childText)
    {
        var e = new HtmlElement(childName, childText);
        root.Elements.Add(e);
    }

    public override string ToString() => root.ToString();
}

Oto przykład jego użycia:

var builder = new HtmlBuilder("ul");
builder.AddChild("li", "Twitter");
builder.AddChild("li", "Discord");
WriteLine(builder.ToString());

Co daje nam taki rezultat:

Działanie mojego HtmlBuilder

Uczą się na naszym poprzednim przykładzie składnie użycia budowniczego możemy skrócić budując tak zwany "Fluent Interface" czy "Fluent Method".

Przerabiajmy naszą metodę dodawania elementów HTML tak, aby ona zwracała samego budowniczego.

public HtmlBuilder AddChild(string childName, string childText)
{
    var e = new HtmlElement(childName, childText);
    root.Elements.Add(e);
    return this;
}

Dzięki temu nie będziemy musieli co chwilę odwoływać się do naszego obiektu.

var builder = new HtmlBuilder("ul");
builder.AddChild("li", "Facebook").AddChild("li", "Instagram");
WriteLine(builder.ToString());

Czy możemy ulepszyć ten przykład? Możemy do naszego budowniczego dodać informację o przesunięciach pomiędzy kolejnymi elementami HTML.

Łatwo jest to osiągnąć trzeba tylko przerobić kod naszej cegły elementu HTML.

protected const int indentSize = 2;

public int CurrentIndent { get; set; }

public StringBuilder Build(StringBuilder sb,HtmlElement htmlElement)
{
    sb.Append(new String(' ', htmlElement.CurrentIndent));

    sb.AppendLine($"<{htmlElement.Name}>");

    if (!string.IsNullOrWhiteSpace(htmlElement.Text))
    {
        sb.Append(new String(' ', htmlElement.CurrentIndent + 2));
        sb.AppendLine(htmlElement.Text);
    }

    foreach (var item in htmlElement.Elements)
    {
        item.CurrentIndent = htmlElement.CurrentIndent + indentSize;
        Build(sb, item);
    }

    sb.Append(new String(' ', htmlElement.CurrentIndent));
    sb.AppendLine($"</{htmlElement.Name}>");

    return sb;
}

Jak widać przesunięcia działają.

HtmlBuilder działanie z odstępami

Budowniczy a dziedziczenie i Fluent Interface

Tworzenie kilku budowniczych, którzy dziedziczą po sobie tworzy Ciekawy problem. Sam nie miałem okazji takiego budowniczego stworzyć, ale umówmy problem.

Mamy klasę, która reprezentuje zakupy. W niej mam informację o tym, kiedy był danych zakup i co kupiłem.

public class Shopping
{
    public string When;
    public string What;
}

Stworzyłem klasę abstrakcyjnego budowniczego i mój pomysł polega na tym, aby dla każdego pola był osobny budowniczy dziedziczący po każdym poprzednim budowniczym.

public abstract class ShoppingBuilder
{
    protected Shopping shopping = new Shopping();
    public Shopping Build()
    {
        return shopping;
    }
}

Dla określenia czasu zakupu będę miał takie budowniczego, a drugi będzie dziedziczył po pierwszy i będzie miał metodę do uzupełnienia właściwości "co kupiłem"

public class ShoppingTimeBuilder : ShoppingBuilder
{
    public ShoppingTimeBuilder When(string when)
    {
        shopping.When = when;
        return this;
    }
}

public class ShoppingWhatBuilder : ShoppingTimeBuilder
{
    public ShoppingWhatBuilder What(string what)
    {
        shopping.What = what;
        return this;
    }
}

Takie kombinowanie z dziedziczeniem oczywiście przerywa mechanizm "Fluent Interface", ponieważ moje metody budujące zwracają różne instancje różnych budowniczych. Ten kod także nie kompiluje się w C# 10. 

var me = new ShoppingWhatBuilder() 
.When("Today")
.What("Game") // will not compile
.Build();

O dziwo ten problem można rozwiązać, ale technika jest szalona jak wściekły pies. Trzeba stworzyć dla każdego takie budowniczego osobny typ generyczny, który daje nam możliwość zwracania ostatniego typu, który będzie po nim dziedziczył.

public class ShoppingTimeBuilder<SELF> : ShoppingBuilder
where SELF : ShoppingTimeBuilder<SELF>
{
    public SELF When(string when)
    {
        shopping.When = when;
        return (SELF)this;
    }
}

Następny budowniczy w parametrze SELF zwróci siebie i według tego mechanizmu metoda poprzednika też zwróci typ klas, który jest nad nim

public class ShoppingWhatBuilder<SELF>
: ShoppingTimeBuilder<ShoppingWhatBuilder<SELF>>
where SELF : ShoppingWhatBuilder<SELF>
{
    public SELF What(string what)
    {
        shopping.What = what;
        return (SELF)this;
    }
}

Nikt normalny by nie pisał takiego kodu, bo jest tutaj pewien problem.

Jeśli będziesz chciał stworzyć kolejnego budowniczego to będziesz musiał do niego i do każdego następnego  napisać dziwny łańcuszek typów generycznych, które zawierają kolejne typy generyczne i tak dalej.

A ja tego czegoś użyć. Musisz stworzyć oddzielny typ, który będzie dziedziczył po ostatnim takim szalonym generycznym budowniczym.

public class Shopping2
{
    public string When;
    public string What;

    public class Builder : ShoppingWhatBuilder<Builder>
    {
        internal Builder() { }
    }

    public static Builder New => new Builder();
}

Ta klasa będzie zawierać wszystkie metody ze wszystkich budowniczych. Tak ten kod działa, ale jest to bardzo pokręcone.

var me = Shopping2.New
.When("Today")
.What("Game") 
.Build();

Podsumowanie

To wszystko, co dla Ciebie przygotowałem, jeśli chodzi o ten wzorzec projektowy. Jak widzisz ten wzorzec projektowy jest jednym z ważniejszych technik w programowaniu. Było tak wczoraj, jest tak dziś i będzie tak jutro. 

Planuje zrobić jeden projekt na GitHubie, w którym będą wszystkie przykłady wzorców. Do zobaczenia

PanNiebieski/DesignPatternsInCSharp (github.com)