Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Design Patterns (GoF and modern)  Module 3 – Structural patterns: Adapter to Proxy

Adapter: make incompatible interfaces fit

Wrap a class you cannot change behind the interface your code expects: object and class adapters, fakes for tests, and the adapters in standard libraries.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java temurin-25.0.4+101.0.LTS, Go 1.27.1 and Rust 1.99.0
  • By MySmartCoPilot
Free: 20 minutes

What you will learn

  • Implement an object adapter that wraps a third-party API behind your own interface
  • Compare object adapters, class adapters and language conversions such as Rust's From and Into
  • Use adapters to keep external SDKs out of your tests
  • Recognise adapters in standard libraries even when their documentation uses another name
  • Design one interface with an implementation per platform, held together by shared test cases

Before you start

What you’ll learn · Before you start

What you will learn

  • Implement an object adapter that wraps a third-party API behind your own interface
  • Compare object adapters, class adapters and language conversions such as Rust's From and Into
  • Use adapters to keep external SDKs out of your tests
  • Recognise adapters in standard libraries even when their documentation uses another name
  • Design one interface with an implementation per platform, held together by shared test cases

Before you start

On this page

A clinic’s booking app sends reminders through a text-message provider whose library wants a dictionary with the number under the key msisdn, as 12 digits with no plus sign, and reports failures as numeric status codes. Let forty places in the app build those dictionaries and read those codes, and a change of provider means forty edits, while every test needs the provider’s library. Adapter puts the whole translation in one class: it implements the interface your code expects, holds the object you cannot change, and turns each call into calls on that object.

Below, that adapter runs in 13 languages, a fake takes its place in tests, and object adapters, class adapters and conversions are compared on the same code. Then you will find the adapters already inside standard libraries and give one interface an implementation on three platforms.

An SDK whose habits would spread through your code

The app needs one operation: send(phone, text), which returns an id for the message or raises an error the app understands. The provider’s client library, invented for this lesson but with habits that real ones share, offers something else:

  • one dispatch() method that takes a dictionary, with the number under msisdn (the telecom term for a full mobile number);
  • the number as 12 digits with the country code, no plus sign and no spaces;
  • a status code in the reply, 0 for a message it accepted and another number for one it refused, never an exception.

You cannot change the library, and you should not let its habits spread. The pattern has four parts. The client is the code that needs the operation, here remind(). The target interface: The interface that client code calls and the application owns, such as a Notifier with send(phone, text). An adapter implements it on top of an object with another interface. is the one the client calls, Notifier, and it belongs to the application. The adaptee: The existing object whose interface does not fit and which you cannot change, such as a provider's SDK client. An adapter holds it and translates calls into calls on it. is the class you cannot change, the provider’s SmsGatewayClient. The adapter, SmsNotifier, implements the target interface and holds the adaptee:

Application code calls the Notifier interface; SmsNotifier implements it and calls the SDK client it holds.remind(), SignupServiceapplication codeNotifier (interface)send(phone, text) -> message idraises NotifierErrorSmsNotifier (the adapter)reshapes the call, converts the number,maps status codes to NotifierErrorSmsGatewayClient (the SDK)dispatch(payload) -> replystatus codes, never exceptionscallsimplemented byholds one and calls dispatch()

An object adapter between the application and a text-message SDK

Text description of the diagram

The diagram shows four boxes in one column, joined by arrows from top to bottom.

  1. The application code, remind() and SignupService, calls the Notifier interface.
  2. Notifier is the application's own interface: send(phone, text) returns a message id or raises NotifierError. A dashed arrow says it is implemented by the adapter.
  3. SmsNotifier, the adapter, reshapes each call, converts the phone number to the SDK's format and maps the SDK's status codes to NotifierError.
  4. SmsGatewayClient, the SDK, is held by the adapter, which calls its dispatch() method. The SDK takes a dictionary and reports failures as status codes, never as exceptions.

The application code has no arrow to the SDK: only the adapter knows it.

The client has no arrow to the SDK at all. Follow both reminders of the program below through the three objects, one accepted and one refused, and watch what each side gets to see:

Interactive · message by message Two reminders through the SMS adapter Step through both calls of send(). The client sees only Notifier, and the SDK sees only its own dictionary.
Two reminders through the SMS adapter, step 9 of 9. Messages so far: remind() to SmsNotifier: send(phone, text); SmsNotifier to SmsGatewayClient: dispatch(payload); SmsGatewayClient to SmsNotifier: code 0, GW-000001; SmsNotifier to remind(): GW-000001; remind() to SmsNotifier: send(phone, text); SmsNotifier to SmsGatewayClient: dispatch(payload); SmsGatewayClient to SmsNotifier: code 4011; SmsNotifier to remind(): NotifierError.remind()the clientSmsNotifiermsisdn 9198765SmsGatewayClientaccepted: 1send(phone, text)dispatch(payload)code 0, GW-000001GW-000001send(phone, text)dispatch(payload)code 4011NotifierErrornever sees code 4011

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. The client calls Notifier. remind() calls send("+91 98765 43210", "Visit at 10:30."). It knows the Notifier interface and nothing about the provider.
  2. The adapter translates. The adapter keeps the digits of the number, 919876543210, and builds the dictionary the SDK wants: msisdn, body and the sender CLINIC, which was fixed when the adapter was made.
  3. The SDK is called its way. dispatch() receives the dictionary in the SDK's own format, built in this one class. The SDK prints the msisdn it was given.
  4. A status code comes back. The number has 12 digits, so the SDK accepts the message and answers with code 0 and the id GW-000001.
  5. The reply is mapped. Code 0 becomes an ordinary return value, the id alone. remind() prints sent to +91 98765 43210: GW-000001.
  6. A number that is too short. The second reminder goes to +91 98765. The client calls the same send() and cannot tell that anything is different.
  7. Translated the same way. The adapter keeps the 7 digits it finds, 9198765, and passes them on. Deciding which numbers are valid is the SDK's rule, not the adapter's.
  8. Refused with a code. The SDK refuses a number without 12 digits. It raises nothing: it answers with status code 4011.
  9. The code becomes an error. The adapter turns 4011 into a NotifierError in the application's words, cannot send to +91 98765: invalid number. remind() catches it and prints failed, and code 4011 never reaches it.

The client never learns that a dictionary, a 12-digit number or code 4011 exist, and the SDK never sees a NotifierError. Everything that knows both sides is in the one class in the middle.

An object adapter does four jobs, all in one class

Here is that adapter as a program, written once for every language of the tabs. The 13 versions printed identical lines, shown once below the tabs, and the C tab explains why C sits this one out:

One program · 13 languages One reminder sent and one refused through the SMS adapter

Python 3.14 · main.py

from abc import ABC, abstractmethod


class NotifierError(Exception):
    pass


class Notifier(ABC):
    @abstractmethod
    def send(self, phone: str, text: str) -> str: ...


class SmsGatewayClient:
    """The SDK, which cannot change: a dictionary in, a status code out."""

    def __init__(self) -> None:
        self.accepted = 0

    def dispatch(self, payload: dict[str, str]) -> tuple[int, str]:
        msisdn = payload["msisdn"]
        print(f"  SDK got {msisdn}")
        if not (msisdn.isdigit() and len(msisdn) == 12):
            return 4011, ""
        self.accepted += 1
        return 0, f"GW-{self.accepted:06d}"  # code 0: accepted


class SmsNotifier(Notifier):
    """The adapter: a Notifier outside, calls to the SDK inside."""

    def __init__(self, client: SmsGatewayClient, sender: str) -> None:
        self._client = client
        self._sender = sender

    def send(self, phone: str, text: str) -> str:
        digits = "".join(ch for ch in phone if ch.isdigit())
        payload = {"msisdn": digits, "body": text, "sender": self._sender}
        code, message_id = self._client.dispatch(payload)
        if code != 0:
            reason = "invalid number" if code == 4011 else f"code {code}"
            raise NotifierError(f"cannot send to {phone}: {reason}")
        return message_id


def remind(notifier: Notifier, phone: str, time: str) -> None:
    try:
        receipt = notifier.send(phone, f"Visit at {time}.")
        print(f"sent to {phone}: {receipt}")
    except NotifierError as error:
        print(f"failed: {error}")


notifier = SmsNotifier(SmsGatewayClient(), sender="CLINIC")
remind(notifier, "+91 98765 43210", "10:30")
remind(notifier, "+91 98765", "11:00")

In Python: Notifier is an abstract base class, so a subclass that leaves out send() cannot be created. The SDK answers with a tuple, the status code and the id, which send() unpacks.

Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py

JavaScript Node 24 · main.mjs

class NotifierError extends Error {}

/** @typedef {{ send(phone: string, text: string): string }} Notifier */

/** The SDK, which cannot change: a dictionary in, a status code out. */
class SmsGatewayClient {
  accepted = 0;

  dispatch(payload) {
    const { msisdn } = payload;
    console.log(`  SDK got ${msisdn}`);
    if (!/^\d{12}$/.test(msisdn)) return [4011, ""];
    this.accepted += 1;
    return [0, `GW-${String(this.accepted).padStart(6, "0")}`]; // code 0: accepted
  }
}

/** The adapter: a Notifier outside, calls to the SDK inside. */
class SmsNotifier {
  #client;
  #sender;

  constructor(client, sender) {
    this.#client = client;
    this.#sender = sender;
  }

  send(phone, text) {
    const digits = phone.replace(/\D/g, "");
    const [code, messageId] = this.#client.dispatch({
      msisdn: digits,
      body: text,
      sender: this.#sender,
    });
    if (code !== 0) {
      const reason = code === 4011 ? "invalid number" : `code ${code}`;
      throw new NotifierError(`cannot send to ${phone}: ${reason}`);
    }
    return messageId;
  }
}

/** @param {Notifier} notifier @param {string} phone @param {string} time */
function remind(notifier, phone, time) {
  try {
    const receipt = notifier.send(phone, `Visit at ${time}.`);
    console.log(`sent to ${phone}: ${receipt}`);
  } catch (error) {
    if (!(error instanceof NotifierError)) throw error;
    console.log(`failed: ${error.message}`);
  }
}

const notifier = new SmsNotifier(new SmsGatewayClient(), "CLINIC");
remind(notifier, "+91 98765 43210", "10:30");
remind(notifier, "+91 98765", "11:00");

