Skip to main content
added 2 characters in body
Source Link
toolic
  • 16.4k
  • 6
  • 29
  • 221

This design uses TypeIdTypeId and stores factory functions in a HashSet for dynamic report registration.

This design uses TypeId and stores factory functions in a HashSet for dynamic report registration.

This design uses TypeId and stores factory functions in a HashSet for dynamic report registration.

Source Link

Rust Library Design: Enum-Based vs TypeId-Based Plugin Registration for Trait Objects

I'm building a Rust library that supports multiple analysis reports, all implementing a common Report trait. I'm considering two design approaches and would appreciate a code review focusing on maintainability, extensibility, and clean design.

The main decision point is how to store and construct multiple types implementing the Report trait.

Structure 1: Type-Based Plugin System Using TypeId

This design uses TypeId and stores factory functions in a HashSet for dynamic report registration.

report.rs

use std::any::{Any, TypeId};
use std::fmt::Debug;

pub trait Report: Debug + Any {
    fn process(&mut self, match_: &str, index: usize);
    fn as_any(&self) -> &dyn Any;
    fn boxed() -> Box<dyn Report>
    where
        Self: Sized;
}

analysis.rs

#[derive(Debug, Default)]
pub struct AnalysisConfig {
    config: HashSet<fn() -> Box<dyn Report>>,
}

#[derive(Debug, Default)]
pub struct Analysis {
    analysis_config: AnalysisConfig,
    reports: HashMap<TypeId, Box<dyn Report>>,
}

impl AnalysisConfig {
    pub fn add<T: Report>(&mut self) {
        // each type T that implements Report has a boxed fn
        self.config.insert(|| T::boxed());
    }

    pub fn remove<T: Report>(&mut self) {
        // FIX: now remove option is tough due to closure
    }

    pub fn build(&self) {
        // return HashMap<TypeId, Box<dyn Report>> by calling items in self.config
    }
}

impl Analysis {
    pub fn from_analysis_options(options: AnalysisOptions) -> Analysis {
        let mut reports = HashMap::new();

        for option in &options.options {
            let report = report::from_analysis_option(option);
            reports.insert((&*report).type_id(), report);
        }

        Analysis {
            analysis_options: options,
            reports,
        }
    }

    pub fn report<T: report::Report>(&self) -> Option<&T> {
        let r = self.reports.get(&TypeId::of::<T>());
        if let Some(r) = r {
            return r.as_any().downcast_ref::<T>();
        }
        None
    }
}

Structure 2: Enum-Based Static Registration

This approach uses an enum to represent all known reports and a function to match each variant to a concrete implementation.

report.rs

pub trait Report: Debug + Any {
    fn process(&mut self, match_: &str, index: usize);
    fn as_any(&self) -> &dyn Any;
}

pub fn from_analysis_option(option: &AnalysisOption) -> Box<dyn Report> {
    match option {
        AnalysisOption::UniqueMatches => Box::new(UniqueMatchesReport::new()),
        AnalysisOption::MatchFrequency => Box::new(MatchFrequencyReport::new()),
        AnalysisOption::MatchIndices => Box::new(MatchIndicesReport::new()),
        AnalysisOption::MatchCount => Box::new(MatchCountReport::new()),
    }
}

analysis.rs

#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub enum AnalysisOption {
    UniqueMatches,
    MatchFrequency,
    MatchIndices,
    MatchCount,
}

pub struct AnalysisOptions {
    options: HashSet<AnalysisOption>,
}

impl Analysis {
    pub fn from_analysis_options(options: AnalysisOptions) -> Analysis {
        let mut reports = HashMap::new();

        for option in &options.options {
            let report = report::from_analysis_option(option);
            reports.insert((&*report).type_id(), report);
        }

        Analysis {
            analysis_options: options,
            reports,
        }
    }

    pub fn report<T: report::Report>(&self) -> Option<&T> {
        let r = self.reports.get(&TypeId::of::<T>());
        if let Some(r) = r {
            return r.as_any().downcast_ref::<T>();
        }
        None
    }
}
  1. Which approach is more idiomatic in Rust for plugin-style architectures?

  2. Is there a clean way to overcome the downsides of both designs—supporting extensibility while keeping maintainable keys?