Перейти к содержанию

Адаптер

Также известен как: Wrapper, Обёртка, Adapter

Адаптер — это структурный паттерн проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе.

Адаптер

Проблема

Представьте, что вы делаете приложение для торговли на бирже. Ваше приложение скачивает биржевые котировки из нескольких источников в XML, а затем рисует красивые графики.

В какой-то момент вы решаете улучшить приложение, применив стороннюю библиотеку аналитики. Но вот беда — библиотека поддерживает только формат данных JSON, несовместимый с вашим приложением.

Проблема

Подключить стороннюю библиотеку не выйдет из-за несовместимых форматов данных.

Вы смогли бы переписать библиотеку, чтобы та поддерживала формат XML. Но, во-первых, это может нарушить работу существующего кода, который уже зависит от библиотеки. А во-вторых, у вас может просто не быть доступа к её исходному коду.

Решение

Вы можете создать адаптер. Это объект-переводчик, который трансформирует интерфейс или данные одного объекта в такой вид, чтобы он стал понятен другому объекту.

При этом адаптер оборачивает один из объектов, так что другой объект даже не знает о наличии первого. Например, вы можете обернуть объект, работающий в метрах, адаптером, который бы конвертировал данные в футы.

Адаптеры могут не только переводить данные из одного формата в другой, но и помогать объектам с разными интерфейсами работать сообща. Это работает так:

  1. Адаптер имеет интерфейс, который совместим с одним из объектов.
  2. Поэтому этот объект может свободно вызывать методы адаптера.
  3. Адаптер получает эти вызовы и перенаправляет их второму объекту, но уже в том формате и последовательности, которые понятны второму объекту.

Иногда возможно создать даже двухсторонний адаптер, который работал бы в обе стороны.

Решение

Программа может работать со сторонней библиотекой через адаптер.

Таким образом, в приложении биржевых котировок вы могли бы создать класс XML_To_JSON_Adapter, который бы оборачивал объект того или иного класса библиотеки аналитики. Ваш код посылал бы адаптеру запросы в формате XML, а адаптер сначала транслировал входящие данные в формат JSON, а затем передавал бы их методам обёрнутого объекта аналитики.

Аналогия из жизни

Аналогия из жизни

Содержимое чемоданов до и после поездки за границу.

Когда вы в первый раз летите за границу, вас может ждать сюрприз при попытке зарядить ноутбук. Стандарты розеток в разных странах отличаются. Ваша европейская зарядка будет бесполезна в США без специального адаптера, позволяющего подключиться к розетке другого типа.

Структура

Адаптер объектов

Эта реализация использует агрегацию: объект адаптера «оборачивает», то есть содержит ссылку на служебный объект. Такой подход работает во всех языках программирования.

Адаптер объектов

  1. Клиент — это класс, который содержит существующую бизнес-логику программы.
  2. Клиентский интерфейс описывает протокол, через который клиент может работать с другими классами.
  3. Сервис — это какой-то полезный класс, обычно сторонний. Клиент не может использовать этот класс напрямую, так как сервис имеет непонятный ему интерфейс.
  4. Адаптер — это класс, который может одновременно работать и с клиентом, и с сервисом. Он реализует клиентский интерфейс и содержит ссылку на объект сервиса. Адаптер получает вызовы от клиента через методы клиентского интерфейса, а затем переводит их в вызовы методов обёрнутого объекта в правильном формате.
  5. Работая с адаптером через интерфейс, клиент не привязывается к конкретному классу адаптера. Благодаря этому, вы можете добавлять в программу новые виды адаптеров, независимо от клиентского кода. Это может пригодиться, если интерфейс сервиса вдруг изменится, например, после выхода новой версии сторонней библиотеки.

Адаптер классов

Эта реализация базируется на наследовании: адаптер наследует оба интерфейса одновременно. Такой подход возможен только в языках, поддерживающих множественное наследование, например, C++.

Адаптер классов

Адаптер классов не нуждается во вложенном объекте, так как он может одновременно наследовать и часть существующего класса, и часть сервиса.

Псевдокод

В этом шуточном примере Адаптер преобразует один интерфейс в другой, позволяя совместить квадратные колышки и круглые отверстия.

Псевдокод

Пример адаптации квадратных колышков и круглых отверстий.

Адаптер вычисляет наименьший радиус окружности, в которую можно вписать квадратный колышек, и представляет его как круглый колышек с этим радиусом.

// Классы с совместимыми интерфейсами: КруглоеОтверстие и
// КруглыйКолышек.
class RoundHole is
    constructor RoundHole(radius) { ... }

    method getRadius() is
        // Вернуть радиус отверстия.

    method fits(peg: RoundPeg) is
        return this.getRadius() >= peg.getRadius()

class RoundPeg is
    constructor RoundPeg(radius) { ... }

    method getRadius() is
        // Вернуть радиус круглого колышка.