In JavaScript: JavaScript has no interfaces, so Notifier is a JSDoc type that editors check.

Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs

Java 25 LTS · Main.java

import java.util.Locale;
import java.util.Map;

public class Main {
  static class NotifierException extends Exception {
    NotifierException(String message) {
      super(message);
    }
  }

  interface Notifier {
    String send(String phone, String text) throws NotifierException;
  }

  record Reply(int code, String messageId) {} // code 0: accepted

  /** The SDK, which cannot change: a dictionary in, a status code out. */
  static class SmsGatewayClient {
    private int accepted = 0;

    Reply dispatch(Map<String, String> payload) {
      String msisdn = payload.get("msisdn");
      System.out.println("  SDK got " + msisdn);
      if (msisdn.length() != 12 || !msisdn.chars().allMatch(Character::isDigit)) {
        return new Reply(4011, "");
      }
      accepted++;
      return new Reply(0, String.format("GW-%06d", accepted));
    }
  }

  /** The adapter: a Notifier outside, calls to the SDK inside. */
  static class SmsNotifier implements Notifier {
    private final SmsGatewayClient client;
    private final String sender;

    SmsNotifier(SmsGatewayClient client, String sender) {
      this.client = client;
      this.sender = sender;
    }

    @Override
    public String send(String phone, String text) throws NotifierException {
      var digits = new StringBuilder();
      phone.chars().filter(Character::isDigit).forEach(digits::appendCodePoint);
      var reply =
          client.dispatch(Map.of("msisdn", digits.toString(), "body", text, "sender", sender));
      if (reply.code() != 0) {
        var reason = reply.code() == 4011 ? "invalid number" : "code " + reply.code();
        throw new NotifierException("cannot send to " + phone + ": " + reason);
      }
      return reply.messageId();
    }
  }

  static void remind(Notifier notifier, String phone, String time) {
    try {
      var receipt = notifier.send(phone, "Visit at " + time + ".");
      System.out.println("sent to " + phone + ": " + receipt);
    } catch (NotifierException error) {
      System.out.println("failed: " + error.getMessage());
    }
  }

  public static void main(String[] args) {
    Locale.setDefault(Locale.ROOT);
    var notifier = new SmsNotifier(new SmsGatewayClient(), "CLINIC");
    remind(notifier, "+91 98765 43210", "10:30");
    remind(notifier, "+91 98765", "11:00");
  }
}

In Java: The SDK's failure becomes a checked NotifierException, so the compiler makes remind() catch it or declare it. Java names its exception classes with Exception rather than Error.

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java

C++23 · main.cpp

#include <algorithm>
#include <format>
#include <map>
#include <print>
#include <stdexcept>
#include <string>

struct NotifierError : std::runtime_error {
  using std::runtime_error::runtime_error;
};

struct Notifier {
  virtual ~Notifier() = default;
  virtual std::string send(std::string_view phone, std::string_view text) = 0;
};

using Payload = std::map<std::string, std::string>;

bool is_digit(char ch) { return ch >= '0' && ch <= '9'; }

// The SDK, which cannot change: a dictionary in, a status code out.
struct SmsGatewayClient {
  int accepted = 0;

  int dispatch(const Payload &payload, std::string &message_id) {
    const std::string &msisdn = payload.at("msisdn");
    std::println("  SDK got {}", msisdn);
    if (msisdn.size() != 12 || !std::ranges::all_of(msisdn, is_digit)) {
      return 4011;
    }
    message_id = std::format("GW-{:06}", ++accepted);
    return 0;
  }
};

// The adapter: a Notifier outside, calls to the SDK inside.
class SmsNotifier : public Notifier {
public:
  SmsNotifier(SmsGatewayClient &client, std::string sender)
      : client_(client), sender_(std::move(sender)) {}

  std::string send(std::string_view phone, std::string_view text) override {
    std::string digits;
    std::ranges::copy_if(phone, std::back_inserter(digits), is_digit);
    Payload payload{
        {"msisdn", digits}, {"body", std::string(text)}, {"sender", sender_}};
    std::string message_id;
    int code = client_.dispatch(payload, message_id);
    if (code != 0) {
      auto reason = code == 4011 ? std::string("invalid number")
                                 : std::format("code {}", code);
      throw NotifierError(std::format("cannot send to {}: {}", phone, reason));
    }
    return message_id;
  }

private:
  SmsGatewayClient &client_;
  std::string sender_;
};

void remind(Notifier &notifier, std::string_view phone, std::string_view time) {
  try {
    auto receipt = notifier.send(phone, std::format("Visit at {}.", time));
    std::println("sent to {}: {}", phone, receipt);
  } catch (const NotifierError &error) {
    std::println("failed: {}", error.what());
  }
}

int main() {
  SmsGatewayClient client;
  SmsNotifier notifier(client, "CLINIC");
  remind(notifier, "+91 98765 43210", "10:30");
  remind(notifier, "+91 98765", "11:00");
}

In C++: The adapter keeps a reference to the SDK it was given, so the SDK must outlive the adapter. Here the SDK writes the id through a reference and returns the code, in the style of a C library.

Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main

C# 14 · .NET 10 · app.cs

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

var notifier = new SmsNotifier(new SmsGatewayClient(), "CLINIC");
Remind(notifier, "+91 98765 43210", "10:30");
Remind(notifier, "+91 98765", "11:00");

static void Remind(INotifier notifier, string phone, string time)
{
    try
    {
        var receipt = notifier.Send(phone, $"Visit at {time}.");
        Console.WriteLine($"sent to {phone}: {receipt}");
    }
    catch (NotifierException error)
    {
        Console.WriteLine($"failed: {error.Message}");
    }
}

sealed class NotifierException(string message) : Exception(message);

interface INotifier
{
    string Send(string phone, string text);
}

/// <summary>The SDK, which cannot change: a dictionary in, a status code out.</summary>
sealed class SmsGatewayClient
{
    int accepted;

    public (int Code, string MessageId) Dispatch(Dictionary<string, string> payload)
    {
        var msisdn = payload["msisdn"];
        Console.WriteLine($"  SDK got {msisdn}");
        if (msisdn.Length != 12 || !msisdn.All(char.IsAsciiDigit))
        {
            return (4011, "");
        }
        accepted++;
        return (0, $"GW-{accepted:D6}"); // code 0: accepted
    }
}

/// <summary>The adapter: a Notifier outside, calls to the SDK inside.</summary>
sealed class SmsNotifier(SmsGatewayClient client, string sender) : INotifier
{
    public string Send(string phone, string text)
    {
        var digits = string.Concat(phone.Where(char.IsAsciiDigit));
        var (code, messageId) = client.Dispatch(
            new() { ["msisdn"] = digits, ["body"] = text, ["sender"] = sender });
        if (code != 0)
        {
            var reason = code == 4011 ? "invalid number" : $"code {code}";
            throw new NotifierException($"cannot send to {phone}: {reason}");
        }
        return messageId;
    }
}

In C#: Interface names start with I, as .NET's naming guidelines ask. The SDK returns a named tuple, (int Code, string MessageId), which the adapter deconstructs.

Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs

C23 · main.c

C is left out of this example: C has no interfaces or exceptions, so the target would be a struct of function pointers that reports failures through out parameters, another design.

PHP 8.5 · main.php

<?php

declare(strict_types=1);

final class NotifierException extends Exception {}

interface Notifier
{
    public function send(string $phone, string $text): string;
}

/** The SDK, which cannot change: a dictionary in, a status code out. */
final class SmsGatewayClient
{
    private int $accepted = 0;

    /**
     * @param array{msisdn: string, body: string, sender: string} $payload
     * @return array{int, string} the code, 0 when accepted, and the message's id
     */
    public function dispatch(array $payload): array
    {
        $msisdn = $payload['msisdn'];
        echo "  SDK got {$msisdn}\n";
        if (preg_match('/^\d{12}$/', $msisdn) !== 1) {
            return [4011, ''];
        }
        $this->accepted++;
        return [0, sprintf('GW-%06d', $this->accepted)];
    }
}

/** The adapter: a Notifier outside, calls to the SDK inside. */
final class SmsNotifier implements Notifier
{
    public function __construct(private SmsGatewayClient $client, private string $sender) {}

    public function send(string $phone, string $text): string
    {
        $digits = preg_replace('/\D/', '', $phone);
        $payload = ['msisdn' => $digits, 'body' => $text, 'sender' => $this->sender];
        [$code, $messageId] = $this->client->dispatch($payload);
        if ($code !== 0) {
            $reason = $code === 4011 ? 'invalid number' : "code {$code}";
            throw new NotifierException("cannot send to {$phone}: {$reason}");
        }
        return $messageId;
    }
}

function remind(Notifier $notifier, string $phone, string $time): void
{
    try {
        $receipt = $notifier->send($phone, "Visit at {$time}.");
        echo "sent to {$phone}: {$receipt}\n";
    } catch (NotifierException $error) {
        echo "failed: {$error->getMessage()}\n";
    }
}

$notifier = new SmsNotifier(new SmsGatewayClient(), 'CLINIC');
remind($notifier, '+91 98765 43210', '10:30');
remind($notifier, '+91 98765', '11:00');

In PHP: Constructor promotion, private SmsGatewayClient $client, declares and sets the adapter's fields in one line. The payload's keys are written down as a PHPDoc array shape.

Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php

Rust 1.99 · main.rs

use std::collections::HashMap;
use std::fmt;

#[derive(Debug)]
struct NotifierError(String);

impl fmt::Display for NotifierError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str(&self.0)
    }
}

trait Notifier {
    fn send(&mut self, phone: &str, text: &str) -> Result<String, NotifierError>;
}

/// The SDK, which cannot change: a dictionary in, a status code out.
#[derive(Default)]
struct SmsGatewayClient {
    accepted: u32,
}

impl SmsGatewayClient {
    /// Ok with the message's id when it was accepted, or Err with a status code.
    fn dispatch(&mut self, payload: &HashMap<&str, String>) -> Result<String, u32> {
        let msisdn = &payload["msisdn"];
        println!("  SDK got {msisdn}");
        if msisdn.len() != 12 || !msisdn.chars().all(|ch| ch.is_ascii_digit()) {
            return Err(4011);
        }
        self.accepted += 1;
        Ok(format!("GW-{:06}", self.accepted))
    }
}

