透過物件導向設計瞭解元件

「Fuchsia 元件」是軟體執行作業的可組合單元,重視重複使用、隔離和可測試性。

本文件提供 Fuchsia 元件和採用依附元件插入物件導向設計設計類比。這種比喻讓 Fuchsia 開發人員運用現有的「物件導向設計」知識,以熟悉的術語和設計模式開發 Fuchsia 元件。

說明

物件導向程式設計 (OOP) 中,「物件」是一種實體,其中包含用於這些資料的資料方法類別可定義與特定物件類型相關聯的資料和方法。物件是類別的例項。

同樣地,「元件」包含內部程式狀態 (資料) 並公開在內部狀態運作的通訊協定 (一組方法)。當類別在物件中宣告可呼叫方法時,元件資訊清單會在元件中宣告可呼叫的通訊協定。元件會執行個體化為元件執行個體

使用 FIDL 定義的通訊協定,會宣告元件之間的介面。提供通訊協定表示元件實作該通訊協定,類似於類別如何「實作」介面或特徵。

本文件探討實作通訊協定和實作介面的元件之間的類比,而這種比喻延伸了元件和物件與其他元件或物件之間的關係。

「Has-A」(一個物件由其他物件「組成」) 和「Depends-On/Uses-A」這兩個重要關係 (其中一個物件需要其他物件才能正常操作)。

元件可能展現相同的關係。單一元件可能由多個子項元件組成,就像在 OOP 一樣,這些子項也代表元件的實作詳細資料。與納入必要物件的類別建構函式類似,元件資訊清單會宣告其依附的通訊協定。元件架構關注這些相依通訊協定的轉送和滿足方式,讓元件能夠執行,這類似於 OOP 的依附元件插入作業。

OOP 中的其他常見關係是「Is-A」(繼承),類別可以擴充另一個類別的資料和邏輯。元件架構中沒有「繼承」的類比。

這些相似之處在 OOP 和元件架構概念中提供以下對應:

物件導向概念 元件概念
介面 FIDL 通訊協定
類別定義 元件資訊清單
物件 元件執行個體
內部 / 關聯課程 FIDL 資料
依附於/Uses-A 關係 (依附元件) 從父項轉送的功能
有關係 (組成) 子元件
實作介面 展現自我
IS-A 關係 (繼承) 不適用,建議使用「導入」

元件架構提供的功能優於 OOP,但從 OOP 原則開始,可讓您以合理方式大致估算元件設計。

本文件其餘部分將提供更詳細的詳細資料和範例,說明如何將上述 OOP 概念對應到元件概念。

以介面的形式使用 FIDL 通訊協定

許多 OOP 語言都具有「介面」或「特徵」的概念,可透過物件「實作」。在類別定義資料和行為時,介面只會「宣告」類別可能存在的行為。介面的實作項目可以分組和使用。

在 Fuchsia 中,FIDL 會定義元件之間的介面。與 OOP 介面類似,FIDL 通訊協定的實作項目可以交替使用。

範例

假設有一個名為 Duck 的介面,採用名為 Quack 的方法。

FIDL 通訊協定如下:

library fuchsia.animals;

@discoverable
protocol Duck {
  Quack();
}

如要實作這個通訊協定,元件的元件資訊清單中應包含下列程式碼片段:

{
  // ...

  // Declare that this component implements a protocol.
  capabilities: [
    {
      protocol: ["fuchsia.animals.Duck"],
    }
  ],

  // Expose this protocol so that other components may call it.
  expose: [
    {
      protocol: ["fuchsia.animals.Duck"],
      from: "self",
    }
  ],
}

使用各種 OOP 語言時,您可以採用以下方式編寫:

C++

// Duck is an abstract class with a pure virtual method "Quack".
class Duck {
  public:
    virtual void Quack() = 0;
};

// Actually implement Duck.
class MallardDuck : public Duck {
  public:
    void Quack() override { /* ... */ }
}

飛鏢

// All classes in Dart define an interface.
// Omitting the definition of Quack means it can be overridden in a child.
abstract class Duck {
  Quack();
}

MallardDuck implements Duck {
  @override
  Quack() { /* ... */ }
}

Rust

// Rust uses "traits" rather than interfaces.
// A trait may be explicitly implemented for any type.
trait Duck {
  fn quack(&self);
}

// The type MallardDuck implements the "Duck" trait.
struct MallardDuck {
}

impl Duck for MallardDuck {
  fn quack(&self) { /* ... */ }
}

Java

// Explicitly define a public Duck interface.
public interface Duck {
  public void Quack();
}

// Create a class that implements the interface.
public class MallardDuck implements Duck {
  @Override
  public void Quack() { /* ... */ }
}

將元件資訊清單做為類別

OOP 的核心概念是「物件」,其中包含用於處理這些資料的資料和方法。類別型 OOP 語言會定義物件類別的階層,用於描述資料及其關係。類別可以多次例項化為物件,也可以模組化。

同樣地,Fuschia 系統定義為元件階層,每個元件都是由其元件資訊清單定義。元件資訊清單定義了元件類別,該類別可以執行個體化並使用模組化。

物件和元件皆代表可重複使用的行為與資料單位,並依其實作的介面分組。

範例

假設有一個元件擲出 N 面骰。也就是說,在要求元件時,元件會傳回 [1, N] 範圍中的值。

在 Fuchsia 中,我們定義了元件介面一部分的通訊協定:

library fuchsia.dice;

// fuchsia.dice.Roller supports rolling a single die
protocol Roller {

  // Method "Roll" takes no arguments, and it returns an "outcome" that is a 64-bit unsigned integer.
  Roll() -> (struct {
    outcome uint64
  });
}

