Przejdź do głównej zawartości

[OOP] Na czym polega wzorzec projektowy Adapter?

Wzorzec projektowy adapter umożliwia współpracę klasom o niekompatybilnych interfejsach.

Jest to strukturalny wzorzec projektowy. Więcej o typach wzorców można poczytać tutaj [under construction].
Adapter czasem nazywany jest wrapperem.

Rozważmy problem gniazdek elektrycznych. W różnych częściach świata używane są rożne ich rodzaje. Polskiej wtyczki nie możemy używać w Stanach Zjednoczonch - nie będzie pasowała do gniazdka. Nie oznacza to, że w Stanach nie możemy używać polskich sprzętów. Wystarczy, że kupimy przejściówkę - czyli właśnie adapter. Stanowi on łącznik między niekompatybilnymi interfejsami - polską wtyczką i amerykańskim gniazdkiem.

Adaptery programistyczne działają na tej samej zasadzie. Przyjrzyjmy się kilku przykładom.

Przykład 1.


Wyobraźmy sobie, że piszemy aplikację dla policji. Policjanci prowadząc sprawy przestępstw zbierają od świadków i z monitoringu dane samochodów, których użyli przestępcy. Jeśli uda się odczytać rejestrację, to znalezienie właściciela samochodu nie jest problemem. Zazwyczaj jednak znamy tylko markę i kolor.

Załóżmy, że istnieje globalna baza samochodów - jest to zewnętrzny serwis, niepowiązany z naszą aplikacją, ale wystawiający publiczne API. Główna metoda dla tego API służy do wyszukiwania samochodu na podstawie różnych danych, a jej sygnatura wygląda tak:

    interface IApiCarSearchEngine
    {
        public ApiCarData Search(
            string color,
            ApiCarBrand brand,
            ApiCountry registrationCountry,            
            string ownerLastName,
            string ownerName,
            int? productionYear);
    }

Podkreślam - jest to metoda należąca do zewnętrznego serwisu, do którego kodu nie mamy dostępu. Wyjaśnijmy pokrótce zastosowane typy:
  • ApiCarData - to klasa reprezentująca dane odnalezionego samochodu
  • ApiCarBrand - to enum, także udostępniony w zewnętrznym API, zawiera wpisy takie jak Volkswagen, Mazda, Citroen itd.
  • ApiCountry - enum należący do API, zawiera wartości takie jak Poland, France, Japan itp. oznaczające kraj, w którym samochód jest zarejestrowany
API działa w ten sposób, że na podstawie przekazanych danych (nie wszystkie muszą być kompletne) stara się odnaleźć najlepiej pasujący samochód w bazie. Może się okazać, że nie znajdzie nic, a wtedy zwraca nulla.

Wróćmy do naszej aplikacji policyjnej. Chcemy wykorzystać w niej zewnętrzne API. Jednak dane, którymi dysponujemy, to:
  • kolor - u nas string
  • marka - także string. To może być problem, bo API używa enuma
  • kraj - zakładamy, że zawsze będzie to Polska
Nie mamy zaś:
  • danych właściciela, bo to właśnie jego próbujemy znaleźć
  • roku produkcji - bo skąd świadkowie mają to wiedzieć (jeśli po wyglądzie samochodu da się odgadnąć z całą pewnością rok produkcji to przepraszam, sama jestem osobą dla której każdy sedan wygląda tak samo)
Zatem z punktu widzenia naszej aplikacji interfejs wyszukiwania samochodu wyglądałby raczej tak:


interface ICarSearchEngine
    {
        public CarData Search(
            string color,
            string brand);
    }

Jak widać, interfejsy te nie są kompatybilne. Napiszmy adapter, który pozwoli im współpracować:


    class CarSearchEngineAdapter : ICarSearchEngine
    {
        private IApiCarSearchEngine _apiCarSearchEngine;

        public CarSearchEngineAdapter(IApiCarSearchEngine apiCarSearchEngine)
        {
            _apiCarSearchEngine = apiCarSearchEngine;
        }

        public CarData Search(string color, string brand)
        {
            var apiBrand = ConvertToApiBrand(brand);

            var apiCarData = _apiCarSearchEngine.Search(
                color,
                apiBrand,
                ApiCountry.Poland,
                null,
                null,
                null
                );

            return ConvertApiCarDataToCarData(apiCarData);
        }

        private ApiBrand ConvertToApiBrand(string brand)
        {
            switch(brand)
            {
                case "Volkswagen":
                    return ApiBrand.Volkswagen;
                case "Citroen":
                    return ApiBrand.Citroen;
                case "Mazda":
                    return ApiBrand.Mazda;
                default:
                    return null;
            }
        }

        private CarData ConvertApiCarDataToCarData(ApiCarData apiCarData)
        {
            return new CarData
            {
                OwnerLastName = apiCarData.OwnerLastName,
                OwnerFirstName = apiCarData.OwnerLastName,
                Country = Enum.GetName(typeof(ApiCountry), apiCarData.Country),
                Brand = Enum.GetName(typeof(ApiBrand), apiCarData.Brand),
            };
        }
    }