/// The adapter: a Notifier outside, calls to the SDK inside.
struct SmsNotifier {
    client: SmsGatewayClient,
    sender: String,
}

impl Notifier for SmsNotifier {
    fn send(&mut self, phone: &str, text: &str) -> Result<String, NotifierError> {
        let digits: String = phone.chars().filter(char::is_ascii_digit).collect();
        let payload = HashMap::from([
            ("msisdn", digits),
            ("body", text.to_string()),
            ("sender", self.sender.clone()),
        ]);
        let reason = match self.client.dispatch(&payload) {
            Ok(message_id) => return Ok(message_id),
            Err(4011) => "invalid number".to_string(),
            Err(code) => format!("code {code}"),
        };
        Err(NotifierError(format!("cannot send to {phone}: {reason}")))
    }
}

fn remind(notifier: &mut impl Notifier, phone: &str, time: &str) {
    match notifier.send(phone, &format!("Visit at {time}.")) {
        Ok(receipt) => println!("sent to {phone}: {receipt}"),
        Err(error) => println!("failed: {error}"),
    }
}

fn main() {
    let client = SmsGatewayClient::default();
    let mut notifier = SmsNotifier {
        client,
        sender: "CLINIC".to_string(),
    };
    remind(&mut notifier, "+91 98765 43210", "10:30");
    remind(&mut notifier, "+91 98765", "11:00");
}

In Rust: The SDK answers with Result<String, u32>, the Rust way to say "an id or a status code", and a match turns Err(4011) into the application's NotifierError.

Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main

TypeScript 6.0 · main.ts

class NotifierError extends Error {}

interface Notifier {
  send(phone: string, text: string): string;
}

type Payload = Record<"msisdn" | "body" | "sender", string>;

/** The SDK, which cannot change: a dictionary in, a status code out. */
class SmsGatewayClient {
  #accepted = 0;

  dispatch(payload: Payload): [code: number, messageId: string] {
    const { msisdn } = payload;
    console.log(`  SDK got ${msisdn}`);
    if (!/^\d{12}$/.test(msisdn)) return [4011, ""];
    this.#accepted += 1;
    return [0, `GW-${String(this.#accepted).padStart(6, "0")}`]; // code 0: accepted
  }
}

/** The adapter: a Notifier outside, calls to the SDK inside. */
class SmsNotifier implements Notifier {
  readonly #client: SmsGatewayClient;
  readonly #sender: string;

  constructor(client: SmsGatewayClient, sender: string) {
    this.#client = client;
    this.#sender = sender;
  }

  send(phone: string, text: string): string {
    const digits = phone.replace(/\D/g, "");
    const [code, messageId] = this.#client.dispatch({
      msisdn: digits,
      body: text,
      sender: this.#sender,
    });
    if (code !== 0) {
      const reason = code === 4011 ? "invalid number" : `code ${code}`;
      throw new NotifierError(`cannot send to ${phone}: ${reason}`);
    }
    return messageId;
  }
}

function remind(notifier: Notifier, phone: string, time: string): void {
  try {
    const receipt = notifier.send(phone, `Visit at ${time}.`);
    console.log(`sent to ${phone}: ${receipt}`);
  } catch (error) {
    if (!(error instanceof NotifierError)) throw error;
    console.log(`failed: ${error.message}`);
  }
}

const notifier = new SmsNotifier(new SmsGatewayClient(), "CLINIC");
remind(notifier, "+91 98765 43210", "10:30");
remind(notifier, "+91 98765", "11:00");

In TypeScript: The compiler checks the adapter against Notifier where it says implements, and nothing of the interface is left when the program runs.

Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts

Go 1.27 · main.go

package main

import (
	"fmt"
	"strings"
	"unicode"
)

// NotifierError says, in the application's words, why a message could not be sent.
type NotifierError struct{ Phone, Reason string }

func (e *NotifierError) Error() string { return "cannot send to " + e.Phone + ": " + e.Reason }

// Notifier is the interface the application owns.
type Notifier interface {
	Send(phone, text string) (string, error)
}

// SmsGatewayClient is the SDK, which cannot be changed: a dictionary in, a status code out.
type SmsGatewayClient struct{ accepted int }

// Dispatch returns the status code, 0 when the message was accepted, and the message's id.
func (c *SmsGatewayClient) Dispatch(payload map[string]string) (code int, messageID string) {
	msisdn := payload["msisdn"]
	fmt.Println("  SDK got", msisdn)
	if len(msisdn) != 12 || strings.Trim(msisdn, "0123456789") != "" {
		return 4011, ""
	}
	c.accepted++
	return 0, fmt.Sprintf("GW-%06d", c.accepted)
}

// SmsNotifier is the adapter: a Notifier outside, calls to the SDK inside.
type SmsNotifier struct {
	client *SmsGatewayClient
	sender string
}

func (n SmsNotifier) Send(phone, text string) (string, error) {
	digits := strings.Map(func(r rune) rune {
		if unicode.IsDigit(r) {
			return r
		}
		return -1
	}, phone)
	payload := map[string]string{"msisdn": digits, "body": text, "sender": n.sender}
	code, messageID := n.client.Dispatch(payload)
	switch code {
	case 0:
		return messageID, nil
	case 4011:
		return "", &NotifierError{Phone: phone, Reason: "invalid number"}
	default:
		return "", &NotifierError{Phone: phone, Reason: fmt.Sprintf("code %d", code)}
	}
}

func remind(notifier Notifier, phone, time string) {
	receipt, err := notifier.Send(phone, "Visit at "+time+".")
	if err != nil {
		fmt.Println("failed:", err)
		return
	}
	fmt.Printf("sent to %s: %s\n", phone, receipt)
}

func main() {
	notifier := SmsNotifier{client: &SmsGatewayClient{}, sender: "CLINIC"}
	remind(notifier, "+91 98765 43210", "10:30")
	remind(notifier, "+91 98765", "11:00")
}

In Go: Dispatch returns two values, the code and the id, and Send returns the id and an error value. NotifierError is a type of its own, so a caller can look for it with errors.As.

Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go

Swift 6.4 · main.swift

struct NotifierError: Error, CustomStringConvertible {
  let description: String
}

protocol Notifier {
  func send(to phone: String, text: String) throws(NotifierError) -> String
}

/// The SDK, which cannot change: a dictionary in, a status code out.
final class SmsGatewayClient {
  private var accepted = 0

  func dispatch(_ payload: [String: String]) -> (code: Int, messageID: String) {
    let msisdn = payload["msisdn", default: ""]
    print("  SDK got \(msisdn)")
    guard msisdn.count == 12, msisdn.allSatisfy(\.isASCIIDigit) else { return (4011, "") }
    accepted += 1
    let number = String(accepted)
    return (0, "GW-" + String(repeating: "0", count: 6 - number.count) + number)  // 0: accepted
  }
}

extension Character {
  var isASCIIDigit: Bool { isASCII && isNumber }
}

/// The adapter: a Notifier outside, calls to the SDK inside.
struct SmsNotifier: Notifier {
  let client: SmsGatewayClient
  let sender: String

  func send(to phone: String, text: String) throws(NotifierError) -> String {
    let digits = String(phone.filter(\.isASCIIDigit))
    let reply = client.dispatch(["msisdn": digits, "body": text, "sender": sender])
    guard reply.code == 0 else {
      let reason = reply.code == 4011 ? "invalid number" : "code \(reply.code)"
      throw NotifierError(description: "cannot send to \(phone): \(reason)")
    }
    return reply.messageID
  }
}

func remind(_ notifier: any Notifier, phone: String, at time: String) {
  do {
    let receipt = try notifier.send(to: phone, text: "Visit at \(time).")
    print("sent to \(phone): \(receipt)")
  } catch {
    print("failed: \(error)")
  }
}

let notifier = SmsNotifier(client: SmsGatewayClient(), sender: "CLINIC")
remind(notifier, phone: "+91 98765 43210", at: "10:30")
remind(notifier, phone: "+91 98765", at: "11:00")

In Swift: send(to:text:) is declared throws(NotifierError), so every caller knows the one error it can throw. Without Foundation the zeros of the message's id are padded by hand.

Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main

Ruby 4.0 · main.rb

# frozen_string_literal: true

# Raised when a message could not be sent.
class NotifierError < StandardError; end

# The SDK, which cannot change: a dictionary in, a status code out.
class SmsGatewayClient
  def initialize
    @accepted = 0
  end

  def dispatch(payload)
    msisdn = payload[:msisdn]
    puts "  SDK got #{msisdn}"
    return [4011, ''] unless msisdn.match?(/\A\d{12}\z/)

    @accepted += 1
    [0, format('GW-%06d', @accepted)] # code 0: accepted
  end
end

# The adapter: a Notifier outside, calls to the SDK inside. Ruby has no interface
# declarations: a Notifier is anything with deliver(phone, text).
class SmsNotifier
  def initialize(client, sender)
    @client = client
    @sender = sender
  end

  def deliver(phone, text)
    payload = { msisdn: phone.delete('^0-9'), body: text, sender: @sender }
    code, message_id = @client.dispatch(payload)
    return message_id if code.zero?

    reason = code == 4011 ? 'invalid number' : "code #{code}"
    raise NotifierError, "cannot send to #{phone}: #{reason}"
  end
end

def remind(notifier, phone, time)
  receipt = notifier.deliver(phone, "Visit at #{time}.")
  puts "sent to #{phone}: #{receipt}"
rescue NotifierError => e
  puts "failed: #{e.message}"
end

notifier = SmsNotifier.new(SmsGatewayClient.new, 'CLINIC')
remind(notifier, '+91 98765 43210', '10:30')
remind(notifier, '+91 98765', '11:00')

In Ruby: Every Ruby object already has a send method, which calls another method by its name, so the interface's method is called deliver here. phone.delete('^0-9') keeps only the digits.

Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb

Kotlin 2.4 · Main.kt

class NotifierException(
    message: String,
) : Exception(message)

interface Notifier {
    fun send(
        phone: String,
        text: String,
    ): String
}

/** The SDK, which cannot change: a dictionary in, a status code out. */
class SmsGatewayClient {
    private var accepted = 0

    fun dispatch(payload: Map<String, String>): Pair<Int, String> {
        val msisdn = payload.getValue("msisdn")
        println("  SDK got $msisdn")
        if (msisdn.length != 12 || !msisdn.all { it in '0'..'9' }) return 4011 to ""
        accepted++
        return 0 to "GW-" + accepted.toString().padStart(6, '0') // code 0: accepted
    }
}

