Soft_Job 板


LINE

终於有空来加入讨论啦~ 这边有 markdown 好读版:https://hackmd.io/@rayshih/SyAAwbxkd 这边我也来提一下我的看法。为了阅读方便我把一些 code snippet 复制在这边: ```java= public double shippingFee(String shipper, double length, double width, double height, double weight) { if (shipper.equals("black cat")) { return // some calculation } else if (shipper.equals("hsinchu")) { return // some calculation } else if (shipper.equals("post office")) { return // some calculation } else { throw new IllegalArgumentException("shipper not exist"); } } ``` 然後以下是 refactor 过後的程式码片段: 首先你订了个 interface ```java= public interface Shipper { double calculateFee(Product product); } ``` 然後所有 shipper 都会实作这个 interface: ```java= public class BlackCat implements Shipper { public BlackCat() { } @Override public double calculateFee(Product product) { return // some calculation } } ``` 於是本来的 function shippingFee 被 refactor 成这样: ```java= public class Cart { private final HashMap<String, Shipper> shippers = new HashMap<>() {{ put("black cat", new BlackCat()); put("hsinchu", new Hsinchu()); put("post office", new PostOffice()); }}; public double shippingFee(String shipperName, Product product) { if (shippers.containsKey(shipperName)) { return shippers.get(shipperName).calculateFee(product); } throw new IllegalArgumentException("shipper not exist"); } } ``` 首先,这根本不能算「策略模式」,只能算是一般的多型应用,不过我这边不是很想讨 论 strategy pattern 本身,有兴趣的可以去 wiki 比较一下差在哪里。 ## 所以原本的 code 到底有什麽问题? 基本上有两点可以讨论: 1. 不同计算方法都被写在同一个 function 里 2. 如果 caller 丢了一个不认识的 shipperName,这 function 就会丢出 exception ### 1. 不同计算方法都被写在同一个 function 里 原 solution 定义了一个 interface,所以要实作这个 function 必须建立一个 class 来实作这个 interface,所以算是有解决到这个问题。但其实单纯的为不同的 shipper 建立相对应的 function 就行了,并没有必要多一个 interface: ```java= private double blackCatShippingFee(Product product) { return // The calculation for black cat } // hsinchuShippingFee and postOfficeShippingFee are similar public double shippingFee(String shipper, Product product) { if (shipper.equals("black cat")) { return this.blackCatShippingFee(product); } else if (shipper.equals("hsinchu")) { return this.hsinchuShippingFee(product); } else if (shipper.equals("post office")) { return this.postOfficeShippingFee(product); } else { throw new IllegalArgumentException("shipper not exist"); } } ``` ### 2. 如果 caller 丢了一个不认识的 shipperName,这 function 就会丢出 exception 假设今天,我们新增了一个货运商,工程师记得要建立一个新的 class 并实作 Shipper interface,但是他忘了把它加入 shippers hashmap,又刚好没写测试,於是 rollout 之後就触发了 exception,就 QQ 惹。 有没有方法可以保证不会有例外呢?这问题就有点有趣了,但首先让我们先换一个语言 kotlin: ```kotlin= sealed class Shipper { object BlackCat: Shipper() object Hsinchu: Shipper() object PostOffice: Shipper() } data class Product( val length: Double, val height: Double, val weight: Double, ) fun shippingFee(shipper: Shipper, product: Product): Double { return when(shipper) { is Shipper.BlackCat -> { // computation here } is Shipper.Hsinchu -> { // computation here } is Shipper.PostOffice -> { // computation here } } } fun main(args: Array<String>) { val product = Product(7.0, 8.0, 9.0) println(shippingFee(Shipper.BlackCat, product)) } ``` 因为 kotlin 的 when 有提供 exhausive check 的功能。只要使用 sealed class, compiler 就会帮你检查你有没有漏掉的 case。所以假设我们新增一个新的 case 像这样 : ```kotlin= sealed class Shipper { object BlackCat: Shipper() object Hsinchu: Shipper() object PostOffice: Shipper() object Lalamove: Shipper() // 新增的部分 } ``` Compiler 会直接吐一个 error 给你: ``` main.kt:15:10: error: 'when' expression must be exhaustive, add necessary 'Lalamove' branch or 'else' branch instead return when(shipper) { ``` 这时,工程师就可以根据 compiler 的提醒来做相对应的修正。 所以其实可以去掉 exception, or can't I? ## 如果 Shipper 是由 user 输入的资料决定的呢? 因为这边 Shipper 是自定义的 data type,所以需要有个过程把 user input 转换成这 些 data type ```kotlin= val userInput = // some function return user input val shipper = when (userInput) { 'black cat' -> Shipper.BlackCat 'hsinchu' -> Shipper.Hsinchu 'post office' -> Shipper.PostOffice else -> throw IllegalArgumentException('shipper not exist') } println(shippingFee(shipper, product)) ``` 只有在 shipper 事先定义好的情况下才能去掉 exception,所以如果 shipper 是由 user input 决定的话,就还是会有 user 输入没有合作的货运商的状况,这个时候还是 需要 throw exception 才行。 这时,你可能会问,既然还是要 throw exception,那有差吗?有的。 ## 差别在於:例外是从哪里、何时丢出来的 想像一下如果你的 shippingFee 是在整个 callstack 里面的第十层,也就是 shipperName 就这样一路的被传了十层。一旦 exception 出现,你也要花不少时间去 trace code 才会知道这个 shipperName 是在什麽情况下变成不支援的 value。 相较之下,因为上面提到的 conversion from user input to Shipper data type 本身 就是一个检查的过程,所以如果我们在 user input 之後马上做 conversion,那万一出 现了 exception,我们也可以很快地知道到底是从哪里开始错的。 ## Boundary between Safe and Unsafe 这个架构基本上可以被视为两个部分 1. conversion and check 2. execution 而其中因为 exhausive check 的关系,第二部分可以说是 safe 的,相较之下第一部分 就是 unsafe。也就是我们可以通过这个手法把系统切割成 safe 跟 unsafe。我们可以 对 unsafe 的部分做更完整的测试。safe 的部分就可以相对放心。 当然,你也可以把 fee computation 放进 shipper 并封装成一个 interface/function ,不过我觉得这相对来说比较不重要,有兴趣的可以自己查查资料看要怎麽做。 可惜的是 Java 并没有这样的设计,所以如果使用的是 Java,有很大的机率你还是需要 各种 throw new Exception。 ## 结语 虽然现在 Design Pattern 看起来是个显学,不过在使用 Design Pattern 之前,要先能 够先了解问题本身的性质,再去看看使否有模式可以解决,不然很容易会变成为套而套, 而所谓的 refactor 也不一定能带来什麽实质的效用。 另外,有一些人会倡导 Design Pattern 是 language agnostic,其实并不尽然,这篇使 用 kotlin 的 sealed class 就是一个例子。 --
QR Code