Przyjrzyjmy się uważnie, co robi ten adapter. Implementuje on nasz własny, wewnętrzny interfejs ICarSearchEngine. Jako pole prywatne posiada zaś obiekt implementujący interfejs zewnątrzny - IApiCarSearchEngine. W metodzie Search adapter używa zewnętrznego API, dostosowując do niego parametry:

  • tłumaczy nazwę marki, która w naszym systemie jest stringiem, na enuma należącego do API
  • wstawia "na sztywno" Polskę jako kraj rejestracji samochodu
  • wstawia nulle w pola, których i tak nie znamy
  • na koniec tłumaczy to, co zwróciła funkcja z API (obiekt ApiCarData) na nasz wewnętrzny obiekt (CarData). Tłumaczy także enumy na stringi
Dzięki temu w naszej aplikacji możemy korzystać z zewnętrznego API nawet o tym nie wiedząc. Nie musimy się za każdym razem przejmować dostosowaniem parametrów i zwracanego typu. CarSearchEngineAdapter "owija" (czyli wrappuje) zewnętrzne API, zasłaniając je przed konsumentami interfejsu ICarSearchEngine

Przykład 2.

Czasem adaptera musimy użyć nie po to, by dostosować do siebie interfejsy, ale by móc poradzić sobie z przetestowaniem naszej aplikacji. Rozważmy następujący przykład:


    class CurrentDateToStringConverter
    {
        public string Convert()
        {
            return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
        }
    }

Jak widać, zadaniem tej klasy jest zwrócenie stringa z aktualną datą i czasem. Spróbujmy dopisać do tej klasy testy jednostkowe:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
using NUnit.Framework;
using CSharpPractices;

namespace CSharpPracticesTests
{
    [TestFixture]
    public class CurrentDateToStringConverterTests
    {
        [Test]
        public void CurrentTimeShallBeConvertedProperly()
        {
            var result = new CurrentDateToStringConverter().Convert();

            // hmm... i co teraz?
        }
    }
}

Unit test stanowi problem: skąd mamy wiedzieć jaką wartość zwróci DateTime.Now w momencie wywoływania testu? Chcielibyśmy móc wprost określić, że w teście aktualna data to na przykład 28 listopad 2018, godzina 23:03:06:222. Ale jak to zrobić? Właściwość DateTime.Now jest statyczna, a zatem nie możemy jej zamockować.

Możemy sobie poradzić wrapując klasę DateTime w niestatyczny odpowiednik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using System;
namespace CSharpPractices
{
    public interface IDateTime
    {
        DateTime Now { get; }
    }

    public class DateTimeAdapter : IDateTime
    {
        public virtual DateTime Now => DateTime.Now;
    }

    public class CurrentDateToStringConverter
    {
        private IDateTime _dateTime;

        public CurrentDateToStringConverter(IDateTime dateTime)
        {
            _dateTime = dateTime;
        }
        public string Convert()
        {
            return _dateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
        }
    }

    class Program
    {

        static void Main()
        {
            Console.WriteLine(new CurrentDateToStringConverter(new DateTimeAdapter()).Convert());
            Console.ReadKey();
        }
    }
}

Dzięki temu w możemy napisać test jednostkowy:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
using NUnit.Framework;
using CSharpPractices;
using Moq;
using System;

namespace CSharpPracticesTests
{
    [TestFixture]
    public class CurrentDateToStringConverterTests
    {
        [Test]
        public void CurrentTimeShallBeConvertedProperly()
        {
            var dateTimeMock = new Mock<IDateTime>();
            dateTimeMock.SetupGet(a => a.Now).Returns(new DateTime(2018, 11, 23, 23, 3, 6, 222));

            var result = new CurrentDateToStringConverter(dateTimeMock.Object).Convert();

            Assert.AreEqual("2018-11-23 23:03:06.222", result);
        }
    }
}

Test jest niezależny od prawdziwego aktualnego czasu. Adapter pomógł nam poradzić sobie ze statyczną właściwością Now z klasy Datetime.

W poście tym korzystałam z:

  • https://dzone.com/articles/design-patterns-explained-adapter-pattern-with-cod
  • https://www.udemy.com/design-patterns-csharp-dotnet



Komentarze