/** The adapter: a Notifier outside, calls to the SDK inside. */
class SmsNotifier(
    private val client: SmsGatewayClient,
    private val sender: String,
) : Notifier {
    override fun send(
        phone: String,
        text: String,
    ): String {
        val digits = phone.filter { it in '0'..'9' }
        val payload = mapOf("msisdn" to digits, "body" to text, "sender" to sender)
        val (code, messageId) = client.dispatch(payload)
        if (code != 0) {
            val reason = if (code == 4011) "invalid number" else "code $code"
            throw NotifierException("cannot send to $phone: $reason")
        }
        return messageId
    }
}

fun remind(
    notifier: Notifier,
    phone: String,
    time: String,
) {
    try {
        val receipt = notifier.send(phone, "Visit at $time.")
        println("sent to $phone: $receipt")
    } catch (error: NotifierException) {
        println("failed: ${error.message}")
    }
}

fun main() {
    val notifier = SmsNotifier(SmsGatewayClient(), "CLINIC")
    remind(notifier, "+91 98765 43210", "10:30")
    remind(notifier, "+91 98765", "11:00")
}

In Kotlin: Kotlin has no checked exceptions, so nothing makes remind() catch NotifierException. The SDK returns a Pair, and val (code, messageId) takes it apart.

Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar

Dart 3.13 · main.dart

final class NotifierException implements Exception {
  NotifierException(this.message);

  final String message;

  @override
  String toString() => message;
}

abstract interface class Notifier {
  String send(String phone, String text);
}

/// The SDK, which cannot change: a dictionary in, a status code out.
final class SmsGatewayClient {
  int _accepted = 0;

  (int, String) dispatch(Map<String, String> payload) {
    final msisdn = payload['msisdn']!;
    print('  SDK got $msisdn');
    if (!RegExp(r'^\d{12}$').hasMatch(msisdn)) return (4011, '');
    _accepted++;
    return (
      0,
      'GW-${_accepted.toString().padLeft(6, '0')}',
    ); // code 0: accepted
  }
}

/// The adapter: a Notifier outside, calls to the SDK inside.
final class SmsNotifier implements Notifier {
  SmsNotifier(this._client, this._sender);

  final SmsGatewayClient _client;
  final String _sender;

  @override
  String send(String phone, String text) {
    final digits = phone.replaceAll(RegExp(r'\D'), '');
    final payload = {'msisdn': digits, 'body': text, 'sender': _sender};
    final (code, messageId) = _client.dispatch(payload);
    if (code != 0) {
      final reason = code == 4011 ? 'invalid number' : 'code $code';
      throw NotifierException('cannot send to $phone: $reason');
    }
    return messageId;
  }
}

void remind(Notifier notifier, String phone, String time) {
  try {
    final receipt = notifier.send(phone, 'Visit at $time.');
    print('sent to $phone: $receipt');
  } on NotifierException catch (error) {
    print('failed: $error');
  }
}

void main() {
  final notifier = SmsNotifier(SmsGatewayClient(), 'CLINIC');
  remind(notifier, '+91 98765 43210', '10:30');
  remind(notifier, '+91 98765', '11:00');
}

In Dart: Notifier is an abstract interface class, which code in other libraries may implement but not extend. The SDK returns a record, (int, String), which a pattern takes apart.

Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart

What every language printed

  SDK got 919876543210
sent to +91 98765 43210: GW-000001
  SDK got 9198765
failed: cannot send to +91 98765: invalid number

SmsNotifier.send() does the four jobs of an adapter, and each one is a line or two:

  1. It reshapes the call. Two arguments become one dictionary with the SDK’s key names.
  2. It converts the data. The number as people write it, “+91 98765 43210”, becomes the SDK’s 919876543210.
  3. It supplies what the SDK needs and the interface leaves out. The sender name is fixed once, when the adapter is created, so callers never pass it.
  4. It maps the reply. Code 0 becomes the message’s id. Any other code becomes a NotifierError with a reason in the application’s words, which remind() caught for the short number without ever reading code 4011.

The indented lines were printed by the SDK during each call. Both times it received its own dictionary, with the number already reduced to digits, and only one place in the program builds that dictionary.

An adapter that hands back the SDK's reply

A send() that returns the SDK’s reply object, return reply, looks shorter, but now every caller must test reply.code, and the SDK’s status codes have leaked into forty places again. Return what the target interface promises, the message’s id, and turn every failure into the interface’s own error, as SmsNotifier does.

An adapter translates; it does not decide. Rules such as retries, message templates and rate limits belong in the application, or in a wrapper around the adapter, so that the adapter for a second provider does not have to copy them.

Key idea

The adapter is the only code that knows both interfaces: it reshapes each call, converts the data, adds what the SDK needs and maps every reply, so the rest of the program depends on its own interface alone.

Checkpoint

Check: What an adapter translates, and what it leaves alone · The client, the target, the adapter and the adaptee

  1. Question 1 of 2 In the program of the 14 tabs, which code ever builds a dictionary with the key msisdn?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Only SmsNotifier.send(), the adapter

    Reshaping the call is the adapter's first job. send() turns the phone number and the text into the SDK's dictionary, and nothing else in the program knows that msisdn, a 12-digit number or code 4011 exist.

  2. Question 2 of 2 The provider's next SDK version renames dispatch() to submit(). Which code has to change?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Only SmsNotifier, the adapter

    The client depends on the target interface and the adapter on the adaptee. A change of the adaptee's names, formats or codes therefore lands in one class, which is the reason to write an adapter at all.

A fake behind the same interface keeps the SDK out of tests

Because the application depends only on Notifier, a test can hand it a fake: A working stand-in for a real dependency, used in tests: for example a notifier that records the messages it is asked to send instead of sending them.: a few lines that implement the same interface and record what they are asked to send. The service below sends sign-up codes, and its tests give it a RecordingNotifier instead of the adapter:

Testing a service with a fake Notifier

fake/test_signup.py

"""Tests of SignupService with a fake Notifier: no provider, no network, and the SDK is never imported."""

import sys

from notifier import Notifier, NotifierError
from signup import SignupService


class RecordingNotifier(Notifier):
    """A fake: it keeps every message, and fails for the numbers it is told to fail."""

    def __init__(self, failing: frozenset[str] = frozenset()) -> None:
        self.messages: list[tuple[str, str]] = []
        self._failing = failing

    def send(self, phone: str, text: str) -> str:
        if phone in self._failing:
            raise NotifierError(f"cannot send to {phone}")
        self.messages.append((phone, text))
        return f"fake-{len(self.messages)}"


def test_sends_the_code():
    fake = RecordingNotifier()
    service = SignupService(fake, make_code=lambda: "482913")
    assert service.start("+91 98765 43210")
    assert fake.messages == [("+91 98765 43210", "Your sign-up code is 482913")]


def test_failed_send_keeps_nothing_pending():
    fake = RecordingNotifier(failing=frozenset({"+91 90000 00000"}))
    service = SignupService(fake, make_code=lambda: "111111")
    assert not service.start("+91 90000 00000")
    assert service.pending == {}


for name, test in list(globals().items()):
    if name.startswith("test_"):
        test()
        print("passed:", name)
print("SDK module loaded:", "sms_sdk" in sys.modules)

Output

passed: test_sends_the_code
passed: test_failed_send_keeps_nothing_pending
SDK module loaded: False

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 test_signup.py

fake/signup.py

"""A service that sends texts. It is given a Notifier when it is created; it never makes one itself."""

from collections.abc import Callable

from notifier import Notifier, NotifierError


class SignupService:
    def __init__(self, notifier: Notifier, make_code: Callable[[], str]) -> None:
        self._notifier = notifier
        self._make_code = make_code
        self.pending: dict[str, str] = {}

    def start(self, phone: str) -> bool:
        """Send a sign-up code to phone; remember it only if the text went out."""
        code = self._make_code()
        try:
            self._notifier.send(phone, f"Your sign-up code is {code}")
        except NotifierError:
            return False
        self.pending[phone] = code
        return True

fake/notifier.py

"""The interface the application owns: what any notifier must offer."""

from abc import ABC, abstractmethod


class NotifierError(Exception):
    """A message could not be sent; the text says why."""


class Notifier(ABC):
    @abstractmethod
    def send(self, phone: str, text: str) -> str:
        """Send text to phone (written as "+91 98765 43210") and return the message's id."""

fake/sms_sdk.py

"""A stand-in for a text-message provider's client library, invented for this lesson.

Its interface is the one you cannot change: one dispatch() call takes a dictionary, the phone number is called
"msisdn" and must be 12 digits with the country code and no plus sign, and failures come back as status codes
instead of exceptions.
"""


class SmsGatewayClient:
    def __init__(self) -> None:
        self.accepted = 0

    def dispatch(self, payload: dict[str, str]) -> tuple[int, str]:
        """Return the status code, 0 when the message was accepted, and the message's id."""
        msisdn = payload["msisdn"]
        print(f"  SDK got {msisdn}")
        if not (msisdn.isdigit() and len(msisdn) == 12):
            return 4011, ""
        self.accepted += 1
        return 0, f"GW-{self.accepted:06d}"

Both tests passed, and the last line shows that the SDK’s module, which sits in the same folder, was never loaded: the tests need no provider account, no network and no test phone numbers. Press Edit, give the second test’s fake an empty set of failing numbers, frozenset(), and run it again: the second test now fails, because the service sends the code and keeps it pending. The adapter still needs tests of its own, but only a few, against the provider’s test environment or recorded replies, because it is the one class that knows the SDK.

Scaled up to a whole application, this is what Alistair Cockburn named ports and adapters: An architecture, also called hexagonal architecture, in which the application defines an interface, a port, for each conversation with the outside world, and each technology plugs in through an adapter., also called hexagonal architecture. The application defines a port, an interface such as Notifier, for each conversation with the outside world, and each technology that plugs into a port gets its own adapter. His article describes the idea as a particular use of the Adapter pattern.

Object adapter, class adapter or a conversion

