ProxyWzór.23 We wzorcu projektowy "Dekorator" widzieliśmy jak można dodawać kolejne funkcjonalności bez zmiany oryginalnego zachowania. Wzorzec Proxy próbuje osiągnąć to samo tylko gorzej. Warto zaznaczyć, że ten wzorzec nie ma jednej słusznej implementacji. Wiele osób podchodzi do tego wzorca na wiele sposobów. 

Gdyby pada słowo "Proxy" to zazwyczaj mówimy o pośredniku komunikacyjnym między serwerami, które gadają do siebie.

Wzorzec projektowy Proxy też jest takim pośrednikiem między obiektami i jego rola polega na zabezpieczeniu,rozszerzeniu, zmodyfikowaniu jakieś innej funkcji systemu, która jest  pod nim.

W zależności od celu wzorzec ten będzie miał inną implementację. Dlatego nie ma on jednego dobrego podejścia.

Spójrz więc na te różne podejścia do tego wzorca. :

  • Protection Proxy,
  • Property Proxy,
  • Virtual Proxy 
  • Communication Proxy
  • i wiele innych, które możesz znaleźć w sieci

Protection Proxy

Idea "Protection proxy" jak sama nazwa wskazuje polega na zabezpieczeniu pewnego obiektu przed jakimś warunkiem biznesowym.

Mam więc komputer, który potrafi uruchamiać kod.

public class Computer : IComputer 
{
    public void RunCode()
    {
        Console.WriteLine("WriteCode");
    }
}

Później stwierdziłem, że nie każdy programista przed komputerem będzie mógł swój kod uruchomić. Wymyśliłem taką zasadę biznesową, że jeśli jesteś programistą XML lub HTML to niestety komputer powinien odmówić uruchomienia twojego kodu.

Z jakiegoś powodu nie mogę zmienić już swojej klasy Computer, aby to zrobić. Przyzwyczaja się do tego, bo zazwyczaj przez to, że nie możemy czegoś zmienić tworzymy klasy Proxy w każdym przykładzie w tym wpisie.

Postanowiłem napisać interfejs, który w żaden sposób nie modyfikuje już istniejącej klasy Computer.

public interface IComputer
{
    void RunCode();
}

Nasze proxy będzie zależne od "programisty". Ma on informację o tym, w czym on programuje.

public class Programer
{
    public PrograminLanguage Language { get; set; }
    public Programer(PrograminLanguage programinLanguage)
    {
        Language = programinLanguage;
    }
}

public enum PrograminLanguage
{
    JavaScript,
    CSharp,
    Java,
    HTML,
    XML
}

Jak wygląda nasze proxy? Jak widzisz pobieram sobie w konstruktorze instancje danego programisty. 

public class ComputerProxy : IComputer
{
    private Computer computer = new Computer();
    private Programer _programer;

    public ComputerProxy(Programer programer)
    {
        _programer = programer;
    }

    public void RunCode()
{ if (_programer.Language == PrograminLanguage.HTML) { Console.WriteLine("Html nie jest językiem programowania więc tego nie uruchamiam"); return; } if (_programer.Language == PrograminLanguage.XML) { Console.WriteLine("XML nie jest językiem programowania więc tego nie uruchamiam"); return; } computer.RunCode(); } }

Później w zależności od tego, w czym nasz programista programuje to albo uruchomimy kod, albo wyświetliły wiadomość o tym właśnie naszym zabezpieczeniu przed programistami XML i HTML.

Oto jak wygląda użycie naszego proxy.

IComputer com = new ComputerProxy
    (new Programer(PrograminLanguage.HTML));
com.ExecuteCode();

Czy ten wzorzec jest dobry? Wiesz, że lubię zadawać takie pytania. Największy problem tego wzorca leży w tym, że o ile może się wydawać, że klasy Computer i ComputerProxy są podobne to mają zupełnie inne konstruktory.

Czyli jeśli ComputerProxy ma służy za rozszerzenie istniejącej już funkcjonalności to ciężko tobię będzie zastąpić istniejące już użycia klasy Computer w systemie.

Zwłaszcza jeśli nie korzystałeś ze wstrzykiwania zależności i musi teraz taką poprawę zrobić w całym systemie.

Ten wzorzec ma sens, jeśli masz kontener wstrzykiwania zależności w przeciwnym wypadku niczego tak naprawdę nie rozwiązujesz i tak naprawdę tworzysz sobie kolejny problem na przyszłość.

Property Proxy