// Устаревший, несовместимый класс: КвадратныйКолышек.
class SquarePeg is
    constructor SquarePeg(width) { ... }

    method getWidth() is
        // Вернуть ширину квадратного колышка.


// Адаптер позволяет использовать квадратные колышки и круглые
// отверстия вместе.
class SquarePegAdapter extends RoundPeg is
    private field peg: SquarePeg

    constructor SquarePegAdapter(peg: SquarePeg) is
        this.peg = peg

    method getRadius() is
        // Вычислить половину диагонали квадратного колышка по
        // теореме Пифагора.
        return peg.getWidth() * Math.sqrt(2) / 2


// Где-то в клиентском коде.
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // TRUE

small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // Ошибка компиляции, несовместимые типы

small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // TRUE
hole.fits(large_sqpeg_adapter) // FALSE

Применимость

Когда вы хотите использовать сторонний класс, но его интерфейс не соответствует остальному коду приложения.

Адаптер позволяет создать объект-прокладку, который будет превращать вызовы приложения в формат, понятный стороннему классу.

Когда вам нужно использовать несколько существующих подклассов, но в них не хватает какой-то общей функциональности, причём расширить суперкласс вы не можете.

Вы могли бы создать ещё один уровень подклассов и добавить в них недостающую функциональность. Но при этом придётся дублировать один и тот же код в обеих ветках подклассов.

Более элегантным решением было бы поместить недостающую функциональность в адаптер и приспособить его для работы с суперклассом. Такой адаптер сможет работать со всеми подклассами иерархии. Это решение будет сильно напоминать паттерн Декоратор.

Шаги реализации

  1. Убедитесь, что у вас есть два класса с несовместимыми интерфейсами:
    • полезный сервис — служебный класс, который вы не можете изменять (он либо сторонний, либо от него зависит другой код);
    • один или несколько клиентов — существующих классов приложения, несовместимых с сервисом из-за неудобного или несовпадающего интерфейса.
  2. Опишите клиентский интерфейс, через который классы приложения смогли бы использовать класс сервиса.
  3. Создайте класс адаптера, реализовав этот интерфейс.
  4. Поместите в адаптер поле, которое будет хранить ссылку на объект сервиса. Обычно это поле заполняют объектом, переданным в конструктор адаптера. В случае простой адаптации этот объект можно передавать через параметры методов адаптера.
  5. Реализуйте все методы клиентского интерфейса в адаптере. Адаптер должен делегировать основную работу сервису.
  6. Приложение должно использовать адаптер только через клиентский интерфейс. Это позволит легко изменять и добавлять адаптеры в будущем.

Преимущества и недостатки

Преимущества

  • Отделяет и скрывает от клиента подробности преобразования различных интерфейсов.

Недостатки

  • Усложняет код программы из-за введения дополнительных классов.

Примеры реализации паттерна

Python

  • Сложность: 1/3
  • Популярность: 3/3
  • Применимость: Паттерн можно часто встретить в Python-коде, особенно там, где требуется конвертация разных типов данных или совместная работа классов с разными интерфейсами.
  • Признаки применения паттерна: Адаптер получает конвертируемый объект в конструкторе или через параметры своих методов. Методы Адаптера обычно совместимы с интерфейсом одного объекта. Они делегируют вызовы вложенному объекту, превратив перед этим параметры вызова в формат, поддерживаемый вложенным объектом.

Этот пример показывает структуру паттерна Адаптер, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.

class Target:
    """
    Целевой класс объявляет интерфейс, с которым может работать
    клиентский код.
    """

    def request(self) -> str:
        return "Target: The default target's behavior."


class Adaptee:
"""
Адаптируемый класс содержит некоторое полезное поведение, но его интерфейс
несовместим с существующим клиентским кодом. Адаптируемый класс нуждается в
некоторой доработке, прежде чем клиентский код сможет его использовать.
"""
    def specific_request(self) -> str:
        return ".eetpadA eht fo roivaheb laicepS"


class Adapter(Target, Adaptee):
"""
Адаптер делает интерфейс Адаптируемого класса совместимым с целевым
интерфейсом благодаря множественному наследованию.
"""

    def request(self) -> str:
        return f"Adapter: (TRANSLATED) {self.specific_request()[::-1]}"


def client_code(target: "Target") -> None:
"""
Клиентский код поддерживает все классы, использующие интерфейс Target.
"""
    print(target.request(), end="")


if __name__ == "__main__":
    print("Client: I can work just fine with the Target objects:")
    target = Target()
    client_code(target)
    print("\n")

    adaptee = Adaptee()
    print("Client: The Adaptee class has a weird interface. "
        "See, I don't understand it:")
    print(f"Adaptee: {adaptee.specific_request()}", end="\n\n")

    print("Client: But I can work with it via the Adapter:")
    adapter = Adapter()
    client_code(adapter)
Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

Этот пример показывает структуру паттерна Адаптер, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.