元件的資訊清單如下:

// dice_roller.cml

{
  // The execution details for the component.
  //
  // This section says the component should be run as a normal ELF binary
  // (e.g. C++ or Rust) by executing the file at path "bin/dice_roller"
  // in the containing package.
  //
  // It also says to pass the command line arguments '"--sides" "6"' to
  // the program. It is the program's responsibility to parse its command
  // line parameters. In this case we want a 6-sided die.
  program: {
    runner: "elf",
    binary: "bin/dice_roller",
    args = [
      "--sides",
      "6",
    ],
  },

  // Declare the protocols the component implements (see previous section)
  capabilities: [{
    protocol: ["fuchsia.dice.Roller"],
  }],

  // Expose the protocols the component implements.
  expose: [{
    protocol: ["fuchsia.dice.Roller"],
    from: "self",
  }],
}

類似的類別定義如下:

C++

class DiceRoller : public Roller {
  public:
    DiceRoller(uint64_t number_of_sides) : number_of_sides(number_of_sides) {}
    uint64_t Roll() override;

  private:
    const uint64_t number_of_sides;
};

飛鏢

class DiceRoller implements dice.Roller {
    final int numberOfSides;
    DiceRoller({required this.numberOfSides});

    @override
    int Roll() { /* ... */ }
}

Rust

pub struct DiceRoller {
    number_of_sides: u64,
}

impl DiceRoller {
  pub fn new(number_of_sides: u64) -> Self {
    Self{ number_of_sides }
  }
}

impl Roller for DiceRoller {
  pub fn roll(&self) -> u64 { /* ... */ }
}

Java

class DiceRoller implements Roller {
    private long numberOfSides;

    public DiceRoller(long numberOfSides) {
      this.numberOfSides = numberOfSides;
    }

    @Override
    public long Roll() { /* ... */ }
}

在每個範例中,有一個 DiceRoller 會從 Roller 介面實作 Roll 方法。DiceRoller 會透過其輸入引數進行參數化,該引數會指定骰子的面數。

在 OOP 範例中,您可以使用任意數量的面定義 DiceRoller,但元件資訊清單指定值 6。

將元件例項做為物件,做為組合的子項

物件是使用 OOP 語言建立類別的例項,「元件執行個體」則是資訊清單定義的元件例項。物件和元件可以多次例項化,以便在不同情況下重複使用。

物件和元件執行個體執行個體化的方式主要有所不同。

在 OOP 語言中,物件可透過呼叫「建構函式」來建立,而在某些語言中,則是透過呼叫「解構工具」來刪除。有多種策略和設計模式來抽象物件建立方式 (例如工廠模式),但該物件最後一律會在某個位置建立。

相反地,元件執行個體通常是在靜態階層中定義。只要將元件指定為其他元件的子項,就足以讓子項「存在」。但同時不代表元件實際上正在執行。一般而言,元件只有在「繫結」至其公開的能力時才執行。在 OOP 方面,就像是第一次對方法呼叫方法 (Late BindingLazy 初始化) 時,物件會存在一樣。元件有專屬的「生命週期」,但大部分不需要觀察到。

靜態元件初始化作業的例外狀況是動態元件集合。集合本身是靜態定義,但集合中的元件可透過動態建立、繫結至已公開的能力,然後刪除。雖然元件架構不會免費提供延遲繫結,但這樣做會在 OOP 中以存放物件的集合表示。

元件的狀態由本身的狀態和子項的子項組成,類似於物件狀態和包含物件的狀態組成。元件的行為包含本身的行為,以及元件透過通訊協定與子項的互動,類似於物件行為是本身的行為,以及物件透過方法與內含物件的互動方式。

範例

在此範例中,有一個代表「使用者工作階段」的物件階層。UserSession 包含一個 User 和多個 Apps

您可以使用元件實作這個結構,如下所示:

// user_session.cml
{
  // ...
  // user_session has a static child called "user", declared in user.cml
  children: [
    {
      name: "user",
      url: "fuchsia-pkg://fuchsia.com/session_example#meta/user.cm",
    },
  ],
  // user_session has a collection for dynamic components, called "apps"
  collections: [
    {
      name: "apps",
    }
  ],
  // access the User protocol from the child called "user".
  use: [
    {
      protocol: "fuchsia.session_example.User",
      from: "#user",
    },
  ],
}
// user.cml
{
  // ..

  // Expose the User capability, which provides information and actions on the current user.
  capabilities: [
    { protocol: "fuchsia.session_example.User" },
  ],
  expose: [
    {
      protocol: "fuchsia.session_example.User",
      from: "self",
    },
  ],
}
// C++-like pseudocode for interacting with the child components from the session.

// Create any arbitrarily named app in the apps collection with just a URL to execute.
CreateChild("apps", "my_app", "fuchsia-pkg://..." /* url to the app to run */);

// Accessing exposed protocols causes the component to actually run. The
// output parameter is a Directory handle over which capabilities are accessed.
OpenExposedDir("apps", "my_app", &out_dir);

// Open any arbitrary capability on the bound component.
// Assuming that the "Controller" protocol has a method called "ExecuteCommand".
out_dir.Open("fuchsia.my_app.Controller").ExecuteCommand();

// Connect to the protocol from the static child.
// This is available in the incoming namespace for session, since it
// "uses" the capability.
incoming_namespace.Open("fuchsia.session_example.User").GetName();

元件架構允許任何元件在「應用程式」集合中啟動,只要滿足其依附元件即可 (請參閱後續章節)。

C++

