我觉得面向对象是一种比较好的管理复杂度的方法,贫血模型本身很反面向对象,没有对数据进行细致地分类,很容易出来一堆长参数列表的方法,那些 manager 也很容易被设计成单例或者干脆就是只含静态方法的类。而这些方法里边的逻辑本该可以被更好地组织起来。
哈哈,说白了我个人比较排斥贫血模型。
那难怪了 ~ 话说现在前端有没有类似的框架?我前段时间琢磨着拿 Backbone 配合独立的 virtual DOM lib(例如 maquette)用起来算了。
居然还有在用 Backbone 的。说真的我特别喜欢这类啥都没有的框架。
我会把 by_xxx 的参数 price 和 months 放到构造方法的参数列表里,再 new 出 4 个对象分别取 attributes。按照你目前的写法,其它方法会依赖 by_xxx 方法先执行,如果 by 方法不调用,其它方法会出错,这样的对象在使用上不太友好,别人不容易注意到这个约束。
剩余的部分我跟你差不多。另外我会照习惯把 SaleDetail#attributes 之外的方法全部声明为 private,并且不开放 attr_reader。
嗯… 然后我可能不会写那个 %w[month quarter semi_annual year].index_with
🙄🙄 我这也有你要不?
不是一本一本卖啊,我每次都是同时卖好几本,每一本书只要挨个条码扫一下,然后等着顺丰上门来收就好了,打包给你收走,对方会出运费。
可以试试多抓鱼。
看懂的人,都会明白,看不懂的人,都不明白,哈哈哈
rubymine 不贵么各位?
比如说 whenever, sidekiq 这样的框架,我倾向于选择第三方的代码。首先这两个东西跟业务基本上不会有冲突,只解决技术问题。其次它们解决自身领域的问题很专业,考虑得很全面,不需要我操心太多。当然,同时也能省下我不少的开发和维护的时间。
但像权限管理这样的逻辑,很多时候很依赖具体的业务细节,开源出来的第三方代码库如果解决得问题过于全面,适用性就必然低,很有可能与使用者本身的需求有冲突;如果做得很通用,又变得几乎没有什么存在的意义——基本上它本身就没做什么事情。
确实。这问题本身也没有标准答案。具体怎么选择,我觉得要看自己所拥有的时间资源、负责这件事情的人本身的代码能力、第三方代码库本身的质量以及该代码库本身是否包揽了太多我不希望它做的事情等等…
其实仔细想的话,“不自己造轮子”这种行为只降低了做一件事情起步的成本。在我看来,它对“把一件事做好”并没有太大帮助。
我会把 2 和 1 的顺序倒一下。然后如果代码质量可以,3 我觉得无所谓。
不过在这之前,我更倾向于自己写,其次才考虑引入第三方库。比如说我现在后台权限管理的逻辑是自己写的。有时候发现其实大家能共用的逻辑没那么多,为了图省事反而费事。
只针对 8 楼回复,与主帖不是太相关。
在我的理解当中,private 是用来限制人的,不是限制程序的,让别的程序员不要瞎调用。相当于是个声明或者约定:这个方法在不是必须的情况下请不要在该类的外部调用。
其次,private 的一个优点是,在重构的时候,如果要移除某个方法,或者要重命名某个方法,该方法是 private 的,那么你就能很清楚的知道,我只要在这个类内部做修改就行了,不需要整个项目全局搜索对该方法的调用。
在 Ruby 代码当中,编写所有方法的时候我首先考虑的是 private,尤其是“写入”(修改了该类的成员变量)的方法。只有在发现该类的外部真的需要调用的时候,才会修改为 protected 或者 public。没有必要的情况下绝不暴露接口。
一个方法一旦成为 public,就相当于开放了一个全局的约定,所有人都知道你有这个方法,所有人都可以调用,这时候如果你要修改它,相当于修改了之前公布出来的约定,那么所有使用过这个方法的人有可能都要修改自己的代码来重新调用这个方法。
另一方面,暴露越多的接口,对外部来说这个类越不简洁、越复杂,或者换句话说:越难用。本来一个类就是封装了对该类成员变量操作的所有逻辑,如果把细枝末节的 private 操作都暴露出来,那封装本身就变得没什么意义,本该封装起来的复杂性还是暴露给了外部。外部的调用者很容易受诱惑去调用这些 public 接口,复杂性就这样外泄了,系统逻辑就容易变得混乱。所以写一个方法,private 是首选,真的需要开放的时候再开放。同时这样首先考虑 private 的编写原则也完全避免了楼主所担忧的问题。
还有,父类的 private 方法对子类来说未必就是 private 的,子类本来就是父类的特例化,在一些情况下,外部需要调用子类的某个方法,而该方法在父类当中是 private 的这种情况并不少见。 回头看了一下,这句与主题无关。
结论就是,楼主可能思考的方向错了。
Refactor 和项目大小有什么关系?
看完描述,我第一个反应是:如果 var_a, var_b, var_c 都是临时变量,并且方法 a 和方法 b 只负责处理这 3 个临时变量。那么把方法 a 和 b 放在当前这个类里的意义是?
这种情况下我会把计算出 var_a 和 var_b 的那些变量拿出来放到一个新的类里,变成成员变量,然后在新的类里面组织这些成员变量的方法,把方法 a 和 b 移到这个类里面。
楼主的描述还是比较模糊,比如说计算出 var_a 的变量有哪些,我们根本没法知道。其实讨论这样的问题把真实代码拿出来最好。
说个题外话,有时候我蛮提倡过度设计的。
首先,“过度设计”好过“没有设计”,这两者相较起来,前者起码说明程序员在思考,说明这些代码是被管理着的。
其次,“完全没有设计”是一个极端,“过度设计”是另一个极端。程序员要两个极端都到过,似乎才更好把握“中间”在哪里,才能更好的权衡。和“过度设计”深入地打过交道,才能更清楚地知道它到底长什么样,好像不用一开始就拒绝它吧,你说呢?
其实我觉得“让表达更清晰”这一个理由就足够让我动手对代码进行封装了。而“能不能被重用”有时候更像是个结果:短小的代码更有机会被重用,也更容易被重用。
有许多程序员认为把实现完全展开,“平铺直叙”的代码更“直观”,而另一部分程序员认为合理封装的代码更“直观”。如果对于评价一件事物好和坏的标准不一样,那好像确实只能得出不同甚至相反的结论。
另外,保证接口的逻辑正确这种事情是由提供接口的人来做的,而不是使用接口的人。我觉得这里职责就有点没分清楚了。
临时变量的作用域是局部的,如果某个方法里的代码需要不断地访问这个临时变量,那么这个方法就必定会被拉得很长。临时变量很多时候是拆分方法的一个障碍。
短小的方法总是比长方法容易阅读和维护,这是我们写代码时的价值观之一,也是对方法进行拆分的动机。如果连这一点都不认同,那么确实,拆分方法没有意义,消除临时变量也没有什么意义了。
另一方面,一个对象首先是由一组逻辑上相互关联的成员变量组成的,然后再附带了一些针对这些成员变量进行操作的方法。当一个方法里出现许多临时变量的时候,程序员就要分析对这些临时变量进行操作的代码应该是属于当前对象的职责,还是属于另一个新对象的职责。
如果参与某个临时变量的计算过程的整块代码使用的全部都是该类内部的成员变量或实例方法,说明这块代码本就属于当前类的职责,那么这个临时变量可以很容易的被抽出来变成该类自身的一个方法
#重构前
def method_a
variable_a = @instance_variable_a + get_b
variable_b = variable_a - CONSTANT_B
puts variable_a + variable_b
end
def get_b
@instance_variable_c * CONSTANT_A
end
#重构后
def method_a
puts variable_a + variable_b
end
def get_b
@instance_variable_c * CONSTANT_A
end
def variable_a
@instance_variable_a + get_b
end
def variable_b
variable_a - CONSTANT_B
end
如果参与临时变量计算过程的代码块使用的主要是传进来的参数与该类本身的某一两个成员变量或者实例方法,那么这块代码很可能属于另一个还未被发现的对象。我就直接搬重构手法“使用方法对象替换方法”的例子吧:
#重构前
class Account
def gamma(input_val, quantity, year_to_date)
important_value1 = (input_val * quantity) + delta
important_value2 = (input_val * year_to_date) + 100
if (year_to_date - important_value1) > 100
important_value2 -= 20
end
important_value3 = important_value2 * 7
important_value3 - 2 * important_value1
end
end
#重构后
class Account
def gamma(input_val, quantity, year_to_date)
Gamma.new(self, input_val, quantity, year_to_date).compute
end
end
class Gamma
def initialize(account, input_val, quantity, year_to_date)
@account = account
@input_val = input_val
@quantity = quantity
@year_to_date = year_to_date
end
def compute
important_value3 - 2 * important_value1
end
def important_value1
(@input_val * @quantity) + @account.delta
end
def important_value2
result = (@input_val * @year_to_date) + 100
if (@year_to_date - important_value1) > 100
result -= 20
end
result
end
def important_value3
important_value2 * 7
end
end
这两种情况无论哪一种都是在消除临时变量。简单地说,就是临时变量的出现经常意味着有某个对象的职责未被封装好。
对于你说的后面那半句,其实我想聊的还多一些。
我们去 KFC 点餐的时候,如果是一家店的常客,当服务员对我们说“套餐 A 今天有优惠,只要 18 元”,我们马上就可以做出选择“要还是不要套餐 A”。
但如果我们是第一次来这家店,那么肯定要先搞清楚套餐 A 里到底有些什么东西,比如中可乐、小薯条、奥尔良鸡腿堡……
再假如,我们是第一次吃 KFC,不知道中可乐有多大、小薯条有多小、奥尔良鸡腿堡里面到底有哪些料,可能又要花时间弄明白这些名字到底是什么意思。
这个例子里就已经有 3 次概念的封装了。我们人类认识事物的方式以及交流方式就是这样:先弄明白细节,然后把有共同点的细节归纳成一个抽象的概念,再用抽象的概念来进行高效的交流。上面的例子当中,“套餐 A”这个概念最抽象,也最高效;“中可乐”、“小薯条”这些概念抽象层次低一些,交流起来也相对麻烦一点;第三种情况明显就最麻烦,估计是服务员最不愿意遇到的情况了吧。
在代码里高效的交流也是这样,如果程序员读一段代码,仅仅从名字上就能知道这段代码在干嘛,那他根本没必要去看具体的实现,一句抽象的话能说清楚的事情就不用把细节展开来啰嗦。
class GetOrderItems
def get
items = []
response = throttle(2){ @client.list_order_items(@amazon_order_id).parse }
items += Common::Helper.array(response['OrderItems']['OrderItem'])
while(next_token = response['NextToken']) {
response = throttle(2){ @client.list_order_items_by_next_token(next_token).parse }
items += Common::Helper.array(response['OrderItems']['OrderItem'])
}
items
end
end
对于上面这段代码,首先从类名 GetOrderItems
和 public 方法名 get
就可以知道这个类的职责,如果你在阅读代码时满足于此,那就没必要翻开 get
方法来读它的实现。所以封装很重要,封装之后要起一个容易看懂的名字,否则还是要打开方法看具体的实现,所以可以说命名决定着封装的成功还是失败。
当你想了解 get
方法的时候,可能又发现 get
方法的实现还是有些复杂,也许这个例子不容易看出来这个 get
方法有多难读,但是我把它再整理一下:
class GetOrderItems
def initialize
#其它代码
@items=[]
end
def get
fill_items
fill_next_page while more_items?
@items
end
private
def fill_items
@response = throttle(2){ @client.list_order_items(@amazon_order_id).parse }
collect_items
end
def fill_next_page
@response = throttle(2){ @client.list_order_items_by_next_token(@next_token).parse }
collect_items
end
def more_items?
@next_token = @response['NextToken']
end
def collect_items
@items += Common::Helper.array(@response['OrderItems']['OrderItem'])
end
end
你看这个 get
方法的结构会不会清晰得多?同样也是仅用方法名就表达了它所做的事情,起码描绘清楚了这段代码的结构,把抽象层次更低的细节封装了起来。如果读到 get
方法里这 3 句代码已经足够让你明白它在做什么,就没必要再往下挖,看更具体的实现。
在楼主那个例子里,如果代码确实只有楼主贴出来的那部分那么简单,我个人其实还是会把变量赋值的细节封装起来,虽然从变量名能看出来意思,但是这个赋值总是让我忍不住要去读它后面的表达式。但是如果团队里有人坚持要这么写,我觉得也不是什么问题——前提是代码只有楼主贴出来的那部分那么简单。
其实我觉得后半部分这个话题蛮好。关于整理代码的意识,很多时候——就像你说的——人们习惯于总结出一些非此即彼的“百分之百”的标准,更不愿意在具体的场景下具体分析,引用一段话:
并不是所有的临时变量都是不好的,也并不是所有参数多于 1 个的方法就是有问题的。这里是故意把“大量临时变量”中的“大量”给省去了,把“多个参数”写成了“2 个参数”。因为不在具体的上下文当中,“大量”和“多个”根本没个标准。
这里想要表达的是,所有的 bad smell 都是一种反模式,所谓“模式”就是很容易被人发现的一种有规律的定式,一旦遇到就要动脑子分析,和具体的规模无关。但很多程序员倾向于总结出非此即彼的固定标准,因为具体问题具体分析太费脑子。所以:临时变量既然不可能完全消除,就完全不需要消除;方法参数既然不可能永远不超过 1 个那就可以是 5, 6, 7, 8 甚至 10 多个;方法行数既然不能全部减少到 5 行那就说明 100 来行也是没问题的。最终失去了对 bad smell 的警觉。
而训练有素的程序员会把这些模式当作“触发器”,时时刻刻警觉着分析这些现象在当前的情况下到底算是个问题还是算正常情况。训练有素的程序员知道很多东西不是非此即彼,不可以一概而论。
但是很多情况下我们似乎都倾向于追求和谐的统一,因为那看起来更不需要思考。只是采用这种“不需要思考”的方式写代码造成结果却往往是需要更多的思考,因为混乱的代码结构维护起来一定比条理清晰的代码费脑得多。
讨论起写代码的方法就比较兴奋,忍不住地想表达自己的想法,可能比较啰嗦,还会跑题,见谅。
是的。
关于你说的第 2 点,我知道有人会有疑问。
其实我考虑得比我上面说的还要多一些,把方法抽出来只是第一步。
根据贴出来的代码,他这里的 param 我估计是个临时的 hash 变量,类似这样:
def action_name
param = {}
# ...其它代码...
param[:type_id] = if params[:thirdCategory].present?
params[:thirdCategory]
elsif params[:secondCategory].present?
params[:secondCategory]
elsif params[:topCategory].present?
params[:topCategory]
end
# ...其它代码...
SomeClass.new(param).do_something
end
假如真的是这样的话,我最终的代码会是:
def action_name
# ...其它代码...
SomeClass.new(param).do_something
end
private
def param
{
:type_id => type_id,
:other_keys => other_values
}
end
def type_id
if params[:thirdCategory].present?
params[:thirdCategory]
elsif params[:secondCategory].present?
params[:secondCategory]
elsif params[:topCategory].present?
params[:topCategory]
end
end
当然,如果代码确实只是楼主贴出来的那部分那么简单,那么你说的没错,完全可以直接使用临时变量来存储这个表达式的结果,用变量名来表明意思。我第一步这么做只是在我的意识当中,“临时变量”是个 bad smell(关于这一点如果需要讨论的话,咱们还可以继续展开),我会对临时变量有所警惕,要不要消除临时变量我都会加以判断——如你所说:任何百分百的东西都是伪命题。
而关于第 3 点,代码结构怎么组织的问题,我觉得只要是个正常的对象,组织代码的方式其实都差不多:mixin、继承、组合。除非像 Rails 的 Controller 这样,把单次生命周期内上下文不相关的成员变量强塞在一起组成一个对象,每次实例化 Controller 对象之后又仅仅只调用它的其中的一个 public 方法——通过这种方式来隔离作用域,这样想要组织代码确实比较头疼。
param[:type_id] = params[:thirdCategory].presence || params[:secondCategory].presence || params[:topCategory].presence
v.s
param[:type_id] = type_id
谁短?你没有给出所有代码,没有上下文我只能写成这样。下一步我估计这个 param[:type_id] 都可以省掉(我注意到你这个 param 没有 s),直接改为在所有的地方调用 type_id 方法。
而 type_id 方法无论是写成
def type_id
if params[:thirdCategory].present?
params[:thirdCategory]
elsif params[:secondCategory].present?
params[:secondCategory]
elsif params[:topCategory].present?
params[:topCategory]
end
end
还是写成
def type_id
params[:thirdCategory].presence || params[:secondCategory].presence || params[:topCategory].presence
end
对使用的人或者阅读的人来说都没有影响。
至于 params.values_at(:a, :b, :c).find(&:present?)
反而读着比楼主的还费解一些,当然封装之后也关系不大。
基本上稍微有点复杂的表达式都应该封装成方法,无论是为了重用,还是仅仅只为了可读性。
好像很多程序员经常会进入一个误区,就是在短短的几行代码具体的实现上纠结所谓的“优雅”。 其实想要“优雅”封装成一个方法,起好名字就完了。
def type_id
if params[:thirdCategory].present?
params[:thirdCategory]
elsif params[:secondCategory].present?
params[:secondCategory]
elsif params[:topCategory].present?
params[:topCategory]
end
end
需要用的地方只要调用 type_id 就好,方法实现得怎么丑陋都没关系。代码就是要在命名上让别人一眼看明白,具体实现本就该被封装起来,里面的代码你们爱怎么折腾怎么折腾,只要逻辑不会出错就随便你改。
好奇你们现在不搞 Ruby 了么?
图呢?
这里面其实有两件事情。首先是重新整理数据库结构以及相关的代码,其次是换数据库,哪件先做都可以,但一次只应该做其中的一件。
同时使用两个数据库纯粹增加复杂性。
哈哈哈哈
福建 Ruby 比较难
过来抱一下,嗯嘛 ~ 晚上想吃什么?
存在“两个很大的对象会比多个小的对象更好”这种情况吗?
404 page not found