class Target:
    """
    Целевой класс объявляет интерфейс, с которым может работать
    клиентский код.
    """

    def request(self) -> str:
        return "Target: The default target's behavior."


class Adaptee:
"""
Адаптируемый класс содержит некоторое полезное поведение, но его интерфейс
несовместим с существующим клиентским кодом. Адаптируемый класс нуждается в
некоторой доработке, прежде чем клиентский код сможет его использовать.
"""

    def specific_request(self) -> str:
        return ".eetpadA eht fo roivaheb laicepS"


class Adapter(Target):
"""
Адаптер делает интерфейс Адаптируемого класса совместимым с целевым
интерфейсом благодаря агрегации.
"""

    def __init__(self, adaptee: Adaptee) -> None:
        self.adaptee = adaptee

    def request(self) -> str:
        return f"Adapter: (TRANSLATED) {self.adaptee.specific_request()[::-1]}"


def client_code(target: Target) -> None:
"""
Клиентский код поддерживает все классы, использующие интерфейс Target.
"""

    print(target.request(), end="")


if __name__ == "__main__":
print("Client: I can work just fine with the Target objects:")
target = Target()
client_code(target)
print("\n")

    adaptee = Adaptee()
    print("Client: The Adaptee class has a weird interface. "
        "See, I don't understand it:")
    print(f"Adaptee: {adaptee.specific_request()}", end="\n\n")

    print("Client: But I can work with it via the Adapter:")
    adapter = Adapter(adaptee)
    client_code(adapter)
Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

PHP

  • Сложность: 1/3
  • Популярность: 3/3
  • Применимость: Паттерн можно часто встретить в PHP-коде, особенно там, где требуется конвертация разных типов данных или совместная работа классов с разными интерфейсами.
  • Признаки применения паттерна: Адаптер получает конвертируемый объект в конструкторе или через параметры своих методов. Методы Адаптера обычно совместимы с интерфейсом одного объекта. Они делегируют вызовы вложенному объекту, превратив перед этим параметры вызова в формат, поддерживаемый вложенным объектом.

Этот пример показывает структуру паттерна Адаптер, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом. После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.

<?php
/**
* Целевой класс объявляет интерфейс, с которым может работать клиентский код.
*/

class Target
{
    public function request(): string
    {
        return "Target: The default target's behavior.";
    }
}

/**
* Адаптируемый класс содержит некоторое полезное поведение, но его интерфейс
* несовместим с существующим клиентским кодом. Адаптируемый класс нуждается в
* некоторой доработке, прежде чем клиентский код сможет его использовать.
*/

class Adaptee
{
    public function specificRequest(): string
    {
        return ".eetpadA eht fo roivaheb laicepS";
    }
}

/**
* Адаптер делает интерфейс Адаптируемого класса совместимым с целевым
* интерфейсом.
*/

class Adapter extends Target
{
private $adaptee;

    public function __construct(Adaptee $adaptee)
    {
        $this->adaptee = $adaptee;
    }

    public function request(): string
    {
        return "Adapter: (TRANSLATED) " . strrev($this->adaptee->specificRequest());
    }
}

/**
* Клиентский код поддерживает все классы, использующие целевой интерфейс.
*/

function clientCode(Target $target)
{
    echo $target->request();
}

echo "Client: I can work just fine with the Target objects:\n";
$target = new Target();
clientCode($target);
echo "\n\n";

$adaptee = new Adaptee();
echo "Client: The Adaptee class has a weird interface. See, I don't understand it:\n";
echo "Adaptee: " . $adaptee->specificRequest();
echo "\n\n";

echo "Client: But I can work with it via the Adapter:\n";
$adapter = new Adapter($adaptee);
clientCode($adapter);
Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

Паттерн Адаптер позволяет использовать сторонние или устаревшие классы, даже если они несовместимы с основной частью кода. Например, вместо того, чтобы переписывать интерфейс уведомлений вашего приложения для поддержки каждого стороннего сервиса вроде Slack, Facebook, SMS и прочих, вы создаёте под эти сервисы набор специальных обёрток, которые приводят вызовы из приложения к требуемым сторонними классами интерфейсу и формату.

<?php
/**
* Целевой интерфейс предоставляет интерфейс, которому следуют классы вашего
* приложения.
*/

interface Notification
{
    public function send(string $title, string $message);
}

/**
* Вот пример существующего класса, который следует за целевым интерфейсом.
*
* Дело в том, что у большинства приложений нет чётко определённого интерфейса.
* В этом случае лучше было бы расширить Адаптер за счёт существующего класса
* приложения. Если это неудобно (например, SlackNotification не похож на
* подкласс EmailNotification), тогда первым шагом должно быть извлечение
* интерфейса.
*/

class EmailNotification implements Notification
{
    private $adminEmail;

    public function __construct(string $adminEmail)
    {
        $this->adminEmail = $adminEmail;
    }