// User is an abstract class representing a user of the session.
// It declares the "GetName" method all users must implement.
class User {
  public:
    virtual std::string GetName()  = 0;
};

// App is class representing the interface to apps.
class App {
  /* ... */
};

class Session {
  public:
    Session() : user(/* initialize user */) {}

    void AddApp(App app) {
      apps.push_back(std::move(app));
    }

  private:
    std::unique_ptr<User> user;

    // Note that in C++ the collection needs to be typed, while in component
    // terms all components share a base type.
    std::vector<App> apps;
};

飛鏢


interface User {
  String GetName();
}

class Session {
    final User user;
    final List<Object> apps = [];
    Session() :  user = /* initialize user */;

    // Similar to how all components share a "base type", Dart's Object
    // type can be dynamically cast to a desired interface.
    //
    // Casting will fail if the Object does not implement the type
    // requested, similar to how connecting to a non-exposed capability
    // fails for a component.
    void AddApp(Object app) {
      apps.add(app);
    }
}

Rust

pub trait User {
    fn get_name() -> String;
}

pub trait App {
  /* ... */
}

pub struct Session {
  user: User,

  // Note that in Rust the collection needs to be typed, while in component
  // terms all components share a base type.
  apps: Vec<Box<dyn App>>;
}

impl Session {
  pub fn new() -> Self {
    Self{ user: /* initialize user */, apps: Vec::new() }
  }

  pub fn add_app(&mut self, app: Box<dyn App>) {
    self.apps.push(app);
  }
}

Java

interface User {
  String GetName();
}

class Session {
    private User user;
    private List<Object> apps = new ArrayList<Object>();

    public Session() {
      user = /* initialize user */;
    }

    // Similar to how all components share a "base type", Java's Object
    // type can be dynamically cast to a desired interface.
    //
    // Casting will fail if the Object does not implement the type
    // requested, similar to how connecting to a non-exposed capability
    // fails for a component.
    public void AddApp(Object app) {
      apps.add(app);
    }
}

以內部或相關類別的形式呈現 FIDL 資料

在 OOP 中,常見的物件是會對其他物件採取行動。本文件前述章節的重點,是指出物件對其他行為使用依附元件的情況。不過,如果物件依附於其他物件中的資料,也要注意這點。這在容器介面中很常見,其中的一個物件負責維護其他物件的集合,並公開介面以某種方式操控集合。

元件最適合呈現具有複雜行為的物件,而不是做為資料容器。FIDL 提供能向通訊協定傳入和傳出的可擴充資料結構,而且相較於元件,這些類型更適合用來呈現資料。

一般來說,如果介面呼叫的是純舊資料類型,則資料應儲存在元件中、使用 FIDL tables 宣告,並由在 table 上提供存取子和變動器的通訊協定公開。

在執行作業前依命令建構資料類型的建構工具介面也可在 FIDL 中呈現最佳效果。

範例

在這個範例中,我們會建立包含多個 Items 待銷售的 Store 介面。客戶可以建立 Cart,用於新增商品,最終為 Checkout()

library fuchsia.store;

// An Item is a plain data type describing individual items in the store.
type Item = table {
  // Each Item has a unique ID, which is used to reference the object.
  1: id uint64;
  2: name string;
  3: price_in_cents uint32;
  4: quantity_in_stock: uint32;
}

type StoreError = strict enum {
  ITEM_NOT_FOUND = 1;
  INVALID_QUANTITY = 2;
};

protocol Store {
  // Add new items to the store.
  // No return code, so this operation is asynchronous and can fail silently.
  AddItem(struct {
    item: Item;
  });

  // Set the price on an existing item, by id.
  // Fails if the item is not found.
  SetPrice(struct {
    item_id: uint64;
    new_price: uint32;
  }) error StoreError;

  // Add (or subtract) additional stock of an item.
  // Fails if the item is not found or if you would be left with an
  // invalid quantity of the item.
  AddStock(struct {
    item_id: uint64;
    additional_quantity: int32;
  }) error StoreError;

  // Create a new Cart interface to shop at the store.
  // Note that this takes a "resource" struct, because request is a
  // Zircon handle.
  CreateCart(resource struct {
    request: server_end:Cart;
  });
};

type CartError = strict enum {
  PAYMENT_FAILURE = 1;
  NOT_ENOUGH_IN_STOCK = 2;
};

// Cart uses the builder pattern to create a set of items and checkout atomically.
protocol Cart {
  // Add a specific quantity of an item by id to the cart.
  AddItem(struct {
    item_id: uint64;
    quantity: uint32;
  });

  // Add a coupon code to the cart.
  AddCouponCode(struct {
    code: string;
  });

  // Checkout all previously added items atomically.
  // Fails if payment fails or if there are not enough items in stock
  // to satisfy the request.
  Checkout() error CartError;
};
// Pseudo-code for interacting with the store.

StoreProxy store = connect_to<Store>();
store.AddItem(Item{.id = 1, .name = "Fuchsia Coffee Mug", .price_in_cents = 1299, .quantity_in_stock = 30});
store.AddItem(Item{.id = 2, .name = "Fuchsia Blanket", .price_in_cents = 3499, .quantity_in_stock = 10});
store.SetPrice({.item_id = 2, .new_price = 2999});
store.AddStock({.item_id = 1, .additional_quantity = -10});

CartProxy cart;
store.CreateCart(cart.NewRequest());
cart.AddItem({.item_id = 2, .quantity = 1});
cart.AddItem({.item_id = 1, .quantity = 5});
cart.AddCouponCode("FUCHSIA-ROCKS");
cart.Checkout();

實作 Store 介面的元件負責根據通訊協定的合約維護項目組合。

C++


