北海 - Rust与面向对象(三)

news2024/9/29 11:25:14

策略模式

上节说到,模板方法变化一下就能成策略模式,怎么变化的?且看策略模式典型案例:

pub trait Fly {
    fn fly(&self);
}

pub trait Quack {
    fn quack($self);
}

/// 先以静多态的方式实现
/// 似 trait Fly + Quack就是Duck,只是Fly和Quack独立地变化
struct Duck<F, Q> 
where
    F: Fly,
    Q: Quack,
{
    fly_behabior: F,      // 单看这个成员,与模版方法如出一辙
    quack_behavior: Q,    // 一样,将不同的算法部分交给子类去实现
}

impl<F, Q> Duck<F, Q> 
where
    F: Fly,
    Q: Quack,
{
    pub fn new(fly_behavior: F, quack_behavior: Q) {
        Self { fly_behavior, quack_behavior }
    }
}

/// 实现不同的Fly、Quack策略,参考下图,省略...
/// 下图引用自 Oreilly.Head First Design Pattern

6ddae2c06eb504606ae1ed3b56f1f6c6.jpeg

以上是策略模式的简单案例,策略模式可以说是模板方法的衍生变化。还记得上一章中第一种模板方法的实现方式不,单看Fly就是模板方法:模板方法里子类完全不依赖父类,干净地完成算法策略,那子类就能够依赖注入到父类中;最好这种子类不止一个,比如不仅有Fly还有Quack,就是纯正的策略组合模式了。了解这种变化可以帮助区分二者,比那说不清道不明的优缺点、适用场景描述能让你更清晰、透彻地认识到两者的差别与联系。

策略模式,公认的妙。上面是静多态实现的策略模式,会遇到类型爆炸的问题,比如有2种飞行方式、3种呱呱叫方式,那总共有2*3=6种复合类型,体现了组合是类型系统中的积类型。在嵌入式上,因为内存环境限制,类型爆炸导致程序大小变大成了问题,不得不改用动多态,以减少类爆炸带来的影响。

/// 动多态,类型统一了,类型也不会爆炸了
struct DynamicDuck {
    fly_behavior: Box<dyn Fly>,
    quack_behavior: Box<dyn Quack>,
}

面向对象语言,都是动多态,Java对象皆引用,当引用没地方用了就垃圾回收;C++没有指针则玩不转面向对象,只可能将子类指针赋值给父类指针来多态,无法将子类对象赋值给父类对象来多态吧!所以面向对象的策略模式是动多态,天然无类型爆炸问题。

那类型爆炸一定差吗,类型统一就肯定好吗?先讨论下类型爆炸合理不。自然界生物划分“界门纲目科属种”,动物界有那么多动物,比如都是猫科动物,难道老虎和狮子还不配拥有个自己的类型吗,只能共用猫类型吗?要是想为老虎这个类型单独实现点东西,但不想为狮子也实现这个东西,共用猫类型就不行了!这样看起来,接受类型爆炸挺好,类型完整,也没几个类型,程序大小允许就可以,相比于动不动就异步的task、协程,只要不是大规模类型爆炸,可以忍。而类型统一就会造成一种“类型丢失”,它的不良影响发生在后续为Duck添加其它行为时,这些行为并非所有Duck都需要的时候。比如为绿头鸭实现捕猎,为橡皮鸭实现电动,它们不再是所有鸭子都应有的行为,已有点不再适合使用新策略扩展(可不是所有扩展的行为都是鸭子通用型的Swim、Display,策略模式只拣好的说),但动多态却因“类型丢失”而不知所措,这其实是个难处理的点,本质是为了减少类型爆炸而采用动多态统一类型的牺牲。

/// 静多态可以直接别名
type MallardDuck = Duck<...>;
type RubberDuck = Duck<...>;
type DecoyDuck = Duck<...>;


/// 动多态因“类型丢失”,只能使用NewType,并在NewType中约束DynamicDuck。
/// 那这样,类型还是难免爆炸了啊!
struct MallardDuck(DynamicDuck);
struct RubberDuck(DynamicDuck);
struct DecoyDuck(DynamicDuck);

/// 仅为绿头鸭MallardDuck实现捕猎
impl MallardDuck {
    fn hunt(&self) {
        ...
    }
}