    public function send(string $title, string $message): void
    {
        mail($this->adminEmail, $title, $message);
        echo "Sent email with title '$title' to '{$this->adminEmail}' that says '$message'.";
    }
}

/**
* Адаптируемый класс – некий полезный класс, несовместимый с целевым
* интерфейсом. Нельзя просто войти и изменить код класса так, чтобы следовать
* целевому интерфейсу, так как код может предоставляться сторонней библиотекой.
*/

class SlackApi
{
    private $login;
    private $apiKey;

    public function __construct(string $login, string $apiKey)
    {
        $this->login = $login;
        $this->apiKey = $apiKey;
    }

    public function logIn(): void
    {
        // Send authentication request to Slack web service.
        echo "Logged in to a slack account '{$this->login}'.\n";
    }

    public function sendMessage(string $chatId, string $message): void
    {
        // Send message post request to Slack web service.
        echo "Posted following message into the '$chatId' chat: '$message'.\n";
    }
}

/**
* Адаптер – класс, который связывает Целевой интерфейс и Адаптируемый класс.
* Это позволяет приложению использовать Slack API для отправки уведомлений.
*/

class SlackNotification implements Notification
{
    private $slack;
    private $chatId;

    public function __construct(SlackApi $slack, string $chatId)
    {
        $this->slack = $slack;
        $this->chatId = $chatId;
    }

    /**
    * Адаптер способен адаптировать интерфейсы и преобразовывать входные данные
    * в формат, необходимый Адаптируемому классу.
    */
    public function send(string $title, string $message): void
    {
        $slackMessage = "#" . $title . "# " . strip_tags($message);
        $this->slack->logIn();
        $this->slack->sendMessage($this->chatId, $slackMessage);
    }
}

/**
* Клиентский код работает с классами, которые следуют Целевому интерфейсу.
*/

function clientCode(Notification $notification)
{
// ...

    echo $notification->send("Website is down!",
    "<strong style='color:red;font-size: 50px;'>Alert!</strong> " .
    "Our website is not responding. Call admins and bring it up!");

    // ...
}

echo "Client code is designed correctly and works with email notifications:\n";
$notification = new EmailNotification("[email protected]");
clientCode($notification);
echo "\n\n";

echo "The same client code can work with other classes via adapter:\n";
$slackApi = new SlackApi("example.com", "XXXXXXXX");
$notification = new SlackNotification($slackApi, "Example.com Developers");
clientCode($notification);
Client code is designed correctly and works with email notifications:
Sent email with title 'Website is down!' to '[email protected]'
that says '<strong style='color:red;font-size: 50px;'>Alert!</strong>
Our website is not responding. Call admins and bring it up!'.

The same client code can work with other classes via adapter:
Logged in to a slack account 'example.com'.
Posted following message into the 'Example.com Developers' chat: '#Website is down!# Alert! Our website is not responding. Call admins and bring it up!'.

Задачи

Задача 1. Адаптер платёжного шлюза

В системе уже есть интерфейс PaymentProcessor с методом pay(amount, currency). Появился новый провайдер StripeSDK с другим API. Реализуйте Адаптер StripeAdapter, который оборачивает StripeSDK и делает его совместимым с интерфейсом PaymentProcessor без изменения клиентского кода.

from abc import ABC, abstractmethod

# Существующий интерфейс, с которым работает клиентский код
class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, amount: float, currency: str) -> str: pass

    @abstractmethod
    def refund(self, transaction_id: str, amount: float) -> str: pass

# Существующая реализация, которая уже работает
class PayPalProcessor(PaymentProcessor):
    def pay(self, amount: float, currency: str) -> str:
        return f"PayPal: charged {amount} {currency}"

    def refund(self, transaction_id: str, amount: float) -> str:
        return f"PayPal: refunded {amount} for tx {transaction_id}"

# Новый сторонний SDK с несовместимым API
class StripeSDK:
    def create_charge(self, amount_cents: int, currency_code: str, metadata: dict) -> dict:
        return {
            "id": "ch_stripe_123",
            "status": "succeeded",
            "amount": amount_cents,
            "currency": currency_code,
        }

    def create_refund(self, charge_id: str, amount_cents: int) -> dict:
        return {"id": "re_stripe_456", "status": "succeeded", "amount": amount_cents}

# TODO: реализовать StripeAdapter(PaymentProcessor),
# который оборачивает StripeSDK и адаптирует его к интерфейсу PaymentProcessor.
# Учтите: Stripe работает с центами (amount * 100), PaymentProcessor — с рублями/долларами.

# Клиентский код — не должен меняться
def checkout(processor: PaymentProcessor, amount: float, currency: str):
    result = processor.pay(amount, currency)
    print(result)

checkout(PayPalProcessor(), 1500.0, "RUB")
# checkout(StripeAdapter(StripeSDK()), 1500.0, "RUB")  # должно работать так же
<?php
// Существующий интерфейс, с которым работает клиентский код
interface PaymentProcessor {
    public function pay(float $amount, string $currency): string;
    public function refund(string $transactionId, float $amount): string;
}

