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

Fuchsia 元件是可組合的軟體單元 而注重重複使用、隔離和可測試性。

本文件使用 Fuchsia 的類比喻 元件以及物件導向設計 (透過依附元件) 插入。這個比喻方便 Fuchsia 開發人員 以運用現有的物件導向設計知識 採用熟悉的術語和設計模式的 Fuchsia 元件。

簡介

物件導向程式設計 (OOP) 中,物件指的是實體 ,包含用於該資料的資料方法。A 罩杯 class 定義與特定類型相關聯的資料和方法 物件物件是類別的例項。

同樣地,元件包含內部程式狀態 (資料) 並公開 在內部狀態運作的通訊協定 (方法群組)。地點 類別可宣告物件 (元件) 的可呼叫方法 資訊清單會在 元件。系統會將元件例項化為元件執行個體

使用 FIDL 定義的「通訊協定」,宣告 元件。提供通訊協定是指元件會實作 類似於類別可「實作」介面或特徵的方式

本文件將探討導入元件之間的類比 實作介面的通訊協定和類別,這種類比 元件和物件與其他元件之間的關係 或物件

「有-A」是兩個重要的關係(這個物件在這個例子中是 和「Depends-On/Uses-A」(在這個情況下 其他物件才能正常運作)。

元件可能會有這些相同的關係。單一元件可能 由多個子元件組成,就像在 OOP 中 一個元件的實作詳細資料。類似商家 加入包含必要物件、元件的類別建構函式 資訊清單宣告了依附的通訊協定。元件架構 且擔心這些相依通訊協定的轉送和回應方式有疑慮 以便元件執行,類似依附元件的方式 插入適用於 OOP,

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

這些相似之處結合了 OOP 之間的對應表 和元件架構概念:

物件導向概念 元件概念
介面 FIDL 通訊協定
類別定義 元件資訊清單
物件 元件執行個體
內部 / 相關類別 FIDL 資料
依附元件 -開啟/使用-A 關係 (依附元件) 從父項轉送的功能
包含關係 (樂曲) 子項元件
實作介面 向自己公開能力
Is-A 關係 (繼承) 不適用,建議使用「實作」

元件架構提供超越 OOP 的功能、 但從 OOP 原則開始,我們會提供您合理的預估值 以及完成的元件設計

本文件的其他部分會詳細說明,並舉例說明如何 將上述 OOP 概念對應到元件概念。

以 FIDL 通訊協定做為介面

許多 OOP 語言都具備「介面」或「特徵」的概念 可由物件implemented類別會定義資料和 介面只會宣告可能出現在 類別。介面的實作元件可以分組,並交替使用。

在 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 語言會定義物件類別的階層結構,用於描述資料 及其關係類別可例項化至物件中 可能會以模組方式使用

Fuchsia 系統定義為元件階層 而每個模組都是由其元件資訊清單定義。元件 資訊清單定義了元件類別,可例項化並 例如模組化

物件和元件都代表可重複使用的行為單位 資料,並依導入的介面分組。

範例

假設要擲出 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 語言來說,您可以透過呼叫 建構函式:在某些語言中,物件會藉由呼叫 destructor。已有許多策略和設計模式 建立離開物件 (例如工廠模式) 但最後,物件一律會在某處明確建立

相反地,元件執行個體通常是在靜態的 階層只要將元件指定為另一個元件的子項即可 就足以讓兒童「存在」。存在 但並不表示元件實際上正在執行。一般價格 只有在繫結至特定項目時,系統才會執行元件 公開的功能在 OOP 的術語中 (類型為 延遲繫結延遲初始化)。 元件有專屬的生命週期

靜態元件初始化的例外情形是動態元件 元件集合集合本身為 會以靜態方式定義,不過集合中的元件可以動態定義 建立,繫結至開啟已公開的能力, 和已刪除。以集合表示 物件保存在 OOP 中,但元件架構提供延遲繫結 。

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

範例

在這個範例中,有代表「user」的物件階層 「工作階段」這段文字。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 中呈現最佳效果。

範例

在本範例中,我們會建立含有數字的 Store 介面。 Items 出售。客戶可以建立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() { /* ... */ }
  }
}

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

插入依附元件是一種技術 物件的依附元件會以引數的形式傳遞至物件 由物件本身建構或找到這會讓 物件控制項 保持開啟。這項功能尤其強大,可讓您測試相互互動的物件 而且不必實際在測試中呼叫這些服務 可以管理叢集設定,像是節點 資源調度、安全性和其他預先設定項目插入依附元件的其中一項常用用途是傳遞時間 物件介面會以其他方式讀取系統時鐘。 呼叫端就可以傳入提供 固定時間值用於測試,而在正式環境中 可以讀取即時的實作

元件之間的通訊協定使用方式基本上是以 依附元件插入技術每個元件都會明確定義 您必須為 uses 和以下通訊協定提供這些通訊協定 和 OOP 類別宣告 所需的依附元件)。

與某些依附元件插入架構不同,後者可提供依附元件 所有功能都會明確轉送 將來源連結至目的地本文的前幾個章節 文件說明元件如何透過 exposing 事件實作介面。 因此效能相當卓越如此一來,該元件的父項即可存取 offer 以便支援其他元件 (use 的通訊協定)。

透過這種方式建構元件的原因和 OOP 依附元件插入的功能:附屬行為可替換 對測試、演化與擴充性而言至關重要

範例

本範例實作需要處理抵免額的 Purchaser 結帳流程中的卡片。部分設定 (例如測試) 您是否不想實際透過信用卡收費 相當高昂)!而是改為提供 CreditCardCharger Purchaser用於向信用卡收費。在我們的測試情境中 並不實際上對卡片扣款的假 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);
  }
}

模擬架構適用於許多可處理設定傳回語言的語言 以及檢查呼叫引數以上程式碼展示了 這些架構的功能