瞎扯淡 当我们在谈论类的时候我们在谈论什么

hooopo · 2015年03月31日 · 最后由 bhuztez 回复于 2015年04月01日 · 2821 次阅读

上周写了一下对 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 的时候,其实是在组织信息。把番茄划分到水果还是蔬菜体现的是不同的视角,不过硬要说番茄是植物也没错,但无意义。

面向对象设计实践指南 2 - 设计具有单一职责的类

  • OOD 的失败并非是编码技术的失败,实际上是视角的失败。
  • 尝试用一句话来描述类。如果这种描述中出现“或、和、并”这样的字,那么这个类就具备了多种职责。

如果客户或产品给我的需求是:根据用户点击返回相应结果,而实际上让我做的是一个 Ruby China......细思恐极。

植物啊!经典

随着对事物的理解和认识深入,更细化的分类不断产生。因为人们找到了更多的共性和特性。所谓“高内聚”说的就是这个意思。

我更多理解为 不是找到了共性和特性,这好像是强调相似类或概念之间的继承和交集关系等,高内聚不是分类之下的最小单元,而是只是相对于要设计出的软件规模,选择把哪些个若干元素更适合放在一起,而不是和其他元素杂乱在一起,或者过度设计到很多类和模块里。

归纳起来,简单的原则就是,“组合优于继承”,从这个原则去理解 高内聚 和 低耦合 会更有意义。面向对象的 高内聚 和 低耦合 应该被抛弃,它只适合于软件复杂度比较小能驾驭的框架,比如 Rails,虽然这个 Rails 已经很满足一般小网站大部分需求了,但是后续的维护和壮大却不得不是个坑。

我和 历史学家 Hayden White 说“理解的源头就是分类”理解不一样的是,我认为理解的源头或本质是上下文,它包含 分类,组合,联想,等等。

在我现在要设计中的 Human 编程语言 https://github.com/human-lang/draft 里,我尝试想抛弃掉编程语言里“类 (Class)”和 类方法 等这些让人匪夷所思的概念。具体还在酝酿中。。。

4 楼 已删除

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)从现实和自然语言的视角来看,类是因为相似事物的属性,而混杂随意的概念集合。和编程中的严格的数学约束虽然有相通的地方,实际上则是貌合神离。

#6 楼 @Rei 赞,Show 本身就和 ActiveRecord 之类的类天然有耦合性,在这里类的主要作用变异为了组织信息,而非组织数据。与 OOP 中类的概念天然背道而驰。

P.S. 组织信息所用的类必须是 singleton class。

#6 楼 @Rei “单一职责”并不是说一个类只能有一个方法吧。

应该是指一个方法只做 一件事,一个类只做针对它自身成员变量的操作(方法数量并不受限呀),同时一个类里不能包含 太多 关系不是那么密切的成员变量。其实简单点说就是你说的“如果一个类承担了 太多工作,那就提炼类”。

还是借用这句话,法典这种东西,更像是指南,而不是规矩。

#9 楼 @emanon 我没说过“单一职责”是指一个类只能有一个方法。

什么是一件事,什么是太多关系,这些就够好好扯淡的了。模式的好处是让人知道它的存在,坏处就是总是在谈论它的存在。

在《松本行弘的程序世界》里有这样一句话:

结构化编程基本上实现了控制流程的结构化。但是程序流程虽然结构化了,要处理的数据却并没有被结构化。面向对象的设计方法是在结构化编程对控制流程实现了结构化后,又加上了对数据的结构化。

“类”就是一个结构体,包括流程和数据。

类是对象的模板,相当于对象的雏形。

我觉得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

@bhuztez 21 天学通 Erlang!!!快!!!

需要 登录 后方可回复, 如果你还没有账号请 注册新账号