// Существующая реализация, которая уже работает
class PayPalProcessor implements PaymentProcessor {
    public function pay(float $amount, string $currency): string {
        return "PayPal: charged {$amount} {$currency}";
    }

    public function refund(string $transactionId, float $amount): string {
        return "PayPal: refunded {$amount} for tx {$transactionId}";
    }
}

// Новый сторонний SDK с несовместимым API
class StripeSDK {
    public function createCharge(int $amountCents, string $currencyCode, array $metadata): array {
        return ['id' => 'ch_stripe_123', 'status' => 'succeeded', 'amount' => $amountCents];
    }

    public function createRefund(string $chargeId, int $amountCents): array {
        return ['id' => 're_stripe_456', 'status' => 'succeeded', 'amount' => $amountCents];
    }
}

// TODO: реализовать StripeAdapter implements PaymentProcessor,
// который оборачивает StripeSDK и адаптирует его к интерфейсу PaymentProcessor.
// Учтите: Stripe работает с центами (amount * 100), PaymentProcessor — с рублями/долларами.

// Клиентский код — не должен меняться
function checkout(PaymentProcessor $processor, float $amount, string $currency): void {
    echo $processor->pay($amount, $currency) . "\n";
}

checkout(new PayPalProcessor(), 1500.0, 'RUB');
// checkout(new StripeAdapter(new StripeSDK()), 1500.0, 'RUB'); // должно работать так же

Задача 2. Адаптер стороннего логгера

Приложение использует интерфейс Logger с методами info(), warning() и error(). Нужно подключить стороннюю библиотеку MonologLogger, у которой другой API. Реализуйте Адаптер MonologAdapter, не меняя клиентский код и не модифицируя MonologLogger.

from abc import ABC, abstractmethod
from datetime import datetime

# Интерфейс, которого ожидает приложение
class Logger(ABC):
    @abstractmethod
    def info(self, message: str) -> None: pass

    @abstractmethod
    def warning(self, message: str) -> None: pass

    @abstractmethod
    def error(self, message: str, exception: Exception = None) -> None: pass

# Сторонняя библиотека с другим API (изменять нельзя)
class MonologLogger:
    def __init__(self, channel: str):
        self.channel = channel

    def add_record(self, level: str, msg: str, context: dict = None) -> None:
        ts = datetime.now().strftime("%H:%M:%S")
        ctx = f" {context}" if context else ""
        print(f"[{ts}] {self.channel}.{level}: {msg}{ctx}")

    def push_handler(self, handler_name: str) -> None:
        print(f"[Monolog] Handler '{handler_name}' registered")

# TODO: реализовать MonologAdapter(Logger),
# который делегирует вызовы к MonologLogger.add_record()
# с соответствующим уровнем: INFO, WARNING, ERROR.
# При вызове error() с exception — передавать её в context.

# Клиентский код
def process_order(logger: Logger, order_id: int):
    logger.info(f"Обработка заказа #{order_id}")
    try:
        if order_id < 0:
            raise ValueError("ID не может быть отрицательным")
        logger.info(f"Заказ #{order_id} успешно обработан")
    except ValueError as e:
        logger.error("Ошибка обработки заказа", exception=e)

# process_order(MonologAdapter(MonologLogger("app")), 42)
# process_order(MonologAdapter(MonologLogger("app")), -1)
<?php
// Интерфейс, которого ожидает приложение
interface Logger {
    public function info(string $message): void;
    public function warning(string $message): void;
    public function error(string $message, ?\Throwable $exception = null): void;
}

// Сторонняя библиотека с другим API (изменять нельзя)
class MonologLogger {
    public function __construct(private string $channel) {}

    public function addRecord(string $level, string $msg, array $context = []): void {
        $ts  = date('H:i:s');
        $ctx = $context ? ' ' . json_encode($context) : '';
        echo "[{$ts}] {$this->channel}.{$level}: {$msg}{$ctx}\n";
    }

    public function pushHandler(string $handlerName): void {
        echo "[Monolog] Handler '{$handlerName}' registered\n";
    }
}

// TODO: реализовать MonologAdapter implements Logger,
// который делегирует вызовы к MonologLogger::addRecord()
// с соответствующим уровнем: INFO, WARNING, ERROR.
// При вызове error() с exception — передавать её в context.

// Клиентский код
function processOrder(Logger $logger, int $orderId): void {
    $logger->info("Обработка заказа #{$orderId}");
    try {
        if ($orderId < 0) throw new InvalidArgumentException('ID не может быть отрицательным');
        $logger->info("Заказ #{$orderId} успешно обработан");
    } catch (InvalidArgumentException $e) {
        $logger->error('Ошибка обработки заказа', $e);
    }
}