※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 36.227.16.212 (台湾)
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/Soft_Job/M.1610784096.A.DA8.html
1F:推 netburst: sealed class真D爽 01/16 16:10
2F:推 IcecreamHsu: 同意 Design pattern 是为了解决特定语言的缺陷 01/16 16:18
3F:→ alihue: 没 syntax highlight 还是写在部落格比较好XD 01/16 16:23
4F:推 wulouise: 应该说语言特性可以用design pattern补救吧? 01/16 16:23
5F:推 wulouise: 如果这个interface有三个func 原本是三个长得很像的if e 01/16 16:28
6F:→ wulouise: lse,变成没有,也不会有人忘掉三个地方都要实作 01/16 16:28
7F:→ electgpro: alihue 最上面有 markdown 连结喔~ 01/16 17:21
8F:推 landlord: 我认同其实只是多型取代 condition logic 唷 01/16 19:53
9F:推 jej: 其实我想说... Pattern像是武学的内功心法 如果你想练 01/16 21:05
10F:→ jej: 可以尝试着不要用套件 自己写 写不通的时候参考别人怎麽写 01/16 21:05
11F:→ jej: 例如说可以参考Apache tomcat怎麽实作filter 01/16 21:05
12F:→ jej: 或是spring怎麽建构整个架构 为什麽annotation可以有作用 01/16 21:05
13F:→ jej: 每个套件里面pattern都用超凶的 不注意思考还会以为理所当然 01/16 21:05
14F:推 roccqqck: Ray大大必推 01/16 21:10
15F:→ electgpro: 个人觉得什麽像内功心法之类的,都是似懂非懂才会用的 01/16 23:22
16F:→ electgpro: 「形容词」,而且光看别人怎麽用 pattern 大概也很容 01/16 23:23
17F:→ electgpro: 易就学个皮毛而已 01/16 23:23
18F:推 csieflyman: 学 design pattern 的方式就是要自己试着写框架给别人 01/17 00:02
19F:→ csieflyman: 用 框架作者及使用者双方都不能修改对方的程式码 而且 01/17 00:02
20F:→ csieflyman: 要有弹性能让人插入客制化逻辑 如果常有 breaking cha 01/17 00:02
21F:→ csieflyman: nge 没人受得了 如果你的专案程式码都是自己写的 而且 01/17 00:02
22F:→ csieflyman: 习惯想改就改 那有没有套 pattern 就不是必要的了 01/17 00:02
23F:推 wvwvwvwvwv: 同意楼上 01/17 15:01
24F:→ yfr: 不过以型态来说所有design pattern都是oo的一些应用跟写法 01/17 15:08
25F:→ yfr: 我倒是蛮想知道你所谓的策略模式长怎样 01/17 15:09
26F:→ electgpro: 不是所有 design pattern 都要从 OO 出发 01/17 17:27
27F:→ electgpro: 我所谓的策略模式就是 wiki 上面写的 01/17 17:28
28F:→ fantasychese: 看完wiki感觉你只是自以为比较懂策略模式而已 01/17 18:08
29F:→ yfr: 嗯,的确不是全部,只能说几乎。context 对不太上,只能先这 01/17 18:23
30F:→ yfr: 样。 01/17 18:23
31F:→ electgpro: yfr 你这个就 OO 本位主义啊 XD 01/17 18:58
32F:→ electgpro: fantasychese 你有看 wiki 范例就会知道差别。不过如果 01/17 19:02
33F:→ electgpro: 你要说没有违反文字描述,那照这个思路,我也可以说所 01/17 19:03
34F:→ electgpro: 有有用到多型的都算「策略模式」:p 01/17 19:03
35F:→ yfr: 我大概懂你的意思了,少了一个context传入策略的这个封装。 01/17 20:19
36F:→ yfr: 的确,如果按照这个定义你说的没错。不过那篇文章的本意也不 01/17 20:23
37F:→ yfr: 在这名词的定义就是,要完成策略模式需要再加点工 01/17 20:23
38F:→ yfr: 文字没有温度,无法表达语气,以上讨论没有不敬的意思 01/17 20:25
39F:→ yfr: 可能跟我是C跟Java出身的有关,通常不太OO的语言,Design Pa 01/17 20:27
40F:→ yfr: ttern我都看不太懂 (无奈 01/17 20:27
41F:→ yfr: 你是不是擅长函数式语言呢? 01/17 20:37
42F:→ electgpro: 还行,FP 水很深 01/17 21:35
43F:→ electgpro: 不过我写这篇本来就没有要讨论策略模式,我都明写了 :p 01/17 21:37
44F:→ yfr: 网路讨论就是这样,比较难抓到讨论的点(笑 01/17 21:41
45F:→ yfr: 一个东西大家可能都有不同看法 01/17 21:41
46F:推 wulouise: 可以先说你看的是中文还是英文wiki,原po缺的是什麽吗? 01/18 22:18
47F:→ wulouise: 因为你说原文不算是,只是缺少context传入?还是更多, 01/18 22:21
48F:→ wulouise: 这篇文章对未来读者的价值就少了一段,建议补足 01/18 22:21
49F:→ wulouise: 我认为两篇文章都是懂得人在写,两方都省略说法,到最後 01/18 22:22
50F:→ wulouise: 不懂的人看了还是不懂,对新手没什麽效益 01/18 22:22
51F:推 vi000246: 其实我在原文的底下推文连结 就有解决你说的shipperName 01/20 01:41
52F:→ vi000246: 问题了 01/20 01:41
53F:→ vi000246: https://dotnetfiddle.net/L2BGVY 01/20 01:41
54F:→ electgpro: vi000246 你这个也是 strategy pattern 只不过用的是 01/20 14:37
55F:→ electgpro: generic aka type level application 去做 injection 01/20 14:38
56F:→ electgpro: 是个满好的方式,但还是没有办法避免 conversion 喔~ 01/20 14:39
57F:→ electgpro: 尤其是在转换的时候在 c# 应该会用到 reflection 01/20 14:40
58F:→ electgpro: wulouise 我再强调一次,我写这篇文没有要讨论策略模式 01/20 14:44
59F:→ electgpro: 我想讨论的是本来的 code 有什麽样的问题以及如何重构 01/20 14:45
60F:推 jackypan1989: 感觉很多人还是太聚焦在策略模式上 XD 01/20 15:13
61F:→ jackypan1989: 如果一开始就学一个没啥缺陷的语言就没这问题 01/20 15:14







like.gif 您可能会有兴趣的文章
icon.png[问题/行为] 猫晚上进房间会不会有憋尿问题
icon.pngRe: [闲聊] 选了错误的女孩成为魔法少女 XDDDDDDDDDD
icon.png[正妹] 瑞典 一张
icon.png[心得] EMS高领长版毛衣.墨小楼MC1002
icon.png[分享] 丹龙隔热纸GE55+33+22
icon.png[问题] 清洗洗衣机
icon.png[寻物] 窗台下的空间
icon.png[闲聊] 双极の女神1 木魔爵
icon.png[售车] 新竹 1997 march 1297cc 白色 四门
icon.png[讨论] 能从照片感受到摄影者心情吗
icon.png[狂贺] 贺贺贺贺 贺!岛村卯月!总选举NO.1
icon.png[难过] 羡慕白皮肤的女生
icon.png阅读文章
icon.png[黑特]
icon.png[问题] SBK S1安装於安全帽位置
icon.png[分享] 旧woo100绝版开箱!!
icon.pngRe: [无言] 关於小包卫生纸
icon.png[开箱] E5-2683V3 RX480Strix 快睿C1 简单测试
icon.png[心得] 苍の海贼龙 地狱 执行者16PT
icon.png[售车] 1999年Virage iO 1.8EXi
icon.png[心得] 挑战33 LV10 狮子座pt solo
icon.png[闲聊] 手把手教你不被桶之新手主购教学
icon.png[分享] Civic Type R 量产版官方照无预警流出
icon.png[售车] Golf 4 2.0 银色 自排
icon.png[出售] Graco提篮汽座(有底座)2000元诚可议
icon.png[问题] 请问补牙材质掉了还能再补吗?(台中半年内
icon.png[问题] 44th 单曲 生写竟然都给重复的啊啊!
icon.png[心得] 华南红卡/icash 核卡
icon.png[问题] 拔牙矫正这样正常吗
icon.png[赠送] 老莫高业 初业 102年版
icon.png[情报] 三大行动支付 本季掀战火
icon.png[宝宝] 博客来Amos水蜡笔5/1特价五折
icon.pngRe: [心得] 新鲜人一些面试分享
icon.png[心得] 苍の海贼龙 地狱 麒麟25PT
icon.pngRe: [闲聊] (君の名は。雷慎入) 君名二创漫画翻译
icon.pngRe: [闲聊] OGN中场影片:失踪人口局 (英文字幕)
icon.png[问题] 台湾大哥大4G讯号差
icon.png[出售] [全国]全新千寻侘草LED灯, 水草

请输入看板名称,例如:iOS站内搜寻

TOP