@
lanlanye模式(pattern)这话题不大有趣。背后想要复用设计的需求是真实的,然而基本无解(了解清楚模式是什么不能帮你省多少事)。
模式在此指工程过程中(被假定)可复用的一些实践。真正容易的部分已经被编码到程序中涵盖了,所以这里说的特指无法通过编程手段自动化复用的部分。
模式有个共同点:有名字。这提供“行话”,具体意义经常通过局限性(不能做什么,什么不算是模式的实例)体现。
按规模模式通常分为三类。
最易局部实现的有时叫代码模式,更常叫做惯用法(idiom)。因为和具体语言特性相关这种经常被视为实现手段而非“设计”模式,但其实不确切,因为一些惯用法(如 RAII )很明确地会影响并传染到跨模块的 API 上,而这至少算得上是一种详细设计。叫成“代码模式”也不甚对路。
惯用法的意义在于无法被自动隐含到其它代码中隐式复用,要设计和实现人员长点心记得什么时候适合用,但每个实例又不都一样。
最大的一类是架构模式,涉及到系统整体的宏观设计却又依赖具体项目的经验而具有显著性。讨论架构模式通常较困难,它关注的问题非常大,很多解法和涉众利益相关。除了可用资源,架构模式的选型瓶颈还有涉众的偏好(或者说,眼界),我不认为缺乏具体项目背景的情况下有多少讨论余地。
架构模式的意义体现在它是一类可复用问题解法抽象层次的上限(其实只要你敢想,问题的规模就没有上界)。只要一个问题的规模似乎足够大,遇事不决就有倒腾架构的余地。但还是那句话,什么问题真能算得上架构问题,得看涉众偏好,有些模式也有模糊性(比如 MVC 能算架构模式也能算设计模式)。一般更小规模的模式能确切解决的问题,都不算架构模式问题;所以说叫“上限”。
现实不允许你什么都赖架构,又有些不是记几个惯用法就能解决的。这类问题的套路性一般夹在需求和实现之间的阶段中体现;这种高不成低不就的模式,就被称为所谓的设计模式。然而,它要解决的有多少算设计得打问号。虽然不像惯用法高度依赖具体语言特性,设计模式通常依赖范型(paradigm)更甚于问题域。
例如,OOP 的设计模式和 FP 的设计模式经常不通用,而且对面看来都是冗余的。所谓设计模式跟惯用法没那么大的区别,无非是依赖一个或者几个语言还是依赖一类或者几类语言,中间没有明确界限。
设计模式中经典的 OOP 的部分,跟 OOD 其实没什么关联(虽然实现滥用类后可能强行对应得上)。后者重点是建模,而 OOP 所谓设计只是如何选择声称支持 OOP 的语言提供的特性,跟建模难以对应。
语言设计倒是会提供一些直接的对应。比如说“类”,经常是语言而非模式提供。也有例外,如 C 的标准输入 /输出流用 FILE*其实就是一种基于类的 OOP 实现,但似乎没谁把这种实现方式叫模式,连惯用法都不算。
于是,我不认为类似的所谓模式有多少讨论的意义——既不能帮助明确问题域的边界,又难以涵盖现实存在的(不把模式当回事的用户创造的而不会特意取名)具体语用的惯例,作为单独的复用基础欠缺现实意义。
设计模式还引出一大堆现实问题。
第零,滥用。这个之后讲。
第一,取名不见得有正面作用。
如很多开发者会把单态(monostate)混淆成单例(singleton),以至于不看实际怎么写的单例就判断不出说的是哪个。虽然在此两者经常有足够大的共性使混淆被容忍,但是要想提防混淆,还不如直接说满足什么需求来得清楚。模式提供名字的作用就整体不那么靠谱了。
第二,随意强调即便没被滥用,也容易造成刻板印象下的误导。
一些 OOP 设计模式跟 OOP 也没什么关系。
比如所谓工厂方法(factory method) ,其实只是对一些类 Simula 的 OOP 语言的类的构造器无法多态的变通,而像 C++的解法 make_xxx 根本用的就是参数多态(parametric polymorphism)而不是 OO 特供的包含多态(inclusion polymorphism),一点都不 OO 好吧。
如果说这不是惯用法,反而有些怪。但不同语言用不同多态都能实现,都不见得算 OO ,放到惯用法里是不是又太屈尊了?就有点尬。
第三,可能被语言的演进自然淘汰。
像 C++17 出来 deduction guide 后很多 make_xxx 就下岗了,因为直接声明对象也不用写参数类型,相当于构造函数隐含了参数多态,那何必多此一举 make_xxx ?
(不过因为一些理论上的问题,不管能不能有 C++17 用,我都避免 deduction guide 。)
这倒是从反面体现了一些设计模式的积极作用:揭示日常依赖使用这些模式的语言设计的不足,给语言补充特性指明一个方向。
再说滥用设计模式。
多数工程中这也不算是头等问题,因为味儿(如代码中的废话(boilerplate))明显,一眼看得出来,敢不敢砍掉看执行力。更大的问题是阻碍整体语用的理解,进一步阻碍对语言设计、范型偏好乃至选型策略等一系列问题的正常认知。
OOP 的设计模式啰嗦得太明显所以问题还不普遍( GoF 的书用 C++,然后设计模式长期被 C++用户普遍嫌弃,倒是语言演化一贯后进的 Java 用户更吃这一套才给发扬光大),至少没到 OOP 用户写点程序就想到模式的地步。
这个问题更多体现在某些 FP 语言用户上。
典型的反面教材是 Haskell 的 monad I/O 。这里为实现个 do-notation 的语法糖只是个惯用法,但 API 非得这样依赖 monad 的设计就硬生生地强行搞成了“设计”模式。以至于有用户说 I/O 必 monad ,有点纠正不过来了。
其实 monad 无非是个类似 OOP 的 interface ,顶用的核心原因,甚至都不是 interface 内部能 typecheck 的东西,而是更上层的 monad law 这样的约定(或者说公理(axiom))。然鹅,更基本的其实是 applicative functor……好了,大多数用户到这里就表示投降:不要念经,还是死记硬背 monad 吧。
这里甚至还没说到杯具的表象:用了所谓的 monadic interface ,相当于写的随便到处 CPS(continuation passing style)变换过的程序,于是 Haskell 用户正常情况下是没资格有用其它大多语言的 direct style 表达 I/O 操作的本事的。(其实也有不 monad 的古董,但是用起来体验嘛……)
而杯具的实质是啥?是因为 Haskell 用户很多都是 PFP(purely functional programming)教徒,认为无条件默认拒绝副作用是好的,于是为了有效 wrap 必须具有副作用的 I/O primitive 又想藏起来这些味道不对的东西,就想方设法搞出了 monadic interface 的风格来分离再做成语法糖。
但为何拒绝副作用就好?怕是十个用户九个半都答不对,撑死嘟囔些 referential transparency 就不错了。(然鹅这种解释是错的,PFP 不是支持 RT 的必要条件,OOP 用户都知道还能隐藏内部状态呢……并且,RT 的实际含义比 99%以上的 PFP 语言用户理解的微妙得多,有兴趣补课的搜 Referential transparency, definiteness and unfoldability )。
真正明白点实质的会知道非得这样 monad 才写得顺,在于 Haskell 的 PFP+默认 lazy eval 的设计(不信改掉这些规则试试)。为什么 lazy 是好的?因为 lazy 能 declarative 。为什么 declarative 是好的?……然后怕是又有不少于八成的概率如蜜传如蜜,绕回到 lazy 就是好的这种同义反复上。
也不怪这些用户。实质问题的起源是 Haskell 的一些设计者认为 PFP+默认 lazy 能让 equational theory 会足够强,而默认允许 equational reasoning 能便于程序优化啦 blah blah……至于实际情况呢?反正看着 GHC 一坨 thunk 我就积点口德懒得吐槽了。虽然还是特别想骂骂 seq magic 这种又当又立的。
至于啥叫 equational theory 又如何判断强度呢……算了,超纲太多,自己找 paper 吧,我也弃疗了。。。
讲到这里,就能看出光一个设计模式就能蕴含多少咖喱味的屎了。
使用 OOP 模式的语言也有设计的必要性稀里糊涂这问题,然而好巧不巧至少 Java 非常不擅长缩短屎味废话,并且 OOP 的一些 calculi 都比较拉胯在学术界也没 FP 的 model 那么有存在感,所以 OOP 的设计模式(的屎味溢出)还是挺受控的……大部分工业界写 OOP 代码的都会闻得到点味道,可能忍耐的阈值不太一样。
以上主要围绕设计模式和惯用法的边界展开批判。设计模式和架构模式之间也有暧昧之处,主要例子是 MVC 。
先前提过架构模式通常会关系到不同的涉众。这里建议 MVC 被当作设计模式还是架构模式,一般主要看各个部分组件预期给谁写。如果设计 MVC 框架,打算具体的 M 、V 、C 实例的设计和实现分包给非框架作者的其他用户,这就算架构范畴。否则,如果都自己搞定,可认为这就是关于设计的。这种设计模式倒是真靠近一点实在的设计。
但是我还是比较倾向于把这类模式处理成解决架构问题的套路。如果不打算分包出去,其实不用那么纠结组件边界。事实上,主流客户端 GUI 框架声称用到 MVC 或者变体的,这里都不那么规矩,都普遍进行了偷懒而不是提供符合架构要求的职责的组件(好像就 Swing 比较规整一点)。倒是 MVVM 因为一开始就是 WPF 这种先整出来的,边界还算严格。(但 MVVM 是不是适合这样的需求就另说了。)
所以,整体建议是:惯用法和架构模式看看无妨,当背景知识长见识,被误导风险不大;至于设计模式,不想进 PLT 的坑就别靠太近了,免得积累不够,嘲讽不到位,还被设计模式厨二(虽然也许当事人甚至不知道这叫设计模式)挖坑反埋了。
(我能在一些坑里跳,是因为有积累能对着一堆鸟语写的 spec 实时脑补出一些 formal model 出来,提前看清楚一些问题,比别人多出来的一些时间就能搜到更多黑历史文献了。不建议一般用户复制这种低效的做法。)
DDD 偏架构,但主要给管理者看,多数人也不见得有条件实施。
如果想要从头开始体验抽象,复习 SICP 。先区分清楚抽象掉的能是什么,再考虑建模方法。
层次不一样具体操作不一样,要求也不一样,不要想着上中下三路一口气通吃。
搞 PL 的应该关心的是怎么让编程语言更顶用而不是逼着外行用户不得不放出什么设计模式之类的妖魔鬼怪给语言功能缺陷擦屁股。
工业界负责落实实现的普通用户应该致力达到一个合格水准:看到什么需要设计的东西,就要想着尽量用项目允许的语言趁手写出来(或者保持有底气造反推翻不恰当的选型决策),不看任何所谓的设计模式,也要做到能随手发明出来怎么实现,不在详细设计上阻塞。至于命名,不是想要找别人的废话批判一番的就算了,别添乱了。
精力过剩就找 leader 讨论需求和针对问题域的真正的设计,不要花大把时间纠结在如何给实现方法擦屁股的伪设计上,除非你致力于成为屁股理论专家。
最后跑点题,所谓语法(syntax)也是一坨 formal pattern 。现在 PL 界看来折腾语法过头,特别是搞 type theory 的整个拿语法当方法论整,而对语义注意经常不足,有滥用模式的传统艺能味儿了(即便这 pattern 不需要人去抄)。和一些设计模式的鼓吹者之间有类似共通问题:希望整出一些能摆脱 case by case analysis 而允许机械地复用的东西偷懒,却忽视了和实际需求的联系。