// processOrder(new MonologAdapter(new MonologLogger('app')), 42);
// processOrder(new MonologAdapter(new MonologLogger('app')), -1);

Задача 3. Адаптер для источников данных

Система читает данные через интерфейс DataReader с методами read_all() и read_by_id(id). Нужно подключить два новых источника с разными API: CsvFileReader и JsonApiClient. Реализуйте адаптеры для каждого, чтобы клиентский код мог работать с ними через единый интерфейс.

from abc import ABC, abstractmethod

# Интерфейс, с которым работает приложение
class DataReader(ABC):
    @abstractmethod
    def read_all(self) -> list[dict]: pass

    @abstractmethod
    def read_by_id(self, record_id: int) -> dict | None: pass

# Первый несовместимый источник — CSV-файл
class CsvFileReader:
    def __init__(self, filepath: str):
        self.filepath = filepath

    def open(self) -> None:
        print(f"[CSV] Открыт файл: {self.filepath}")

    def fetch_rows(self) -> list[list[str]]:
        # Симуляция: возвращает строки вместо словарей, первая — заголовки
        return [
            ["id", "name", "price"],
            ["1", "Книга", "350"],
            ["2", "Ручка", "50"],
            ["3", "Тетрадь", "80"],
        ]

    def close(self) -> None:
        print(f"[CSV] Файл закрыт: {self.filepath}")

# Второй несовместимый источник — JSON API
class JsonApiClient:
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url
        self.api_key = api_key

    def get_collection(self, endpoint: str) -> dict:
        # Симуляция ответа API
        return {
            "data": [
                {"id": 1, "attributes": {"name": "Книга",    "price": 350}},
                {"id": 2, "attributes": {"name": "Ручка",    "price": 50}},
                {"id": 3, "attributes": {"name": "Тетрадь",  "price": 80}},
            ],
            "meta": {"total": 3},
        }

    def get_single(self, endpoint: str, record_id: int) -> dict | None:
        collection = self.get_collection(endpoint)
        for item in collection["data"]:
            if item["id"] == record_id:
                return item
        return None

# TODO: реализовать CsvDataAdapter(DataReader) и JsonApiAdapter(DataReader)

# Клиентский код
def print_catalog(reader: DataReader):
    items = reader.read_all()
    for item in items:
        print(f"  [{item['id']}] {item['name']} — {item['price']} руб.")

# print_catalog(CsvDataAdapter(CsvFileReader("products.csv")))
# print_catalog(JsonApiAdapter(JsonApiClient("https://api.shop.com", "secret"), endpoint="products"))
<?php
// Интерфейс, с которым работает приложение
interface DataReader {
    public function readAll(): array;
    public function readById(int $recordId): ?array;
}

// Первый несовместимый источник — CSV-файл
class CsvFileReader {
    public function __construct(private string $filepath) {}

    public function open(): void { echo "[CSV] Открыт файл: {$this->filepath}\n"; }

    public function fetchRows(): array {
        return [
            ['id', 'name', 'price'],
            ['1', 'Книга', '350'],
            ['2', 'Ручка', '50'],
            ['3', 'Тетрадь', '80'],
        ];
    }

    public function close(): void { echo "[CSV] Файл закрыт: {$this->filepath}\n"; }
}

// Второй несовместимый источник — JSON API
class JsonApiClient {
    public function __construct(private string $baseUrl, private string $apiKey) {}

    public function getCollection(string $endpoint): array {
        return [
            'data' => [
                ['id' => 1, 'attributes' => ['name' => 'Книга',   'price' => 350]],
                ['id' => 2, 'attributes' => ['name' => 'Ручка',   'price' => 50]],
                ['id' => 3, 'attributes' => ['name' => 'Тетрадь', 'price' => 80]],
            ],
            'meta' => ['total' => 3],
        ];
    }

    public function getSingle(string $endpoint, int $recordId): ?array {
        foreach ($this->getCollection($endpoint)['data'] as $item) {
            if ($item['id'] === $recordId) return $item;
        }
        return null;
    }
}

// TODO: реализовать CsvDataAdapter implements DataReader
// и JsonApiAdapter implements DataReader

// Клиентский код
function printCatalog(DataReader $reader): void {
    foreach ($reader->readAll() as $item) {
        echo "  [{$item['id']}] {$item['name']} — {$item['price']} руб.\n";
    }
}

// printCatalog(new CsvDataAdapter(new CsvFileReader('products.csv')));
// printCatalog(new JsonApiAdapter(new JsonApiClient('https://api.shop.com', 'secret'), 'products'));

Задача 4. Адаптер геолокационных сервисов

Приложение работает с интерфейсом GeoService для получения координат и адресов. Нужно подключить два внешних API с разными форматами ответов: GoogleMapsAPI и YandexMapsAPI. Реализуйте адаптеры для каждого так, чтобы приложение могло переключаться между провайдерами без изменений в коде.

from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class Coordinates:
    lat: float
    lng: float

