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

2023-12-25 08:20
文章标签 rust 面向对象 北海

本文主要是介绍北海 - Rust与面向对象(三),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

策略模式

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

pub trait Fly {fn fly(&self);
}pub trait Quack {fn quack($self);
}/// 先以静多态的方式实现
/// 似 trait Fly + Quack就是Duck,只是Fly和Quack独立地变化
struct Duck<F, Q> 
whereF: Fly,Q: Quack,
{fly_behabior: F,      // 单看这个成员,与模版方法如出一辙quack_behavior: Q,    // 一样,将不同的算法部分交给子类去实现
}impl<F, Q> Duck<F, Q> 
whereF: 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> 
whereF: Fly,Q: Quack,
{fn fly(&self) {self.fly_behavior.fly();}
}impl<F, Q> Quack for Duck<F, Q>
whereF: 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) 
whereT: 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重新实现的需求,那可能会有帮助,欢迎大家友好讨论!)

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

这篇关于北海 - Rust与面向对象(三)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/534845

相关文章

Rust 数据类型详解

《Rust数据类型详解》本文介绍了Rust编程语言中的标量类型和复合类型,标量类型包括整数、浮点数、布尔和字符,而复合类型则包括元组和数组,标量类型用于表示单个值,具有不同的表示和范围,本文介绍的非... 目录一、标量类型(Scalar Types)1. 整数类型(Integer Types)1.1 整数字

Rust中的Option枚举快速入门教程

《Rust中的Option枚举快速入门教程》Rust中的Option枚举用于表示可能不存在的值,提供了多种方法来处理这些值,避免了空指针异常,文章介绍了Option的定义、常见方法、使用场景以及注意事... 目录引言Option介绍Option的常见方法Option使用场景场景一:函数返回可能不存在的值场景

【Rust练习】12.枚举

练习题来自:https://practice-zh.course.rs/compound-types/enum.html 1 // 修复错误enum Number {Zero,One,Two,}enum Number1 {Zero = 0,One,Two,}// C语言风格的枚举定义enum Number2 {Zero = 0.0,One = 1.0,Two = 2.0,}fn m

linux中使用rust语言在不同进程之间通信

第一种:使用mmap映射相同文件 fn main() {let pid = std::process::id();println!(

ffmpeg面向对象-待定

1.常用对象 rtsp拉流第一步都是avformat_open_input,其入参可以看下怎么用: AVFormatContext *fmt_ctx = NULL;result = avformat_open_input(&fmt_ctx, input_filename, NULL, NULL); 其中fmt_ctx 如何分配内存的?如下 int avformat_open_input(

第二十四章 rust中的运算符重载

注意 本系列文章已升级、转移至我的自建站点中,本章原文为:rust中的运算符重载 目录 注意一、前言二、基本使用三、常用运算符四、通用约束 一、前言 C/C++中有运算符重载这一概念,它的目的是让即使含不相干的内容也能通过我们自定义的方法进行运算符操作运算。 比如字符串本身是不能相加的,但由于C++中的String重载了运算符+,所以我们就可以将两个字符串进行相加、但实际

chapter06 面向对象基础 知识点Note

文章目录 前言类的设计 属性和行为对象的内存解析 (堆 栈 方法区)类的成员之一 变量(属性) field类的成员之二 方法 method对象数组方法重载 overload可变个数的形参 语法糖方法的值传递机制递归关键字package importMVC设计模式import导入面向对象特征之一 封装类的成员之三 构造器JavaBeanUML类图 前言 ` 面向对象封装 面向

【JVM】JVM栈帧中的动态链接 与 Java的面向对象特性--多态

栈帧 每一次方法调用都会有一个对应的栈帧被压入栈(虚拟机栈)中,每一个方法调用结束后,都会有一个栈帧被弹出。 每个栈帧中包括:局部变量表、操作数栈、动态链接、方法返回地址。 JavaGuide:Java内存区域详解(重点) 动态链接 动态链接:指向运行时常量池中该栈帧所属方法的引用。 多态 多态允许不同类的对象对同一消息做出响应,但表现出不同的行为(即方法的多样性)。 多态

【Rust光年纪】Rust 机器人学库全景:功能、安装与API概览

机器人学+Rust语言=无限可能:六款库带你开启创新之旅! 前言 随着机器人技术的快速发展,对于机器人学领域的高效、可靠的编程语言和库的需求也日益增加。本文将探讨一些用于 Rust 语言的机器人学库,以及它们的核心功能、使用场景、安装配置和 API 概览,旨在为机器人学爱好者和开发人员提供参考和指导。 欢迎订阅专栏:Rust光年纪 文章目录 机器人学+Rust语言=无限可能:

java基础总结15-面向对象11(抽象类)

下面通过一下的小程序深入理解抽象类 因此在类Animal里面只需要定义这个enjoy()方法就可以了,使用abstract关键字把enjoy()方法定义成一个抽象方法,定义如下:public abstract void enjoy();   从某种意义上来说,抽象方法就是被用来重写的,所以在父类声明的抽象方法一定要在子类里面重写。如果真的不想在子类里面重写这个方法,那么可以再在子类里