// Create a plain old data type for Item.
struct Item {
  uint64_t id;
  std::string name;
  uint32_t price_in_cents;
  uint32_t quantity_in_stock;
};

// Enumerate the return values of cart operations.
enum CartResult {
  OK = 0,
  PAYMENT_FAILURE = 1,
  NOT_ENOUGH_IN_STOCK = 2,
};

class Cart {
  public:
    // Cart is owned by a store, and it requires the pointer back to
    // its owner to implement Checkout.
    Cart(Store* store);

    // Adding items and coupon codes cannot fail.
    void AddItem(uint64_t item_id, uint32_t quantity);
    void AddCouponCode(std::string code);

    // Perform the checkout operation by acting upon store_ in some way.
    CartResult Checkout();

  private:
    // Create an inner class representing the pair of item id and quantity.
    struct ItemQuantity {
      uint64_t item_id;
      uint32_t quantity;
    };

    // The parent store, not owned.
    Store* store_;
    std::vector<ItemQuantity> item_requests_;
    std::vector<std::string> coupon_codes_;
};

// Enumerate return values of store operations.
enum StoreResult {
  OK = 0,
  ITEM_NOT_FOUND = 1,
};

class Store {
  public:
    // Add new items to the store.
    void AddItem(Item item);

    // Set properties of items based on id.
    StoreResult SetPrice(uint64_t item_id, uint32_t new_price);
    StoreResult AddStock(uint64_t item_id, int32_t additional_quantity);

    // Create a new Cart for this store, referencing the Store that owns the Cart.
    // Carts are owned by a store, and must be deleted before the Store is.
    Cart* CreateCart() {
      carts_.emplace_back(Cart(this));
      return &cards_.back();
    }
  private:
    std::vector<Item> items_;
    std::vector<Cart> carts_;
};

飛鏢


// Create a class containing the data for items.
class Item {
  final int id;
  final String name;
  int priceInCents;
  int quantityInStock;

  Item({
    required this.id,
    required this.name,
    required this.priceInCents,
    this.quantityInStock = 0
  });
}

// Since Dart doesn't have tuples, create a pair type for id and quantity.
class ItemQuantity {
  final int itemId;
  int quantity;

  ItemQuantity({required this.itemId, required this.quantity});
}

// Represent the various results for cart operations.
enum CartResult {
  ok,
  paymentFailure,
  notEnoughInStock,
}

class Cart {
  final Store store;

  final List<ItemQuantity> _items = [];
  final List<String> _couponCodes = [];

  // A Cart needs to refer back to its Store to implement Checkout.
  Cart({required this.store});

  void AddItem(int itemId, int quantity) {
    _items.add(ItemQuantity(itemId: itemId, quantity: quantity);
  }

  void AddCouponCode(String code) {
    _couponCodes.add(code);
  }

  CartResult Checkout() { /* ... */ }
}

// Represent the results for store operations.
enum StoreResult {
  ok,
  itemNotFound,
}

class Store {
  final List<Item> _items = [];
  final List<Cart> _carts = [];

  void AddItem(Item item) { _items.add(item); }

  StoreResult SetPrice(int item_id, int new_price) { /* ... */ }
  StoreResult AddStock(int item_id, int additional_quantity) { /* ... */ }

  // Create a cart that refers back to this owning store.
  Cart CreateCart() {
    var ret = Cart(this);
    _carts.add(ret);
    return ret;
  }
}

Rust


// Create a data struct for Item information.
pub struct Item {
  pub id: u64,
  pub name: String,
  pub price_in_cents: u32,
  pub quantity_in_stock: u32,
}

pub struct Cart {
  // Carts need to act on their parent Store, but we want to avoid cyclic references.
  // Use a Weak pointer so that the Store can be deleted independent of its Carts.
  // Mutex is used for interior mutability.
  store: Weak<Mutex<Store>>,
  items: Vec<(u64, u32)>,
  coupon_codes: Vec<String>,
}

impl Cart {
  pub fn new(store: Weak<Mutex<Store>>) -> Self {
    Self {
      store,
      items: vec![],
      coupon_codes: vec![],
    }
  }

  pub fn add_item(&mut self, item_id: u64, quantity: u32) {
    self.items.push((item_id, quantity));
  }

  pub fn add_coupon_code(&mut self, code: String) {
    self.coupon_codes.push(code);
  }

  // Checkout consumes the Cart builder and returns the result.
  pub fn checkout(self) -> Result<(), Error> { /* ... */ }
}

pub struct Store {
  items: Vec<Item>,

  // Note that we do not need to maintain ownership over Carts, since
  // they can exist independent of the Store they are from. Checkout will
  // presumably fail if the Store was deleted before it is called.
}

impl Store {
  pub fn new() -> Arc<Mutex<Self>> {
    Arc::new(Mutex::new(Self {
      items: vec![],
      carts: vec![],
    }));
  }

  pub fn add_item(&mut self, item: Item) { items.push(item); }
  pub fn set_price(&mut self, item_id: u64, new_price: u32) -> Result<(), Error> { /* ... */ }
  pub fn add_stock(&mut self, item_id: u64, additional_quantity: i32) -> Result<(), Error> { /* ... */ }

  pub fn create_cart(self_: Arc<Mutex<Self>>) -> Cart {
    Cart::new(self_.downgrade())
  }
}

Java

// Create a class containing the data for items.
public class Item {
  public int id;
  public String name;
  public int priceInCents;
  public int quantityInStock;
}

// Since Java doesn't have tuples, create a pair type for id and quantity.
class ItemQuantity {
  public int item_id;
  public int quantity;

