Fuchsia 组件是可组合的软件执行单元,强调重用性、隔离性和可测试性。
本文档将 Fuchsia 组件与采用依赖项注入的面向对象设计进行了类比。通过这种类比,Fuchsia 开发者可以运用其现有的面向对象设计知识,使用熟悉的术语和设计模式来开发 Fuchsia 组件。
简介
在面向对象的编程 (OOP) 中,对象是一个包含数据和对该数据进行操作的方法的实体。类用于定义与特定类型的对象相关联的数据和方法。对象是类的实例化。
同样,组件包含内部程序状态(数据),并公开可对其内部状态进行操作的协议(方法组)。类声明对象上可调用的方法,而组件清单则声明组件上可调用的协议。组件实例化为组件实例。
使用 FIDL 定义的协议用于声明组件之间的接口。提供协议意味着组件实现了该协议,类似于类可以实现接口或特征。
本文档探讨了实现协议的组件与实现接口的类之间的相似之处,这种相似之处也延伸到了组件和对象与其他组件或对象相关的方式。
两种重要的关系是“Has-A”(一个对象由其他对象组成)和“Depends-On/Uses-A”(一个对象需要另一个对象存在才能正常运行)。
组件可能也具有这些关系。单个组件可能由多个子组件组成,与 OOP 类似,这些子组件的存在是组件的实现细节。与接受必需对象的类构造函数类似,组件清单会声明其依赖的协议。组件框架关注的是如何路由和满足这些依赖协议,以便组件可以执行,这类似于 Dependency Injection 在 OOP 中的工作方式。
OOP 中的另一种常见关系是“Is-A”(继承),其中一个类可以扩展另一个类的数据和逻辑。在组件框架中,没有与继承类似的概念。
总而言之,这些相似之处使得 OOP 和组件框架概念之间存在以下对应关系:
| 面向对象的概念 | 组件概念 |
|---|---|
| 接口 | FIDL 协议 |
| 课程定义 | 组件清单 |
| 对象 | 组件实例 |
| 内部 / 关联的类 | FIDL 数据 |
| “依赖于”/“使用”关系(依赖项) | 从父级路由的功能 |
| Has-A 关系(组合) | 子级组件 |
| 实现接口 | 公开来自自身的 capability |
| Is-A 关系(继承) | 不适用,首选“实现” |
组件框架提供的功能远超 OOP,但从 OOP 原理入手,您将能够合理地近似估算最终的组件设计。
本文档的其余部分将更详细地介绍如何将上述 OOP 概念映射到组件概念,并提供相关示例。
作为接口的 FIDL 协议
许多面向对象的编程语言都有接口或特征的概念,这些概念可由对象实现。类定义数据和行为,而接口仅声明类可能具有的行为。接口的实现者可以分组并互换使用。
在 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 系统被定义为组件的层次结构,每个组件都由其组件清单定义。组件清单定义了可实例化并以模块化方式使用的组件类。
对象和组件都表示可重用的行为和数据单元,按其实现的接口进行分组。
示例
假设有一个用于掷 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 术语中,这就像是首次调用对象的方法时,对象才开始存在(一种后期绑定或延迟初始化)。组件有自己的生命周期,在很大程度上不需要进行观测。
静态组件初始化的例外情况是动态组件集合。集合本身是静态定义的,但集合中的组件可能会被动态创建、通过打开公开的功能进行绑定,以及销毁。这在 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 中得到最佳表示。
示例
在此示例中,我们将创建一个包含待售 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_;
};
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() { /* ... */ }
}
}
以依赖项注入形式实现的功能路由
依赖注入是一种技术,其中对象的依赖项作为参数传递给对象,而不是由对象本身构建或查找。这样一来,对象创建者就可以控制对象所依赖的行为的实现。它尤其强大,可用于测试与外部服务互动的对象,而无需在测试设置中实际调用这些服务。依赖注入的一个常见用途是将 Time 接口传递给原本会读取系统时钟的对象。然后,调用方能够传入一个为测试提供固定时间值的实现,并在生产环境中传入一个读取实际时间的实现。
组件之间的协议使用从根本上来说是基于依赖项注入技术构建的。每个组件都会明确定义其 uses 的协议,并且必须提供这些协议才能实例化组件(类似于 OOP 类在其构造函数中声明所有必需的依赖项)。
与某些依赖项注入框架(其中依赖项可由注册表构建)不同,所有能力都通过组件清单从来源显式路由到目的地。本文档的前面部分介绍了组件如何通过 exposing 协议来实现接口。这样一来,相应组件的父级便能够将该协议offer给其他组件,以满足它们的依赖项(它们use的协议)。
以这种方式构建组件的原因与 OOP 依赖注入类似:可以根据测试、发展和可扩展性的需要交换依赖行为。
示例
此示例实现了一个 Purchaser,该 Purchaser 需要在购买流程中处理信用卡。在某些设置(例如测试)中,您不希望实际向信用卡扣款(这可能会非常昂贵!)。相反,我们将提供一个 CreditCardCharger,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());
}
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);
}
}
许多语言都有模拟框架,可用于设置返回值和检查调用实参。上述代码展示了这些框架的功能是如何实现的。