There are three ways to make one interface fit another, and the choice depends on what your language offers and on what the adapter’s clients should be able to reach.

  • An object adapter: An adapter that keeps the adaptee in a field and implements the target interface by forwarding each call to it, so it can wrap any instance, including one created elsewhere. holds the adaptee in a field and forwards calls to it, as SmsNotifier does. It works with any instance of the adaptee’s class, subclasses included.
  • A class adapter: An adapter that inherits from the adaptee's class and implements the target interface, so one object is both, and every public method of the adaptee stays reachable. inherits from the adaptee’s class and implements the target interface, so one object is both. It needs multiple inheritance (C++, Python) or a class plus an interface (Java).
  • A conversion does the adapting without a new class, when the target is a function type or a value type.

The program below builds the first two over the same SDK, with the classes of the block above split into modules:

An object adapter and a class adapter, side by side

kinds/two_adapters.py

"""The same adapter two ways: holding an SDK client (object adapter) and inheriting from the SDK class (class adapter)."""

from notifier import Notifier, NotifierError
from sms_adapter import SmsNotifier
from sms_sdk import SmsGatewayClient


class SmsNotifierByInheritance(SmsGatewayClient, Notifier):
    """A class adapter: it is an SmsGatewayClient and a Notifier at once."""

    def __init__(self, sender: str) -> None:
        super().__init__()
        self._sender = sender

    def send(self, phone: str, text: str) -> str:
        digits = "".join(ch for ch in phone if ch.isdigit())
        code, message_id = self.dispatch({"msisdn": digits, "body": text, "sender": self._sender})
        if code != 0:
            raise NotifierError(f"cannot send to {phone}: code {code}")
        return message_id


class CountingGatewayClient(SmsGatewayClient):
    """A client that was set up elsewhere: this one counts the requests it is given."""

    def __init__(self) -> None:
        super().__init__()
        self.requests = 0

    def dispatch(self, payload: dict[str, str]) -> tuple[int, str]:
        self.requests += 1
        return super().dispatch(payload)


counting = CountingGatewayClient()
by_object = SmsNotifier(counting, sender="CLINIC")
by_class = SmsNotifierByInheritance(sender="CLINIC")

for name, adapter in [("object adapter", by_object), ("class adapter", by_class)]:
    print(f"{name}: a Notifier: {isinstance(adapter, Notifier)}, has dispatch(): {hasattr(adapter, 'dispatch')}")
    print("  sent:", adapter.send("+91 98765 43210", "Hello"))
print("requests the counting client saw:", counting.requests)

# Code holding the class adapter can step around the interface and call the SDK itself.
print("around the interface:", by_class.dispatch({"msisdn": "1", "body": "", "sender": "?"}))

Output

object adapter: a Notifier: True, has dispatch(): False
  SDK got 919876543210
  sent: GW-000001
class adapter: a Notifier: True, has dispatch(): True
  SDK got 919876543210
  sent: GW-000001
requests the counting client saw: 1
  SDK got 1
around the interface: (4011, '')

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 two_adapters.py

kinds/sms_adapter.py

"""The adapter: a Notifier on the outside, calls to SmsGatewayClient on the inside."""

from notifier import Notifier, NotifierError
from sms_sdk import SmsGatewayClient


class SmsNotifier(Notifier):
    def __init__(self, client: SmsGatewayClient, sender: str) -> None:
        self._client = client
        self._sender = sender

    def send(self, phone: str, text: str) -> str:
        digits = "".join(ch for ch in phone if ch.isdigit())
        payload = {"msisdn": digits, "body": text, "sender": self._sender}
        code, message_id = self._client.dispatch(payload)
        if code != 0:
            reason = "invalid number" if code == 4011 else f"code {code}"
            raise NotifierError(f"cannot send to {phone}: {reason}")
        return message_id

kinds/sms_sdk.py

"""A stand-in for a text-message provider's client library, invented for this lesson.

Its interface is the one you cannot change: one dispatch() call takes a dictionary, the phone number is called
"msisdn" and must be 12 digits with the country code and no plus sign, and failures come back as status codes
instead of exceptions.
"""


class SmsGatewayClient:
    def __init__(self) -> None:
        self.accepted = 0

    def dispatch(self, payload: dict[str, str]) -> tuple[int, str]:
        """Return the status code, 0 when the message was accepted, and the message's id."""
        msisdn = payload["msisdn"]
        print(f"  SDK got {msisdn}")
        if not (msisdn.isdigit() and len(msisdn) == 12):
            return 4011, ""
        self.accepted += 1
        return 0, f"GW-{self.accepted:06d}"

kinds/notifier.py

"""The interface the application owns: what any notifier must offer."""

from abc import ABC, abstractmethod


class NotifierError(Exception):
    """A message could not be sent; the text says why."""


class Notifier(ABC):
    @abstractmethod
    def send(self, phone: str, text: str) -> str:
        """Send text to phone (written as "+91 98765 43210") and return the message's id."""

The object adapter wrapped a client that was set up elsewhere, a subclass that counts its requests, without any new code: it takes whatever instance it is given, and the count shows that its one request went through that client. The class adapter is an SmsGatewayClient itself, so it has dispatch(), and the last line called the SDK directly, around the interface the adapter exists to enforce. To count its requests you would need yet another subclass.

Rust shows the conversion style. Implementing the From trait for a type gives you the matching Into through a blanket implementation in the standard library, so a function can accept anything that converts into the type it wants:

A sensor's reading converted at the boundary, in Rust Rust · native/from_into.rs
/// Tenths of a degree Fahrenheit, as a sensor library reports a reading.
struct SensorTenthsF(i32);

/// The unit this program works in.
struct Celsius(f64);

impl From<SensorTenthsF> for Celsius {
    fn from(reading: SensorTenthsF) -> Self {
        Celsius((f64::from(reading.0) / 10.0 - 32.0) * 5.0 / 9.0)
    }
}

/// Accepts anything that converts into Celsius, so callers never convert by hand.
fn report(reading: impl Into<Celsius>) -> String {
    let c: Celsius = reading.into();
    format!("{:.1} °C", c.0)
}

fn main() {
    println!("{}", report(SensorTenthsF(986))); // 98.6 °F
    println!("{}", report(SensorTenthsF(-400))); // -40.0 °F
    println!("{}", report(Celsius(21.5))); // every type converts into itself
}

Output

37.0 °C
-40.0 °C
21.5 °C

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O from_into.rs && ./from_into

report() took the sensor’s tenths of a degree Fahrenheit and a ready-made Celsius alike, because every type converts into itself. The standard library’s documentation of From asks for conversions that cannot fail and points to TryFrom for any that can. When the adaptee is a type from another crate, Rust’s usual adapter is a newtype: A wrapper type with a single field, such as Rust's struct Celsius(f64), that gives an existing type a name and methods of its own at no run-time cost. such as Celsius, with your own methods on it, which the Rust API guidelines call a zero-cost way to wrap an existing type.

Three ways to adapt an interface
Criterion Object adapterClass adapterConversion
How it reaches the adaptee Holds an instance and forwards each callInherits from the adaptee's classTurns a function or a value into the target type
What its clients can call Only the target interfaceThe target interface and every public method of the adapteeWhatever the target type offers
Adapts an object made elsewhere Yes, any instance, subclasses includedNo: it is its own instance of one classYes: the value is converted where it is passed
Needs from the language Nothing specialMultiple inheritance, or a class plus an interfaceFunction types, conversion traits or higher-order functions
When to choose The default: SDK clients, legacy modules, anything you are handedRarely: when you must override the adaptee's own methodsOne function or one value at a boundary

Start with the object adapter

Reach for an object adapter first. Choose a class adapter only when you must override one of the adaptee’s own methods, and a conversion when the boundary is a single function or value.

Checkpoint

Check: Fakes behind the target interface · Object adapter, class adapter or a conversion

  1. Question 1 of 2 RecordingNotifier implements Notifier and records each message instead of sending it. Is it an adapter in the sense of this lesson?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No. It is another implementation of the target interface, and it wraps no adaptee

    Both SmsNotifier and RecordingNotifier implement Notifier, which is why the service accepts either. Only SmsNotifier holds an adaptee and translates for it; the fake is a stand-in that records what it was asked to do.

  2. Question 2 of 2 Why could the last line of two_adapters.py call dispatch() on the class adapter?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The class adapter inherits from SmsGatewayClient, so every public method of the SDK is part of it

    A class adapter is the adaptee and the target at once, so its clients can call the adaptee's methods. An object adapter only holds the adaptee in a field, which is why its hasattr check printed False.

Standard libraries ship adapters under other names

Standard libraries are full of adapters, and their documentation seldom uses the word.

Go’s net/http. The package documentation says it outright: the HandlerFunc type is an adapter that lets an ordinary function serve as an HTTP handler.

A function adapted to Go's Handler interface Go · native/handler.go
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
)

// greet is an ordinary function. On its own it is not an http.Handler: it has no ServeHTTP method.
func greet(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "hello, %s", r.URL.Query().Get("name"))
}

func main() {
	var h http.Handler = http.HandlerFunc(greet) // a type conversion is the whole adapter

	rec := httptest.NewRecorder() // a request and a response in memory: no network
	h.ServeHTTP(rec, httptest.NewRequest("GET", "/greet?name=Asha", nil))
	fmt.Println(rec.Code, rec.Body.String())
}

Output

200 hello, Asha

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run handler.go

greet has the right parameters but no ServeHTTP method, so on its own it is not an http.Handler. http.HandlerFunc(greet) is a type conversion, and the ServeHTTP method of HandlerFunc calls the function. The request and the response stayed in memory through the httptest package, so no network was involved.

Java’s InputStreamReader and Arrays.asList. The JDK documents InputStreamReader as a bridge from byte streams to character streams, but in the classic catalogue’s terms it is an adapter: it takes an InputStream, which reads bytes, and offers the Reader interface, which reads characters, decoding with a character set. Arrays.asList returns a fixed-size list backed by an array, so the array gets the List interface.

Bytes read as characters, and an array used as a List, in Java Java · native/Streams.java
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;

/** Two adapters of the JDK: bytes read as characters, and an array used as a List. */
public class Streams {
    public static void main(String[] args) throws IOException {
        byte[] bytes = "नमस्ते".getBytes(StandardCharsets.UTF_8);
        Reader reader = new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8);
        int chars = 0;
        while (reader.read() != -1) chars++;
        System.out.println(bytes.length + " bytes in, " + chars + " chars out");