  public ItemQuantity(int item_id, int quantity) {
    this.item_id = item_id;
    this.quantity = quantity;
  }
}

// Represent the various results for cart operations.
public enum CartResult {
  OK,
  PAYMENT_FAILURE,
  NOT_ENOUGH_IN_STOCK,
}

// Represent the results for store operations.
enum StoreResult {
  ok,
  itemNotFound,
}

class Store {
  private final List<Item> items = new ArrayList<Item>();
  private final List<Cart> carts = new ArrayList<Cart>();

  public void AddItem(Item item) { items.add(item); }

  public StoreResult SetPrice(int item_id, int new_price) { /* ... */ }
  public StoreResult AddStock(int item_id, int additional_quantity) { /* ... */ }

  public Cart CreateCart() {
    Cart ret = new Cart();
    carts.add(ret);
    return ret;
  }

  // Inner classes in Java can refer to their containing class.
  // This is needed to Checkout can act upon Store.this.
  public class Cart {
    private final List<ItemQuantity> items = new ArrayList<ItemQuantity>();
    private final List<String> couponCodes = new ArrayList<String>();

    void AddItem(int item_id, int quantity) {
      _items.add(ItemQuantity(item_id, quantity));
    }

    void AddCouponCode(String code) {
      _couponCodes.add(code);
    }

    CartResult Checkout() { /* ... */ }
  }
}

功能轉送做為依附元件插入

依附元件插入是一種技術,可將物件的依附元件做為引數傳遞給物件,而非由物件本身建構或找到。這可讓物件的建立者控制物件所依附的行為實作方式。尤其在測試設定中不需要實際呼叫這些服務,就可以測試與外部服務互動的物件。依附元件插入的常見用途,是將 Time 介面傳遞至會讀取系統時鐘的物件。接著,呼叫端可以傳入提供固定時間值進行測試的實作,並在實際工作環境中傳遞讀取即時資料的實作。

在元件之間使用通訊協定,基本上是以依附元件插入技術為基礎。每個元件都會明確定義其 uses 通訊協定,並提供這些通訊協定,才能讓元件執行個體化 (類似 OOP 類別在其建構函式中宣告所有必要依附元件的方式)。

依附元件插入架構可由註冊資料庫建構依附元件,但所有功能都會透過元件資訊清單明確從來源轉送至目的地。本文的先前各節說明元件如何透過 exposing 通訊協定實作介面。這樣可讓該元件的父項向其他元件 offer 該通訊協定,以滿足其依附元件 (即 use 的通訊協定)。

以這種方式建構元件的原因類似 OOP 依附元件插入:可視需要替換測試、改進和擴充能力。

範例

本範例實作的 Purchaser,需要在購買流程中處理信用卡。在部分設定中 (例如測試) 您不想實際向信用卡收費 (這可能會非常昂貴)!我們會改為提供 Purchaser 用來向信用卡扣款的 CreditCardCharger。在測試情境中,我們會提供假的 CreditCardCharger,實際上並不會向卡片扣款。

// fuchsia.store.fidl

type PurchaseError = enum {
  CREDIT_CARD_FAILURE = 1;
  ITEM_NOT_FOUND = 2;
};

protocol Purchaser {
  // Purchase an item by name with a specific credit card.
  // Fails if the item is not found or if the credit card failed to charge.
  Purchase(struct {
    item_name: string,
    credit_card: string,
  }) error PurchaseError;
};

protocol CreditCardCharger {
  // Charge a specific credit card a specific amount.
  // Returns whether the charge is successful.
  Charge(struct {
    credit_card: string,
    amount: int,
  }) -> (struct { success: bool });
};
// purchaser.cml
{
  program: {
    // Instructions for how to run this component.
    /* ... */
  },

  capabilities: [
    { protocol: "fuchsia.store.Purchaser" }
  ],

  // Purchaser is a public interface implemented by this component.
  expose: [
    {
      protocol: "fuchsia.store.Purchaser",
      from: "self",
    }
  ],

  // CreditCardCharger is an interface required by this component to function.
  use: [
    { protocol: "fuchsia.store.CreditCardCharger" }
  ]
}
// real_credit_card_charger.cml
// Implements CreditCardCharger and actually charges credit cards.
{
  // ...
  capabilities: [
    { protocol: "fuchsia.store.CreditCardCharger" }
  ],

  expose: [
    {
      protocol: "fuchsia.store.CreditCardCharger",
      from: "self",
    }
  ],
}
// fake_credit_card_charger.cml
// Implements CreditCardCharger, but does not really charge anything.
{
  // ...
  capabilities: [
    {
      protocol: [
        "fuchsia.store.CreditCardCharger",
        // Interface to control the output of this fake component (FIDL not pictured here).
        "fuchsia.store.testing.CreditCardChargerController",
      ]
    }
  ],

  expose: [
    {
      protocol: [
        "fuchsia.store.CreditCardCharger",
        "fuchsia.store.testing.CreditCardChargerController",
      ],
      from: "self",
    }
  ],
}
// core.cml
//
// Actually add a "Purchaser" to the system, its dependencies, and route
// its protocol to some component implementing a purchase flow.
{
  children: [
    // ...
    {
      name: "purchaser",
      url: "fuchsia-pkg://fuchsia.com/purchaser#meta/purchaser.cml",
    },
    {
      // We want to use the real credit card charger so that we actually charge customers.
      name: "credit_card_charger"
      url: "fuchsia-pkg://fuchsia.com/real_credit_card_charger#meta/real_credit_card_charger.cml",
    },
    {
      name: "real_graphical_purchase_flow"
      url: /* ... */,
    },
  ],
  offer: [
    // Route protocols to satisfy every component's dependencies.
    {
      protocol: "fuchsia.store.CreditCardCharger",
      from: "#credit_card_charger",
      to: "#purchaser",
    },
    {
      protocol: "fuchsia.store.Purchaser",
      from: "#purchaser",
      to: "#real_graphical_purchase_flow",
    },
  ]
}
// test_purchaser.cml
{
  children: [
    {
      // We're going to test the real purchaser component, which is safe since we are mocking its dependency.
      name: "purchaser",
      url: "fuchsia-pkg://fuchsia.com/purchaser#meta/purchaser.cml",
    },
    {
      // We want to use the fake credit card charger so that we don't actually charge cards in tests.
      name: "credit_card_charger"
      url: "fuchsia-pkg://fuchsia.com/fake_credit_card_charger#meta/fake_credit_card_charger.cml",
    },
  ],
  offer: [
    {
      // Inject the fake charger as a dependency to purchaser.
      protocol: "fuchsia.store.CreditCardCharger",
      from: "#credit_card_charger",
      to: "#purchaser",
    }
  ],
  use: [
    {
      // Use Purchaser so we can test it
      protocol: "fuchsia.store.Purchaser",
      from: "#purchaser",
    },
    {
      // Use test charger so we can control what the credit card charger returns.
      protocol: "fuchsia.store.testing.CreditCardChargerController",
      from: "#credit_card_charger",
    },
  ]
}
// Pseudo-code for test_purchaser

PurchaserProxy purchaser = open_service<Purchaser>();
CreditCardChargerController charger = open_service<CreditCardChargerController>();

// Make the card charger always return true, then test successful charge for an existing item.
charger.SetChargeResponse(true);
assert(purchaser.Purchase("existing item", "fake-card"), isNotError);

// Now test what happens when an item is missing.
// Depending on how advanced the mock charger is, we could even check
// that it was not called as a result of this invalid Purchase call.
assert(purchaser.Purchase("missing item", "fake-card"), PurchaseError.ITEM_NOT_FOUND);

// Make the charger return false and try again with an existing item.
// This allows us to test our error handling code paths.
charger.SetChargeResponse(false);
assert(purchaser.Purchase("existing item", "fake-card"), PurchaseError.CREDIT_CARD_FAILURE);

上述系統會以 OOP 語言實作,如下所示:

C++

class Purchaser final {
  public:
    // Purchaser takes as input the credit card charger to use.
    Purchaser(CreditCardCharger* credit_card_charger) :
      credit_card_charger_(credit_card_charger) {}
    PurchaseError Purchase(std::string item_name, std::string credit_card) {
      /* ... */
      // Use the injected credit card charger when needed.
      credit_card_charger_->Charge(std::move(credit_card), /* amount */);
      /* ... */
    }
  private:
    CreditCardCharger* credit_card_charger_;
};

// Abstract base class for concrete credit card chargers.
class CreditCardCharger {
  public:
    virtual bool Charge(std::string credit_card, int amount) = 0;
};

class RealCreditCardCharger : public CreditCardCharger {
  public:
    bool Charge(std::string credit_card, int amount) override {
      /* actually charge credit cards somehow */
    }
};

class MockCreditCardCharger : public CreditCardCharger {
  public:
    // Mock implementation of CreditCardCharger::Charge that returns
    // a configurable error value and records the arguments of its
    // previous call.
    bool Charge(std::string credit_card, int amount) override {
      calls_++;
      last_credit_card_ = std::move(credit_card);
      last_amount_ = amount;
      return return_value_;
    }

