Hello!
This post is the conclusion of my previous question. I would like to gather feedback to the final implementation of the strategy pattern. EDIT: the strategy pattern should allow for client to be able to implement there own strategies (think function pointer, not switch/enum).
My criterias:
- easy to use and hard/impossible to misuse
- easy to implement
- zero-cost abstraction
… take 3!
In C++ my preferred implementation of the strategy pattern is using NVI (Non Virtual Interface):
class BaseStategy {
public:
// public, non-overridable
void do_something() {
// do some common stuff
do_something_impl();
// do some other common stuff
}
private:
// private, must be overriden
virtual void do_something_impl() = 0;
}
class ImplementationStrategy: public BaseStrategy {
void so_something_impl() override { /* ... */ }
}
Since what I want is a specific implementation of the Strategy pattern, I will take the example an example that should be relatively representative of a real example:
- pass self to the inner function (it's technically not needed if we only need static dispatch here, but it's both needed for dynamic dispatch, and if we want to use the same template for another usage).
- the implementation is non-trivial and uses a few helpers that are common to all implementations
First let's define the public interface
use std::fmt;
use std::fmt::{Display, Formatter};
pub trait TextFormatter
{
fn bold<T: Display>(&self, value: T) -> impl Display;
fn strike<T: Display>(&self, value: T) -> impl Display;
}
Since -> impl Trait
isn't valid Rust syntax yet, let's re-write it. This is just a current limitation of the ergonomic of the Rust language and shouldn't be considered a real problem. The implementation details on how to do it is explained later.
This public interface allow anyone to add formatting to anything that implements display. Let's use it.
pub fn usage<T: TextFormatter>(formatter: &T, some_value: i32) {
println!("A number in bold: {}", formatter.bold(some_value));
println!("A number striked: {}", formatter.strike(some_value));
}
This function can only use the public interface of our TextFormatter
and doesn't know any of the implementation which is perfect.
Now let's use this function (assuming that we have two types MarkdownFormatter
and HtmlFormatter
that implements TextFormatter
:
pub fn main() {
// prints
// ```
// A number in bold: **42**
// A number stiked: ~~42~~
// ```
usage(&MarkdownFormatter, 42);
// prints
// ```
// A number in bold: <strong>42</strong>
// A number stiked: <del>42</del>
// ```
usage(&HtmlFormatter, 42);
}
So far, so good. The second task is to creates those implementation. First let's re-write the public interface in valid current Rust:
// public interface
pub struct FormattedBold<'a, F: TextFormatter + 'a + ?Sized, T: Display>(&'a F, T);
pub struct FormattedStrike<'a, F: TextFormatter + 'a + ?Sized, T: Display>(&'a F, T);
pub trait TextFormatter
{
type Self_: private::TextFormatterImpl;
fn bold<'a, T: Display + 'a>(&self, value: T) -> FormattedBold<Self::Self_, T>
where
FormattedBold<'a, Self::Self_, T>: Display + 'a,
Self::Self_: 'a;
fn strike<'a, T: Display + 'a>(&self, value: T) -> FormattedStrike<Self::Self_, T>
where
FormattedStrike<'a, Self::Self_, T>: Display + 'a,
Self::Self_: 'a;
}
Implementing Display
for FormattedBold
and FormattedStrike
is a bit repetitive, so we want to create helpers. Types that implements FormattedText
should only need to implements those helper.
// private interface
pub trait TextFormatterImpl {
fn bold_impl<T: Display>(&self, f: &mut Formatter<'_>, value: &T) -> fmt::Result;
fn strike_impl<T: Display>(&self, f: &mut Formatter<'_>, value: &T) -> fmt::Result;
}
// common to all implementation of TextFormatter
impl<F: TextFormatterImpl, T: Display> Display for FormattedBold<'_, F, T>
{
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
F::bold_impl(&self.0, f, &self.1)
}
}
impl<F: TextFormatterImpl, T: Display> Display for FormattedStrike<'_, F, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
F::strike_impl(&self.0, f, &self.1)
}
}
Each type that implements TextFormatterImpl
should get TextFormatter
for free, so let's add the glue:
impl <TF: TextFormatterImpl> TextFormatter for TF {
type Self_ = Self;
fn bold<'a, T: Display + 'a>(&self, value: T) -> FormattedBold<Self::Self_, T>
where
FormattedBold<'a, Self::Self_, T>: Display + 'a,
Self::Self_: 'a,
{
FormattedBold(&self, value)
}
fn strike<'a, T: Display + 'a>(&self, value: T) -> FormattedStrike<Self::Self_, T>
where
FormattedStrike<'a, Self::Self_, T>: Display + 'a,
Self::Self_: 'a,
{
FormattedStrike(&self, value)
}
}
And now the fun part, let's implement our concrete implementations:
// concrete implementation of TextFormatter
pub struct MarkdownFormatter;
impl TextFormatterImpl for MarkdownFormatter {
fn bold_impl<T: Display>(&self, f: &mut Formatter<'_>, value: &T) -> fmt::Result {
write!(f, "**{}**", value)
}
fn strike_impl<T: Display>(&self, f: &mut Formatter<'_>, value: &T) -> fmt::Result {
write!(f, "~~{}~~", value)
}
}
pub struct HtmlFormatter;
impl TextFormatterImpl for HtmlFormatter {
fn bold_impl<T: Display>(&self, f: &mut Formatter<'_>, value: &T) -> fmt::Result {
write!(f, "<bold>{}</bold>", value)
}
fn strike_impl<T: Display>(&self, f: &mut Formatter<'_>, value: &T) -> fmt::Result {
write!(f, "<del>{}</del>", value)
}
}
As you can see implementing a new text formatter is really easy.
playground
EDIT: I think it was a mistake to have the private implementation (TextFormatterImpl
) in a non-public module since client should be able to implement their own version of TextFormatterImpl
.
I would like to know what you think of this approach.
Personally what I like:
- implementing a specialization of
TextFormatter
is really easy withTextFormatterImpl
. - no implementation details are leaked is the public API (
TextFormatter
).
What I dislike:
- it's not explicit that implementing
TextFormatterImpl
imply that you are in reality implementing aTextFormatter
- it is possible to implement
TextFormatter
without implementingTextFormatterImpl
which could have bad consequences for example ifbold()
orstrike()
had to do some business logic validation prior to callbold_impl()
/strike_impl()
and those validation could be skiped -> it's possible to mis-implement the public API.
Alternatively it would be possible to have TextFormatter
that have TextFormatterImpl
as super trait. It would solve my first pain point (the explicitness) at the cost of making the private API leaking witch I'm not a fan of.