登录
/
注册
首页
论坛
其它
首页
科技
业界
安全
程序
广播
Follow
园子
关于
博客
发1篇日志+1圆
记录
发1条记录+2圆币
发帖说明
登录
/
注册
账号
自动登录
找回密码
密码
登录
立即注册
搜索
搜索
关闭
CSDN热搜
程序园
精品问答
技术交流
资源下载
本版
帖子
用户
软件
问答
教程
代码
VIP申请
网盘
联系我们
道具
勋章
任务
设置
我的收藏
退出
腾讯QQ
微信登录
返回列表
首页
›
业界区
›
业界
›
小例子背后的大道理——用户需求+设计原则+正确应用 =设 ...
小例子背后的大道理——用户需求+设计原则+正确应用 =设计方案
[ 复制链接 ]
荦绅诵
2025-5-29 16:16:54
上回问题回顾
上回的最后,来了两个用户,分别提出了两个不同的需求。一个要求用两个开关控制一个灯,一个要求用一个开关控制所有的灯。本回将就这两个需求进行分析。我写这段话的时候并没有想出这个需求的具体方案,重要的过程,思路有时候比结果更重要。所以,我的方案可能会"跑偏";但是如果你能从过程中体会到些什么,那这篇就没有白写。
两个开关控制一个灯。这个问题好像很简单,把两个Switcher的Switchee都设置为同一个灯不就结了吗?画个对象图会是这个样子。
图1 由双开关控制的灯
有问题吗?
用户的真实需求
考虑一下这个问题。如果你用Switcher1
开
了灯,再去
开
一下Switcher2,灯应该是保持开着还是关了呢?从技术人员的角度来讲,调用的Switcher的开,当然应该保持开啦。但是策划会说,这两个开关应该是相互作用的,还拿出了电路图给我看。这是的确是张真实情况下的双开关电路图。
图2 双路开关电路
Switcher1的开关,拨到左边是开还是关,取决于Switcher2现在是拨在左边儿还是右边。电路图的天然连通性就自然而然地做到了这一点。现实中的Switcher1不会去问Switcher2:嘿,哥们,你现在是个啥状态?而我们的代码中的两个Switcher间也不应该有什么交集。
总而言之,在这个需求的要求下,用户要做的,就是拨一下开关而已(图3中JustSwitch方法的作用)。
对当前设计的改进
在以上需求的约束下,就第一篇开始所写的Switcher而言,就会存在着一个问题。先不说双开关,单单一个开关我们的设计就是不符合产品策划的要求。因为之前写的Switcher类是有两个函数作开关控制的。
public class Switcher
{
public ISwitchable Switchee { get; set; }
public void TurnOn() { Switchee.TurnOn(); }
public void TurnOff() { Switchee.TurnOff(); }
}
代码1
这是有问题的。因为Switcher是直接给用户用的。你觉得用户是想用哪种开关呢?
是
还是
呢?
总不能让用户根据现在灯是开着还是关着让用户按不同的按钮。(使用不同的函数。)所以Switcher的代码应该是这个样子的。
public class Switcher
{
private bool isOn;
public ISwitchable Switchee { get; set; }
public void JustSwitch()
{
// 根据当前状态选择正确的操作。
if (isOn)
{
Switchee.TurnOff();
isOn = false;
}
else
{
Switchee.TurnOn();
isOn = true;
}
}
}
代码2
Switcher自己保存最后一次操作的结果(当前状态),并自动选择正确的操作。
支持双开关
当每个灯只有一个开关的时候,这个代码没有任何问题。但是出现两个开关的话就没这么好办了,自己保存的状态是无效的,可能会被另一个开关改掉。如果要达到和电路图一样的效果,Switcher1要么问Switcher2现在是什么状态,要么问Light是什么状态。
直觉上,问Switcher2这事儿不是个好选择,因为以后还可能会有Switcher3、4。但灯就一个。但是等等,我们现在的接口是什么样的?
图3. 现在的设计
ISwitchable接口只定义了TurnOn和TurnOff两个函数,没有可以用于查询灯的当前状态的方法。这太糟糕了,这意味着接口要改了。改接口永远是最糟糕的事情。《软件框架设计的艺术》里说"API就如同恒星,一旦出现,便与我们永恒存在。",听上去接口写了就不能改,但是我们的情况要好很多,这个接口是公司自己定义的,没有别人用过。所以改改无妨。J只要小小的加一个方法就可以了。
图4. 添加查询接口以支持双开关
Switcher的代码会是这样的。Switcher暴露给用户的应该只有 一个接口。
public interface ISwitchable
{
void TurnOn();
void TurnOff();
bool IsOn();
}
public class Switcher
{
public ISwitchable Switchee { get; set; }
public void JustSwitch()
{
// 根据当前状态选择正确的操作。
if (Switchee.IsOn())
Switchee.TurnOff();
else
Switchee.TurnOn();
}
}
代码3
另一个极品方案
软件开发与建筑施工的最大区别是,软件开发可以选择先盖地下室还是天花板。
——我
当我们把要做的事情抽象一下,就能很容易地从更高的层次思考问题。比如上面,开关要知道灯的状态。可以抽象为:
图5. 开关开灯例子的高度抽象
各位可以想到什么设计上的问题?比较明显的问题有两个。
拉模式 VS推模式。既然图中为拉模式,那么另一个思路就是推模式。也许你听说一个说法,就是推模式比拉模式要好。但是如果真把推模式用在开关开灯的例子上,就成了灯的亮与熄,要去通知开关,以便开关下次Switch的时候,能做出正确地动作。想到这里,我邪恶地笑了。这得多蛋痛啊?
模式的应用,永远要看上下文。
为了抚慰一些推模式死忠们脆弱的心灵,下面会介绍一个可行的推模式开灯设计。
A依赖B。虽然我们有ISwitchable接口,开关不直接依赖灯,但是你看,我们为什么要在ISwitchable接口里加入IsOn函数呢?因为开关需要知道灯的状态。所以说,他们之间不但存在着依赖,而且还直接决定了接口的定义。但是与我第一篇文章中介绍的DIP原则是否冲突呢?这取决于你对开关的定位。如果只是单纯的开关,那么IsOn函数的引入,就是对灯的功能的抽象,也就违反了DIP原则;如果你希望开关有点儿AI,那么显然它得知道更多的信息(但是这违反了单一责任原则)。所以看上去,无论从哪个角度来讲,IsOn的引入都是要违反XXXX原则的。
好,为了不违反所有现有的原则。构想出设计出如下的设计:
图6. 引入AI系统对开关操作进行决策(拉模式)
理智一些吧,我们的开关公司没有上市,既没有资本做AI系统,也没有卡马克这种不要钱只要汉堡和网络的技术狂人,我们的用户也不会像暗黑的死忠一样傻等10年,然后等到一个需要接网线才能使用的电灯开关还能用得很愉悦。在这个发展阶段要做的,只是尽快满足当前的需求。
在达成需求前,技术方案的完美度,永远是第二位的。第一位的,是有效率地执行 + 新颖的思路和方向。思路放后面,是因为对99.9%的情况来说,最不值钱的就是点子,你能想到的,别人可能都已经做出来了。即使是Jobs这种用新意折服世界的人,也要靠"现实扭曲力场"的帮助把自己的观点有效地推行下去。
所以,这个方案虽然很不错,但是我不愿意继续讨论了,因为这在现实中没有意义,脱离现实的例子也就不再是好例子了。
我还想说一句,做项目和做人,都不能走极端。另一个极端是:以敏捷之名,无视一切编码前的设计。我猜这在群人眼中,这个系列的文章没有任何意义。
———————————————————牢骚的休止符—————————————————————
再一个方案
有人可能会说,让灯自己控制自己的状态,也可以解决问题。像下面这样。
图7 另一种解决方案
然后把Light类实现成这个样子:(多酷啊,目前为止代码量最少的方案)
public class Light : ISwitchable
{
private bool isGlowing;
public void JustSwitch()
{
isGlowing = !isGlowing;
}
}
代码 4
这个设计的确可行,但是哪个方案更好呢?这个问题就留给各位读者吧。就拿几个Principle逐个分析下应该就可以分析出个所以然来。(下一节有简单提示)
设计思想(原则)及技术方案的滥用
每种设计都有他的思路和道理。你觉得不可理喻的设计可能恰恰是别人深思熟虑的结果,只是每个人的思路不一样,结果自然也不相同。
但是如果设计思路被某种设计思想占据了绝对主导的地位,就可能会出现设计上的偏差。
我把滥用大体上总结成如下三种:
单纯的滥用。
因为我会这么做,所以就顺便这么做了
。他们的理论基础是:虽然现在没有这要的需求,谁知道以后有没有呢?我多做一点儿还不好?最典型的症状是,所有的类,都有相应的接口,全部使用Dependency Injection来实例化。这不是有病么? 这会引出一个比较大的话题,就是怎样的设计算是过度设计?这需要单独写一节来讨论这个问题。就不在这里展开了。
程度上的滥用。图7的设计,就体现了一个叫做"Tell, Don't Ask"的原则,或者说是为了将这个原则"发挥到极致"而形成的设计。而在这个原则之下写出的代码4又是如此简洁和优美;以至于让想出这个方案的人,很难主动抛弃这个方案。直到看到这个方案不能满足的需求才肯承认问题。
适用范围滥用。把某个原则或是技术方案当万金油。只要能用得上,就一定要用上一用。导致一叶蔽目,不愿意寻求其它更加合适的技术方案(项目时间紧是个最常见的借口,一知道某个方案可行,就马上付诸行动)。一时兴起,画了个漫画。
图8. 因为熟悉或懒惰而舍近求远
第一篇就说过,优秀的设计不是藉由几个原则、模式就可以保证的。何况是某一个原则呢?物极必反。今天就不再啰嗦了。大家也都懂。
设计过程中对某个特定的设计思想或是技术过于执着,往往会形成一个虽可行、却畸形的设计。
图7即是一个例子。
一个真实的案例
有一家著名的咨询公司,2009年接了一个银行的大单,为期一年,预计可以赚到五个亿。但是这个项目现在都还没有结束,项目延期不仅要给银行赔偿,还要继续免费给银行把这个项目做完。(想想国内公司会怎么做?)2010-2011年度,公司接的其它项目赚的钱几乎全部贴给了这个项目。当年全公司员工没有奖金,部分相关高层降职降薪。
为什么?一个可以赚到五个亿的项目却亏了几个亿?
目前项目内员工的工作效率极低,一个普通开发者要两天才能完成一个报表的修改。(现阶段是修改,不是全新开发)。所以说人员成本非常大。项目拖上一个月,数百万就打了水漂。
那么效率为什么这么低?他们所有的业务逻辑都用PL-SQL实现,大的报表,涉及到的PL-SQL动辄上万行,而且层层调用,加之整个系统有数千个表。代码的测试,都要先去数据库造假数据。效率能高就怪了。
为什么要这么搞?因为一开始做系统设计的人,对PL-SQL比较熟悉,对Java不熟悉,所以就把Java当成了UI Wrapper来用。狗屎吧。当然还会有很多其它的因素,但是在技术层面,这绝对是重要因素之一。
因为自己对某个技术比较熟悉,而不愿意在项目中了解和应用更适合的其它技术方案的人套上CTO之类的外衣,恒等于搅屎棍。
支持开关控制多个灯
简单而言,要让一个开关去控制所有的灯。听上去很简单,而且有很多种实现方式。但是如果仔细想想,会发现有很多问题。
需求分析,不同于简单的需求整理
用户给出的需求总会是很概括甚至模糊的,不是用户懒得说,而是用户觉得自己已经说清楚了。以开灯为例,用户需求就是:"要有一个统一开关。",你如果再去追问用户,要怎么个统一法?用户可能就会不高兴了,因为他觉得这是你应该解决的问题。但是如果简单地把"要有统一开关"这个用户需求直接写进需求文档的话,就等着项目失败或是延期吧。
需求分析的第一步,就是要对用户的需求进行分解、细节,找出合理用例。比如:用户要求的是,要有统一开关,但是并没有说每个灯就没有自己的开关。如果每个灯又都有自己的开关,统一开关应该如何与各个专属开关协作呢?从这个角度,就可以找到一些用例。
两个灯,都开着。这时去按统一开关,应该是全关对吧。也就是说,统一开关应该知道当前灯的状态。并按当前灯的状态去执行操作。
三个灯,两个开着。这时去按统一开关呢?统一开关的意思就是要有统一的行为,用户肯定
不会希望
这个统一开关的行为是:把开着的灯关掉,把关着的灯打开。怎么办?统计现在开着的比率?开着的多就全关?那如果是两个灯,一个开着,一个关着呢?还有一个办法是,让统一开关使用代码2的方案:自己记住上次的操作结果。上次是全关,这次就全开;反之亦然。
也就是说,用例1和用例2都是合理的,但却是冲突的
。这种冲突甚至是一种逻辑上的冲突,已经不是技术局限性的问题了。这种情况,在软件开发中也是很常见的。这时,我们拿着自己的分析、自己的想法去询问用户的意见,用户就会很乐意了。人们都喜欢做选择题,而不是做问答题。不是么?
现实中的电路
现实中的电路图有两种做法。(非标准电路,请意会。强弱电相关专业也许可以参考这里)
图9. 现实中的两种简单的总控加分支开关电路
前一张图,分支开关有绝对的控制权;后一张图中开关中有一个二极管,开关的开合用于控制二极管的极性,总开关的作用就是:把开着的关掉,把关着的打开。
看上去就是两种开关嘛。
基于派生类的方案
为了让一个开关可以控制多个灯,对现有程序设计上的改动很小。类图如下:
图10. 多控开关设计
通过派生新的Switcher,来提供不同的功能。在当前的需求下,这个方案是可行的。未来的需求,就留给未来去解决吧。
小结
本节本来是想讲讲需求决定设计这个理念,从两个需求引出两个互不兼容的设计方案。所以找了两个需求一起讲,但是最后这两个需求并没多大的冲突,也就没有达到预期的目标。不过想说的倒是说出来了。就是
项目的设计,最终还是要依赖于需求,任何要做出能够适应未来需求的设计的想法,都是不切实际、劳民伤财的。
(不知道有多少人会把这句话曲解为"设计无用论",如果我认为设计无用,还会写这个系列吗?)
这个系列并不是要讲设计模式,也不是用小例子做各种分析。只是想通过最简单的例子讲一些大道理(原则)。如果没有合适的大道理可说,分析出一百个需求、做出一百个设计,也只是鱼,而非我想说的渔。
距上节的发布也已经很久了,因为每回发表时,都会先想好下回写什么、怎么写,并结尾留个引子。这次很可惜,想好了下回写什么,却死活想不出合适的、简单的需求来引出这个问题,也就不知道怎么写了。所以下回什么能写好我也不知道。大体上,也许还会有下而一些道理希望能和大家分享:
过度设计及关于"过度"的度量。
局部设计最优化与全局次优化之间的权衡。
什么是设计经验及如何正确地借鉴。
新的用户
下一回讲哪个还没有讲好,但是用户的需求却如潮水般涌来。先权且记下:
在开关上加一个小灯,表示现在的灯是开还是关。(因为开关和灯可能并不在一起)
通过开关调节灯的亮度。
在开关上显示灯的耗电量、温度、预期寿命等。
定时开关。
开关声控、手势控。
开关权限控制。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复
使用道具
举报
提升卡
置顶卡
沉默卡
喧嚣卡
变色卡
千斤顶
照妖镜
相关推荐
那些年搞不懂的高深术语——依赖倒置•控制反转•依赖注入•面向接口编程
如何优雅的使用RabbitMQ
分布式锁1 Java常用技术方案
浅谈我对DDD领域驱动设计的理解
游戏编程十年总结(下)
【前端性能】高性能滚动 scroll 及页面渲染优化
验证码对抗之路及现有验证机制介绍
从零开始入门 K8s | 手把手带你理解 etcd
NHibernate之旅(2):第一个NHibernate程序
中文写程序,何陋之有?
公司的中场
Android 系统缺陷不完全点评
谈谈如何从本质上理解sql语句, 存储过程,ORM之间的联系和取舍。
.net环境下跨进程、高频率读写数据
FFmpeg开发笔记(六十二)Windows给FFmpeg集成H.266编码器vvenc
第二个iPhone应用程序:“Say Hello”
从零开始学习jQuery (十一) 实战表单验证与自动完成提示插件
Windows 8 Metro app开发初体验
高级模式
B
Color
Image
Link
Quote
Code
Smilies
您需要登录后才可以回帖
登录
|
立即注册
回复
本版积分规则
回帖并转播
回帖后跳转到最后一页
浏览过的版块
科技
签约作者
程序园优秀签约作者
发帖
荦绅诵
2025-5-29 16:16:54
关注
0
粉丝关注
11
主题发布
板块介绍填写区域,请于后台编辑
财富榜{圆}
敖可
9990
处匈跑
9998
斜素欣
9996
4
森萌黠
9996
5
堵赫然
9996
6
凶契帽
9996
7
柴古香
9996
8
背竽
9996
9
恐肩
9994
10
都硎唷
9994
查看更多