Rust 设计模式实践- 建造者模式

2023-01-24

业务背景

我们现在要实现一个图像处理软件。 要求尽可能设计出灵活 可扩展的架构方案。

思路梳理

在开始编码前,我们要考虑。 我们的软件需要有哪些功能。 不难猜出 对图像的处理手段也就那么几种。

  • 图像缩放
  • 图像旋转
  • 图像滤镜
    • 高斯模糊
    • 对比度

当然实际业务场景肯定要比这个复杂的多。 这里只列出几个常用的,不要纠结这些。我们应该把更多的精力放到架构设计上。

其实我们不难发现,这是一个 序列操作 。 先对图像进行变换,再对图像进行处理。 或者先处理后变换。 万变不离其宗,因此我们可以使用 Vec 存放 操作过程。

开始编码

根据上述思路 我们很容易就写出了 MVP 版本。

impl ImageSpec {
    pub fn new(specs: Vec<Spec>) -> Self {
        Self { specs }
    }
}

// 让 ImageSpec 可以生成一个字符串
impl From<&ImageSpec> for String {
    fn from(image_spec: &ImageSpec) -> Self {
        let data = image_spec.encode_to_vec();
        encode_config(data, URL_SAFE_NO_PAD)
    }
}

// 让 ImageSpec 可以通过一个字符串创建。比如 s.parse().unwrap()
impl TryFrom<&str> for ImageSpec {
    type Error = anyhow::Error;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        let data = decode_config(value, URL_SAFE_NO_PAD)?;
        Ok(ImageSpec::decode(&data[..])?)
    }
}

// 辅助函数,photon_rs 相应的方法里需要字符串
impl filter::Filter {
    pub fn to_str(&self) -> Option<&'static str> {
        match self {
            filter::Filter::Unspecified => None,
            filter::Filter::Oceanic => Some("oceanic"),
            filter::Filter::Islands => Some("islands"),
            filter::Filter::Marine => Some("marine"),
        }
    }
}

// 在我们定义的 SampleFilter 和 photon_rs 的 SamplingFilter 间转换
impl From<resize::SampleFilter> for SamplingFilter {
    fn from(v: resize::SampleFilter) -> Self {
        match v {
            resize::SampleFilter::Undefined => SamplingFilter::Nearest,
            resize::SampleFilter::Nearest => SamplingFilter::Nearest,
            resize::SampleFilter::Triangle => SamplingFilter::Triangle,
            resize::SampleFilter::CatmullRom => SamplingFilter::CatmullRom,
            resize::SampleFilter::Gaussian => SamplingFilter::Gaussian,
            resize::SampleFilter::Lanczos3 => SamplingFilter::Lanczos3,
        }
    }
}

// 提供一些辅助函数,让创建一个 spec 的过程简单一些
impl Spec {
    pub fn new_resize_seam_carve(width: u32, height: u32) -> Self {
        Self {
            data: Some(spec::Data::Resize(Resize {
                width,
                height,
                rtype: resize::ResizeType::SeamCarve as i32,
                filter: resize::SampleFilter::Undefined as i32,
            })),
        }
    }

    pub fn new_resize(width: u32, height: u32, filter: resize::SampleFilter) -> Self {
        Self {
            data: Some(spec::Data::Resize(Resize {
                width,
                height,
                rtype: resize::ResizeType::Normal as i32,
                filter: filter as i32,
            })),
        }
    }

    pub fn new_filter(filter: filter::Filter) -> Self {
        Self {
            data: Some(spec::Data::Filter(Filter {
                filter: filter as i32,
            })),
        }
    }

    pub fn new_watermark(x: u32, y: u32) -> Self {
        Self {
            data: Some(spec::Data::Watermark(Watermark { x, y })),
        }
    }
}


进行单元测试下

let spec1 = Spec::new_resize(600, 600, resize::SampleFilter::CatmullRom);

let spec2 = Spec::new_filter(filter::Filter::Marine);
let image_spec = ImageSpec::new(vec![spec1, spec2]);
let s: String = image_spec.borrow().into();
println!("{}", s);
assert_eq!(image_spec, s.as_str().try_into().unwrap());

发现问题

我们发现上面的过程有些繁琐。

需要先创建图像操作,然后添加到 Vec 中,然后再调用一些列的转换操作。

有没有更好的解决方案呢?

我们只需要给他提供一系列的操作,然后他帮我们 建造出来

重构代码

这里就要引出: 建造者模式。 通常 命名规范为 xxxBuilder

话不多说 上代码


struct ImageSpecBuilder {
    seq: Vec<Spec>,
}

impl ImageSpecBuilder {
    fn new() -> Self {
        Self { seq: vec![] }
    }

    pub fn with(mut self, s: Spec) -> ImageSpecBuilder {
        self.seq.push(s);
        self
    }
    pub fn build(self) -> ImageSpec {
        ImageSpec { specs: self.seq }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::borrow::Borrow;
    use std::convert::TryInto;

    #[test]
    fn encoded_spec_could_be_decoded() {
        let spec = ImageSpecBuilder::new()
            .with(Spec::new_resize(600, 600, resize::SampleFilter::CatmullRom))
            .with(Spec::new_filter(filter::Filter::Marine))
            .build();
        let s: String = spec.borrow().into();
        println!("{}", s);
        // let spec1 = Spec::new_resize(600, 600, resize::SampleFilter::CatmullRom);

        // let spec2 = Spec::new_filter(filter::Filter::Marine);
        // let image_spec = ImageSpec::new(vec![spec1, spec2]);
        // let s: String = image_spec.borrow().into();
        // println!("{}", s);
        // assert_eq!(image_spec, s.as_str().try_into().unwrap());
    }
}

代码解释: 我们构建了 ImageSpecBuilder 用来 创建我们的 Spec。 他的第一个方法: with 用来放我们每次处理图片的步骤,重点看这里 返回值。 他返回了 ImageSpecBuilder 这是 Rust 中 链式编程 惯用手段。 我们就可以在接下来的操作中 使用 with().with().with()。 然后每次操作 添加到内部 vec 中。 最后我们使用 build 方法 构建Spec 对象。