Адаптер — это структурный паттерн проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе.
Проблема
Представьте, что вы делаете приложение для торговли на бирже. Ваше приложение скачивает биржевые котировки из нескольких источников в XML, а затем рисует красивые графики.
В какой-то момент вы решаете улучшить приложение, применив стороннюю библиотеку аналитики. Но вот беда — библиотека поддерживает только формат данных JSON, несовместимый с вашим приложением.
Подключить стороннюю библиотеку не выйдет из-за несовместимых форматов данных.
Вы смогли бы переписать библиотеку, чтобы та поддерживала формат XML. Но, во-первых, это может нарушить работу существующего кода, который уже зависит от библиотеки. А во-вторых, у вас может просто не быть доступа к её исходному коду.
Решение
Вы можете создать адаптер. Это объект-переводчик, который трансформирует интерфейс или данные одного объекта в такой вид, чтобы он стал понятен другому объекту.
При этом адаптер оборачивает один из объектов, так что другой объект даже не знает о наличии первого. Например, вы можете обернуть объект, работающий в метрах, адаптером, который бы конвертировал данные в футы.
Адаптеры могут не только переводить данные из одного формата в другой, но и помогать объектам с разными интерфейсами работать сообща. Это работает так:
Адаптер имеет интерфейс, который совместим с одним из объектов.
Поэтому этот объект может свободно вызывать методы адаптера.
Адаптер получает эти вызовы и перенаправляет их второму объекту, но уже в том формате и последовательности, которые понятны второму объекту.
Иногда возможно создать даже двухсторонний адаптер, который работал бы в обе стороны.
Программа может работать со сторонней библиотекой через адаптер.
Таким образом, в приложении биржевых котировок вы могли бы создать класс XML_To_JSON_Adapter, который бы оборачивал объект того или иного класса библиотеки аналитики. Ваш код посылал бы адаптеру запросы в формате XML, а адаптер сначала транслировал входящие данные в формат JSON, а затем передавал бы их методам обёрнутого объекта аналитики.
Аналогия из жизни
Содержимое чемоданов до и после поездки за границу.
Когда вы в первый раз летите за границу, вас может ждать сюрприз при попытке зарядить ноутбук. Стандарты розеток в разных странах отличаются. Ваша европейская зарядка будет бесполезна в США без специального адаптера, позволяющего подключиться к розетке другого типа.
Структура
Адаптер объектов
Эта реализация использует агрегацию: объект адаптера «оборачивает», то есть содержит ссылку на служебный объект. Такой подход работает во всех языках программирования.
Клиент — это класс, который содержит существующую бизнес-логику программы.
Клиентский интерфейс описывает протокол, через который клиент может работать с другими классами.
Сервис — это какой-то полезный класс, обычно сторонний. Клиент не может использовать этот класс напрямую, так как сервис имеет непонятный ему интерфейс.
Адаптер — это класс, который может одновременно работать и с клиентом, и с сервисом. Он реализует клиентский интерфейс и содержит ссылку на объект сервиса. Адаптер получает вызовы от клиента через методы клиентского интерфейса, а затем переводит их в вызовы методов обёрнутого объекта в правильном формате.
Работая с адаптером через интерфейс, клиент не привязывается к конкретному классу адаптера. Благодаря этому, вы можете добавлять в программу новые виды адаптеров, независимо от клиентского кода. Это может пригодиться, если интерфейс сервиса вдруг изменится, например, после выхода новой версии сторонней библиотеки.
Адаптер классов
Эта реализация базируется на наследовании: адаптер наследует оба интерфейса одновременно. Такой подход возможен только в языках, поддерживающих множественное наследование, например, 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
Применимость
Когда вы хотите использовать сторонний класс, но его интерфейс не соответствует остальному коду приложения.
Адаптер позволяет создать объект-прокладку, который будет превращать вызовы приложения в формат, понятный стороннему классу.
Когда вам нужно использовать несколько существующих подклассов, но в них не хватает какой-то общей функциональности, причём расширить суперкласс вы не можете.
Вы могли бы создать ещё один уровень подклассов и добавить в них недостающую функциональность. Но при этом придётся дублировать один и тот же код в обеих ветках подклассов.
Более элегантным решением было бы поместить недостающую функциональность в адаптер и приспособить его для работы с суперклассом. Такой адаптер сможет работать со всеми подклассами иерархии. Это решение будет сильно напоминать паттерн Декоратор.
Шаги реализации
Убедитесь, что у вас есть два класса с несовместимыми интерфейсами:
полезный сервис — служебный класс, который вы не можете изменять (он либо сторонний, либо от него зависит другой код);
один или несколько клиентов — существующих классов приложения, несовместимых с сервисом из-за неудобного или несовпадающего интерфейса.
Опишите клиентский интерфейс, через который классы приложения смогли бы использовать класс сервиса.
Создайте класс адаптера, реализовав этот интерфейс.
Поместите в адаптер поле, которое будет хранить ссылку на объект сервиса. Обычно это поле заполняют объектом, переданным в конструктор адаптера. В случае простой адаптации этот объект можно передавать через параметры методов адаптера.
Реализуйте все методы клиентского интерфейса в адаптере. Адаптер должен делегировать основную работу сервису.
Приложение должно использовать адаптер только через клиентский интерфейс. Это позволит легко изменять и добавлять адаптеры в будущем.
Преимущества и недостатки
Преимущества
Отделяет и скрывает от клиента подробности преобразования различных интерфейсов.
Недостатки
Усложняет код программы из-за введения дополнительных классов.
Примеры реализации паттерна
Python
Сложность: 1/3
Популярность: 3/3
Применимость: Паттерн можно часто встретить в Python-коде, особенно там, где требуется конвертация разных типов данных или совместная работа классов с разными интерфейсами.
Признаки применения паттерна: Адаптер получает конвертируемый объект в конструкторе или через параметры своих методов. Методы Адаптера обычно совместимы с интерфейсом одного объекта. Они делегируют вызовы вложенному объекту, превратив перед этим параметры вызова в формат, поддерживаемый вложенным объектом.
Этот пример показывает структуру паттерна Адаптер, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
classTarget:""" Целевой класс объявляет интерфейс, с которым может работать клиентский код. """defrequest(self)->str:return"Target: The default target's behavior."classAdaptee:"""Адаптируемый класс содержит некоторое полезное поведение, но его интерфейснесовместим с существующим клиентским кодом. Адаптируемый класс нуждается внекоторой доработке, прежде чем клиентский код сможет его использовать."""defspecific_request(self)->str:return".eetpadA eht fo roivaheb laicepS"classAdapter(Target,Adaptee):"""Адаптер делает интерфейс Адаптируемого класса совместимым с целевыминтерфейсом благодаря множественному наследованию."""defrequest(self)->str:returnf"Adapter: (TRANSLATED) {self.specific_request()[::-1]}"defclient_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:IcanworkjustfinewiththeTargetobjects:
Target:Thedefaulttarget's behavior.Client: The Adaptee class has a weird interface. See, I don'tunderstandit:
Adaptee:.eetpadAehtforoivaheblaicepS
Client:ButIcanworkwithitviatheAdapter:
Adapter:(TRANSLATED)SpecialbehavioroftheAdaptee.
Этот пример показывает структуру паттерна Адаптер, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
classTarget:""" Целевой класс объявляет интерфейс, с которым может работать клиентский код. """defrequest(self)->str:return"Target: The default target's behavior."classAdaptee:"""Адаптируемый класс содержит некоторое полезное поведение, но его интерфейснесовместим с существующим клиентским кодом. Адаптируемый класс нуждается внекоторой доработке, прежде чем клиентский код сможет его использовать."""defspecific_request(self)->str:return".eetpadA eht fo roivaheb laicepS"classAdapter(Target):"""Адаптер делает интерфейс Адаптируемого класса совместимым с целевыминтерфейсом благодаря агрегации."""def__init__(self,adaptee:Adaptee)->None:self.adaptee=adapteedefrequest(self)->str:returnf"Adapter: (TRANSLATED) {self.adaptee.specific_request()[::-1]}"defclient_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:IcanworkjustfinewiththeTargetobjects:
Target:Thedefaulttarget's behavior.Client: The Adaptee class has a weird interface. See, I don'tunderstandit:
Adaptee:.eetpadAehtforoivaheblaicepS
Client:ButIcanworkwithitviatheAdapter:
Adapter:(TRANSLATED)SpecialbehavioroftheAdaptee.
PHP
Сложность: 1/3
Популярность: 3/3
Применимость: Паттерн можно часто встретить в PHP-коде, особенно там, где требуется конвертация разных типов данных или совместная работа классов с разными интерфейсами.
Признаки применения паттерна: Адаптер получает конвертируемый объект в конструкторе или через параметры своих методов. Методы Адаптера обычно совместимы с интерфейсом одного объекта. Они делегируют вызовы вложенному объекту, превратив перед этим параметры вызова в формат, поддерживаемый вложенным объектом.
Этот пример показывает структуру паттерна Адаптер, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
<?php/*** Целевой класс объявляет интерфейс, с которым может работать клиентский код.*/classTarget{publicfunctionrequest():string{return"Target: The default target's behavior.";}}/*** Адаптируемый класс содержит некоторое полезное поведение, но его интерфейс* несовместим с существующим клиентским кодом. Адаптируемый класс нуждается в* некоторой доработке, прежде чем клиентский код сможет его использовать.*/classAdaptee{publicfunctionspecificRequest():string{return".eetpadA eht fo roivaheb laicepS";}}/*** Адаптер делает интерфейс Адаптируемого класса совместимым с целевым* интерфейсом.*/classAdapterextendsTarget{private$adaptee;publicfunction__construct(Adaptee$adaptee){$this->adaptee=$adaptee;}publicfunctionrequest():string{return"Adapter: (TRANSLATED) ".strrev($this->adaptee->specificRequest());}}/*** Клиентский код поддерживает все классы, использующие целевой интерфейс.*/functionclientCode(Target$target){echo$target->request();}echo"Client: I can work just fine with the Target objects:\n";$target=newTarget();clientCode($target);echo"\n\n";$adaptee=newAdaptee();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=newAdapter($adaptee);clientCode($adapter);
Client:IcanworkjustfinewiththeTargetobjects:
Target:Thedefaulttarget's behavior.Client: The Adaptee class has a weird interface. See, I don'tunderstandit:
Adaptee:.eetpadAehtforoivaheblaicepS
Client:ButIcanworkwithitviatheAdapter:
Adapter:(TRANSLATED)SpecialbehavioroftheAdaptee.
Паттерн Адаптер позволяет использовать сторонние или устаревшие классы, даже если они несовместимы с основной частью кода. Например, вместо того, чтобы переписывать интерфейс уведомлений вашего приложения для поддержки каждого стороннего сервиса вроде Slack, Facebook, SMS и прочих, вы создаёте под эти сервисы набор специальных обёрток, которые приводят вызовы из приложения к требуемым сторонними классами интерфейсу и формату.
<?php/*** Целевой интерфейс предоставляет интерфейс, которому следуют классы вашего* приложения.*/interfaceNotification{publicfunctionsend(string$title,string$message);}/*** Вот пример существующего класса, который следует за целевым интерфейсом.** Дело в том, что у большинства приложений нет чётко определённого интерфейса.* В этом случае лучше было бы расширить Адаптер за счёт существующего класса* приложения. Если это неудобно (например, SlackNotification не похож на* подкласс EmailNotification), тогда первым шагом должно быть извлечение* интерфейса.*/classEmailNotificationimplementsNotification{private$adminEmail;publicfunction__construct(string$adminEmail){$this->adminEmail=$adminEmail;}publicfunctionsend(string$title,string$message):void{mail($this->adminEmail,$title,$message);echo"Sent email with title '$title' to '{$this->adminEmail}' that says '$message'.";}}/*** Адаптируемый класс – некий полезный класс, несовместимый с целевым* интерфейсом. Нельзя просто войти и изменить код класса так, чтобы следовать* целевому интерфейсу, так как код может предоставляться сторонней библиотекой.*/classSlackApi{private$login;private$apiKey;publicfunction__construct(string$login,string$apiKey){$this->login=$login;$this->apiKey=$apiKey;}publicfunctionlogIn():void{// Send authentication request to Slack web service.echo"Logged in to a slack account '{$this->login}'.\n";}publicfunctionsendMessage(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 для отправки уведомлений.*/classSlackNotificationimplementsNotification{private$slack;private$chatId;publicfunction__construct(SlackApi$slack,string$chatId){$this->slack=$slack;$this->chatId=$chatId;}/** * Адаптер способен адаптировать интерфейсы и преобразовывать входные данные * в формат, необходимый Адаптируемому классу. */publicfunctionsend(string$title,string$message):void{$slackMessage="#".$title."# ".strip_tags($message);$this->slack->logIn();$this->slack->sendMessage($this->chatId,$slackMessage);}}/*** Клиентский код работает с классами, которые следуют Целевому интерфейсу.*/functionclientCode(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=newEmailNotification("developers@example.com");clientCode($notification);echo"\n\n";echo"The same client code can work with other classes via adapter:\n";$slackApi=newSlackApi("example.com","XXXXXXXX");$notification=newSlackNotification($slackApi,"Example.com Developers");clientCode($notification);
Clientcodeisdesignedcorrectlyandworkswithemailnotifications:
Sentemailwithtitle'Website is down!'to'developers@example.com'thatsays'<strong style='color:red;font-size:50px;'>Alert!</strong>Our website is not responding. Call admins and bring it up!'.
Thesameclientcodecanworkwithotherclassesviaadapter:
Loggedintoaslackaccount'example.com'.
Postedfollowingmessageintothe'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 без изменения клиентского кода.
fromabcimportABC,abstractmethod# Существующий интерфейс, с которым работает клиентский кодclassPaymentProcessor(ABC):@abstractmethoddefpay(self,amount:float,currency:str)->str:pass@abstractmethoddefrefund(self,transaction_id:str,amount:float)->str:pass# Существующая реализация, которая уже работаетclassPayPalProcessor(PaymentProcessor):defpay(self,amount:float,currency:str)->str:returnf"PayPal: charged {amount}{currency}"defrefund(self,transaction_id:str,amount:float)->str:returnf"PayPal: refunded {amount} for tx {transaction_id}"# Новый сторонний SDK с несовместимым APIclassStripeSDK:defcreate_charge(self,amount_cents:int,currency_code:str,metadata:dict)->dict:return{"id":"ch_stripe_123","status":"succeeded","amount":amount_cents,"currency":currency_code,}defcreate_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 — с рублями/долларами.# Клиентский код — не должен менятьсяdefcheckout(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// Существующий интерфейс, с которым работает клиентский кодinterfacePaymentProcessor{publicfunctionpay(float$amount,string$currency):string;publicfunctionrefund(string$transactionId,float$amount):string;}// Существующая реализация, которая уже работаетclassPayPalProcessorimplementsPaymentProcessor{publicfunctionpay(float$amount,string$currency):string{return"PayPal: charged {$amount}{$currency}";}publicfunctionrefund(string$transactionId,float$amount):string{return"PayPal: refunded {$amount} for tx {$transactionId}";}}// Новый сторонний SDK с несовместимым APIclassStripeSDK{publicfunctioncreateCharge(int$amountCents,string$currencyCode,array$metadata):array{return['id'=>'ch_stripe_123','status'=>'succeeded','amount'=>$amountCents];}publicfunctioncreateRefund(string$chargeId,int$amountCents):array{return['id'=>'re_stripe_456','status'=>'succeeded','amount'=>$amountCents];}}// TODO: реализовать StripeAdapter implements PaymentProcessor,// который оборачивает StripeSDK и адаптирует его к интерфейсу PaymentProcessor.// Учтите: Stripe работает с центами (amount * 100), PaymentProcessor — с рублями/долларами.// Клиентский код — не должен менятьсяfunctioncheckout(PaymentProcessor$processor,float$amount,string$currency):void{echo$processor->pay($amount,$currency)."\n";}checkout(newPayPalProcessor(),1500.0,'RUB');// checkout(new StripeAdapter(new StripeSDK()), 1500.0, 'RUB'); // должно работать так же
Задача 2. Адаптер стороннего логгера
Приложение использует интерфейс Logger с методами info(), warning() и error(). Нужно подключить стороннюю библиотеку MonologLogger, у которой другой API. Реализуйте АдаптерMonologAdapter, не меняя клиентский код и не модифицируя MonologLogger.
fromabcimportABC,abstractmethodfromdatetimeimportdatetime# Интерфейс, которого ожидает приложениеclassLogger(ABC):@abstractmethoddefinfo(self,message:str)->None:pass@abstractmethoddefwarning(self,message:str)->None:pass@abstractmethoddeferror(self,message:str,exception:Exception=None)->None:pass# Сторонняя библиотека с другим API (изменять нельзя)classMonologLogger:def__init__(self,channel:str):self.channel=channeldefadd_record(self,level:str,msg:str,context:dict=None)->None:ts=datetime.now().strftime("%H:%M:%S")ctx=f" {context}"ifcontextelse""print(f"[{ts}] {self.channel}.{level}: {msg}{ctx}")defpush_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.# Клиентский кодdefprocess_order(logger:Logger,order_id:int):logger.info(f"Обработка заказа #{order_id}")try:iforder_id<0:raiseValueError("ID не может быть отрицательным")logger.info(f"Заказ #{order_id} успешно обработан")exceptValueErrorase:logger.error("Ошибка обработки заказа",exception=e)# process_order(MonologAdapter(MonologLogger("app")), 42)# process_order(MonologAdapter(MonologLogger("app")), -1)
<?php// Интерфейс, которого ожидает приложениеinterfaceLogger{publicfunctioninfo(string$message):void;publicfunctionwarning(string$message):void;publicfunctionerror(string$message,?\Throwable$exception=null):void;}// Сторонняя библиотека с другим API (изменять нельзя)classMonologLogger{publicfunction__construct(privatestring$channel){}publicfunctionaddRecord(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";}publicfunctionpushHandler(string$handlerName):void{echo"[Monolog] Handler '{$handlerName}' registered\n";}}// TODO: реализовать MonologAdapter implements Logger,// который делегирует вызовы к MonologLogger::addRecord()// с соответствующим уровнем: INFO, WARNING, ERROR.// При вызове error() с exception — передавать её в context.// Клиентский кодfunctionprocessOrder(Logger$logger,int$orderId):void{$logger->info("Обработка заказа #{$orderId}");try{if($orderId<0)thrownewInvalidArgumentException('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. Реализуйте адаптеры для каждого, чтобы клиентский код мог работать с ними через единый интерфейс.
fromabcimportABC,abstractmethod# Интерфейс, с которым работает приложениеclassDataReader(ABC):@abstractmethoddefread_all(self)->list[dict]:pass@abstractmethoddefread_by_id(self,record_id:int)->dict|None:pass# Первый несовместимый источник — CSV-файлclassCsvFileReader:def__init__(self,filepath:str):self.filepath=filepathdefopen(self)->None:print(f"[CSV] Открыт файл: {self.filepath}")deffetch_rows(self)->list[list[str]]:# Симуляция: возвращает строки вместо словарей, первая — заголовкиreturn[["id","name","price"],["1","Книга","350"],["2","Ручка","50"],["3","Тетрадь","80"],]defclose(self)->None:print(f"[CSV] Файл закрыт: {self.filepath}")# Второй несовместимый источник — JSON APIclassJsonApiClient:def__init__(self,base_url:str,api_key:str):self.base_url=base_urlself.api_key=api_keydefget_collection(self,endpoint:str)->dict:# Симуляция ответа APIreturn{"data":[{"id":1,"attributes":{"name":"Книга","price":350}},{"id":2,"attributes":{"name":"Ручка","price":50}},{"id":3,"attributes":{"name":"Тетрадь","price":80}},],"meta":{"total":3},}defget_single(self,endpoint:str,record_id:int)->dict|None:collection=self.get_collection(endpoint)foritemincollection["data"]:ifitem["id"]==record_id:returnitemreturnNone# TODO: реализовать CsvDataAdapter(DataReader) и JsonApiAdapter(DataReader)# Клиентский кодdefprint_catalog(reader:DataReader):items=reader.read_all()foriteminitems: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// Интерфейс, с которым работает приложениеinterfaceDataReader{publicfunctionreadAll():array;publicfunctionreadById(int$recordId):?array;}// Первый несовместимый источник — CSV-файлclassCsvFileReader{publicfunction__construct(privatestring$filepath){}publicfunctionopen():void{echo"[CSV] Открыт файл: {$this->filepath}\n";}publicfunctionfetchRows():array{return[['id','name','price'],['1','Книга','350'],['2','Ручка','50'],['3','Тетрадь','80'],];}publicfunctionclose():void{echo"[CSV] Файл закрыт: {$this->filepath}\n";}}// Второй несовместимый источник — JSON APIclassJsonApiClient{publicfunction__construct(privatestring$baseUrl,privatestring$apiKey){}publicfunctiongetCollection(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],];}publicfunctiongetSingle(string$endpoint,int$recordId):?array{foreach($this->getCollection($endpoint)['data']as$item){if($item['id']===$recordId)return$item;}returnnull;}}// TODO: реализовать CsvDataAdapter implements DataReader// и JsonApiAdapter implements DataReader// Клиентский кодfunctionprintCatalog(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. Реализуйте адаптеры для каждого так, чтобы приложение могло переключаться между провайдерами без изменений в коде.
<?phpclassCoordinates{publicfunction__construct(publicfloat$lat,publicfloat$lng){}}classAddress{publicfunction__construct(publicstring$country,publicstring$city,publicstring$street,publicstring$building,){}}// Интерфейс, которого ожидает приложениеinterfaceGeoService{publicfunctiongeocode(string$address):Coordinates;publicfunctionreverseGeocode(Coordinates$coords):Address;publicfunctiondistanceKm(Coordinates$origin,Coordinates$destination):float;}// Первый внешний API (изменять нельзя)classGoogleMapsAPI{publicfunctionfindPlace(string$query):array{return['geometry'=>['location'=>['lat'=>55.7558,'lng'=>37.6173]]];}publicfunctionreverseGeocode(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'],]]]];}publicfunctiondistanceMatrix(array$origins,array$destinations):array{return['rows'=>[['elements'=>[['distance'=>['value'=>5200]]]]]];}}// Второй внешний API (изменять нельзя)classYandexMapsAPI{publicfunctiongeocode(string$address):array{return['response'=>['GeoObjectCollection'=>['featureMember'=>[['GeoObject'=>['Point'=>['pos'=>'37.6173 55.7558']]]]]]];}publicfunctionreverseGeocode(float$lon,float$lat):array{return['response'=>['GeoObjectCollection'=>['featureMember'=>[['GeoObject'=>['metaDataProperty'=>['GeocoderMetaData'=>['AddressDetails'=>['Country'=>['CountryName'=>'Россия','Locality'=>['LocalityName'=>'Москва','Thoroughfare'=>['ThoroughfareName'=>'Тверская','Premise'=>['PremiseNumber'=>'1']]]]]]]]]]]]];}publicfunctionroute(string$fromPoint,string$toPoint):array{return['route'=>['legs'=>[['steps'=>[['distance'=>5200]]]]]];}}// TODO: реализовать GoogleMapsAdapter implements GeoService// и YandexMapsAdapter implements GeoService// Клиентский кодfunctionshowLocationInfo(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) и прозрачно конвертировать значения в обоих направлениях. Продемонстрируйте работу на примере метеостанции.
fromabcimportABC,abstractmethodclassCelsiusSensor(ABC):@abstractmethoddefget_celsius(self)->float:pass@abstractmethoddefset_celsius(self,value:float)->None:passclassFahrenheitSensor(ABC):@abstractmethoddefget_fahrenheit(self)->float:pass@abstractmethoddefset_fahrenheit(self,value:float)->None:pass# Существующий датчик, работает в ЦельсииclassThermometerCelsius(CelsiusSensor):def__init__(self,initial:float=0.0):self._value=initialdefget_celsius(self)->float:returnself._valuedefset_celsius(self,value:float)->None:self._value=value# Американская система мониторинга — ожидает ФаренгейтclassUSWeatherSystem:defread_temperature(self,sensor:FahrenheitSensor)->str:returnf"Температура: {sensor.get_fahrenheit():.1f}°F"defset_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);