Richard Grantham
Rust by Tengyart

Implementing the Specification Pattern in Rust

Filed under Development , Rust on

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 Boxes. 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