        String[] seats = {"A1", "A2", "A3"};
        List<String> list = Arrays.asList(seats);
        list.set(0, "B1");
        System.out.println("array after list.set: " + Arrays.toString(seats));
        try {
            list.add("A4");
        } catch (UnsupportedOperationException e) {
            System.out.println("list.add: UnsupportedOperationException");
        }
    }
}

Output

18 bytes in, 6 chars out
array after list.set: [B1, A2, A3]
list.add: UnsupportedOperationException

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Streams.java

The Hindi greeting is six Java characters, and it took 18 bytes in UTF-8, three each; the reader turned the bytes back into 6 characters. set() on the list changed the array underneath, because the list is only a view of it. add() failed: an array cannot grow, so the adapter cannot offer that part of List. An adapter can promise only what its adaptee can do.

Python and Node.js. Python’s io.TextIOWrapper is a text stream on top of a buffered binary stream, the same job that InputStreamReader does in Java. Node.js has util.promisify, which takes a function in the error-first callback style and returns a version that returns promises, the same move that the exercise below asks you to make by hand.

One interface on three platforms: Indian digit grouping

Indian digit grouping

Amounts in India are written with the last three digits grouped first and then pairs: 12,34,567 reads as 12 lakh, 34 thousand and 567, and 1,00,00,000 is one crore. Many platforms group digits only in threes, so an app that shows rupee amounts on several platforms needs one interface, IndianAmountFormatter, with one implementation per platform. Its format(paise) takes a whole number of paise, never a floating-point number of rupees, and returns text such as ₹12,34,567.50.

All three implementations below read the same nine cases from one file. Python first:

Python: the digits grouped by hand

india/amounts.py

"""IndianAmountFormatter in Python: the digits are grouped by hand, so no locale setting is involved."""

from abc import ABC, abstractmethod


class IndianAmountFormatter(ABC):
    @abstractmethod
    def format(self, paise: int) -> str:
        """Return the amount as rupees with Indian digit grouping, such as "₹12,34,567.50"."""


class GroupingFormatter(IndianAmountFormatter):
    def format(self, paise: int) -> str:
        sign = "-" if paise < 0 else ""
        rupees, rest = divmod(abs(paise), 100)
        digits = str(rupees)
        head, groups = digits[:-3], [digits[-3:]]  # the last three digits, then pairs
        while head:
            groups.insert(0, head[-2:])
            head = head[:-2]
        return f"{sign}₹{','.join(groups)}.{rest:02d}"


print("Python's own format(1234567.5, ','):", format(1234567.5, ","))

formatter: IndianAmountFormatter = GroupingFormatter()
passed = 0
cases = [line.split() for line in open("cases.txt", encoding="utf-8") if not line.startswith("#")]
for paise, expected in cases:
    got = formatter.format(int(paise))
    passed += got == expected
    print(f"{paise:>12} -> {got}")
print(f"{passed} of {len(cases)} cases match")

Output

Python's own format(1234567.5, ','): 1,234,567.5
          50 -> ₹0.50
       99999 -> ₹999.99
      100000 -> ₹1,000.00
     1234567 -> ₹12,345.67
    10000000 -> ₹1,00,000.00
   123456750 -> ₹12,34,567.50
  1000000000 -> ₹1,00,00,000.00
 12345678900 -> ₹12,34,56,789.00
     -123450 -> -₹1,234.50
9 of 9 cases match

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 amounts.py

india/cases.txt

# amount in paise, then the text every IndianAmountFormatter must return
50 ₹0.50
99999 ₹999.99
100000 ₹1,000.00
1234567 ₹12,345.67
10000000 ₹1,00,000.00
123456750 ₹12,34,567.50
1000000000 ₹1,00,00,000.00
12345678900 ₹12,34,56,789.00
-123450 -₹1,234.50

Python’s own format() with a comma groups in threes, as the first line shows, so the Python version groups the digits itself. It could ask the locale module instead, but that works only where the operating system provides an Indian English locale, and setlocale() changes a setting of the whole process, which its documentation warns is not thread-safe on most systems.

Node.js already groups digits this way. Its Intl.NumberFormat for the locale en-IN printed 12,34,567.5, so the Node.js version is a true adapter: it turns paise into rupees and lets Intl do the rest.

Node.js: an adapter over Intl.NumberFormat JavaScript · india/amounts.mjs
// IndianAmountFormatter in Node.js: an adapter over Intl.NumberFormat, which already groups digits this way for en-IN.
import { readFileSync } from 'node:fs';

/** @typedef {{ format(paise: number): string }} IndianAmountFormatter */

class IntlAmountFormatter {
  #intl = new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR' });

  /** @param {number} paise a whole number of paise */
  format(paise) {
    return this.#intl.format(paise / 100);
  }
}

console.log("Intl's own format for en-IN:", new Intl.NumberFormat('en-IN').format(1234567.5));

/** @type {IndianAmountFormatter} */
const formatter = new IntlAmountFormatter();
const cases = readFileSync('cases.txt', 'utf8')
  .split('\n')
  .filter((line) => line && !line.startsWith('#'))
  .map((line) => line.split(' '));
let passed = 0;
for (const [paise, expected] of cases) {
  const got = formatter.format(Number(paise));
  if (got === expected) passed++;
  console.log(`${paise.padStart(12)} -> ${got}`);
}
console.log(`${passed} of ${cases.length} cases match`);

Output

Intl's own format for en-IN: 12,34,567.5
          50 -> ₹0.50
       99999 -> ₹999.99
      100000 -> ₹1,000.00
     1234567 -> ₹12,345.67
    10000000 -> ₹1,00,000.00
   123456750 -> ₹12,34,567.50
  1000000000 -> ₹1,00,00,000.00
 12345678900 -> ₹12,34,56,789.00
     -123450 -> -₹1,234.50
9 of 9 cases match

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node amounts.mjs

Java does not. Its NumberFormat for en-IN printed the amount in groups of three, and so did a DecimalFormat given the pattern #,##,##0.00. A DecimalFormat keeps a single grouping size, the number of digits after the last separator of its pattern, so the extra separator changes nothing, as the class’s documentation explains, and the Java version groups by hand too.

Java: the JDK's output, then the digits grouped by hand Java · india/Amounts.java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Locale;

/** IndianAmountFormatter in Java: the JDK's formatters group in threes, so the digits are grouped by hand. */
public class Amounts {
    interface IndianAmountFormatter {
        String format(long paise);
    }

    static final class GroupingFormatter implements IndianAmountFormatter {
        public String format(long paise) {
            String sign = paise < 0 ? "-" : "";
            long abs = Math.abs(paise);
            String digits = Long.toString(abs / 100);
            Deque<String> groups = new ArrayDeque<>();
            int end = digits.length();
            groups.addFirst(digits.substring(Math.max(0, end - 3))); // the last three digits, then pairs
            for (end -= 3; end > 0; end -= 2) {
                groups.addFirst(digits.substring(Math.max(0, end - 2), end));
            }
            return String.format(Locale.ROOT, "%s₹%s.%02d", sign, String.join(",", groups), abs % 100);
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("NumberFormat for en-IN: " + NumberFormat.getCurrencyInstance(Locale.of("en", "IN")).format(1234567.5));
        System.out.println("DecimalFormat(\"#,##,##0.00\"): " + new DecimalFormat("#,##,##0.00").format(1234567.5));

        IndianAmountFormatter formatter = new GroupingFormatter();
        int passed = 0;
        int total = 0;
        for (String line : Files.readAllLines(Path.of("cases.txt"), StandardCharsets.UTF_8)) {
            if (line.isEmpty() || line.startsWith("#")) continue;
            String[] parts = line.split(" ");
            String got = formatter.format(Long.parseLong(parts[0]));
            total++;
            if (got.equals(parts[1])) passed++;
            System.out.printf("%12s -> %s%n", parts[0], got);
        }
        System.out.println(passed + " of " + total + " cases match");
    }
}

Output

NumberFormat for en-IN: ₹1,234,567.50
DecimalFormat("#,##,##0.00"): 1,234,567.50
          50 -> ₹0.50
       99999 -> ₹999.99
      100000 -> ₹1,000.00
     1234567 -> ₹12,345.67
    10000000 -> ₹1,00,000.00
   123456750 -> ₹12,34,567.50
  1000000000 -> ₹1,00,00,000.00
 12345678900 -> ₹12,34,56,789.00
     -123450 -> -₹1,234.50
9 of 9 cases match

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Amounts.java

All three pass the same nine cases. Only one of them adapts anything; the other two exist because their platform’s formatter does something else. That is the point of owning the interface: callers cannot tell an adapter from a hand-written implementation, and when a platform gains the feature later, only its implementation changes, checked by the same file of cases.

Lakh Crore Converter (to Million, Billion) See how any amount reads in the Indian and the international systems.

Checkpoint

Check: Adapters inside standard libraries · One interface, an implementation per platform

  1. Question 1 of 2 Arrays.asList(seats).add("A4") threw UnsupportedOperationException. What does that say about adapters?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: An adapter can offer the target interface only as far as its adaptee can, and an array cannot grow

    The list is a fixed-size view of the array, which is why set() changed seats. Adapting an interface cannot add abilities the adaptee lacks, so an adapter refuses the operations it cannot honour.

  2. Question 2 of 2 Java's own formatters printed 1,234,567.50 for en-IN. What did owning IndianAmountFormatter let the Java version do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Group the digits by hand behind the same format(paise), passing the same cases as the Node.js adapter

    With the interface owned by the app, each platform gets the implementation it needs: an adapter where the platform already groups the Indian way, hand-written code where it does not, all held to one file of cases.

Practise: an adapter for a callback-style weather client

The legacy weather client of this exercise answers through callbacks, in degrees Fahrenheit, with numbers as strings. Write the adapter that gives the rest of the program a typed result instead; the sample tests run in your browser, in Python or in JavaScript:

Exercise · Easy · Python, JavaScript

Put a modern interface on a legacy weather client

A legacy weather client has an awkward interface: you call fetch_conditions(code, on_success, on_failure) in Python, or fetchConditions(code, onSuccess, onFailure) in JavaScript. It reports through callbacks, in degrees Fahrenheit, with every number as a string:

  • on success it calls on_success({"tempF": "98.6", "humidity": "54"});
  • on failure it calls on_failure(code, message) with a number: 404 when it has no station for the city, anything else when the service is down.

Write WeatherService, an adapter that gives the rest of the program a small interface instead:

  • current(city) asks the legacy client for the city code with surrounding spaces removed and in capitals, so " bom " asks for "BOM".
  • It returns a reading of the city code in capitals, the temperature in degrees Celsius rounded to one decimal place, and the humidity as a whole number. Celsius is (F − 32) × 5 / 9.
    • Python: current() returns a Reading (the dataclass in weather.py), with celsius a float and humidity an int. Round with round(value, 1).
    • JavaScript: current() returns a Promise of { city, celsius, humidity }, with numbers, not strings. Round with Math.round(value * 10) / 10.
  • Errors become the adapter's own: code 404 raises (Python) or rejects with (JavaScript) UnknownCity / UnknownCityError; any other code gives WeatherUnavailable / WeatherUnavailableError, whose message contains the code.
  • Nothing of the legacy client leaks out: no dictionaries, no strings for numbers, no callbacks.

In Python the legacy client calls one of the callbacks before fetch_conditions returns. In JavaScript it calls back later, so wait for it with a Promise. The sample tests import your code from weather.py or weather.mjs.

Python · Starter code · weather.py

"""An adapter that puts a small, typed interface on a legacy weather client."""

from dataclasses import dataclass


@dataclass(frozen=True)
class Reading:
    city: str  # the city code in capitals, such as "DEL"
    celsius: float  # rounded to one decimal place
    humidity: int  # per cent


class UnknownCity(Exception):
    """The legacy client has no station for the city (its code 404)."""


class WeatherUnavailable(Exception):
    """Any other failure of the legacy client; the message contains its code."""


class WeatherService:
    def __init__(self, legacy) -> None:
        self._legacy = legacy

    def current(self, city: str) -> Reading:
        # Replace this line with your code.
        raise NotImplementedError("current() is not written yet")
The sample tests · test_weather.py
from weather import Reading, UnknownCity, WeatherService, WeatherUnavailable


class FakeLegacy:
    """Behaves like the legacy client: callbacks, Fahrenheit, numbers as strings."""

    def __init__(self, stations=None, failure=None):
        self.stations = stations or {}
        self.failure = failure
        self.asked = []

    def fetch_conditions(self, code, on_success, on_failure):
        self.asked.append(code)
        if self.failure:
            on_failure(*self.failure)
        elif code in self.stations:
            on_success(dict(self.stations[code]))
        else:
            on_failure(404, "no such station")


def test_celsius():
    """converts the Fahrenheit string to degrees Celsius"""
    legacy = FakeLegacy({"DEL": {"tempF": "98.6", "humidity": "54"}})
    assert WeatherService(legacy).current("DEL") == Reading("DEL", 37.0, 54)


def test_rounding():
    """rounds to one decimal place, below zero too"""
    legacy = FakeLegacy({"BLR": {"tempF": "71", "humidity": "80"}, "LEH": {"tempF": "-4", "humidity": "31"}})
    service = WeatherService(legacy)
    assert service.current("BLR").celsius == 21.7
    assert service.current("LEH").celsius == -20.0


def test_city_code():
    """asks for the code without spaces and in capitals, and reports it that way"""
    legacy = FakeLegacy({"BOM": {"tempF": "86", "humidity": "70"}})
    reading = WeatherService(legacy).current(" bom ")
    assert legacy.asked == ["BOM"]
    assert reading.city == "BOM"


def test_no_legacy_types():
    """returns a Reading with a float and an int, never the legacy strings"""
    legacy = FakeLegacy({"MAA": {"tempF": "95", "humidity": "66"}})
    reading = WeatherService(legacy).current("MAA")
    assert type(reading) is Reading
    assert type(reading.celsius) is float
    assert type(reading.humidity) is int


def test_reading_is_a_value():
    """returns a reading that callers cannot change by accident"""
    reading = WeatherService(FakeLegacy({"DEL": {"tempF": "98.6", "humidity": "54"}})).current("DEL")
    try:
        reading.celsius = 0.0
    except AttributeError:
        return
    raise AssertionError("a Reading should be frozen")


def test_unknown_city():
    """turns code 404 into UnknownCity"""
    try:
        WeatherService(FakeLegacy()).current("XYZ")
    except UnknownCity:
        return
    raise AssertionError("expected UnknownCity")


def test_service_down():
    """turns any other code into WeatherUnavailable, naming the code"""
    try:
        WeatherService(FakeLegacy(failure=(503, "maintenance"))).current("DEL")
    except WeatherUnavailable as error:
        assert "503" in str(error)
        return
    raise AssertionError("expected WeatherUnavailable")

JavaScript · Starter code · weather.mjs

// An adapter that puts a small, promise-based interface on a legacy weather client.

export class UnknownCityError extends Error {}
export class WeatherUnavailableError extends Error {}

export class WeatherService {
  constructor(legacy) {
    this.legacy = legacy;
  }

  /** Resolves to { city, celsius, humidity }; rejects with UnknownCityError or WeatherUnavailableError. */
  current(city) {
    // Replace this line with your code.
    return Promise.reject(new Error('current() is not written yet'));
  }
}
The sample tests · weather.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { UnknownCityError, WeatherService, WeatherUnavailableError } from './weather.mjs';

/** Behaves like the legacy client: callbacks a moment later, Fahrenheit, numbers as strings. */
class FakeLegacy {
  constructor(stations = {}, failure = null) {
    this.stations = stations;
    this.failure = failure;
    this.asked = [];
  }

  fetchConditions(code, onSuccess, onFailure) {
    this.asked.push(code);
    setTimeout(() => {
      if (this.failure) onFailure(...this.failure);
      else if (code in this.stations) onSuccess({ ...this.stations[code] });
      else onFailure(404, 'no such station');
    }, 5);
  }
}

test('converts the Fahrenheit string to degrees Celsius', async () => {
  const legacy = new FakeLegacy({ DEL: { tempF: '98.6', humidity: '54' } });
  assert.deepEqual(await new WeatherService(legacy).current('DEL'), { city: 'DEL', celsius: 37, humidity: 54 });
});

test('rounds to one decimal place, below zero too', async () => {
  const service = new WeatherService(new FakeLegacy({ BLR: { tempF: '71', humidity: '80' }, LEH: { tempF: '-4', humidity: '31' } }));
  assert.equal((await service.current('BLR')).celsius, 21.7);
  assert.equal((await service.current('LEH')).celsius, -20);
});

test('asks for the code without spaces and in capitals, and reports it that way', async () => {
  const legacy = new FakeLegacy({ BOM: { tempF: '86', humidity: '70' } });
  const reading = await new WeatherService(legacy).current(' bom ');
  assert.deepEqual(legacy.asked, ['BOM']);
  assert.equal(reading.city, 'BOM');
});

test('returns numbers, never the legacy strings', async () => {
  const reading = await new WeatherService(new FakeLegacy({ MAA: { tempF: '95', humidity: '66' } })).current('MAA');
  assert.equal(typeof reading.celsius, 'number');
  assert.equal(typeof reading.humidity, 'number');
  assert.deepEqual(Object.keys(reading).sort(), ['celsius', 'city', 'humidity']);
});

test('rejects with UnknownCityError for code 404', async () => {
  await assert.rejects(new WeatherService(new FakeLegacy()).current('XYZ'), UnknownCityError);
});

test('rejects with WeatherUnavailableError for any other code, naming the code', async () => {
  const service = new WeatherService(new FakeLegacy({}, [503, 'maintenance']));
  await assert.rejects(service.current('DEL'), WeatherUnavailableError);
  await assert.rejects(service.current('DEL'), /503/);
});
A hint

The adapter's job is to turn the two callbacks into one result. In Python, write two small functions inside current() that store what they receive (the data, or the code and message) in a variable that current() can read after fetch_conditions returns, then convert or raise. In JavaScript, put the call inside new Promise((resolve, reject) => …), and let one callback call resolve with the converted reading and the other call reject with the right error.

The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Celsius to Fahrenheit Converter Check a few Fahrenheit and Celsius values by hand before you write the conversion.

Finally, a round of questions on clients, adapters, fakes and the libraries above. Weak spots come up first, and each correct answer raises the difficulty of the next:

Practice that adapts to you

Practise this lesson

17 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 17 In the clinic's program, which class is the adaptee?

    Choose one answer.

    Show the answer: Question 1 of 17

    Answer: SmsGatewayClient, the provider's library

    The four parts are the client (remind()), the target interface (Notifier), the adapter (SmsNotifier) and the adaptee (SmsGatewayClient). Only the adapter knows both the target and the adaptee.

  2. Question 2 of 17 A teammate proposes moving the Notifier interface into the provider's SDK package, so that every app of the company uses the same one. What does that cost?

    Choose one answer.

    Show the answer: Question 2 of 17

    Answer: The application would depend on the provider's package again, so a change of provider would reach every app

    The point of the target interface is that the application owns it. Placed inside the SDK, it ties the application to that provider again, and the adapter no longer separates anything.

  3. Question 3 of 17 Every dictionary the SDK received had sender=CLINIC. Where does that value come from?

    Choose one answer.

    Show the answer: Question 3 of 17

    Answer: The adapter, which was given the sender name once, when it was created

    An adapter often has to provide values the adaptee requires but the target interface never mentions, such as a sender name or an account key. It takes them once, when it is created, so no caller has to know them.

  4. Question 4 of 17 The SDK starts answering code 4290 when too many messages are sent. What does this lesson's adapter do with such a reply?

    Choose one answer.

    Show the answer: Question 4 of 17

    Answer: It raises a NotifierError, cannot send to the number: code 4290, and remind() prints failed

    Mapping the reply means turning every possible answer of the adaptee into the target's terms. Here code 0 becomes the id and any other code becomes a NotifierError, so a new code is reported as a failure with its number.

  5. Question 5 of 17 The clinic wants every failed reminder tried once more after a minute. Where should that rule go?

    Choose one answer.

    Show the answer: Question 5 of 17

    Answer: In the application, or in a wrapper around a Notifier, so that it works with any provider's adapter

    The lesson's rule is that an adapter translates and does not decide. Retries, templates and rate limits are policies of the application; written once around the Notifier interface, they apply to every adapter behind it.

  6. Question 6 of 17 test_signup.py runs its two tests with the fake Notifier, then checks whether the SDK's module was loaded. What does it print?