    // Set the value that will be returned when calling Charge
    void SetReturnValue(bool return_value) { return_value_ = return_value; }

    // Get the parameters of the last call to Charge.
    const std::string& GetLastCreditCard() const { return last_credit_card_; }
    int GetLastAmount() const { return last_amount_; }
    size_t GetCallCount() const { return calls_; }

  private:
    bool return_value_ = true;
    size_t calls_ = 0;
    std::string last_credit_card_;
    int last_amount_ = 0;
};

// Production code
int main() {
  auto charger = std::make_unique<RealCreditCardCharger>();
  Purchaser purchaser(charger.get());
  // use purchaser in the program flow

  /* ... */
}

// Test code (assuming GoogleTest)
TEST(Purchaser, Success) {
  // Test that a purchase can succeed.
  // We expect that when a purchase is completed for an item costing
  // $100 that the CreditCardCharger is called with amount = 100.
  auto charger = std::make_unique<MockCreditCardCharger>();
  Purchaser purchaser(charger.get());
  EXPECT_EQ(PurchaseResult::OK, purchaser.Purchase("Item costing $100", "1234567890"));
  EXPECT_EQ(1, charger->GetCallCount());
  EXPECT_EQ("1234567890", charger->GetLastCreditCard());
  EXPECT_EQ(100, charger->GetLastAmount());
}

TEST(Purchaser, ItemNotFound) {
  // Test that we do not actually try to charge a credit card if the item is not found.
  auto charger = std::make_unique<MockCreditCardCharger>();
  Purchaser purchaser(charger.get());
  EXPECT_EQ(PurchaseResult::ITEM_NOT_FOUND, purchaser.Purchase("Not found item", "1234567890"));
  EXPECT_EQ(0, charger->GetCallCount());
}

TEST(Purchaser, CardChargeFailure) {
  // Test that a purchase can fail.
  auto charger = std::make_unique<MockCreditCardCharger>();
  Purchaser purchaser(charger.get());
  charger->SetReturnValue(false);
  EXPECT_EQ(PurchaseResult::CREDIT_CARD_FAILURE,
            purchaser.Purchase("Item costing $100", "1234567890"));
  EXPECT_EQ(1, charger->GetCallCount());
  EXPECT_EQ("1234567890", charger->GetLastCreditCard());
  EXPECT_EQ(100, charger->GetLastAmount());
}

飛鏢

class Purchaser {
  final CreditCardCharger creditCardCharger;

