[Program design question] Composing structs that implement same traits

Hi,

I have a (G)UI struct with a bunch of methods, and at some point I realized that I want to optionally log all these methods. Instead of adding logging the to (G)UI crate I defined a UI trait, implemented that in the (G)UI crate, and then created a logging crate with a logger struct that also implements the UI trait. The idea is that I want to optionally compose the GUI type with the logger type so that instead of calling two methods every time or doing logging in the GUI crate I define the GUI and logging in completely separate crates (none uses the other), I want to just do ui.method() and it both updates the GUI and logs.

For that I defined this struct:

#[derive(Clone)]
pub struct CombinedUIs<UI1, UI2> {
    ui1: UI1,
    ui2: UI2,
}

and implemented the UI trait for it:

impl<UI1: UI, UI2: UI> UI for CombinedUIs<UI1, UI2> {
    ...
}

However this type is really hard to use:

  • I can't return different impl UIs in a conditional so I can't have a variable with type impl UI that is either CombinedUIs<GUI, Logger> or just GUI.

  • If I Box my UI impls, in addition to having yet another layer of indirection I lose Clone even though both the logger and the GUI are Clone.

    (I need Clone to be able to share references of this value with tokio tasks)

At this point I'm a bit lost and I'm looking for alternative designs. The idea behind my original idea was that

  • Logger and GUI code should be completely separated
  • The use site should not care about whether the logging is enabled or not, the code should be the same regardless and there should be no mentions of any concrete types other than the UI trait.
  • I shouldn't have to call two methods every time I update the GUI (one for the actual update, one for logging).

Any pointers would be appreciated.