Fuchsia 元件是可組合的軟體執行單元,強調重複使用、隔離和可測試性。
本文將比較 Fuchsia 元件和物件導向設計,並說明依附元件注入。透過這個類比,Fuchsia 開發人員可以運用現有的物件導向設計知識,使用熟悉的詞彙和設計模式開發 Fuchsia 元件。
簡介
在物件導向程式設計 (OOP) 中,物件是包含資料和方法的實體,這些方法會對該資料執行作業。類別會定義與特定型別物件相關聯的資料和方法。物件是類別的例項。
同樣地,元件包含內部程式狀態 (資料),並公開可對內部狀態執行的通訊協定 (方法群組)。類別會在物件上宣告可呼叫的方法,而元件資訊清單則會在元件上宣告可呼叫的通訊協定。元件會例項化為元件例項。
通訊協定是使用 FIDL 定義,用於宣告元件之間的介面。提供通訊協定表示元件會實作該通訊協定,類似於類別實作介面或特徵的方式。
本文探討實作通訊協定的元件與實作介面的類別之間的相似之處,以及元件和物件與其他元件或物件的關係。
兩個重要的關係是「Has-A」(其中一個物件是由其他物件組成),以及「Depends-On/Uses-A」(其中一個物件需要另一個物件才能正常運作)。
元件也可能呈現這些關係。單一元件可能由多個子項元件組成,與 OOP 類似,這些子項的存在是元件的實作詳細資料。與接收必要物件的類別建構函式類似,元件資訊清單會宣告所依附的通訊協定。元件架構負責路由及滿足這些相依通訊協定,讓元件得以執行,這與 OOP 的依附元件插入類似。
OOP 中另一個常見的關係是「Is-A」(繼承),也就是類別可以擴充另一個類別的資料和邏輯。在元件架構中,沒有類似於「繼承」的概念。
這些相似之處共同提供 OOP 和元件架構概念之間的下列對應關係:
| 物件導向概念 | 元件概念 |
|---|---|
| 介面 | FIDL 通訊協定 |
| 類別定義 | 元件資訊清單 |
| 物件 | 元件執行個體 |
| 內部 / 關聯類別 | FIDL 資料 |
| Depends-On/Uses-A 關係 (依附元件) | 從父項資源轉送的功能 |
| Has-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 { /* ... */ }
}
Dart
// 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 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;
};
Dart
class DiceRoller implements dice.Roller {
final int numberOfSides;
DiceRoller({required this.numberOfSides});
@override
int Roll() { /* ... */ }
}
荒漠油廠
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 術語來說,這就像是首次呼叫物件的方法時,物件才會存在 (屬於延遲繫結或延遲初始化)。元件有自己的生命週期,大部分情況下不需要觀察。
動態元件集合是靜態元件初始化的例外狀況。集合本身是靜態定義,但集合中的元件可能會動態建立、透過開啟公開能力繫結,以及毀損。在 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;
};
Dart
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);
}
}
荒漠油廠
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_;
};
Dart
// 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;
}
}
荒漠油廠
// 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 通訊協定實作介面。這會讓該元件的父項能夠將該通訊協定提供給其他元件,以滿足其依附元件 (這些元件use的通訊協定)。offer
以這種方式建構元件的原因,與 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());
}
Dart
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);
});
});
}
荒漠油廠
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);
}
}
許多語言都有模擬架構,可處理設定傳回值和檢查呼叫引數。上述程式碼示範如何實作這些架構的功能。