  // Purchaser takes as input the credit card charger to use.
  Purchaser({required this.creditCardCharger});
  PurchaseError Purchase(String itemName, String creditCard) {
    /* ... */
    // Use the injected credit card charger when needed.
    creditCardCharger.Charge(creditCard, /* amount */);
    /* ... */
  }
};

// Abstract base class for concrete credit card chargers.
abstract class CreditCardCharger {
  bool Charge(String creditCard, int amount);
};

class RealCreditCardCharger implements CreditCardCharger {
  @override
  bool Charge(String creditCard, int amount) {
    /* actually charge credit cards somehow */
  }
};

class MockCreditCardCharger implements CreditCardCharger {
  bool _returnValue = true;
  int _calls = 0;
  String _lastCreditCard = '';
  int _lastAmount = 0;

  // Mock implementation of CreditCardCharger::Charge that returns
  // a configurable error value and records the arguments of its
  // previous call.
  @override
  bool Charge(String creditCard, int amount) {
    _calls++;
    _lastCreditCard = creditCard;
    _lastAmount = amount;
    return _returnValue;
  }

  // Set the value that will be returned when calling Charge
  void set returnValue(int v) {
    _returnValue = v;
  }

  // Get the parameters of the last call to Charge.
  int get calls => _calls;
  String get lastCreditCard => _lastCreditCard;
  int get lastAmount => _lastAmount;
};

// Production code
void main() {
  final charger = RealCreditCardCharger();
  Purchaser purchaser(creditCardCharger: charger);
  // use purchaser in the program flow

  /* ... */
}

// Test code (assuming package:test)
import 'package:test/test.dart';

void main() {
  group('Purchaser', () {
    test('succeeds', () {
      // Test that a purchase can succeed.
      // We expect that when a purchase is completed for an item costing
      // $100 that the CreditCardCharger is called with amount = 100.
      final charger = MockCreditCardCharger();
      final purchaser = Purchaser(creditCardCharger: charger);

      expect(purchaser.Purchase("Item costing $100", "1234567890"), PurchaseResult.ok);
      expect(charger.calls, 1);
      expect(charger.lastCreditCard, "1234567890");
      expect(charger.amount, 100);
    });

    test('fails when item is not found', () {
      // Test that we do not actually try to charge a credit card if the item is not found.
      final charger = MockCreditCardCharger();
      final purchaser = Purchaser(creditCardCharger: charger);

      expect(purchaser.Purchase("Not found item", "1234567890"), PurchaseResult.itemNotFound);
      expect(charger.calls, 0);
    });

    test('fails when card cannot be charged', () {
      // Test that a purchase can fail.
      final charger = MockCreditCardCharger();
      final purchaser = Purchaser(creditCardCharger: charger);

      charger.returnValue = false;
      expect(purchaser.Purchase("Item costing $100", "1234567890"), PurchaseResult.creditCardFailure);
      expect(charger.calls, 1);
      expect(charger.lastCreditCard, "1234567890");
      expect(charger.amount, 100);
    });
  });
}

Rust

pub struct Purchaser {
  credit_card_charger: Box<dyn CreditCardCharger>,
}

impl Purchaser {
  // Purchaser takes as input the credit card charger to use.
  pub fn new(credit_card_charger: Box<dyn CreditCardCharger>) -> Self {
    Self { credit_card_charger }
  }

  pub fn purchase(&mut self, item_name: String, credit_card: String) {
    /* ... */
    // Use the injected credit card charger when needed.
    self.credit_card_charger.charge(creditCard, /* amount */);
    /* ... */
  }

  // For testing only, allow a Purchaser to be destroyed and converted
  // back to it CreditCardCharger.
  //
  // Alternatively, we could take a non-owning reference to the dependency.
  #[cfg(test)]
  pub fn to_charger(mut self) -> Box<dyn CreditCardCharger> {
    self.credit_card_charger
  }
}

// Trait implemented by concrete credit card chargers.
trait CreditCardCharger {
  fn charge(credit_card: String, amount: i32) -> bool;
}

struct RealCreditCardCharger {}

impl CreditCardCharger for RealCreditCardCharger {
  fn charge(&mut self, credit_card: String, amount: i32) -> bool {
    /* actually charge credit cards somehow */
  }
};

// Mock implementation of CreditCardCharger that returns
// a configurable error value and records the arguments of its
// previous call.
pub struct MockCreditCardCharger {
  return_value: bool,
  calls: usize,
  last_credit_card: Option<String>,
  last_amount: Option<i32>,
}

impl MockCreditCardCharger {
  pub fn new() -> Self {
    Self {
      return_value: true,
      calls: 0,
      last_credit_card: None,
      last_amount: None,
    }
  }

  // Set the value that will be returned when calling charge
  pub fn set_return_value(&mut self, return_value: bool) {
    self.return_value = return_value;
  }

  // Get the parameters of the last call to charge.

  pub fn get_last_credit_card<'a>(&'a self) -> Option<&'a str> {
    self.last_credit_card.as_deref()
  }

  pub fn get_last_amount(&self) -> Option<i32> {
    self.last_amount.clone()
  }

  pub fn get_calls(&self) -> usize {
    self.calls
  }
}