W C# korzysta z właściwości, które tak naprawdę są dobrze opakowanymi metodami GET i SET dla danego pola.

Czasami chcesz, aby twoje właściwości robiły coś jeszcze, gdy następuje proces przypisania wartość lub jej pobierania.

Dla tego przykładu chcemy zablokować przypisanie wartości do naszej właściwości, gdy próbujemy ustawić jej wartość na domyślną albo na null.

Korzystając z tego faktu możemy także w konsoli wyświetlić jakie wartości są w ogóle przypisywane do naszej właściwości.

Moglibyśmy skorzystać z normalnych właściwości, aby osiągnąć ten cel, ale my chcemy zobaczyć użycie wzorca Proxy, który nazywa się Property Proxy.

Property Proxy opakuje nam każdą właściwości, abyśmy od razu mieli takie zachowanie. Potem będziemy mogli skorzystać z naszej klasy "Property Proxy" w wielu miejscach.

To oczywiście odbije się kosztem wydajności, ale załóżmy, że nagle musimy do 1000 właściwości w różnych klasach takie zachowanie dopisać i musisz tę zmianę w kodzie jakoś zautomatyzować do dalszych poprawek.

Oto jak wygląda nasza klasa, która stworzy nam "Property Proxy".

public class Property<T> where T : new()
{
    private T value;
    private readonly string name;
    public T Value
    {
        get => value;
        set
        {
            if (Equals(this.value, value)) return;
            if (value == null || value.Equals(default(T))) return;

            Console.WriteLine($"Assigning {value} to {name}");
            this.value = value;
        }
    }
    public Property() : this(default(T)) { }
    public Property(T value, string name = "")
    {
        this.value = value;
        this.name = name;
    }

Zwróć uwagę na blokadę uzupełniania wartości w tej klasie.

set
{
    if (Equals(this.value, value)) return;
    if (value == null || value.Equals(default(T))) return;

    Console.WriteLine($"Assigning {value} to {name}");
    this.value = value;
}

...

Musi on zawierać jawną konwersję na typ, który będziemy opakowywać.

public static implicit operator T(Property<T> property)
{
    return property.Value; 
}
public static implicit operator Property<T>(T value)
{
    return new Property<T>(value); 
}

Pozostało nam już użyć tego wzorca:

public class Programer
{
    public Property<int> Luck
      = new Property<int>(10, nameof(Luck));

    public Property<int> Wisdom
        = new Property<int>(6, nameof(Wisdom));

    public Property<int> Intelligence
        = new Property<int>(7, nameof(Intelligence));
}

Niestety takie podejście nie działa. Ten kod nie zrobi nic.

var p = new Programer();
p.Wisdom = 12;
p.Luck = 12;
p.Intelligence = 0;

Problem leży w jawnej konwersji naszego proxy, która niestety nie potrafi modyfikować istniejącej właściwości. Taki kod zadziała, ale nie o to nam chodzi:

var p1 = new Programer();
p1.Wisdom.Value = 12;
p1.Luck.Value = 12;
p1.Intelligence.Value = 0;

Rozwiązanie polega na stworzeniu pól, które będą opakowane naszym proxy, a te pola będą referować się do istniejących już właściwości 

public class Programer2
{
    public readonly Property<int> luck
    = new Property<int>(10, nameof(luck));

    public readonly Property<int> wisdom = 
        new Property<int>(6, nameof(wisdom));

    public readonly Property<int> intelligence =
    new Property<int>(7, nameof(intelligence));

    public int Luck
    {
        get => luck.Value;
        set => luck.Value = value;
    }

    public int Wisdom
    {
        get => wisdom.Value;
        set => wisdom.Value = value;
    }

