ConnectingWzór.7 Większa część kodu, który piszemy, ma różne komponenty (klasy), które gadają ze sobą poprzez bezpośrednią referencję. Jednakże zdarzają sytuację, w których chciałbyś ,aby obiekty nie były świadome swojej egzystencji. 

Może chciałbyś, aby obiekty były świadome siebie, ale nie chcesz przekazywać za każdym razem referencyjni do nich

W końcu za każdym razem, gdy wysyłasz referencje obiektu, to przedłużasz jego życie w pamięci.

Mediator jest wzorce projektowym, który ma ułatwić komunikację pomiędzy obiektami, które fachowo nazywam kompomentami 

Mediator ma dostęp do każdego komponenty . Oznacza to, że powinien być on albo publiczny polem statycznym, albo singletonem, który jest wstrzykiwany do każdego komponentu, który go potrzebuje.

Chat Room

Typowy chat room jest doskonałym przykładem wzorca projektowego Mediator.  

public class UserInChat
{
    public string Name;
    public ChatRoom Room;
    private List<string> _chatLog = new List<string>();

    public UserInChat(string name) => Name = name;

    public void Receive(string sender, string message)
    {
        string s = $"{sender}: '{message}'";
        Console.WriteLine($"[{Name}'s chat session] {s}");
        _chatLog.Add(s);
    }

    public void Say(string message) => Room.Broadcast(Name, message);
    public void PrivateMessage(string who, string message)
    {
        Room.Message(Name, who, message);
    }
}

Oto klasa reprezentująca osobę na chacie. Zakładamy, że nazwa użytkownika jest unikatowa i to będzie jego ID. W klasie mamy listę string, która reprezentuje log chatu dla tego użytkownika.

Co najważniejsze klasa ma referencje do ChatRoom-u. 

 Mamy tutaj także 3 metody:

  • Recive() Metoda ta pozwala na przyjęcie wiadomość do chatu tego użytkownika. W tym przypadku zostanie ona dodana do listy wiadomości
  • Say() Pozwala wysłać wiadomość do wszystkich użytkowników w pokoju
  • PrivateMessage() Wysyłasz wiadomość do określonego użytkownika, podając jego ID, czyli nazwę

A jak wygląda centrum naszej aplikacji, czyli klasa ChatRoom.

public class ChatRoom
{
    private List<UserInChat> users = new List<UserInChat>();

    public void Broadcast(string source, string message)
    {
        throw new NotImplementedException();
    }

    public void Join(UserInChat p)
    {
        throw new NotImplementedException();

    }

    public void Message(string source, string destination,
    string message)
    {
        throw new NotImplementedException();
    }
}
  • Metoda Join pozwala dodać osobę do pokoju. 
  • Broadcast() wysyła wiadomość do wszystkich oprócz oczywiście użytkownika, który tę wiadomość wysłał
  • Message() wysyła wiadomość prywatną.

Implementacja metody Join() jest następująca. 

public class ChatRoom
{
    private List<UserInChat> users = new List<UserInChat>();
    
    public void Join(UserInChat p)
    {
        string joinMsg = $"{p.Name} joins the chat";
        Broadcast("room", joinMsg);
        p.Room = this;
        users.Add(p);
    }

Tak jak w klasycznym IRC, gdy pojawia się nowa osoba w pokoju, to informujemy o tym wszystkich. Referencje do naszego pokoju jest klasa ChatRoom i ją przekazujemy użytkownikowi.

Metoda Broadcast wysłał wszystkim użytkownikom wiadomość, tak jak pisałem wcześniej. 

public class ChatRoom
{
    private List<UserInChat> users = new List<UserInChat>();
    
    public void Broadcast(string source, string message)
    {
        foreach (var p in users)
            if (p.Name != source)
                p.Receive(source, message);
    }
    
