「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 Binding 或 Lazy 初始化) 時,物件會存在一樣。元件有專屬的「生命週期」,但大部分不需要觀察到。
靜態元件初始化作業的例外狀況是動態元件集合。集合本身是靜態定義,但集合中的元件可透過動態建立、繫結至已公開的能力,然後刪除。雖然元件架構不會免費提供延遲繫結,但這樣做會在 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);
}
}
許多語言都存在模擬架構,這些語言會處理設定回傳值及檢查呼叫引數。上述程式碼示範如何實作這些架構的功能。