    public int Intelligence
    {
        get => intelligence.Value;
        set => intelligence.Value = value;
    }
}

Teraz ten kod zadziała. 

var p2 = new Programer2();
p2.Wisdom = 12;
p2.Luck = 12;
p2.Intelligence = 0;

Ten wzorzec wygląda kiepsko, ponieważ trzeba stworzyć oddzielną klasę do tego wzorca, jak i zmodyfikować istniejący już kod, a tego właśnie chcieliśmy uniknąć używając wzorca Proxy.

Te problemy mogą wynikać z ograniczeń języka C#, ale ciężko jest obronić ten wzorzec. Zmodyfikowaliśmy lekko kod, aby w teorii nie pisać jeszcze więcej kodu, który będzie się powtarzać, ale to od Ciebie zależy decyzja czy ten wzorzec ma sens.

Virtual Proxy

Virtual Proxy już istnieje i nazywa się Lazy<T>.

Lazy<T> utworzy Ci instancje obiektu, gdy ten będzie potrzebny. Chodzi o to, aby uniknąć czkawki przy pierwszym uruchomieniu całego systemu, gdy wszystkie obiekty się tworzą.

Na potrzeby wpisu szybko utworzę swoje Wirtualne Proxy.

interface IFile
{
    void Show();
}

Rola "Virtual Proxy" polega na użyciu oryginalnego API tak, aby stworzyć iluzję jego instancji. A tak naprawdę to oryginalne API zostanie utworzone, dopiero gdy będzie ono potrzebne.

Oto klasa, która odczyta plik tekstowy. Jak widzisz operację odczytu pliku zrobi się już w konstruktorze.

class TextFile : IFile
{
    private readonly string _path;

    private readonly List<string> _texts;

    private int _lines;
    public TextFile(string path)
    {
        this._path = path;
        _texts = new List<string>();

        using (StreamReader file = new StreamReader(_path))
        {
            int counter = 0;
            string ln;

            while ((ln = file.ReadLine()) != null)
            {
                _texts.Add(ln);
                counter++;
            }
            file.Close();
            _lines = counter;

        }
    }

    public void Show()
    {
        foreach (var item in _texts)
        {
            Console.WriteLine("===========");
            Console.WriteLine(item);
        }
        Console.WriteLine($"How many lines : {_lines}");
    }
}

Chcielibyśmy jednak aby ten konstruktor nie próbował otworzyć pliku natychmiast.

TextFile file = new TextFile(@"D:\text.txt");

Załóżmy, że nie możemy zmieniać tej istniejącej klasy, czyli musimy napisać swoje proxy, które nam zmodyfikuje takie zachowanie.

Oto nasze "Virtual Proxy"

class LazyTextFile : IFile
{
    private readonly string _path;

    private TextFile _textFile;

    public LazyTextFile(string path)
    {
        this._path = path;
    }

    public void Show()
    {
        if (_textFile == null)
            _textFile = new TextFile(this._path);

        _textFile.Show();
    }
}

Ta klasa stworzy instancje "TextFile" gdy ten będzie potrzebny, gdy skorzystamy z metody "Show()".

Obie klasy korzystają z tego samego interfejsu i mają taki sam konstruktor. Znaczy to, że łatwo możesz podmienić implementacje.

LazyTextFile file = new LazyTextFile(@"D:\text.txt");

file.Show();

Communication Proxy

Zadaniem tego proxy jest zasymulować zachowanie pewnego serwisu i jego metody, gdy ten znajduje się gdzieś w innym zasobie sieciowym.

Oto prosty przykład takiego proxy. Mamy interfejs, który sprawdza, czy dany serwis działa.

interface ICheck
{
    string Check(string message);
}

W aplikacji ASP.NET Core te sprawdzenie wyglądałoby tak.

[Route("api/[controller]")]
public class HomeController : ICheck
{
    [HttpGet("{message}")]
    public string Check(string message)
    {
        return message + "pong";
    }
}

W innej aplikacji chcemy uruchomić tę metodę. Tylko dla niej te API istnieje jako zasób sieciowy, który żyje pod jakiś adresem.

class RemoteHomeCheck : ICheck
{
    public string Check(string message)
    {
        string uri = "http://localhost:7149/api/home/" + message;
        return new WebClient().DownloadString(uri);
    }
}

Nasze "Communication Proxy" ten problem rozwiąże i z punktu widzenia użytkownika tworzymy iluzję tego, że ta usługa jest w naszej aplikacji. 

Chociaż tak naprawdę wysyłamy zapytanie HTTP, aby otrzymać rezultat oryginalnego API. 

Podsumowanie

W tym wpisie pokazałem Ci wiele różnych proxy. W przeciwieństwie do dekoratora Proxy nie wystawia publicznie oryginalnego API, które on przykrywa.

Zazwyczaj nie wymaga on modyfikacji oryginalnego kodu, ale jak sam się przekonałeś nie zawsze tak jest. 

Analogicznie możemy napisać "Logging Proxy", który jak się domyślasz przykrywa oryginalne API tylko po to, aby dodać informację o logowaniu.

Istnieje mnóstwo wzorców "Proxy". Istnieje szansa, że sam napisałeś w jakimś celu Proxy i nawet o tym nie wiedziałeś, że jest to oficjalny wzorzec projektowy.