业务背景
我们现在要实现一个图像处理软件。 要求尽可能设计出灵活 可扩展的架构方案。
思路梳理
在开始编码前,我们要考虑。 我们的软件需要有哪些功能。 不难猜出 对图像的处理手段也就那么几种。
- 图像缩放
- 图像旋转
- 图像滤镜
- 高斯模糊
- 对比度
当然实际业务场景肯定要比这个复杂的多。 这里只列出几个常用的,不要纠结这些。我们应该把更多的精力放到架构设计上。
其实我们不难发现,这是一个 序列操作 。 先对图像进行变换,再对图像进行处理。 或者先处理后变换。 万变不离其宗,因此我们可以使用 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 对象。