States
A State is a representation of a domain-specific object at a certain time. Its role is to ensure the business rules are respected and produce Events. It should have a default implementation, which is the initial value. It is okay to have placeholder values for the default implementation. You can also use Option<T>
or a custom enum
to represent a value that is not yet available.
- Structure: Primarily a straightforward object, characterized by its properties and data fields.
- Evolution: Capable of evolving over time with events.
- Non-Public Exposure: Not intended for public access, hence capable of holding sensitive information.
- Flexibility: Adaptable in representing a wide range of domain-specific scenarios.
- Meaningful: its name and properties are intentionally descriptive and relevant to its domain.
Creating States
A todo-list is a list of tasks that can be in progress or completed. We will use the exact same words to have a loyal representation of the domain.
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct TodoList {
next_index: TaskIndex,
tasks: HashMap<TaskIndex, Completed>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub enum Completed {
Yes,
No,
}
Computing States
You can build a new state by applying a sequence of events to an initial state. This is called event replay. This process cannot fail, as the events are immutable and cannot be changed. Thus, you should not change the events after they have been created. If you really need to, you can do event upcasting, which is a process of converting an event to a new version of the event or migrating existing data to the new model.
fn apply(&mut self, events: &[Self::Event]) {
for event in events {
match event {
TodoListEvent::TaskAdded(index, ..) => {
self.tasks.insert(*index, Completed::No);
self.next_index += 1;
}
TodoListEvent::TaskRemoved(index) => {
self.tasks.remove(index);
}
TodoListEvent::TaskCompleted(index) => {
self.tasks.insert(*index, Completed::Yes);
}
}
}
}
Producing Events
Messages are processed with the state, which will either produce events or an error.
fn send(&self, message: &Self::Message) -> Result<Vec<Self::Event>, Self::Error> {
let mut events = Vec::new();
match message {
TodoListMessage::AddTask(name) => {
events.push(TodoListEvent::TaskAdded(self.next_index, name.clone()));
}
TodoListMessage::RemoveTask(index) | TodoListMessage::CompleteTask(index) => {
let task = self.tasks.get(index);
match task {
None => return Err(TodoListError::TaskNotFound(*index)),
Some(Completed::Yes) => {
return Err(TodoListError::TaskAlreadyCompleted(*index))
}
Some(Completed::No) => match message {
TodoListMessage::RemoveTask(_) => {
events.push(TodoListEvent::TaskRemoved(*index));
}
TodoListMessage::CompleteTask(_) => {
events.push(TodoListEvent::TaskCompleted(*index));
}
_ => {}
},
}
}
}
Ok(events)
}
Implementing the State
trait
You can implement the State
trait from the forgen
crate.
pub trait State: Default + Serialize + for<'de> Deserialize<'de> {
type Error: Display = AnyError;
type Event: Serialize + for<'de> Deserialize<'de>;
type Message;
fn apply(&mut self, events: &[Self::Event]);
fn send(&self, message: &Self::Message) -> Result<Vec<Self::Event>, Self::Error>;
}