原型法

此原型法非原型模式,而是类似JavaScript中的原型扩展,在JS中,能够很轻松地为String类型“原地”扩展方法,如:

String.prototype.isDigit = function() {

return this.length && !(/\D/.test(this));

};

这个能力其实很好用,但是C++无法这样,一直觉得std::string的功能不足,想为其添加更丰富的如trim/split之类的语义,只能采用继承或者组合代理方式:

继承:用一个新类继承std::string,并为新类实现trim/split组合代理:用一个新类组合std::string,并为新类代理所有std::string的方法,包括各类构造方法和析构方法,再为新类实现trim/split

然后,使用std::string的地方替换成新类。这时候那种都比较复杂,组合的方式更复杂一些,所以也别无脑相信面向对象里“组合一定优于继承”。幸运的是,Rust能轻易完成原型法,比如有个bytes库提供了可廉价共享的内存缓冲区,避免不必要的内存搬运拷贝,bytes::BytesMut实现了可变缓冲区bytes::BufMut,有一系列为其写入u8、i8、u16、i16、slice等基础类型的接口,对于基础的通用的在bytes库中已经足够了,现在有个网络模块,想往bytes::BytesMut中写入std::net::SocketAddr结构,Rust可轻易为BytesMut扩展实现put_socket_addr:

pub trait WriteSocketAddr {

fn put_socket_addr(&mut self, sock_addr: &std::net::SocketAddr);

}

impl WriteSocketAddr for bytes::BytesMut {

fn put_socket_addr(&mut self, sock_addr: &std::net::SocketAddr) {

match sock_addr {

SocketAddr::V4(v4) => {

self.put_u8(4); // 代表v4地址族

self.put_slice(v4.ip().octets().as_ref());

self.put_u16(v4.port());

}

SocketAddr::V6(v6) => {

self.put_u8(6); // 代表v6地址族

self.put_slice(v6.ip().octets().as_ref());

self.put_u16(v6.port());

}

}

}

}

然后就可以使用BytesMut::put_socket_addr了,只需use WriteSocketAddr引入这个trait就可以,是不是很轻松!为何会这么容易?先看JS的原型法,其背后是原型链在支撑,调用String的方法,不仅在String对象里面查找,还会层层向String的父级、祖父级prototype查找,一旦找到就可以调用,而每个prototype本质上都是个Object,可以获取并编辑它们,ES6的继承本质上也是原型链。所以可以拿到String类的prototype,在它上面为其增加isDigit,就能让所有的String对象都能享受isDigit函数的便利,可谓十分方便。但是C++就不行了,也想拿到std::string的函数表,然后一通编辑为其添加trim/split行为,奈何C++不允许这危险的操作啊,只能派生子类,即便子类仅仅只包含一个std::string。那Rust为何可以,关键就是trait函数表与传统面向对象的虚函数表解藕了,后果就是,类型没有绑死函数表,可以为类型增加新trait函数表,然后就有了上面的Rusty原型法。类似的还可以为Rust的String扩展is_digit/is_email/is_mobile,一样地简单。一般有ext模块,就很可能发现原型法的身影,比如tokio::io::AsyncReadExt。

原型法是最能体现trait函数表与传统面向对象虚函数表分离优势的设计模式!注意,Rust的原型法并没有产生任何新类型,只是增加了一个新的trait函数表,所以一开始称之为“原地”扩展,是比JS更干净的原型法,个人非常喜欢用这个模式,能用就用!更进阶的,Rust还能为所有实现了bytes::BufMut的类型扩展实现WriteSocketAddr特型,而不仅仅只为bytes::BytesMut实现:

/// 可以这样读:为所有实现了ButMut特型的类型实现WriteSocketAddr

/// bytes::BytesMut也不过是T的一种,代码复用性更佳

impl WriteSocketAddr for T {

fn put_socket_addr(&mut self, sock_addr: &std::net::SocketAddr) {

// 同样的代码

}

}

原型法跟模板方法还有些联系,也算模板方法衍生出来的设计模式,因为子类如果不依赖父类,并且子类还不需要有任何字段,不需要有自己独特的结构就能实现算法策略时,那子类也不用依赖注入到父类了,直接在父类的基础上“原地“扩展,更加轻量。总结一下模板方法的衍生变化:

模板方法:

子类拥有自己的结构,并依赖父类的结构和行为才能完成,是模板方法子类拥有自己的结构,但不依赖父类结构和行为也能完成,可不用继承转而采用组合依赖注入,最好多达2个以上组合,达成策略组合模式子类不需有自己的结构(或者一个空结构),依赖父类的结构和行为就能完成,只是算法在父类模块中不通用而没实现,可不用继承也不用组合,“原地”扩展,原型法即可子类不需有自己的结构,也不依赖父类,那这么独立也跟父类没任何关系了,理应属于其它模块

