Cross-platform abstract design

I aim to implement platform abstraction where callers use a unified interface without platform-specific logic. My initial approach returned platform-specific types directly via conditional compilation:

// -- snap --
pub trait BtDevice {
    fn disconnect(&self) -> Result<(), Box<dyn std::error::Error>>;
}
​
#[derive(Default)]
pub struct WindowsBtDevice;
​
#[derive(Default)]
pub struct LinuxBtDevice;
​
#[derive(Default)]
pub struct MacOSBtDevice;
​
#[derive(Default)]
pub struct UnknownBtDevice;
​
pub struct Device;
​
impl Device {
    #[cfg(target_os = "windows")]
    pub fn new() -> WindowsBtDevice {
        WindowsBtDevice::default()
    }
    
    #[cfg(target_os = "linux")]
    pub fn new() -> LinuxBtDevice {
        LinuxBtDevice::default()
    }
       
    #[cfg(target_os = "macos")]
    pub fn new() -> MacOSBtDevice {
        MacOSBtDevice::default()
    }
 
    #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
    pub fn new() -> UnkonwnBtDevice {
        UnknownBtDevice::default()
    }
}
​
impl BtDevice for WindowsBtDevice {
    fn disconnect(&self) -> Result<(), Box<dyn std::error::Error>> {
        // do_something...
    }
}
​

Copilot Reviewer recommended refactoring to return a unified Deviceenum encapsulating all platforms:

// -- snap --
pub trait BtDevice {
    fn disconnect(&self) -> Result<(), Box<dyn std::error::Error>>;
}
​
#[derive(Default)]
pub struct WindowsBtDevice;
​
#[derive(Default)]
pub struct LinuxBtDevice;
​
#[derive(Default)]
pub struct MacOSBtDevice;
​
#[derive(Default)]
pub struct UnknownBtDevice;
​
pub enum Device {
    Windows(WindowsBtDevice),
    Linux(LinuxBtDevice),
    MacOS(MacOSBtDevice),
    Unknown(UnknownBtDevice),
}
​
impl Device {
    pub fn new() -> Self {
        #[cfg(target_os = "windows")]
        {
            Device::Windows(WindowsBtDevice::default())
        }
        #[cfg(target_os = "linux")]
        {
            Device::Linux(LinuxBtDevice::default())
        }
        #[cfg(target_os = "macos")]
        {
            Device::MacOS(MacOSBtDevice::default())
        }
        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
        {
            Device::Unknown(UnknownBtDevice::default())
        }
    }
}
​
impl BtDevice for Device {
    fn disconnect(&self) -> Result<(), Box<dyn std::error::Error>> {
        match self {
            // i don't like this :< 
            Device::Windows(device) => device.disconnect(),
            Device::Linux(device) => todo!(),
            Device::MacOS(device) => todo!(),
            Device::Unknown(device) => todo!(),
        }
    }
    
}
​
impl BtDevice for WindowsBtDevice {
    fn disconnect(&self) -> Result<(), Box<dyn std::error::Error>> {
        // do_something...
    }
}
​

My concern: The match in BtDevice for Device requires boilerplate delegation for every platform variant. Is this enum-based design truly beneficial compared to the initial approach?

Given what you have shown… then no. There’s no benefit from having closed-ended polymorphism. On top of that, you already have open-ended polymorphism with the BtDevice trait, which seems to be the type of polymorphism your design would benefit the most from.

3 Likes

Thank you! I decide to follow the former

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.