上周写了一下对 Service Object 的理解,简单来说是 Rails 里实践单一职责的一种方式,用来提取非 ActiveRecord 类。
没想到的是大家对单一职责理解还存在分歧:
leekelby
上面举例里,就包括了所有: init_beanstream_payment_gateway init_creditcard_options send_payment_info_to_gateway log_credit_card_transaction
也是一团麻,怎么保证 OrderChargeLogic 这个 class 自身的“单一职责”。
https://twitter.com/chloerei/status/582474394208862209
我不买“单一职责原则”的帐,这其实是个文字游戏。只要找到适合的主谓宾,任何多接口主体都可以解释成单一职责,例如 Ruby China 网站 —— 根据用户点击返回相应结果 —— 单一职责。
下面谈一下我对类、单一职责的一点理解。实际上,任何类比的故事都不能证明一个道理的正确性的,它只能帮助你理解这个道理想表达什么意思。
几天前,和朋友去一家云南餐馆吃饭,餐桌上放了一盆铜钱草,朋友问服务员,“这是什么?”,服务员一脸鄙夷的答“植物啊!”
面向对象设计里的类和信息架构里的类很相似。拿植物分类来说,桃、杏、李、樱原本都属于蔷薇科李属。如今已经被植物学家切分成多个类,桃花划归在桃属,杏花梅花在杏属,樱花和樱桃在樱属。
历史学家 Hayden White 说“理解的源头就是分类”。
随着对事物的理解和认识深入,更细化的分类不断产生。因为人们找到了更多的共性和特性。所谓“高聚合”说的就是这个意思。
当我们在创建 class 的时候,其实是在组织信息。把番茄划分到水果还是蔬菜体现的是不同的视角,不过硬要说番茄是植物也没错,但无意义。
如果客户或产品给我的需求是:根据用户点击返回相应结果,而实际上让我做的是一个 Ruby China......细思恐极。
随着对事物的理解和认识深入,更细化的分类不断产生。因为人们找到了更多的共性和特性。所谓“高内聚”说的就是这个意思。
我更多理解为 不是找到了共性和特性,这好像是强调相似类或概念之间的继承和交集关系等,高内聚不是分类之下的最小单元,而是只是相对于要设计出的软件规模,选择把哪些个若干元素更适合放在一起,而不是和其他元素杂乱在一起,或者过度设计到很多类和模块里。
归纳起来,简单的原则就是,“组合优于继承”,从这个原则去理解 高内聚 和 低耦合 会更有意义。面向对象的 高内聚 和 低耦合 应该被抛弃,它只适合于软件复杂度比较小能驾驭的框架,比如 Rails,虽然这个 Rails 已经很满足一般小网站大部分需求了,但是后续的维护和壮大却不得不是个坑。
我和 历史学家 Hayden White 说“理解的源头就是分类”理解不一样的是,我认为理解的源头或本质是上下文,它包含 分类,组合,联想,等等。
在我现在要设计中的 Human 编程语言 https://github.com/human-lang/draft 里,我尝试想抛弃掉编程语言里“类 (Class)”和 类方法 等这些让人匪夷所思的概念。具体还在酝酿中。。。
OrderChargeLogic 的职责就是协调,和其他 Class 互动就是它的工作。好比程序员负责写他那部分的代码是他的职责,但项目经理就这个管管,那个看看,都是在做自己的工作。
"单一职责"就是个伸缩尺,根据需要放大缩小。按照定义,Rails 的 Controller 就是不是单一职责的,因为每个 Controller 居然承担了某个资源的全部 CRUD,有时还有 Member / Collection 操作。那么这还要不要拆呢,这时候可以说服自己,CRUD 都属于操作单一资源,所以这是单一职责,于是心安了。看,这是不是文字游戏。标准库里面的类到底承担了多少职责,这时候应该选择视而不见。
当然也有贯彻落实“单一职责”,并把它作为设计哲学的人,于是搞出这样的代码:
class Show
include Lotus::Action
def call(params)
@article = Article.find params[:id]
end
end
这就是 Lotus 框架 http://lotusrb.org/ ,我刚看到的时候费解为什么会有人想写这种代码啊。别误解我,我觉得这个框架挺好的,喜欢这类风格的人就可以跟着去了,免得祸害 Rails。
后来读了 Steve Yegge 的《名词王国里的执行》(Execution in the Kingdom of Nouns),发现原来真的有人把 Java 社区批判过的东西当宝捡起来。这里的 call
不就履行了 execute
的职责吗?
TopicCommentCreator.new(topic, comment_params).execute
好,topic.comments.create(params)
坏。 —— 程序猿庄园戒律
得了吧,我编程的时候才不想着什么原则什么模式。如果一个类承担了太多工作,那就 提炼类;如果一个类做得事情太少了,那就 内联化类。要学习整理代码的技术,《重构》是本好书,它列出了一大堆模式,但不会列一堆框框条条让你一定要遵守,要具体情况具体分析。
当你一直往上把事情弄得太抽象,就会像上太空一样没有氧气。有时候这些聪明的思想家就是停不下来,然后就创造出这些荒唐又无所不包的高层次宇宙景像,这些东西什么都好,就是完全没有实际的意义。 —— Joel Spolsky,别让架构太空人吓到你
PS:新手应该多研究代码,少谈些模式,我推荐一个项目: https://github.com/rubygems/rubygems.org 。
补充一下。
1)从数学概念上来看。类是一种限制,或者说一种集合。
它定义了两个要素:一个是允许取值的集合,一个是允许参与的运算。例如 int 类型在 Java 中既定义了介于 -2 的 31 次方和 2 的 31 次方 – 1 之间的整数集合,也隐式限制了该集合上的整数所能进行的运算。
如果我们需要将 2.add(2) 的结果得到“22”,则需要将它转换为另一个 String 类。
从这个意义上来说,类型本身对类型行为就存在约束。因此 Linus 说:“Bad programmers worry about the code. Good programmers worry about data structures and their relationships.”。
2)以 OOP 的视角来看,code 即类的 method,data structures and their relationships 即类的 attributes,类的耦合关系。 从类的实例化,多态,继承、组合等各种类的关系来看类,都有一翻解释,话题太大。
3)从现实和自然语言的视角来看,类是因为相似事物的属性,而混杂随意的概念集合。和编程中的严格的数学约束虽然有相通的地方,实际上则是貌合神离。
在《松本行弘的程序世界》里有这样一句话:
结构化编程基本上实现了控制流程的结构化。但是程序流程虽然结构化了,要处理的数据却并没有被结构化。面向对象的设计方法是在结构化编程对控制流程实现了结构化后,又加上了对数据的结构化。
“类”就是一个结构体,包括流程和数据。
类是对象的模板,相当于对象的雏形。
我觉得OrderChargeLogic
设计成一个 module 可能会更合理些。
第一回合:
@rei 的原推应该是想表达:由于不同的人对单一职责的粒度定义不同,导致这条规则并不能真正解决问题。得出软件开发没有银弹的结论。
但是,也正是由于每个人的角度,定义,知识空间差异很大,于是产生了本帖,以及下面的评论。
第二回合:
本帖 @hooopo 想表达的是:通过对一个定义的差异理解,来讨论这个定义,本身没有意义。 这句话对不对呢,当然对。但是和 @Rei 的原推讨论的内容一样么,显然不一样。
第三回合:
随着讨论的深入,产生了 #6 楼 @Rei 的举例,有人贯彻落实“单一职责”成这样:
class Show
include Lotus::Action
def call(params)
@article = Article.find params[:id]
end
end
是想继续说明,由于粒度定义不同,导致符合规则的做法有很多种,不解决问题。
第四回合:
#9 楼 @emanon “单一职责”并不是说一个类只能有一个方法吧。
这句话依然是对的,但是跟 #6 楼 @Rei 的举例角度无关, @Rei 显然不是说单一职责就是这么实现的,而是说,这是一个不太好的另一种实现,以此说明统一规则被不同人解释,会产生不同的结果。
……
回帖中还有很多有价值的评论,大部分人说的都非常正确,但是由于各自知识空间和观察角度的区别,产生了很多的分歧。
这事儿又回到了原推说的:对于这件事的争论,也产生了不同的理解,你看,不同人还是会对同一个定义得出不同实现的吧,所以软件开发没有银弹。
本帖处于瞎扯淡
节点,请放心扯淡,一切责任由节点负责
你这个问题实质上是因为你的 gateway 在时间上不是独立的,只能算是伪 Service Object。在真 Service Object 面前
%% request handler
case gateway:charge(Order) of
{ok, Result} ->
done(Result);
{error, Reason} ->
report_error(Reason)
end.
%% gateway
charge(Order) ->
gen_server:call(gateway, {charge, Order}).
handle_call({charge, Order}, From, State) ->
spawn(?MODULE, handle_charge, [From, Order]),
{noreply, NewState}.
handle_charge(From, Order) ->
process_flag(trap_exit, true),
Pid = spawn_link(?MODULE, do_charge, [self(), Order]),
receive
{Pid, Result} ->
gen_server:reply(From, Result);
{'EXIT', Pid, Reason} when Reason =/= normal ->
gen_server:reply(From, {error, {crash, Reason}})
end.
do_charge(From, Order) ->
Credential = application:get_env(gateway, credential),
{Amount, CreditCard, Options} = order_info(Order),
Result = purchase(Credential, Amount, CreditCard, Options),
log_transaction_result(Order, Result),
From ! {self(), Result}.
现在你可以动态修改你的 gateway credential 而不必重启服务,还不赶紧换成 Erlang