回到面向对象,凡是Rust能轻松做到的,面向对象却无法轻松做到的,就是面向对象该被批评的点。。面向对象说我服,早知道也不把虚函数表与对象内存结构绑死了。所谓长江后浪推前浪,新语言把老语言拍死在沙滩上,即便C++20如此强大,不改变虚函数表的基础设计,在原型法上也永远追赶不上Rust语言的简洁。

装饰器模式

上节说到,策略模式,要是为复合类型也实现trait,就类似装饰器模式,因为装饰器无论是内部委托成员,还是外部装饰器自己,都得实现同一个名为Decorate的trait,就是为了让它们可以相互嵌套组合:

trait Decorate {

fn decorate(&mut self, params...);

}

/// 一个静多态的装饰器

struct SomeDecorator {

delegate: D, // 必要的委托

...

}

/// 还得为Decorator自己实现Decorate特型

impl Decorate for SomeDecorator {

fn decorate(&mut self, params...) {

// 1. SomeDecorator itself do sth about params

self.do_sth_about_params(params...); // 这是真正要装饰的实现

// 2. then turn self.delegate

self.delegate.decorate(params...); // 这一句都相同,1、2步的顺序可互换

}

}

/// 另一个装饰器

struct AnotherDecorator {

delegate: T,

...

}

impl Decorate for AnotherDecorator {

fn decorate(&mut self, params...) {

// 1. AnotherDecorator itself do sth about params

self.do_sth_about_params(params...);

// 2. then turn self.delegate

self.delegate.decorate(params...); // 这一句都相同

}

}

/// 必要的终结型空装饰器

struct NullDecorator;

impl Decorator for NullDecorator { /*do nothing*/ }

/// 使用上

let d = SomeDecorator::new(AnotherDecorator::new(NullDecorator));

d.decorate();

SomeDecorator/AnoterDecorator是真正的装饰器,会有很多个,功能各异,每个Decorator所包含的相应的结构可能也不同。装饰器在使用上,就像链表一样,一个处理完之后,紧接着下一个节点再处理,它把链表结构包含进了装饰器的结构里面,并用接口/trait来统一类型。上述实现有重复代码,就是调用委托的装饰方法,还能继续改进:

/// 装饰的其实是一个处理过程

trait Handle {

fn handle(&mut self, params...);

}

trait Decorate {

fn decorate(&mut self, params...);

}

/// 装饰器的终结

struct NullDecorator;

impl Decorate for NullDecorator {

fn decorate(&mut self, params...) {

// do nothing

}

}

/// 通用型装饰器,像是链表节点串联前后2个处理器节点

struct Decorator {

delegate: D,

handler: H, // 这又是个干净的模板方法,将变化交给子类

}

/// 通用装饰器本身也得实现Decorate特质,可以作为另一个装饰器的D

impl Decorate for Decorator {

fn decorate(&mut self, params...) {

// 这两步可互换

self.handler.handle(params);

self.delegate.decorate(params);

}

}

/// 下面的处理器只关注处理器自己的实现就好了

struct SomeHandler { ... };

impl Handler for SomeHandler { ... }

struct AnotherHandler { ... };

impl Handler for AnotherHandler { ... }

/// 使用上

let d = Decorator {

delegate: Decorator {

delegate: NullDecorator,

handler: AnotherHandler,

},

handler: SomeHandler,

};

d.decorate(params...);

可以看出,装饰器很像链表,emm...大家都知道链表在Rust中较复杂,那链表有多复杂,装饰器就有多复杂。上面的静多态实现也是不行的,不同的装饰器组合,就会产生不同的类型,类型可能随着Handler类型数目增加呈其全排列阶乘级类型爆炸,忍不了,必须得换用指针。装饰器模式,Rust实现起来不如传统面向对象,面向对象天然动多态,且Decorator继承可以让D、H两部分合为一体,让H也成装饰类的一个虚函数,都在this指针访问范围内,简单一些。而Rust将装饰器拆解成了链表型,将装饰器的底层结构还原了出来,确实装饰器可以用链表串联起各个处理器一个接一个地调用,效果一样的。只是面向对象技巧隐藏了链表的细节。