    What does this program print? Choose one answer.

    """Tests of SignupService with a fake Notifier: no provider, no network, and the SDK is never imported."""
    
    import sys
    
    from notifier import Notifier, NotifierError
    from signup import SignupService
    
    
    class RecordingNotifier(Notifier):
        """A fake: it keeps every message, and fails for the numbers it is told to fail."""
    
        def __init__(self, failing: frozenset[str] = frozenset()) -> None:
            self.messages: list[tuple[str, str]] = []
            self._failing = failing
    
        def send(self, phone: str, text: str) -> str:
            if phone in self._failing:
                raise NotifierError(f"cannot send to {phone}")
            self.messages.append((phone, text))
            return f"fake-{len(self.messages)}"
    
    
    def test_sends_the_code():
        fake = RecordingNotifier()
        service = SignupService(fake, make_code=lambda: "482913")
        assert service.start("+91 98765 43210")
        assert fake.messages == [("+91 98765 43210", "Your sign-up code is 482913")]
    
    
    def test_failed_send_keeps_nothing_pending():
        fake = RecordingNotifier(failing=frozenset({"+91 90000 00000"}))
        service = SignupService(fake, make_code=lambda: "111111")
        assert not service.start("+91 90000 00000")
        assert service.pending == {}
    
    
    for name, test in list(globals().items()):
        if name.startswith("test_"):
            test()
            print("passed:", name)
    print("SDK module loaded:", "sms_sdk" in sys.modules)
    Show the answer: Question 6 of 17

    Answer: it prints

    passed: test_sends_the_code
    passed: test_failed_send_keeps_nothing_pending
    SDK module loaded: False

    Both tests pass: the fake records the first message, and for the failing number it raises NotifierError, so start() returns False and keeps nothing pending. Only the adapter knows the SDK, and the tests never import it.

  7. Question 7 of 17 What does a fake need in order to stand in for SmsNotifier in the tests of SignupService?

    Choose one answer.

    Show the answer: Question 7 of 17

    Answer: An implementation of Notifier's send(), nothing more

    The service was written against the target interface, so the tests can satisfy it with a few lines that implement send() and record what they were asked to send.

  8. Question 8 of 17 In the terms of ports and adapters, what are Notifier, SmsNotifier and RecordingNotifier?

    Choose one answer.

    Show the answer: Question 8 of 17

    Answer: Notifier is a port, and SmsNotifier and RecordingNotifier are two adapters that plug into it

    Ports and adapters generalises the pattern to a whole application: one port per conversation with the outside, such as Notifier, and one adapter per technology behind it, the SMS provider in production and a recording fake in tests.

  9. Question 9 of 17 Which kind of adapter is the usual default, and why?

    Choose one answer.

    Show the answer: Question 9 of 17

    Answer: The object adapter, because it works with any instance of the adaptee and exposes only the target interface

    An object adapter holds the adaptee in a private field, so it adapts any instance it is handed and its clients see only the target interface. Class adapters and conversions are for narrower cases.

  10. Question 10 of 17 Your framework creates the SDK client with logging and connection settings already configured. Which kind of adapter can use that exact instance?

    Choose one answer.

    Show the answer: Question 10 of 17

    Answer: An object adapter, which is given the instance and holds it

    The class adapter in two_adapters.py had to create its own SDK instance through super().__init__(), so an instance configured elsewhere was out of its reach. The object adapter wrapped one without any new code.

  11. Question 11 of 17 Why could the Rust report() take a ready-made Celsius as well as a SensorTenthsF?

    Choose one answer.

    Show the answer: Question 11 of 17

    Answer: Every type converts into itself, and implementing From for Celsius gave the matching Into

    From<SensorTenthsF> for Celsius is the adapter, a conversion with no new class. Into comes for free from the standard library, and the conversion of a type into itself lets callers pass a Celsius unchanged.

  12. Question 12 of 17 In handler.go, what turns the function greet into an http.Handler?

    Choose one answer.

    Show the answer: Question 12 of 17

    Answer: The type conversion http.HandlerFunc(greet), whose ServeHTTP method calls greet

    Go's documentation calls HandlerFunc an adapter that lets ordinary functions be used as HTTP handlers. The conversion is the adapter, and its ServeHTTP method is the forwarding call.

  13. Question 13 of 17 Which of these give something that already exists a different interface, as an adapter does?

    Choose every answer that is right.

    Show the answer: Question 13 of 17

    Answer:

    • Go's http.HandlerFunc(f), which gives a plain function a ServeHTTP method
    • Java's InputStreamReader, which reads characters from a stream of bytes
    • Node.js's util.promisify(f), which turns a callback-style function into one that returns a promise

    The first three take an existing function or stream and offer it through another interface. A BufferedInputStream keeps the interface it wraps, and the fake has no adaptee at all.

  14. Question 14 of 17 The UTF-8 bytes of a word go through InputStreamReader, and the reader returns 4 characters, each of which takes 3 bytes in UTF-8. How many bytes went in?

    Type a number.

    A hint

    The Hindi greeting of the example was 6 characters and 18 bytes.

    Show the answer: Question 14 of 17

    Answer: 12 bytes

    The reader decodes 3 bytes into each character, so 4 characters came from 4 × 3 = 12 bytes, in the same way as the example's 18 bytes became 6 characters.

  15. Question 15 of 17 Why does format() take a whole number of paise rather than an amount of rupees with a fraction?

    Choose one answer.

    Show the answer: Question 15 of 17

    Answer: Whole numbers are exact, while a binary floating-point number cannot hold most decimal fractions exactly

    Money is counted in its smallest unit as a whole number so that adding and comparing stay exact. The formatter converts to rupees only to print, which is where a fraction appears.

  16. Question 16 of 17 Of the three implementations of IndianAmountFormatter, which one is an adapter?

    Choose one answer.

    Show the answer: Question 16 of 17

    Answer: The Node.js one, which turns paise into rupees and passes them to Intl.NumberFormat for en-IN

    Only Node.js has a formatter that does the job, so only its implementation adapts one. The other two write the grouping themselves, and callers of format() cannot tell the difference.

  17. Question 17 of 17 A new version of Java groups en-IN amounts the Indian way. What has to change, and how do you know it worked?

    Choose one answer.

    Show the answer: Question 17 of 17

    Answer: Only the Java implementation, which can become an adapter over the JDK's formatter, and it must still pass the shared cases

    Owning the interface turns a platform's new feature into a local change, and a shared file of cases is what shows that the implementations still agree.

Interview questions

Fresher · What is the difference between an object adapter and a class adapter, and which would you use by default?

An object adapter holds the adaptee in a field and implements the target interface by forwarding each call to it, translating names, arguments, units and errors on the way. A class adapter inherits from the adaptee’s class and implements the target interface, so one object is both. The object adapter is the usual default: it works with any instance of the adaptee, including subclasses and objects configured elsewhere, and its clients can reach only the target’s methods. The class adapter needs multiple inheritance, or a class plus an interface as in Java, is tied to one concrete class when the code is written, and exposes the adaptee’s public methods to every client, so code can step around the interface. Choose it only when you must override the adaptee’s own methods.

A good answer to the warm-up invites three more questions about adapters. Choose your reply to each, and read why each reply holds up or falls short:

Checkpoint

Interview round: three follow-up questions

  1. Question 1 of 3 The interviewer asks when you would choose a class adapter over an object adapter. Which answer is strongest?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: When the adapter must override one of the adaptee's own methods, and you accept that clients can reach all its other methods

    A class adapter is worth its costs only when inheritance buys something an object adapter cannot do, such as overriding a method the adaptee calls on itself. Otherwise the object adapter wraps any instance and exposes only the target interface.

  2. Question 2 of 3 How is an adapter different from a decorator such as Java's BufferedInputStream, which also wraps an object?

    Choose one answer.

    Show the answer: Question 2 of 3

    Answer: An adapter offers a different interface from its adaptee's, while a decorator offers the same interface and adds behaviour

    Both patterns wrap an object and forward calls. An adapter's interface is the client's, not the wrapped object's; a decorator keeps the wrapped object's interface, so decorators can be stacked, as the Decorator lesson of this module shows.

  3. Question 3 of 3 You must adapt a type from another package in Rust or Go, neither of which has inheritance. What do you write?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: An object adapter, a struct that holds the value, or a conversion such as Rust's From when the boundary is a value

    The object adapter is the portable form of the pattern: a struct that keeps the adaptee and implements the target. Rust adds conversions, a From implementation or a newtype like Celsius, for values at a boundary.

Key takeaways

  • An adapter implements the interface your code expects and translates each call into calls on an object you cannot change: names, argument shapes, units, errors and replies.
  • Only the adapter knows both sides, so a provider that renames a method or a status code changes one class, and the client never sees the SDK’s habits.
  • A fake that implements the same interface keeps the SDK out of your tests, and the same idea for a whole application is ports and adapters.
  • Prefer the object adapter: it holds the adaptee, works with any instance and keeps the adaptee’s other methods out of reach, while a class adapter exposes them and a conversion suits a single function or value.
  • Standard libraries ship adapters under other names, such as Go’s HandlerFunc, Java’s InputStreamReader and Arrays.asList, Python’s TextIOWrapper and Node.js’s util.promisify, and each can promise only what its adaptee can do.
  • Owning the interface lets each platform have its own implementation behind it, whether that is an adapter or code written by hand, all held to one shared file of cases.

Terms in this lesson

adaptee
The existing object whose interface does not fit and which you cannot change, such as a provider's SDK client. An adapter holds it and translates calls into calls on it.
class adapter
An adapter that inherits from the adaptee's class and implements the target interface, so one object is both, and every public method of the adaptee stays reachable.
fake
A working stand-in for a real dependency, used in tests: for example a notifier that records the messages it is asked to send instead of sending them.
newtype
A wrapper type with a single field, such as Rust's struct Celsius(f64), that gives an existing type a name and methods of its own at no run-time cost.
object adapter
An adapter that keeps the adaptee in a field and implements the target interface by forwarding each call to it, so it can wrap any instance, including one created elsewhere.
ports and adapters
An architecture, also called hexagonal architecture, in which the application defines an interface, a port, for each conversation with the outside world, and each technology plugs in through an adapter.
target interface
The interface that client code calls and the application owns, such as a Notifier with send(phone, text). An adapter implements it on top of an object with another interface.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.