I recently thought it would be a fun idea to learn Rust. Not sure why, it’s not like I don’t have enough on my plate as it is. But it seemed like a good idea at the time. My day job restricts me to JVM languages, so learning something new and excitingly different was quite appealing. I went through the Rustlings exercises and thought I’d jump straight in by reimplementing a personal project in Rust.
Of course, being the conscientious developer I am I immediately set about ensuring that I’d be able to pursue a DDD route, and to that end started by porting over relevant parts my Java DDD library to Rust. It was here I encountered my first hurdle coming at Rust as a Java developer: how do I write my Specification
interface as a Rust trait.
public interface Specification<T> {
boolean isSatisfiedBy(T t);
default Specification<T> and(final Specification<T> specification) {
return AndSpecification.of(this, specification);
}
default Specification<T> or(final Specification<T> specification) {
return OrSpecification.of(this, specification);
}
default Specification<T> not() {
return NotSpecification.of(this);
}
}
So, obviously, being a Java developer, I wrote this:
pub trait Specification<T> {
fn is_satisfied_by(&self, t: &T) -> bool;
fn and(&self, spec2: Specification<T>) -> Specification<T>;
fn or(&self, spec2: Specification<T>) -> Specification<T>;
fn not(&self) -> Specification<T>;
}
This doesn’t compile. But Rust’s compiler is kind enough to tell you what’s wrong, where, and how to fix it.
error[E0782]: trait objects must include the dyn keyword help: add dyn keyword before this trait
Okay them. Let’s do that.
pub trait Specification<T> {
fn is_satisfied_by(&self, t: &T) -> bool;
fn and(&self, spec2: dyn Specification<T>) -> dyn Specification<T>;
fn or(&self, spec2: dyn Specification<T>) -> dyn Specification<T>;
fn not(&self) -> dyn Specification<T>;
}
Awesome. That compiles. So now I can go ahead and implement AndSpecification
, OrSpecification
and NotSpecification
and flesh out those functions.
pub struct AndSpecification<T> {
spec1: dyn Specification<T>,
spec2: dyn Specification<T>,
}
So, of course that doesn’t compile. Luckily I’m told why.
the size for values of type (dyn Specification<T> + 'static) cannot be known at compilation time E0277 doesn't have a size known at compile-time Help: the trait Sized is not implemented for (dyn Specification<T> + 'static) Note: only the last field of a struct may have a dynamically sized type Help: change the field's type to have a statically known size Help: borrowed types always have a statically known size Help: the Box type always has a statically known size and allocates its contents in the heap
Fine, let’s put it in a box and implement Specification
, doing the same for OrSpecification
and NotSpecification
.
pub struct AndSpecification<T> {
spec1: Box<dyn Specification<T>>,
spec2: Box<dyn Specification<T>>,
}
impl<T> Specification<T> for AndSpecification<T> {
fn is_satisfied_by(&self, t: &T) -> bool {
self.spec1.is_satisfied_by(t) && self.spec2.is_satisfied_by(t)
}
}
pub struct OrSpecification<T> {
spec1: Box<dyn Specification<T>>,
spec2: Box<dyn Specification<T>>,
}
impl<T> Specification<T> for OrSpecification<T> {
fn is_satisfied_by(&self, t: &T) -> bool {
self.spec1.is_satisfied_by(t) || self.spec2.is_satisfied_by(t)
}
}
pub struct NotSpecification<T> {
spec: Box<dyn Specification<T>>,
}
impl<T> Specification<T> for NotSpecification<T> {
fn is_satisfied_by(&self, t: &T) -> bool {
self.spec.is_satisfied_by(t)
}
}
Great. Now we need to implement and
, or
and not
in the trait and we can use it, right? Er…
fn and(&self, spec2: dyn Specification<T>) -> dyn Specification<T> {
todo!()
}
Firstly, the spec2
argument and and the return type need to be put in Box
es. Okay, let’s do that and see if we can return an AndSpecification
.
fn and(&self, spec2: Box<dyn Specification<T>>) -> Box<dyn Specification<T>> {
Box::new(AndSpecification { spec1: Box::new(self), spec2 })
}
Yeah, no dice. It does not like Box::new(self)
.
the trait bound &Self: Specification<_> is not satisfied E0277 the trait Specification<_> is not implemented for &Self Help: the following other types implement trait Specification<T>: AndSpecification<T> NotSpecification<T> OrSpecification<T> Note: required for the cast to the object type dyn Specification<_>
It look a lot of reading, searching, trying and failing before I think I happened upon the solution here. One of the compiler error messages I received suggesting implementing the methods in a separate trait, but I had no idea what to do with it until Googling the correct combination of words and phrases took me to this section of The Book. What I ended up with was this:
pub trait Specification<T> {
fn is_satisfied_by(&self, t: &T) -> bool;
}
pub trait SpecificationComposition<T> {
fn and(self: Box<Self>, spec2: Box<dyn Specification<T>>) -> Box<dyn Specification<T>>;
fn or(self: Box<Self>, spec2: Box<dyn Specification<T>>) -> Box<dyn Specification<T>>;
fn not(self: Box<Self>) -> Box<dyn Specification<T>>;
}
impl<T: 'static, S: Specification<T> + 'static> SpecificationComposition<T> for S {
fn and(self: Box<Self>, spec2: Box<dyn Specification<T>>) -> Box<dyn Specification<T>> {
Box::new(AndSpecification { spec1: self, spec2 })
}
fn or(self: Box<Self>, spec2: Box<dyn Specification<T>>) -> Box<dyn Specification<T>> {
Box::new(OrSpecification { spec1: self, spec2 })
}
fn not(self: Box<Self>) -> Box<dyn Specification<T>> {
Box::new(NotSpecification { spec: self })
}
}
So, how do you use it? Consider this test for the and
method.
#[cfg(test)]
mod tests {
use super::*;
struct Person {
name: String,
}
struct NameIsNotEmpty {}
impl Specification<Person> for NameIsNotEmpty {
fn is_satisfied_by(&self, t: &Person) -> bool {
!t.name.is_empty()
}
}
struct NameMaxLength {
size: usize,
}
impl Specification<Person> for NameMaxLength {
fn is_satisfied_by(&self, t: &Person) -> bool {
t.name.len() <= self.size
}
}
#[test]
fn does_and_specifications_work() {
let spec = Box::new(NameIsNotEmpty {}).and(Box::new(NameMaxLength { size: 10 }));
assert_eq!(
spec.is_satisfied_by(&Person {
name: String::from("Rick")
}),
true
);
assert_eq!(
spec.is_satisfied_by(&Person {
name: String::from("")
}),
false
);
assert_eq!(
spec.is_satisfied_by(&Person {
name: String::from("Bibbedy Bobbedy Boo")
}),
false
);
}
}
I’ve set up two specifications: one to ensure a person’s name is not empty and one to test its length does not exceed a certain amount. 10 characters in this case. The test passes and I’ve achieved step 1 on my Rust DDD journey.
Banner image by Tengyart on Unsplash