@dataclass
class Address:
    country: str
    city: str
    street: str
    building: str

# Интерфейс, которого ожидает приложение
class GeoService(ABC):
    @abstractmethod
    def geocode(self, address: str) -> Coordinates: pass

    @abstractmethod
    def reverse_geocode(self, coords: Coordinates) -> Address: pass

    @abstractmethod
    def distance_km(self, origin: Coordinates, destination: Coordinates) -> float: pass

# Первый внешний API (изменять нельзя)
class GoogleMapsAPI:
    def find_place(self, query: str) -> dict:
        # Симуляция ответа Google
        return {"geometry": {"location": {"lat": 55.7558, "lng": 37.6173}}}

    def reverse_geocode(self, lat: float, lng: float) -> dict:
        return {"results": [{"address_components": [
            {"types": ["country"],              "long_name": "Россия"},
            {"types": ["locality"],             "long_name": "Москва"},
            {"types": ["route"],                "long_name": "Тверская улица"},
            {"types": ["street_number"],        "long_name": "1"},
        ]}]}

    def distance_matrix(self, origins: list, destinations: list) -> dict:
        return {"rows": [{"elements": [{"distance": {"value": 5200}}]}]}

# Второй внешний API (изменять нельзя)
class YandexMapsAPI:
    def geocode(self, address: str) -> dict:
        # Симуляция ответа Яндекса
        return {"response": {"GeoObjectCollection": {"featureMember": [
            {"GeoObject": {"Point": {"pos": "37.6173 55.7558"}}}
        ]}}}

    def reverse_geocode(self, lon: float, lat: float) -> dict:
        return {"response": {"GeoObjectCollection": {"featureMember": [
            {"GeoObject": {"metaDataProperty": {"GeocoderMetaData": {
                "AddressDetails": {"Country": {
                    "CountryName": "Россия",
                    "Locality": {"LocalityName": "Москва",
                                 "Thoroughfare": {"ThoroughfareName": "Тверская",
                                                  "Premise": {"PremiseNumber": "1"}}}
                }}
            }}}}
        ]}}}

    def route(self, from_point: str, to_point: str) -> dict:
        return {"route": {"legs": [{"steps": [{"distance": 5200}]}]}}

# TODO: реализовать GoogleMapsAdapter(GeoService) и YandexMapsAdapter(GeoService)

# Клиентский код
def show_location_info(service: GeoService, address: str):
    coords = service.geocode(address)
    print(f"Координаты: {coords.lat}, {coords.lng}")
    addr = service.reverse_geocode(coords)
    print(f"Адрес: {addr.city}, {addr.street}, {addr.building}")

# show_location_info(GoogleMapsAdapter(GoogleMapsAPI()), "Тверская 1, Москва")
# show_location_info(YandexMapsAdapter(YandexMapsAPI()), "Тверская 1, Москва")
<?php
class Coordinates {
    public function __construct(public float $lat, public float $lng) {}
}

class Address {
    public function __construct(
        public string $country,
        public string $city,
        public string $street,
        public string $building,
    ) {}
}

// Интерфейс, которого ожидает приложение
interface GeoService {
    public function geocode(string $address): Coordinates;
    public function reverseGeocode(Coordinates $coords): Address;
    public function distanceKm(Coordinates $origin, Coordinates $destination): float;
}

// Первый внешний API (изменять нельзя)
class GoogleMapsAPI {
    public function findPlace(string $query): array {
        return ['geometry' => ['location' => ['lat' => 55.7558, 'lng' => 37.6173]]];
    }

    public function reverseGeocode(float $lat, float $lng): array {
        return ['results' => [['address_components' => [
            ['types' => ['country'],       'long_name' => 'Россия'],
            ['types' => ['locality'],      'long_name' => 'Москва'],
            ['types' => ['route'],         'long_name' => 'Тверская улица'],
            ['types' => ['street_number'], 'long_name' => '1'],
        ]]]];
    }

    public function distanceMatrix(array $origins, array $destinations): array {
        return ['rows' => [['elements' => [['distance' => ['value' => 5200]]]]]];
    }
}

// Второй внешний API (изменять нельзя)
class YandexMapsAPI {
    public function geocode(string $address): array {
        return ['response' => ['GeoObjectCollection' => ['featureMember' => [
            ['GeoObject' => ['Point' => ['pos' => '37.6173 55.7558']]]
        ]]]];
    }

    public function reverseGeocode(float $lon, float $lat): array {
        return ['response' => ['GeoObjectCollection' => ['featureMember' => [
            ['GeoObject' => ['metaDataProperty' => ['GeocoderMetaData' => [
                'AddressDetails' => ['Country' => [
                    'CountryName' => 'Россия',
                    'Locality' => ['LocalityName' => 'Москва',
                        'Thoroughfare' => ['ThoroughfareName' => 'Тверская',
                            'Premise' => ['PremiseNumber' => '1']]]
                ]]
            ]]]]
        ]]]];
    }

