KompozycjaWzór.20 Czym jest ten kompozyt? Dlaczego zalicza się go do wzorców "Structural Patterns"?

Nasze obiekty składają się z innych obiektów. Najłatwiejszym przykładem kompozytu jest klasa, która implementuje interfejs IEnumerable<T> gdzie T jest tym innym obiektem. Czyli najłatwiejszym kompozytem jest klasa, która zachowuje się jak kolekcja innych obiektów?

Nie, jeszcze czegoś tutaj brakuje.

Nie musi to być koniecznie interfejs  IEnumerable<T>  bo w .NET taki interfejsów do budowania kolekcji jest więcej : Collection<T>, List<T>.

Alternatywnie twój kompozyt może mieć właściwość, która wystawia listę innych obiektów. Zazwyczaj tak jest, ale traktowanie kompozytu jako kolekcję też ma swoje zalety.

O co chodzi więc z tym wzorcem? O ile przykład z tworzeniem swojej kolekcji jest prosty i zrozumiały to zapomniałem zaznaczyć bardzo ważną rzecz.

Otóż kompozyt...ta kolekcja innych elementów także ma swoją tożsamość. Wyobraź sobie baterię, która składa się ze 100 komórek energii. 

Bateria ta ma swoją tożsamość być może numer seryjny i identyfikacyjny, ale także składa się z 100 oddzielnych obiektów.

public class Battery
{
    public int SerialNumber { get; set; }

    public List<Cell> Cells { get; set; }

    public Battery()
    {
        Cells = new List<Cell>();
        for (int i = 0; i < 100; i++)
        {
            Cells.Add(new Cell());
        }
    }
}

public class Cell
{}

Kompozytem też może być klasa umowy, która zawiera w sobie kolekcję swoim poprzednich wersji. Kto mówił, że obiekt wewnątrz twojego obiektu musi być czymś innym.

public class Contract
{
    public int Id { get; set; }
    public string Content  { get; set; }

    public List<Contract> PreviousVersions { get; set; }
}

Kompozytem też jest kielich, który ma w sobie kolekcję zdarzeń, które zaszły właśnie w nim

public class Glass
{
    public List<EventsOnGlass> Events { get; set; }

    public string Name { get; set; }
}

public abstract class EventsOnGlass
{}

Jeśli jesteś doświadczonym programistą to możesz mi zadać inne dobre pytanie. Czy ten wzorzec projektowy "Composite" to w sumie nie to samo co pojęcie "Aggregate" znany z programowania w stylu Domain-Driven-Desing?

Moim zdaniem pojęcie wzorca projektowego "Composite" i "Aggregate", to jedno i to samo. Dlatego, jeśli miałeś okazję kiedyś tworzyć agregaty, to też wiesz jak stworzyć kompozyty. 

Grupowanie klas jako drzewo

Wzorzec projektowy "Composite" grupuje więc klasy w taki sposób, że sama grupa także reprezentuje rzeczywisty elementy, który ma swoją tożsamość i inne właściwości.

Przykładowo, gdybyś chciał mapować tagi HTML to byś stworzył kompozyt, ponieważ każdy element HTML może zawierać dzieci, czyli kolejne elementy HTML.

public class UlHtmlElement
{
    public virtual string Name { get; set; } = "ul";
    public string CssClass;

    private Lazy<List<HtmlElement>> children =
    new Lazy<List<HtmlElement>>();

    public List<HtmlElement> Children => children.Value;
}

Nie każdy taki element musi zawierać kolejne. Jest to fantastyczny przykład, ponieważ jak widzisz każdy element jest traktowany jako jednostka, a zarazem jako kontener na kolejne elementy.

public class LiHtmlElement : UlHtmlElement
{
    public override string Name => "li";
}
public class OlHtmlElement : UlHtmlElement
{
    public override string Name => "ol";
}

Możemy stworzyć analogiczny przykład dla grafik, które mogą zawierać kolejne grafiki. Czyli traktujemy je zarazem jako jednostki, a zarazem jako kontenery na kolejne elementy.

public class Graphic
{
    public virtual string Name { get; set; } = "Group";
    public string Color;

    private Lazy<List<GraphicObject>> children =
    new Lazy<List<GraphicObject>>();

    public List<GraphicObject> Children => children.Value;
}

public class Circle : Graphic
{
    public override string Name => "Circle";
}
public class Square : Graphic
{
    public override string Name => "Square";
}

Do klasy głównej możemy napisać metodę drukującą całe drzewo pogrupowanych tak w sobie obiektów.