    public void Message(string source, string destination,
    string message)
    {
        users.FirstOrDefault(p => p.Name == destination)
        ?.Receive(source, message);
    }

Metoda Message wyszukuje użytkownika i jeśli go znajdzie to wyślę TYLKO do niego prywatną wiadomość.

Wracają do użytkownika. Jak widzisz, metoda Say używa metody Broadcast, aby wysłać wiadomość wszystkim. Natomiast metoda PrivateMessage korzysta z metody Message i ona wyślę wiadomość tylko do jednego użytkownika.

public void Say(string message) => Room.Broadcast(Name, message);

public void PrivateMessage(string who, string message)
{
    Room.Message(Name, who, message);
}

Metoda Receive wyświetli wiadomość w konsoli dla danego użytkownika.

public void Receive(string sender, string message)
{
    string s = $"{sender}: '{message}'";
    Console.WriteLine($"[{Name}'s chat session] {s}");
    _chatLog.Add(s);
}

Pora zobaczyć jak nasz ChatRoom działa.

var room = new ChatRoom();
var adam = new UserInChat("Adam");
var ewa = new UserInChat("Ewa");

room.Join(adam);
room.Join(ewa);

adam.Say("Cześć, co tam ? Jak leci wam ten poniedziałek?");
ewa.Say("Kod nie działa tak jak chce");
var darek = new UserInChat("Darek");
room.Join(darek);
darek.Say("Co nowego?");
ewa.PrivateMessage("Darek", "Cieszę się, że dołączyłeś");

Oto przykład wzorca Mediator. Nasz ChatRoom jest obiektem komunikującym się pomiędzy użytkownikami

Pytanie, czy można użyć tego wzorca jeszcze lepiej

Mediator z zdarzeniami 

Wiem, za każdym razem, gdy mówimy o zdarzeniach, to znaczy, że mamy tutaj jakąś wariancję ze wzorcem projektowym Observer. 

Co to ma do wzorca Mediator? Pomysł jest taki, by mieć zdarzenie, które będzie współdzielone przez wiele obiektów. Niektóre obiekty będą odpalać te zdarzenia, a niektóre obiekty będą wychwytywać odpalone zdarzenie. 

Zróbmy jakiś inny przykład. Mam szkolną salę, a w niej są uczniowie i nauczyciele. Gdy uczeń skończy dwa zadania, chcemy, aby nauczyciel dał pochwałę danemu uczniowi.

Nauczyciel musi mieć więc złapać zdarzenie ukończonego zadania i sprawdzić, które zadanie to jest.

Najpierw stworzyłem zdarzenie, które ma metodę abstrakcyjną, która będzie drukować komunikaty związane z działaniem tej aplikacji.

public abstract class ClassRoomEventArgs : EventArgs
{
    public abstract void Print();
}

Teraz dziedziczę po tym zdarzeniu i tworze zdarzenie ukończonego zadania przez studenta.

public class StudentDoneTaskEventArgs : ClassRoomEventArgs
{
    public string StudentName;
    public int TaskDoneSoFar;
    public StudentDoneTaskEventArgs
    (string playerName, int taskDoneSoFar)
    {
        StudentName = playerName;
        TaskDoneSoFar = taskDoneSoFar;
    }
    public override void Print()
    {
        Console.WriteLine($"{StudentName} done task!" +
        $"(this is thier {TaskDoneSoFar} task)");
    }
}

A co z naszą salą lekcyjną? Ponownie budujemy Mediatora, ale w tym razem nie ma w nim żadnych metod, ponieważ nie są one potrzebne. Nasz mediator ma wyzwalacz zdarzenia ClassRoomEventArgs i metodę FIRE , by te zdarzenie uruchomić.

class ClassRoom
{
    public event EventHandler<ClassRoomEventArgs> Events;
    public void Fire(ClassRoomEventArgs args)
    {
        Events?.Invoke(this, args);
    }
}

Teraz przechodzimy do studenta. Ma on imię i licznik zrobionych zadań. Musi on mieć referencję do sali lekcyjnej, by uruchomić zdarzenia z sali lekcyjnej. 

Zdarzenie jest uruchomione, za każdy razem, gdy student zrobi zadanie.

class Student
{
    private string name;
    private int taskDone = 0;
    private ClassRoom classRoom;
    public Student(ClassRoom classRoom, string name)
    {
        this.name = name;
        this.classRoom = classRoom;
    }
    public void MakeTaskDone()
    {
        taskDone++;
        var args = new StudentDoneTaskEventArgs(name, taskDone);
        classRoom.Fire(args);
    }
}

Teraz kto będzie odbierał te zdarzenie? Oczywiście nauczyciel. W konstruktorze nauczyciel subskrybuje się do zdarzenia skończonych zadań studentów.

Co więcej, mogę obsłużyć to zadanie, gdy student zrobi więcej niż dwa zadania. Czyli przy trzecim zadaniu nauczyciel powinien powiedzieć "Well Done".

class Teacher
{
    private ClassRoom game;
    public Teacher(ClassRoom game)
    {
        this.game = game;

        game.Events += (sender, args) =>
        {
            if (args is StudentDoneTaskEventArgs scored
            && scored.TaskDoneSoFar > 2)
            {
                Console.
                WriteLine($"Teacher says: well done, {scored.StudentName}");
            }
        };
    }
}

Poza tym muszę sprawdzić, czy argumenty zdarzenia są odpowiedniego typu.

var classRoom = new ClassRoom();
var student = new Student(classRoom, "Darek");
var teacher = new Teacher(classRoom);
student.MakeTaskDone();
student.MakeTaskDone();
student.MakeTaskDone();

Krótkie sprawdzenie rozwiązania i rzeczywiście nauczyciel zacznie gratulować, dopiero gdy student zrobi trzecie zadanie

Podsumowanie:

Wzorzec projektowy Mediator polega na posiadaniu jednego obiektu, który łączy wszystkie inne komponenty systemu. 

W tym wpisie zobaczyliśmy prostą implementację tego wzorca, jak i ciekawszą wersję, która korzystała z zdarzeń, do których można się podpiąć lub nie.

Szczerze myślę, że jest to jeden ze wzorów, z którego korzystamy nawet nieświadomie.