不过Rust有个很牛逼的装饰器,就是迭代器的map、step_by、zip、take、skip这些函子,它们可以随意串联组合调用,本质就是装饰器,只不过仅限于用在迭代器场景。如果装饰器能这样实现,能惰性求值,也能够编译器內联优化,就太强了。不过,各个装饰器功能不同,恐怕不能像迭代器函子那样都有清晰的语义,因此没有统一的装饰器库。不过装饰器实现时,肯定可以借鉴迭代器的函子思路。这样一来的话,Rust的装饰器又丝毫不弱于传统面向对象的了。而且,高,实在是高,妙,实在是妙!

/// 以下仅作摘选,让大家一窥迭代器函子的装饰器怎么玩的

pub trait Iterator {

type Item;

// Required method

fn next(&mut self) -> Option;

// Provided methods

// 像下面这样的函数还有76个,每个函数都映射到一个具体的装饰器,它们都返回一个装饰函子impl Iterator

// 装饰器函数基本都定义完了,未来无法扩展?还记得原型法吗,为所有实现了Iterator的类型实现IteratorExt

// 仅挑选一个step_by作为案例

#[inline]

#[stable(feature = "iterator_step_by", since = "1.28.0")]

#[rustc_do_not_const_check]

fn step_by(self, step: usize) -> StepBy

where

Self: Sized,

{

StepBy::new(self, step)

}

}

/// StepBy装饰器,如第一种实现那样的写法

pub struct StepBy {

iter: I, // 装饰器的delegate

step: usize,

first_take: bool,

}

/// 再为StepBy实现Iterator

impl Iterator for StepBy

where

I: Iterator,

{

type Item = I::Item;

#[inline]

fn next(&mut self) -> Option {

self.spec_next()

}

}

// 使用上,有别于传统装饰器模式从构建上去串联,这是利用返回值链式串联,顿时清晰不少

vec![1, 2, 3].iter().skip(1).map(|v| v * 2);

小结

至此,模板方法的变化告一断落。之前,有人说Rust不支持面向对象,导致Rust不好推广,实际上并不是,哪个OO设计模式Rust实现不了,还更胜一筹。因此,并非Rust不支持面向对象!有些设计模式,Rust天生也有,如:

单例模式:其实单例模式如果不是为了懒加载,跟使用全局变量没啥差别;如果为了懒加载,那lazy_static或者once_cell就够用。(补充:标准库已经标准化成OnceLock了)代理模式:NewType模式作代理挺好;或者原型法“原地”扩展代理行为迭代器模式:Rust的迭代器是我见过最NB的迭代器实现了状态机模式:Rust语言官方文档中的NewType+enum状态机模式,这种静多态的状态机非常严格,使用上都不会出错,所有状态组合还可以用enum统一起来,比面向对象的状态机模式要好

还有一些设计模式,跟其它模式很像,稍加变化:

适配器模式:同代理模式差别不大,很可能得有自己的扩展结构,然后得有额外“兼容处理”逻辑来体现“适配”桥接模式:就是在应用策略模式过滤器模式:就是在应用装饰器模式

还有一些设计模式,读者可自行用Rust轻松实现,如观察者模式之流。后续不会为这些设计模式单独成文了,除非它有点意思,访问者模式就还可以,只不过实际应用不咋多。有想用Rust实现哪个设计模式有疑问的,可留言交流。

罗列所有设计模式没啥意思,我也无力吐槽这么多设计模式,至今很多人仍区分不清某些设计模式的区别,因为设计模式在描述它们的时候,云里雾里的需求描述,关注点、应用场景不一样云云,什么模式都得来一句让“抽象部分”与“实现部分”分离,跟都整过容一样相似的描述,让人傻傻分不清。至今我再看各种设计模式,想去了解其间区别,都觉得无聊了,浪费时间!被大众广泛记住的设计模式就那么几个,因为基础的设计就那么几个,当你在使用接口、指针/引用、组合的时候,其实就在不知不觉中使用设计模式了。

上段是在批评设计模式没错,并不是说设计模式一无是处,能总结出模式作为编程界通用设计语言意义非凡。懂它肯定比不懂的强,要是都能区分清各类设计模式了,肯定是高手中的高手了,看懂这一系列文章不难。设计模式的套用,归根结底是为了代码复用,良好的可读性。大家看到相似模式的代码,一提那种设计模式就能明白。遗憾的是,即便是同一个设计模式,因为乱七八糟的类型、胡乱命名、粗糙的掺杂不少杂质的实现,为不停变化的需求弄的面目全非者,让人读起来,实在很难对的上有某种设计,这并非设计模式的锅,而是编程素质不专业、太自由发挥、总见多识少地自创概念/二流招式的毛病招致的。

在这方面,Rust的解决方案 极具 吸引力。后续对比着面向对象,讲讲Rusty那味,味道不错但更难掌握,属于基础易懂,逻辑一多就复杂(废话)!

精彩内容

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: