Abstract Factory Advice?


#1

I’m writing a crate that queues closures for async dispatch, with some throttling functionality. I want to provide an abstract factory that is used to construct either a thread-based queue or a futures task-based queue. The client code doesn’t care about the underlying implementation, so I thought an abstract factory would be useful, but is there any pattern for this in Rust (besides the GoF advice)?

I’m tempted to just place an associated function on the package that returns the Queue trait that both thread queue and task queue conform to instead of creating a Factory type just to achieve the same thing.

nq::new(nd::Thread) -> Queue

instead of:

nq::QueueFactory.new(nq::Thread) -> Queue

Also, I’ve read in another post that I’d need to Box the returned value. Is that true, and if so, why?


#2

If you literally mean Rust’s closures, this will be somewhat problematic because each closure is a distinct type, and thus abstracting over them will require type erasure - i.e. a boxed closure, which is an allocation. You may want to consider defining a “Message” abstraction that contains the payload, and then having a common execution facility for it. If the callbacks are truly heterogeneous then you may have no choice but to use boxing.

The other consideration for closures is you’d presumably want the caller to give you an FnOnce closure, which gives the most flexibility in terms of what the closure can do. However, if you end up with type erasure, a Box<FnOnce> is not callable because you won’t be able to move the closure out of the box. There’s FnBox for this, but it’s not a stable API. So you’d likely need to use FnMut or Fn.

The type of queue is determined at runtime, and so you’ll need to erase the concrete type because it’s not known at compile time. Boxing provides the type erasure (i.e. a trait object).


#3

I had decided to use Box<Fn> from what I’d already read. Will I be able to move the Fn closure out of the box then?

So you have to box something when you want to erase it’s type, the compiler won’t let you return a trait object. Can you use a trait bound on my return type, would that not help? It would still need to be in a box?


#4

Fn doesn’t require moving out because its call method takes &self. So you’re fine on that front.

You can also define your own internal trait that allows moving out of a boxed FnOnce closure. Something like:

trait MyBoxFnOnce {
    fn call(self: Box<Self>);
}

impl<F: FnOnce()> MyBoxFnOnce for F {
    fn call(self: Box<Self>) {
        (*self)()
    }
}

Then you can accept a generic F: FnOnce from the caller, but box it up internally into a Box<MyBoxFnOnce> trait object.

You can erase the type with a reference too (e.g. &SomeTrait) but since that’s a reference, you won’t be able to move it across threads. So in practical terms, you’ll need a Box.

But now that I read your post again, you’re thinking of having two separate associated functions with each one returning a specific impl. So in that case, you can use impl Trait to return a particular impl without exposing the concrete type to the caller (impl trait should be in stable in a few weeks). In this case, you don’t need boxing. Sorry, for some reason I thought you were looking at returning a specific type from a single function.


#5

Nope, you had it right the first time.

Regarding the single method to return multiple types, what if I used generics like so (assuming ThreadQueue & TaskQueue both implement Queue):

pub fn new<Q>(q: Q) -> Q
where Q: Queue, {

    Q::new();
}

BTW: How do you get code formatting and syntax highlighting in this post input form?


#6

Here’re a few options (the impl trait versions require nightly for now, as mentioned):

trait Queue { }

#[derive(Default)]
struct ThreadQueue;
#[derive(Default)]
struct TaskQueue;

impl Queue for ThreadQueue {}
impl Queue for TaskQueue {}

// Caller has to know about Q as it's the one selecting it
// You can also put a dedicated `new() -> Self` on the `Queue` trait rather
// than using `Default` here.  However, that method will make `Queue` not object safe (if that's a concern).
fn new<Q: Queue + Default>() -> Q {
    Q::default()
}

// impl Trait approach - this doesn't require caller to know about the concrete types you have
fn new_thread() -> impl Queue {
    ThreadQueue
}

fn new_task() -> impl Queue {
    TaskQueue
}

To format code, you can enclose the code in [code] ... [/code] block or use triple backticks to enclose: ```code here ```


#7

I’m actually using nightly. That default bit interesting. Thanks very much! :slight_smile:


#8

To round out the previous example, here’s a case where you’d need type erasure:

fn pick_queue_dynamically(use_thread: bool) -> Box<Queue> {
    if use_thread {
        Box::new(ThreadQueue)
    } else {
        Box::new(TaskQueue)
    }
}

Here the caller has no clue about specific types, so there’s no generic parameter. You also cannot use impl Trait because two different types are returned, and impl Trait requires a single type.


#9

Thanks! With the previous generic example, could I still call new() instead of default() if I defined it on the Queue trait and implemented on the structs and that would still call the default inits?


#10

Yup that would be fine. Your Queue would be:

trait Queue {
   fn new() -> Self;
}

fn create<Q: Queue>() -> Q {
   Q::new()
}

Of course the caller can just create the instance themselves at this point without going through create().


#11

Ah Self! That’s what I was missing. Thank you, you’ve been extremely helpful :smiley:


#12

You will have problems doing as @vitalyd code suggests. It will likely result in;
“error[E0038]: the trait Queue cannot be made into an object”

If you (and anyone currently) using Nightly (without requiring stable) I would recommend adding;
#![feature(dyn_trait)]
It is very close to being stabilized. Helpful in making the language code slightly easier to understand.


#13

Ah! I have been seeing that error. Thanks for the advice.


#14

This is why I wrote the following comment in that code snippet :slight_smile:

// Caller has to know about Q as it's the one selecting it
// You can also put a dedicated `new() -> Self` on the `Queue` trait rather
// than using `Default` here.  However, that method will make `Queue` not object safe (if that's a concern).

#15

But for full disclosure, you can gate new() on Self: Sized and still make a trait object:

trait Queue { 
    fn new() -> Self where Self: Sized; // this fn is not callable on Queue trait objects
}

fn pick_queue_dynamically(use_thread: bool) -> Box<Queue> {
    if use_thread {
        Box::new(ThreadQueue)
    } else {
        Box::new(TaskQueue)
    }
}