public class GraphicObject
{
    private void Print(StringBuilder sb, int depth)
    {
        sb.Append(new string('+', depth))
        .Append(string.IsNullOrWhiteSpace(Color) ? string.Empty :
        $"{Color} ")
        .AppendLine($"{Name}");
        foreach (var child in Children)
            child.Print(sb, depth + 1);
    }
    public override string ToString()
    {
        var sb = new StringBuilder();
        Print(sb, 0);
        return sb.ToString();
    }

Teraz przedstawmy tę strukturę w formie tekstowej.

var drawing = new GraphicObject { Name = "Rysunek" };
drawing.Children.Add(new Square { Color = "Green" });
drawing.Children.Add(new Circle { Color = "Purple" });

var group = new GraphicObject();
group.Children.Add(new Circle { Color = "Yellow" });
group.Children.Add(new Square { Color = "Yellow" });
drawing.Children.Add(group);
Console.WriteLine(drawing);

Oczywiście takie podejście tworzy pewien problem.

Co, jeśli wiemy, że pewne element nie będą miały swoich dzieci.

O ile elementy HTML dzieci zawsze mogą mieć to elementy jak kwadrat i kółko już takich pod elementów nie będą miały, co sprawia, że ten kod jest w swoim opisie świata jest nieprawidłowy.

W następnym przykładzie pokaże Ci rozwiązanie na tego problemu.

Jeśli co nie powinno zawierać w sobie kolejnego obiektu albo kolekcji to może trzeba potraktować wszystkie elementy jak kolekcję, która czasem zwróci tylko 1 element, a czasem zbiór elementów. Zapewne się zastanawiasz, o czym ja mówię.

Sieci neuronowe dobrze pokażą nam użyteczność takiego podejścia.

Sieci Neuronowe

Machine Learning ostatnio jest bardzo popularne. Jedna z technik "Machine Learning" polega na stworzeniu swojej własnej sztucznej sieci neuronowej, która się uczy jak mózg w człowieku.

Zastanawiałeś się jak przy pomocy klas stworzyć relację między neuronami tak, abyśmy mieli sieć neuronów albo całe pierścienie? Pora Ci taki przykład pokazać.

My skoncentrujemy się na strukturze naszego rozwiązania, dlatego nasze neurony nie będą robić nic. 

public class Neuron
{
    public List<Neuron> In { get; set; }
    public List<Neuron> Out { get; set; }

    public void ConnectTo(Neuron other)
    {
        Out.Add(other);
        other.In.Add(this);
    }

    public Neuron()
    {
        In = new List<Neuron>();
        Out = new List<Neuron>();
    }
}

Nasz neuron powinien mieć informację o tym, jakie neurony do niego wchodzą, a jakie wychodzą. 

Metoda ConnecTo odpowiednio takie powiązanie stworzy między neuronami. 

Teraz chcielibyśmy stworzyć warstwę, która reprezentuje zbiór naszych neuronów.

public class NeuronLayer : Collection<Neuron>
{
    public NeuronLayer(int count)
    {
        while (count-- > 0)
            Add(new Neuron());
    }
}

Tutaj oczywiście pojawia się pewien problem. Chcielibyśmy, aby nasze neurony mogły się łączyć z warstwami i nie tylko. Chcielibyśmy, aby :

  • Neuron mogły się łączyć z innymi neuronem
  • Neuron mogły się łączyć z daną warstwą
  • Warstwa mogła się łączyć z danym neuronem
  • Warstwa mogła się łączyć z inną warstwą

Jak to zrobić? Możemy stworzyć 4 metody, które wszystkie te kombinacje rozwiązują? No chyba nie

Co, jeśli mielibyśmy 3 klas. Wtedy musielibyśmy wyprodukować 9 takich metod.

Rozwiązanie tego problemu polega na zrozumieniu, że i neurony, jak i warstwy mogą być traktowane jako kolekcję.

Neuron będzie kolekcją, którą będzie zwracała tylko siebie. Warstwa neuronów będzie zwracała natomiast zbiór neuronów.

public class Neuron : IEnumerable<Neuron>
{
    public List<Neuron> In { get; set; }
    public List<Neuron> Out { get; set; }

    public IEnumerator<Neuron> GetEnumerator()
    {
        yield return this;
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Nasza warstwa neuronów jak same neurony są złączone teraz interfejsem IEnumerable<Neuron>.

Teraz pozostało stworzyć metodę, która obie te kolekcje będzie łączyć.

public static class ExtensionMethods
{
    public static void ConnectTo(
    this IEnumerable<Neuron> self, IEnumerable<Neuron> other)
    {
        if (ReferenceEquals(self, other)) return;
        foreach (var from in self)
            foreach (var to in other)
            {
                from.Out.Add(to);
                to.In.Add(from);
            }
    }
}

To rozwiązuje wiele problemów. Gdybyśmy chcieli stworzyć pierścień neuronowy, który jest kolejną formą grupowania to nie będzie z tym problemu. Stworzymy wtedy listę list, ale nadal będziemy mieć wspólny interfejs IEnumerable.

Oto dowód tego, że to działa.

Neuron a =  new Neuron();
Neuron b = new Neuron();
a.ConnectTo(b);

List<Neuron> layer = new List<Neuron>();
Neuron c = new Neuron();
Neuron d = new Neuron();
layer.Add(c);
layer.Add(d);

b.ConnectTo(layer);

Podsumowanie

Kompozyt to wzorzec projektowy, który daje wspólny interfejs dla indywidualnych obiektów, jak i kolekcji jeszcze innych obiektów . 

Pokazałem Ci implementacje tego wzorca na dwa sposoby.

  • Wystawiamy w obiekcie po prostu kolekcje innych lub tych samych obiektów jako właściwość, lub pole. Pokazałem Ci to na przykładzie elementów HTML i elementów figur.
  • Sprawiamy, aby obiekt, z którym pracujemy zachowywał się jak kolekcja. Możemy to zrobić poprzez implementacje interfejsu "IEnumerable<T>". Tak jak zrobiliśmy to na przykładzie neuronów.

Do zobaczenia w następnym wpisie.