动多态策略模式再往下写很可能就开始坏味道了。为了解决这个问题,各种奇招就来了,如不管三七二十一,先把捕猎行为塞进Duck中,管其它鸭子会不会错用呢;或者,为橡皮鸭RubberDuck、木头鸭WoodDuck也实现个假的捕猎,这样“捕猎”就又符合新的策略了,又能使用策略模式了;又或者,再来次继承把绿头鸭子类化吧,然后单独给绿头鸭实现捕猎。。然而新类型MallardDuck一方面与动多态复合类型的Duck意义有冲突,不得不在文档中留下一句提醒使用者:“如果想用MallardDuck,请勿使用DynamicDuck构建,而是使用更具体的MallardDuck!”;另一方面,其它类型的Duck也需要子类化吗,若是的话岂不是又免不了类型爆炸了!策略模式这时正失去优雅的光环,它还是那个妙不可言的“策略模式”吗?

Rust语言,则可以静多态一路走到黑,Duck<F, Q>类型当参数时一直泛型约束使用下去。这样看起来,静多态是一种挺好的应对策略模式后续变化的解决方案。Rust还有一种方式,可以终止这种“一直”,就是将有限的静多态类型通过enum和类型统一起来,然后再使用时就不必继续用泛型了,用这个enum和类型就好了。这是个好方法,但也有个弊端,enum和类型终止了模块之外的“扩展性”!在模块之外,再也无法为模块内的enum和类型扩展其它Duck实现,而动多态和一直泛型约束的静多态,则仍不失模块外的扩展性。

策略模式还有个问题,值得探讨,Duck也会飞,也会呱呱叫了,那有没有必要为Duck也实现Fly、Quack特型呢?

/// 有没有必要为Duck实现Fly/Quack trait?
impl<F, Q> Fly for Duck<F, Q> 
where
    F: Fly,
    Q: Quack,
{
    fn fly(&self) {
        self.fly_behavior.fly();
    }
}

impl<F, Q> Quack for Duck<F, Q>
where
    F: Fly,
    Q: Quack,
{
    fn quack(&self) {
        self.quack_behavior.quack();
    }
}

这是个令人迷惑的选项,个人很讨厌这种“都可以”的选项,让人迟迟下不了决策。很多人从“应该不应该”的角度出发,会得到“应该”的答案,Duck应该会飞,所以为Duck实现了Fly特型,后面就可以用Fly来特型约束了。其实,若实现了,就像是另外一个设计模式——装饰器模式了。但我不建议普通的策略模式这样实现,将Fly和Quack组合起来的Duck,不再是飞行策略实现的一种变体,要是RubberDuck也能因满足Fly特型约束,再次充当Duck自己的“翅膀”F,组合成一个新Duck,那这是什么Duck?闹笑话了,一向以“严格”著称的Rust可不喜欢这样做。看起来Duck会飞,和飞行策略的Fly特型有所不同,读者可自行感受,那如何约束Duck,让别人知道Duck也是可飞行的一个类型呢?可以使用AsRef,让鸭子实现AsRef<F: Fly>,意为“Duck拥有飞行的策略”,鸭子自然也会飞,能做所有会飞的类型可以做的事情。

fn fly_to_do_sth<T, F>(fly_animal: &mut T) 
where
    T: AsRef<F>,
    F: Fly,
{
    // Duck也可以作为fly_animal来执行此函数了
}

注意,这里AsRef跟Deref的区别。AsRef可以实现多次,到不同类型的借用转换,比如Duck同时AsRef<F: Fly>和AsRef<Q: Quack>;而Deref只能实现一次到一个主Target的类型转换,而Fly和Quack无论哪个行为,明显都不足以让Duck为其实现Deref,它的父类动物结构,才值得Duck使用Deref。

小结

初识策略模式时,觉得妙不可言,但它其实没提策略模式那逐渐不可控的后续演化,源于为策略模式的复合类型Duck扩展行为时,并非所有Duck都该有这些扩展行为了,它们很可能是某些鸭子独有的,主要原因是动多态造成了“类型丢失”,而解决办法还没法令人满意!因此,策略模式适合后续不再演化的场景。能应对后续演化的,还得是类型完整的静多态思路。

