AdapterWzór.18 Być może zdarzyło Ci się posiadać ładowarkę do telefonu ze złym wejściem.W takiej sytuacji jesteś zmuszony kupić albo nowy kabel z prawidłowym dla Ciebie wejściem, albo adapter.

Być może jako podróżnik nosisz ze sobą adapter, aby podłączyć urządzenie z  Europejskim wejściem do socketów amerykańskich i brytyjskich. 

Wzorzec projektowy adapter polega na tym, że mamy już jakiś interfejs, ale nie podoba nam  się jego wejścia albo wyjścia więc nakładamy na niego swój adapter, który nam da to, co chcemy.

Tak jak każdy wzorzec i ten ma swój diagram UML

Adapter diagram UML

Ja jednak by nie skupił się na strukturze tego wzorca, chociaż zawiera on się w kategorii wzorców "Structural Patterns". Bo jeśli popatrzysz na adapter w .NET to zauważysz, że wiele z nich podchodzi do tego problemu czasem w inny sposób. 

  • LINQ : oferuje wiele adapterów i wszystkie te adaptery współpracują z operatorami LINQ jak Where, Select. Dla zapytań bazo danowych SQL twoje wyrażenie drzewiaste czy funkcja lambda jest tłumaczona zapytanie do bazy. Dla danych XML czy normalnej listy obiektów zapewne będzie to wyglądało inaczej, ale użycie metod Where,Select wygląda tak samo
  • Streams : Strumienie jak "TextWriter", "XmlReader", adaptują strumień do zapisu lub odczytu danego typu danych. Odpowiedni adapter strumienia sobie z tym poradzi nieważne czy to są dane binarne, czy tekstowe.
  • ADO.NET : posiada klasy jak SqlCommand które adaptują zapytania w stylu obiektowym do stylu tekstowego SQL. Każdy adapter ADO.NET odnosi się do innej bazy danych.

To jeszcze nie koniec przykładów: 

P/Invoke

W C# istnieje atrybut [P/Invoke], który służy jako adapter do komunikacji między C# a bibliotekami napisanymi w C lub C++. 

using System.Runtime.InteropServices;

MessageBox(IntPtr.Zero, "Your Text is here", "Attention!", 0);

[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);

To samo tyczy się obiektów COM, z których kiedyś korzystałem, aby tworzyć pliki WORD czy Excel.

WPF i IValueConverter

Ciekawym przykładem adaptera jest "IValueConverter" z WPF.  Ten interfejs służy do tworzenia adapterów między jakiś jednym obiektem a drugim. Ta konwersja zazwyczaj służy do przetworzenia jednego formatu danych na napis do wyświetlenia w kontrolce WPF.

Pora Ci pokazać konkretny przykład. Oto kod XAML naszej aplikacji WPF.

<Window x:Class="WpfIValueConverterIsAExampleOfAdapter.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfIValueConverterIsAExampleOfAdapter"
        mc:Ignorable="d"
        Title="MainWindow" Height="140" Width="250">
    <Window.Resources>
        <local:YesNoToBooleanAdapterConverter x:Key="YesNoToBooleanConverter" />
    </Window.Resources>
    <StackPanel Margin="10">
        <TextBox Name="txtValue" />
        <WrapPanel Margin="0,10">
            <TextBlock Text="Current value is: " />
            <TextBlock Text="{Binding ElementName=txtValue, Path=Text, Converter={StaticResource YesNoToBooleanConverter}}"></TextBlock>
        </WrapPanel>
        <CheckBox IsChecked="{Binding ElementName=txtValue, Path=Text, Converter={StaticResource YesNoToBooleanConverter}}" Content="Yes" />
    </StackPanel>
</Window>

A oto nasza implementacja "IValueConverter". Będzie konwertować napis na wartość logiczną oraz jak widzisz mamy metodę, która działa dokładnie odwrotnie.

public class YesNoToBooleanAdapterConverter : IValueConverter
{
    public object Convert(object value, Type targetType, 
        object parameter, System.Globalization.CultureInfo culture)
    {
        switch (value.ToString().ToLower())
        {
            case "yes":
            case "oui":
            case "tak":
                return true;
            case "no":
            case "non":
            case "nie":
                return false;
        }
        return false;
    }

    public object ConvertBack(object value, Type targetType, 
        object parameter, System.Globalization.CultureInfo culture)
    {
        if (value is bool)
        {
            if ((bool)value == true)
                return "yes";
            else
                return "no";
        }
        return "no";
    }
}

Co wyróżnia ten adapter to fakt, że działa on w dwie strony.  Mogę uzupełnić pole tekstowe i zobaczyć, że mój checkbox się zaznacza.

Przykład IValueConverter w WPF jako adapter

Mogę też zaznaczyć checkbox i zobaczyć jak pojawia się wartość tekstowa w mojej kontrolce.

Przykład IValueConverter w WPF jako adapter działa w dwie strony

