Fuchsia 组件是软件的可组合单元 强调重用、隔离和可测试性的原则。
本文档对 Fuchsia 进行了类比 具有依赖项的组件和面向对象的设计 注射。通过这种类比,Fuchsia 开发者可以 运用现有的面向对象的设计知识来开发 使用熟悉的术语和设计模式的 Fuchsia 组件。
简介
在面向对象编程 (OOP) 中,对象是一个实体 包含数据以及处理该数据的方法。答 class 定义与特定类型相关联的数据和方法 对象。对象是类的实例化。
同样,组件包含内部程序状态(数据),并公开 基于内部状态运行的协议(方法组)。地点 类在对象(即组件)上声明可调用的方法 manifest来声明 组件。组件实例化为组件实例。
协议(使用 FIDL 定义),用于声明 组件。提供协议意味着组件会实现 协议类似于类实现接口或特征的方式。
本文档探讨了用于实现以下目的的组件之间的类比: 实现接口的协议和类,而这种类比可以 以及组件和对象与其他组件的关联方式 或对象。
其中两个重要的关系是“包含-A”(其中一个对象构成 和“Depends-On/Uses-A”(其中,一个对象需要 存在另一个对象才能正常运行)。
组件也可以表现出同样的关系。单个组件 由多个子组件组成,并且在 OOP 中,存在 是组件的实现细节。类似 传递到接受所需对象、组件的类构造函数 声明它们所依赖的协议。组件框架 这些依赖协议如何路由以及满足 使得组件可以执行,类似于依赖项 注入适用于 OOP。
OOP 中的另一种常见关系是“Is-A”(继承),其中 一个类可以扩展另一个类的数据和逻辑。在组件框架中 并不存在与继承类似的类似行为。
这些相似之处共同提供了 OOP 之间的对应关系 和组件框架概念:
面向对象的概念 | 组件概念 |
---|---|
接口 | FIDL 协议 |
类定义 | 组件清单 |
对象 | 组件实例 |
内部 / 关联类 | FIDL 数据 |
依赖/使用-关联(依赖) | 从父级路由的功能 |
有关系(组合) | 子组件 |
实现接口 | 公开自身功能 |
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 { /* ... */ }
}
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
// 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 系统被定义为组件层次结构, 每个文件均由其组件清单定义。组件 manifest 定义了一种可实例化的组件类 以模块化的方式使用
对象和组件都代表可重复使用的行为单元, 数据,按它们实现的接口分组。
示例
假设有一个会掷 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() { /* ... */ }
}
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 术语中,这就像是 它的存在(一个类型 延迟绑定或延迟初始化)。 组件有自己的生命周期,在很大程度上不需要 目标。
静态组件初始化的例外情况是动态组件初始化 组件集合。集合本身 静态定义,但集合中的组件可以动态定义 created(创建),通过打开公开 capability 并已销毁。这将表示为一个集合 在 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);
}
}
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_;
};
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;
}
}
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());
}
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);
});
});
}
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);
}
}
许多语言都提供了用于处理设置返回的模拟框架 以及检查调用参数。上面的代码演示了如何 实现这些框架的功能