Suggestion for better structuring of the code


#1

So, I have this issue of how to correctly structure some of the types in my code and I was hoping for a suggestion. A brief description of the problem goes like this:

  1. I have multiple objects which have a lot (if not all) fields which are common
  2. Each object, however, needs to have the behaviour of a different method
  3. When executing various methods any modifications to the common fields are exactly the same across all objects.
    So the first two suggest that I should use a trait. However, because of 3, in order not to replicate code, I made a common class, which has as fields the base and a Box. However, I’m not too confident this is a good choice and if there is a better one. Below I provide a short snippet of a simplified version of what I have so far (a playground link - https://play.rust-lang.org/?gist=b5694e5bcbc110434a6e175a7643808e)
use std::fmt::Debug;

#[derive(Default, Debug, Clone)]
pub struct Base {
    field1: u32,
    field2: f64,
    //.... more
}

pub trait Api: Debug {
    fn clone_box(&self) -> Box<Api>;
    fn method1(&self, base: &Base) -> f64;
}

#[derive(Debug)]
pub struct Object {
    base: Base,
    api: Box<Api>
}

impl Clone for Object {
    fn clone(&self) -> Self {
        Object {
            base: self.base.clone(),
            api: self.api.clone_box()
        }
    }    
}

impl Object {
    pub fn method(&mut self) -> f64{
        self.base.field1 += 1;
        self.api.method1(&self.base)
    }
}

#[derive(Default, Debug, Clone)]
struct Implementator1 {}
impl Api for Implementator1 {
    fn clone_box(&self) -> Box<Api> {
        Box::new(self.clone())    
    }
    
    fn method1(&self, base: &Base) -> f64 {
        base.field2 + 1.0
    }
}

#[derive(Default, Debug, Clone)]
struct Implementator2 {}
impl Api for Implementator2 {
    fn clone_box(&self) -> Box<Api> {
        Box::new(self.clone())    
    }
    
    fn method1(&self, base: &Base) -> f64 {
        base.field2 + 2.0
    }
}


pub fn main() {
    let o1 = Object{base: Base::default(), api:Box::new(Implementator1{})};
    let o2 = Object{base: Base::default(), api:Box::new(Implementator2{})};
    let mut v = vec![o1, o2];
    println!("{:?}", v);
    println!("{}", v.get_mut(0).unwrap().method());
    println!("{}", v.get_mut(1).unwrap().method());
}

#2

I tried turning your solution inside-out, which is at least different, though may or may not be better:

use std::fmt::Debug;

#[derive(Default, Debug, Clone)]
pub struct Base<T> {
    field1: f64,
    field2: f64,
    //.... more,
    extra: T,
}

trait Api {
    fn method1(&self) -> f64;
}

#[derive(Default, Debug, Clone)]
struct Implementator1 {}

impl Api for Base<Implementator1> {
    fn method1(&self) -> f64 {
        self.field2 + 1.0
    }
}


#[derive(Default, Debug, Clone)]
struct Implementator2 {}

impl Api for Base<Implementator2> {
    fn method1(&self) -> f64 {
        self.field1 + 2.0
    }
}

pub fn main() {
    let o1 = <Base<Implementator1>>::default();
    let o2 = <Base<Implementator2>>::default();
    let v: Vec<Box<Api>> = vec![Box::new(o1), Box::new(o2)];
    println!("{}", v[0].method1());
    println!("{}", v[1].method1());
}

https://play.rust-lang.org/?gist=922a8a751de8f7899ced76fc49cdf129&version=stable


#3

Thanks. I think this indeed is the second possible approach. Can’t say which one is better either way but thanks for sharing I will need to consider it


#4

Can you say a bit more about how these types would be used? And what are you optimizing for? Readability? Conciseness? Performance? Extensibility? Something else? In other words, what’s the criteria for “better”?

To throw another possibility out there:

pub struct Object {
    base: Base,
    api: fn(&Base) -> f64,
}

impl Object {
    fn method(&mut self) -> f64 {
       self.base.field1 += 1;
       (self.api)(&self.base)
    }
}

This is a less flexible form, but avoids generics, traits, and boxes. Any closure that doesn’t capture its environment can be coerced to this fn pointer, so you don’t necessarily need pre-baked functions.

Another design option is to use enums; this might work well if you have a closed set of variants that you control. That would obviate the need for boxes as well.


#5

Ok so to give context - I’m trying to migrate ccxt to Rust. The Base class is essentially what they have for an AbstractExchange. The trait API defines what all crypto exchanges must abide in order to be able to fetch relevant data. And the Object is just a convenience for being able to have multiple exchanges in the same type. I do want to add at some point the async calls such that I can fetch multiple exchanges simultaneously, but the main target, for now, is not raw speed, but more readability and conciseness (since usually, the APIs are quite slow you literally can’t get too much more out of that bottleneck). I’m thnking how to structure this properly so that it is easy to use and maintain.

Closures are also possible, but I do need to go through all of the Exchanges implemented there to make sure no exchange does not have their own adhoc internal state that needs to be maintained.