编程的一大挑战就是为了应对变化,开发者知道的招式变化越多,应对的就越从容,使用看起来正确实际上却会逐渐失控的招式,只会味道越来越坏。变化就是“可扩展性”,谈到“可扩展性”,面向对象说这个我熟,“可扩展性”就是面向对象的目标之一啊!先别轻信,完美应对变化可不容易,即便资深的面向对象专家,都不敢说他写的每个东西都真能满足“单一职责”。。单一职责的足够“原子化”吗?面向对象思想有个老毛病,就是不够具体,让人抓不到,又让人以为抓到了,实际上是面向对象规定的东西,包括它的评论、解释大都泛泛而谈,没有一个度,很难意见统一。

(强调一下:因每个人理解层次不同,这一系列文章无意引战,也不想批评C++,只要C++想,就能实现Rust一样的效果,毕竟现代C++无所不能的。面向对象有些问题值得指出、批评,但个人还是认可面向对象的结构之美。这些文章,仅供大家友好交流Rust和面向对象技术,若有迁移一个面向对象项目到Rust重新实现的需求,那可能会有帮助,欢迎大家友好讨论!)

(原创不易,请在征得作者同意后再搬运,并注明出处!)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1333617.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java调用GDAL实现栅格数据的重采样的一种方法

目录 1.关于重采样 1.1概念 1.2用途 1.3常见算法 2.关于GDAL 2.1GDAL中的重采样算法 3.实现重采样 3.1思路 3.2完整代码 3.3使用QGIS验证效果 1.关于重采样 1.1概念 重采样是以原始图像的像元值或者导出的值填充到新的图像的每个像元的的过程。 1.2用途 在地理信…

【MySQL学习笔记007】约束

1、概述 概念&#xff1a;约束是作用于表中字段上的规则&#xff0c;用于限制存储在表中的数据。 目的&#xff1a;保证数据库中数据的正确、有效性和完整性。 分类 注意&#xff1a;约束是作用于表中字段上的&#xff0c;可以在创建表/修改表的时候添加约束。 2、约束…

文章标题(备注)

现在也裁员了吗&#xff1f;怎么感觉越来越垃圾 这个又是什么&#xff1f;真搞笑&#xff0c;我也没开隐私呀

生物神经网络衍生出的算法

一个生物神经网络的基本结构&#xff1a; 生物神经网络由大量神经元组成&#xff0c;这些神经元之间通过突触相互连接。神经元可以接收来自其他神经元的信号&#xff0c;并根据信号的强度和类型来调整自己的输出信号。这种神经元之间的相互连接和信号传递形成了生物神经网络的基…

记pbcms网站被攻击,很多标题被篡改(1)

记得定期打开网站看看哦! 被攻击后的网站异常表现:网页内容缺失或变更,页面布局破坏,按钮点击无效,...... 接着查看HTML、CSS、JS文件,发现嵌入了未知代码! 攻击1:index.html 或其他html模板页面的标题、关键词、描述被篡改(俗称,被挂马...),如下: 攻击2:在ht…

[原创][R语言]股票分析实战[4]:周级别涨幅趋势的相关性

[简介] 常用网名: 猪头三 出生日期: 1981.XX.XX QQ联系: 643439947 个人网站: 80x86汇编小站 https://www.x86asm.org 编程生涯: 2001年~至今[共22年] 职业生涯: 20年 开发语言: C/C、80x86ASM、PHP、Perl、Objective-C、Object Pascal、C#、Python 开发工具: Visual Studio、D…

<JavaEE> 协议格式 -- 传输层协议 UDP

目录 一、UDP协议格式长啥样&#xff1f; 二、端口号和IP地址 1&#xff09;UDP协议中包含哪两个端口号&#xff1f; 2&#xff09;有没有包含IP地址&#xff1f; 三、UDP报文长度 1&#xff09;UDP报文长度最长多长&#xff1f; 2&#xff09;UDP报文的组成&#xff1f…

相机内参标定理论篇------张正友标定法

一、为什么做相机标定&#xff1f; 标定是为了得到相机坐标系下的点和图像像素点的映射关系&#xff0c;为摄影几何、计算机视觉等应用做准备。 二、为什么需要张正友标定法&#xff1f; 张正友标定法使手工标定相机成为可能&#xff0c;使相机标定不再需要精密的设备帮助。…

嵌入式开发——DMA外设到内存

学习目标 加强理解DMA数据传输过程加强掌握DMA的初始化流程掌握DMA数据表查询理解源和目标的配置理解数据传输特点能够动态配置源数据学习内容 需求 uint8_t data; 串口接收(&data);data有数据了 实现串口的数据接收,要求采用dma的方式。 数据交互流程 CPU配置好DMA外…