Jak widzisz adapter istnieją wszędzie tylko nie zawsze mają taką samą strukturę.

Moim zdaniem ważne jest rozwiązanie danego problemu niż trzymanie konwencji wzorca tak, aby wyglądało to dokładnie tak samo jak w diagramie UML.

Jeśli chodzi o ten wpis to pozostało Ci pokazać na przykładzie jak taki adapter napisać od zera.

Prosty przykład Adaptera, który nic nie zwraca

Pora na przykład książkowy. 

Można go potem użyć praktycznie wystarczy pozmieniać nazwy klas i metod, aby miał to jakiś konkretny cel biznesowy. Mój przykład będzie na razie bazował na ptakach.

Mamy interfejs ptaka. Ptak może latać i wydawać dźwięki.

public interface IBird
{
    //Ptaki implemetują ten interfejs
    //pozwala on im latać i wydawać dzwięki
    public void Fly();
    public void MakeSound();
}

Obiekt bociana może implementować ten interfejs.

public class Stork : IBird
{
    public void Fly()
    {
        Console.WriteLine("Flying");
    }

    public void MakeSound()
    {
        Console.WriteLine("Kle kle");
    }
}

Teraz zakładamy, że my tych klas i interfejsów nie możemy modyfikować i chcemy przerobić dźwięki tych ptaków na zabawki w kształcie kaczki.

public interface IToyDuck
{
    // Ten interface
    // toyducks nie lata
    // ale wydaje dzwięk
    public void Squeak();
}

Możemy stworzyć implementacje tego interfejsu, ale nas interesuje możliwość użycia istniejących klas ptaków i translacji ich na zabawki, które wydają dźwięk.

public class PlasticToyDuck : ToyDuck
{
    public void Squeak()
    {
        Console.WriteLine("Squeak");
    }
}

W tym celu musimy stworzyć swój adapter.

public class BirdAdapter : ToyDuck
{
    Bird bird;
    public BirdAdapter(IBird bird)
    {
        // musimy mieć referencje do obiektu
        // który będziemy adaptować
        this.bird = bird;
    }

    public void Squeak()
    {
        // przetłumacz odpowiednio metody
        bird.MakeSound();
        bird.MakeSound();
    }
}

Nasz adapter wywoła "MakeSound()" dwa razy. Tak rozumiemy translacje działania naszego ptaka.

Użycie tych klas wygląda tak:

Stork stork = new Stork();
IToyDuck toyDuck = new PlasticToyDuck(); Console.WriteLine("Stork...");
sparrow.Fly(); sparrow.MakeSound(); Console.WriteLine("ToyDuck..."); toyDuck.Squeak();

Nas jednak interesuje użycie adaptera i wygląda to tak.

Stork sparrow = new Stork ();
// Zawijamy naszego ptaka do adaptera
// tak aby zachowywał się on jak kaczka
ToyDuck birdAdapter = new BirdAdapter(sparrow);
Console.WriteLine("BirdAdapter...");
birdAdapter.Squeak();

Jak widzisz jest to bardzo prosty wzorzec projektowy. Stwórzmy kolejny przykład tylko tym razem nasze metody będą coś zwracać.

Prosty przykład Adaptera, który tłumaczy wynik

Mam interfejs, który zwraca nam status działania serwera jako liczbę.

public interface IServerCheck
{
    public int IsWorking();
}

Na bazie tych przykładów możemy wnioskować, że statusy liczbowe określają nam czy serwer działa, czy nie. Tylko z jakiegoś powodu nie jest to wartość logiczna, ale liczbowa.

public class OracleServer : IServerCheck
{
    public int IsWorking()
    {
        return 2;
    }
}

public class SQLServer : IServerCheck
{
    public int IsWorking()
    {
        return 0;
    }
}

Stwórzmy więc swój interfejs, który zwracał takie wartości, jakie chcemy

public interface IServerBooleanCheck
{
    public bool? IsWorking();
}

Jego implementacja wyglądałaby tak

public class IISServer : IServerBooleanCheck
{
    public bool? IsWorking()
    {
        return true;
    }
}

Teraz chcielibyśmy napisać adapter do wszystkich klas, które implementują ten nieszczęsny interfejs, który zwraca statusy jako liczby.

public class LogicAdapter : IServerBooleanCheck
{
    IServerCheck server;
    public LogicAdapter(IServerCheck server)
    {
        // musimy mieć referencje do obiektu
        // który będziemy adaptować
        this.server = server;
    }

    public bool? IsWorking()
    {
        int number = server.IsWorking();

        if (number == 1)
            return true;
        else if (number == 2)
            return false;

        return null;
    }

}

Mają taki adapter możemy tłumaczyć działanie wszystkich serwerów jako wartości logiczne.

OracleServer oracle = new OracleServer();
SQLServer sql = new SQLServer();

LogicAdapter adapterOracle = new LogicAdapter(oracle);
LogicAdapter adapterSQL = new LogicAdapter(sql);

Console.WriteLine(adapterOracle.IsWorking());
Console.WriteLine(adapterSQL.IsWorking());

Jak widzisz i ten przykład jest bardzo prosty. W prawdziwym życiu najwięcej czasu spędzisz nad tłumaczeniem wartości, które trafiają do adapteru. 

Sam wzorzec jak widzisz ma prostą strukturę i czasem jak popatrzysz na istniejący kod w .NET nawet nie jego struktura jest ważna a rozwiązanie konkretnego problemu.

Property Adapter (Surrogate)

Jak z prawie każdym wzorcem istnieją inne jego podejścia.

Jeśli masz możliwość modyfikacji klasy to, czemu w niej nie stworzyć właściwości, która przejmie istniejące pola i właściwości i wystawi je w unikatowy, użyteczny sposób dla jakieś innej klasy.

W mojej karierze najczęściej takie właściwości tworzyłem na potrzeby deserializacji danej klasy. Taki przykład właśnie omówimy.

public class World
{
    public Dictionary<string,string> Capitals { get; set; }

    public World()
    {
        Capitals = new Dictionary<string, string>()
        {
            {"Afghanistan","Kabul" },
            {"Brazil","Brasilia" },
            {"Bulgaria","Sofia" },
            {"Bolivia","Sucre" },
            {"Cyprus","Nicosia" },
            {"Denmark","Copenhagen" },
            {"England","London" },
            {"Finland","Kabul" },
            {"France","Paris" },
            {"Poland","Warsaw" },
        };
    }
}

W tej klasie mam słownik państw i ich stolic i  chciałbym mieć możliwość serializacji tej właściwości.

notsupproted.PNG

Nie wiem, czy wiesz, ale XmlSerializer nie ogarnie serializacji słownika. Aby ten problem rozwiązać trzeba albo napisać swoją implementację słownika, który się serializuję albo wystawić właściwość pomocniczą, która będzie łatwiejsza do serializacji.

public class World
{
    [XmlIgnore]
    public Dictionary<string,string> Capitals { get; set; }

    public (string, string)[] CapitalsSerializable
    {
        get
        {
            return Capitals.Keys.Select(country =>
            (country, Capitals[country])).ToArray();
        }
        set
        {
            Capitals = value.ToDictionary(x => x.Item1, x => x.Item2);
        }
    }

    public World()
    {
        Capitals = new Dictionary<string, string>()
        {
            {"Afghanistan","Kabul" },
            {"Brazil","Brasilia" },
            {"Bulgaria","Sofia" },
            {"Bolivia","Sucre" },
            {"Cyprus","Nicosia" },
            {"Denmark","Copenhagen" },
            {"England","London" },
            {"Finland","Kabul" },
            {"France","Paris" },
            {"Poland","Warsaw" },
        };
    }
}

Oznaczamy nasz słownik atrybutem [XmlIgnore], aby on uniknął procesu deserializacji XML.

Natomiast nasza nowa właściwość, która zwróci tablicę Tuplet-ów nie będzie miała takiego problemu. Warto zaznaczyć, że kod musi być napisany w taki sposób, ponieważ klasy Tuplet też mają problemy z deserializacją.

Dla czytelności samego rezultatu XML zapewne bym zrezygnował z Tuplet-ów i stworzyłbym swoją własną klasę.

Sprawdźmy, czy deserializacja XML rzeczywiście teraz zadziała.

XmlSerializer xsSubmit = new XmlSerializer(typeof(World));
var subReq = new World();
var xml = "";

using (var sww = new StringWriter())
{
    using (XmlWriter writer = XmlWriter.Create(sww))
    {
        xsSubmit.Serialize(writer, subReq);
        xml = sww.ToString(); 
    }
}

Console.WriteLine(xml);

Oczywiście to tylko przykład. Gdybyś miał taki problem to warto może poszukać innego serializatora, który nie miałby problemu ze słownikiem.

Łamiemy też tutaj zasady SOLID. W końcu czy ta właściwość rozwiązująca tylko ten konkretny problem powinna być samej klasie? Raczej nie

Jeśli zobaczysz takie właściwości to jest to także dowód pośpiechu programisty, bo można to zrobić to lepiej robiąc normalny adapter, ale czasem zdarza się, że nie ma na to czasu.

Podsumowanie

Adapter jest prosty. Pozwala ci stworzyć interfejs, który potrzebuje do innego interfejsu, którego nie możesz zmienić.

O ile my nie musieliśmy tego robić. Czasem w adapterze warto pomyśleć o mechanizmie cache, gdy tłumaczymy listę elementów na inną listę elementów i wiemy, że wiele razy będzie tłumaczyć te same wartości.

Wtedy sposób byśmy generowali nowe obiekty tylko, gdy byłoby to potrzebne.

To wszystko, co musisz wiedzieć na temat tego wzorca.