impl CreditCardCharger for MockCreditCardCharger {
  fn charge(&mut self, credit_card: String, amount: i32) -> bool {
    self.calls += 1;
    self.last_credit_card = Some(credit_card);
    self.last_amount = Some(amount);
    self.return_value
  }
}

// Production code
fn main() {
  let mut purchaser = Purchaser::new(Box::new(RealCreditCardCharger::new()));
  // use purchaser in the program flow
  /* ... */
}

// Test code (assuming Rust tests)

#[cfg(test)]
mod tests {
  #[test]
  fn success() {
    // Test that a purchase can succeed.
    // We expect that when a purchase is completed for an item costing
    // $100 that the CreditCardCharger is called with amount = 100.
    let mut purchaser = Purchaser::new(Box::new(MockCreditCardCharger::new()));
    assert_eq!(purchaser.purchase("Item costing $100", "1234567890"), PurchaseResult::OK);
    let charger = purchaser.to_charger();
    assert_eq!(charger.get_calls(), 1);
    assert_eq!(charger.get_last_credit_card(), Some("1234567890"));
    assert_eq!(charger.get_last_amount, Some(100i32));
  }

  #[test]
  fn item_not_found() {
    // Test that we do not actually try to charge a credit card if the item is not found.
    let mut purchaser = Purchaser::new(Box::new(MockCreditCardCharger::new()));
    assert_eq!(purchaser.purchase("Item costing $100", "1234567890"), PurchaseResult.ok);
    let charger = purchaser.to_charger();

    assert_eq!(purchaser.purchase("Not found item", "1234567890"), PurchaseResult::ITEM_NOT_FOUND);
    let charger = purchaser.to_charger();
    assert_eq!(charger.get_calls(), 0);
  }

  #[test]
  fn card_charge_fails() {
    // Test that a purchase can fail.

    let mut charger = Box::new(MockCreditCardCharger::new());
    charger.set_return_value(false);
    let mut purchaser = Purchaser::new(charger);
    assert_eq!(purchaser.purchase("Item costing $100", "1234567890"), PurchaseResult::CREDIT_CARD_FAILURE);
    let charger = purchaser.to_charger();

    assert_eq!(charger.get_calls(), 1);
    assert_eq!(charger.get_last_credit_card(), Some("1234567890"));
    assert_eq!(charger.get_last_amount, Some(100i32));
  }
}

Java

class Purchaser {
  private CreditCardCharger creditCardCharger;

  // Purchaser takes as input the credit card charger to use.
  public Purchaser(CreditCardCharger creditCardCharger) {
    this.creditCardCharger = creditCardCharger;
  }

  public PurchaseError Purchase(String itemName, String creditCard) {
    /* ... */
    // Use the injected credit card charger when needed.
    creditCardCharger.Charge(creditCard, /* amount */);
    /* ... */
  }
};

// Interface for concrete credit card chargers.
interface CreditCardCharger {
  public boolean Charge(String creditCard, int amount);
};

class RealCreditCardCharger implements CreditCardCharger {
  @Override
  boolean Charge(String creditCard, int amount) {
    /* actually charge credit cards somehow */
  }
};

class MockCreditCardCharger implements CreditCardCharger {
  private boolean returnValue = true;
  private int calls = 0;
  private String lastCreditCard = '';
  private int lastAmount = 0;

  // Mock implementation of CreditCardCharger::Charge that returns
  // a configurable error value and records the arguments of its
  // previous call.
  @override
  public boolean Charge(String creditCard, int amount) {
    calls++;
    lastCreditCard = creditCard;
    lastAmount = amount;
    return returnValue;
  }

  // Set the value that will be returned when calling Charge
  public void setReturnValue(int v) {
    returnValue = v;
  }

  // Get the parameters of the last call to Charge.
  public int getCalls() { return calls; }
  public String getLastCreditCard() { return lastCreditCard; }
  public int getLastAmount() { return lastAmount; }
};

// Production code
void main() {
  CreditCardCharger charger = new RealCreditCardCharger();
  Purchaser purchaser = new Purchaser(charger);
  // use purchaser in the program flow

  /* ... */
}

// Test code (assuming JUnit)

public class PurchaserTest extends TestCase {
  protected MockCreditCardCharger charger;
  protected Purchaser purchaser;

  protected void setUp() {
    charger = new MockCreditCardCharger();
    purchaser = new Purchaser(charger);
  }

  public void testPurchaseSucceeds() {
    // Test that a purchase can succeed.
    // We expect that when a purchase is completed for an item costing
    // $100 that the CreditCardCharger is called with amount = 100.
    assertEquals(purchaser.Purchase("Item costing $100", "1234567890"), PurchaseResult.OK);
    assertEquals(charger.getCalls(), 1);
    assertEquals(charger.getLastCreditCard(), "1234567890");
    assertEquals(charger.getLastAmount(), 100);
  }

  public void testItemNotFoundError() {
    // Test that we do not actually try to charge a credit card if the item is not found.

    assertEquals(purchaser.Purchase("Not found item", "1234567890"), PurchaseResult.ITEM_NOT_FOUND);
    assertEquals(charger.getCalls(), 0);
  }

  public void testCardChargeFailure() {
    // Test that a purchase can fail.

    charger.returnValue = false;
    assertEquals(purchaser.Purchase("Item costing $100", "1234567890"), PurchaseResult.CREDIT_CARD_FAILURE);
    assertEquals(charger.getCalls(), 1);
    assertEquals(charger.getLastCreditCard(), "1234567890");
    assertEquals(charger.getLastAmount(), 100);
  }
}

許多語言都存在模擬架構,這些語言會處理設定回傳值及檢查呼叫引數。上述程式碼示範如何實作這些架構的功能。