java数据结构与算法刷题-----LeetCode167:两数之和 II - 输入有序数组

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 思路 题目要求我们找到两个数相加的和&#xff0c;等于target指定的值。而…

设计模式--职责链模式

实验15&#xff1a;职责链模式 本次实验属于模仿型实验&#xff0c;通过本次实验学生将掌握以下内容&#xff1a; 1、理解职责链模式的动机&#xff0c;掌握该模式的结构&#xff1b; 2、能够利用职责链模式解决实际问题。 [实验任务]&#xff1a;财务审批 某物资管理系统…

4. java——多态(java巅峰设计,超越了C++的理解,取其精华,去其糟粕)

多态指的是同—个行为具有多个不同表现形式 。是指—个类实例(对象&#xff09;的相同方法在不同情形下具有 不同表现形式。封装和继承是多态的基础&#xff0c;也就是说&#xff0c;多态只是—种表现形式而已。一个对象&#xff0c;同一个方法不同形态&#xff0c;方法必须重…

reactor的原理与实现

网络模型 前情回顾服务器模型 Reactor和 ProactorReactor模型Proactor模型同步I/O模拟Poractor模型Libevent&#xff0c;libev&#xff0c;libuv优先级事件循环线程安全 前情回顾 网络IO&#xff0c;会涉及到两个系统对象&#xff1a;   一个是用户空间调用的进程或线程   …

基于IPP-FFT的线性调频Z(Chirp-Z,CZT)的C++类库封装并导出为dll(固定接口支持更新)

上一篇分析了三种不同导出C++类方法的优缺点,同时也讲了如何基于IPP库将FFT函数封装为C++类库,并导出为支持更新的dll库供他人调用。 在此基础上,结合前面的CZT的原理及代码实现,可以很容易将CZT变换也封装为C++类库并导出为dll,关于CZT的原理和实现,如有问题请参考: …

增量式旋转编码器在STM32平台上的应用

背景 旋钮是仪器仪表上一种常见的输入设备&#xff0c;它的内部是一个旋转编码器&#xff0c;知乎上的这篇科普文章对其工作原理做了深入浅出的介绍。 我们公司的功率分析仪的前面板也用到了该类设备&#xff0c;最近前面板的MCU从MSP430切换成了STM32&#xff0c;因此我要将…

STM32F407-14.3.10-表73具有有断路功能的互补通道OCx和OCxN的输出控制位-1x111

如上表所示&#xff0c;MOE1&#xff0c;OSSR1&#xff0c;CCxE1&#xff0c;CCxNE1时&#xff0c;OCx与OCxN对应端口的输出状态取决于OCx_REF与极性选择&#xff08;CCxP&#xff0c;CCxNP&#xff09; 死区。 -------------------------------------------------------------…

Python学习路线 - Python语言基础入门 - Python基础综合案例 - 数据可视化 - 动态柱状图

Python学习路线 - Python语言基础入门 - Python基础综合案例 - 数据可视化 - 动态柱状图 基础柱状图构建案例效果通过Bar构建基础柱状图反转x和y轴数值标签在右侧 基础时间线柱状图绘制创建时间线创建时间线自动播放时间线设置主题 动态GDP柱状图绘制需求分析列表的sort方法带名…

【SD】差异值 生成 同一人物 制作 表情包 【1】

说明&#xff1a;只对AI生成的人物&#xff0c;效果稳定。 Reference差异值 生成表情 首先生成一张图片。 测试命令&#xff1a;1 man,chibi,full body, 模型&#xff1a;envyclarityxl02_v10.safetensors [f6c13197db] 种子&#xff1a;2704867166 》》测试命令&#xff1a…

java八股 spring + mybatis

Spring常用注解&#xff08;绝对经典&#xff09;_spring注解-CSDN博客 框架篇-02-Spring-单例bean是线程安全的吗_哔哩哔哩_bilibili 1.spring.bean 单例 线程不安全 2.AOP 项目里可以说记录用户登录日志&#xff0c;利用request去获取姓名、ip、、请求方式、url&#xff0…

NUAA-云计算-考试

19级期末 问题 答案: md格式 自己想办法看 # 随堂测验#### 一、请简述GFS 的系统架构和特点。**1. 系统架构**- GFS将整个系统节点分为三类角色&#xff1a;- Client&#xff08;客户端&#xff09;&#xff1a;Client是GFS提供给应用程序的访问接口&#xff0c;以库文件的…