3 个版本 (重大更改)
0.3.0 | 2024 年 3 月 8 日 |
---|---|
0.2.0 | 2024 年 3 月 4 日 |
0.1.0 | 2024 年 3 月 2 日 |
在 开发工具 中排名 528
每月下载量 134
45KB
845 代码行
gear
Gear 是一个 Rust 的组件对象模型。那么,为什么 Rust 需要一个新的对象模型?为什么是 Gear?
面向对象编程的四大支柱
Grady Booch 定义了面向对象编程的四大支柱
- 抽象 不直接处理对象,而是使用抽象,隐藏所有不必要的复杂性。例如,使用基类而不是具体类。这是一个非常好的主意,因为管理复杂性在软件开发中非常重要,如果我们能将其最小化到客户端代码,那么生活就会变得更加简单。
- 封装 应该隐藏状态,以便客户端代码不能直接访问它。特别是,对象通常具有必须保留的不变性,如果状态只能通过方法更改,则可以维护(或至少检查)这些不变性。
- 多态 这允许一个类型用作更抽象的类型。例如,在 GUI 中,复选框可以传递给期望按钮或 ListView 的方法,而 ListView 可以维护一个 View 对象的向量,这些对象实际上可能是复选框和标签。
- 继承 这有两种形式:接口继承和实现继承。接口继承类似于 Rust trait 实现。实现继承是经典的面向对象编程,其中一个类从另一个类继承并调整或添加一些内容。例如,你可能有一个从按钮继承并覆盖 draw 方法的 FlashingButton,以便它闪烁。
这些都是有用的,在 GUI、模拟、游戏、插件架构等重要领域工作得很好。
Rust 与支柱的比较
- 抽象 Rust 通过 traits 处理抽象的主要方式。这些在许多情况下工作得很好,但它们不容易组合。
- 封装 Rust 通过其可见性控制和模块系统很好地处理了这个问题。
- 多态 trait 再次,要么是
impl Trait
,要么是Box<dyn Trait>
。但再次,存在组合问题。例如,如果你想要一个既能像按钮一样操作,又能像视图一样操作的复选框,这会变得非常尴尬。 - 继承 Rust 与接口继承配合良好,但完全不支持实现继承。这样做有充分的理由:虽然实现继承很有用,但它也极其 存在问题,因为它在基类和派生类之间引入了紧密的耦合。正因为这些问题,现在的共识是更倾向于 组合而非继承。
齿轮基础
齿轮对象模型由以下部分组成
- 包含一个或多个对象的组件。
- 实现一个或多个特质的对象。
- 宏允许访问对象实现的特质。
例如,你可能有一个暴露 Draw 特质的组件
let component = get_from_somewhere();
let render = find_trait!(component, Render).unwrap();
render.render(&mut canvas);
齿轮与支柱
- 抽象 客户端代码仅与特质打交道,因此抽象非常出色。总可以向对象或组件添加新的特质,因此组合不会成为问题(甚至可以是动态的)。
- 封装 同样很棒,因为客户端处理的是特质而不是具体类型。
- 多态 组件可以暴露任意数量的特质,而且这一切都隐藏在 Component 中,因此代码可以查询所需的特质。这非常强大,但也存在动态性的缺点:没有编译时检查来确定特质确实存在。
- 继承 行为不是通过扩展类来添加的,而是通过向组件添加对象(或在现有对象上实现新的特质)来添加的。这不如实现继承强大,但它要简单得多,并且远没有那么脆弱。
示例代码
在 repro 中有一个示例,它包含一个狼与羊的模拟。以下是从该示例中摘录的注释代码,以说明齿轮的工作原理
// Gear currently requires nightly and you'll need this in your main.rs
// (or lib.rs).
#![feature(lazy_cell)]
#![feature(ptr_metadata)]
#![feature(unsize)]
use core::sync::atomic::Ordering;
use gear_objects::*;
use paste::paste;
// One of the traits the sim components expose. This is used by the World
// struct to render all of the components in the sim.
pub trait Render {
fn render(&self) -> ColoredString;
}
// Traits exposed by components have to be registered.
register_type!(Render);
// Function to add a wolf to the world and to the Store (the Store
// manages component lifetimes).
pub fn add_wolf(world: &mut World, store: &Store, loc: Point) -> ComponentId {
// The name "wolf" is used with Component's Debug trait.
let mut component = Component::new("wolf");
// Each component is given a unique id allowing components to be
// compared and hashed.
let id = component.id;
add_object!(
component,
Wolf, // object type
Wolf::new(), // object
[Action, Animal, Predator, Render], // traits exposed by this object to the component
[Debug] // repeated traits (these can appear multiple times in a Component)
);
// Wolf is the main object but there are a couple other objects
// which allow code reuse between Wolf and Sheep.
add_object!(component, Mover, Mover::new(), [Moveable]);
add_object!(
component,
Hungers,
Hungers::new(INITAL_HUNGER, MAX_HUNGER),
[Hunger],
[Debug] // Debug is repeated for this Component
);
// Animals are added to the back and grass to the front.
world.add_back(store, loc, component);
id
}
// Concrete object type, not directly used by client code.
struct Wolf {
age: i32, // wolves can die of old age
}
register_type!(Wolf);
// Traits are implemented normally.
impl Render for Wolf {
fn render(&self) -> ColoredString {
if self.age == 0 {
"w".green()
} else {
"w".normal()
}
}
}
// Somewhat simplified version of the World::render method.
// The World works with any components that implement the
// Action and Render traits.
pub fn render(&self, store: &Store) {
for y in 0..self.height {
for x in 0..self.width {
let loc = Point::new(x, y);
// Render the last component at a loc.
if let Some(id) = self.actors.get(&loc).map(|v| v.last()).flatten() {
let component = store.get(*id);
let render = find_trait!(component, Render).unwrap();
let ch = render.render();
print!("{}", ch);
} else {
print!(" ");
}
}
println!();
}
println!();
}
依赖项
~300KB