    public function route(string $fromPoint, string $toPoint): array {
        return ['route' => ['legs' => [['steps' => [['distance' => 5200]]]]]];
    }
}

// TODO: реализовать GoogleMapsAdapter implements GeoService
// и YandexMapsAdapter implements GeoService

// Клиентский код
function showLocationInfo(GeoService $service, string $address): void {
    $coords = $service->geocode($address);
    echo "Координаты: {$coords->lat}, {$coords->lng}\n";
    $addr = $service->reverseGeocode($coords);
    echo "Адрес: {$addr->city}, {$addr->street}, {$addr->building}\n";
}

// showLocationInfo(new GoogleMapsAdapter(new GoogleMapsAPI()), 'Тверская 1, Москва');
// showLocationInfo(new YandexMapsAdapter(new YandexMapsAPI()), 'Тверская 1, Москва');

Задача 5. Двусторонний адаптер форматов температуры

Реализуйте двусторонний адаптер TemperatureAdapter, который позволяет объектам, работающим в шкале Цельсия, взаимодействовать с объектами, работающими в шкале Фаренгейта. Адаптер должен реализовывать оба интерфейса (CelsiusSensor и FahrenheitSensor) и прозрачно конвертировать значения в обоих направлениях. Продемонстрируйте работу на примере метеостанции.

from abc import ABC, abstractmethod

class CelsiusSensor(ABC):
    @abstractmethod
    def get_celsius(self) -> float: pass

    @abstractmethod
    def set_celsius(self, value: float) -> None: pass

class FahrenheitSensor(ABC):
    @abstractmethod
    def get_fahrenheit(self) -> float: pass

    @abstractmethod
    def set_fahrenheit(self, value: float) -> None: pass

# Существующий датчик, работает в Цельсии
class ThermometerCelsius(CelsiusSensor):
    def __init__(self, initial: float = 0.0):
        self._value = initial

    def get_celsius(self) -> float:
        return self._value

    def set_celsius(self, value: float) -> None:
        self._value = value

# Американская система мониторинга — ожидает Фаренгейт
class USWeatherSystem:
    def read_temperature(self, sensor: FahrenheitSensor) -> str:
        return f"Температура: {sensor.get_fahrenheit():.1f}°F"

    def set_alert_threshold(self, sensor: FahrenheitSensor, threshold_f: float) -> None:
        sensor.set_fahrenheit(threshold_f)
        print(f"Порог тревоги установлен: {threshold_f}°F")

# TODO: реализовать TemperatureAdapter(CelsiusSensor, FahrenheitSensor),
# который оборачивает ThermometerCelsius и реализует оба интерфейса.
# Формулы: F = C * 9/5 + 32 | C = (F - 32) * 5/9

# Клиентский код
# thermometer = ThermometerCelsius(20.0)
# adapter = TemperatureAdapter(thermometer)

# us_system = USWeatherSystem()
# print(us_system.read_temperature(adapter))       # 68.0°F
# us_system.set_alert_threshold(adapter, 104.0)    # = 40°C
# print(f"После установки: {thermometer.get_celsius():.1f}°C")  # 40.0°C

```php linenums="1" <?php interface CelsiusSensor { public function getCelsius(): float; public function setCelsius(float $value): void; }

interface FahrenheitSensor { public function getFahrenheit(): float; public function setFahrenheit(float $value): void; }

// Существующий датчик, работает в Цельсии class ThermometerCelsius implements CelsiusSensor { private float $value;

public function __construct(float $initial = 0.0) {
    $this->value = $initial;
}

public function getCelsius(): float { return $this->value; }
public function setCelsius(float $value): void { $this->value = $value; }

}

// Американская система мониторинга — ожидает Фаренгейт class USWeatherSystem { public function readTemperature(FahrenheitSensor $sensor): string { return sprintf("Температура: %.1f°F", $sensor->getFahrenheit()); }

public function setAlertThreshold(FahrenheitSensor $sensor, float $thresholdF): void {
    $sensor->setFahrenheit($thresholdF);
    echo "Порог тревоги установлен: {$thresholdF}°F\n";
}

}

// TODO: реализовать TemperatureAdapter implements CelsiusSensor, FahrenheitSensor, // который оборачивает ThermometerCelsius и реализует оба интерфейса. // Формулы: F = C * 9/5 + 32 | C = (F - 32) * 5/9

// Клиентский код // $thermometer = new ThermometerCelsius(20.0); // \(adapter = new TemperatureAdapter(\)thermometer);

// $usSystem = new USWeatherSystem(); // echo \(usSystem->readTemperature(\)adapter) . "\n"; // 68.0°F // \(usSystem->setAlertThreshold(\)adapter, 104.0); // = 40°C // echo sprintf("После установки: %.1f°C\n", $thermometer->getCelsius()); // 40.0°C