反什么? 听起来可能比实际要复杂得多。 在过去的几十年中,程序员能够识别出在整个代码解决方案中经常发生的“设计”模式的有用选择。 在解决类似问题的同时,他们能够对解决方案进行“分类”,从而阻止他们一直重新发明轮子。 重要的是要注意,与一群高级开发人员的发明相比,这些模式应被更多地视为发现。
如果这对您来说不是什么新鲜事物,并且您认为自己在Ruby / Rails的所有方面都处于初学者的地位,那么这正是为您而写的。 我认为最好将它看作是快速深入研究更深层次的主题的话题,这个话题不会一overnight而就。 不过,我坚信,早日开始学习将对初学者及其指导者大有裨益。
顾名思义,AntiPatterns代表的是模式的反义词。 他们是您绝对应避免的问题解决方案的发现。 他们通常代表没有经验的编码人员的工作,他们不知道自己不知道的东西。 更糟糕的是,它们可能是懒惰的人的输出,他们无缘无故地忽略了最佳实践和工具框架,或者他们认为自己不需要它们。 他们可能希望通过敲定快速,懒惰或肮脏的解决方案而在一开始就节省时间,这会困扰他们或项目生命周期后期的一些遗憾的继任者。
不要低估这些错误决定的含义,无论如何,它们都会困扰您。
主题
- 胖模型
- 缺少测试套件
- 偷窥模型
- 得墨meter耳定律
- 意大利面SQL
胖模型
我敢肯定,当您第一次使用Rails时,您会听到过“胖模型,瘦控制器”的无数次歌唱。 OK,现在算了! 当然,业务逻辑需要在模型层中解决,但是您不应该倾向于将所有内容都毫无意义地塞进去,只是为了避免将线跨入控制器领域。
这是您应该追求的新目标:“瘦身模型,瘦身控制器”。 您可能会问:“好吧,我们应该如何安排代码来实现这一目标?毕竟这是一个零和游戏?” 好点子! 游戏的名称是“ composition”,而Ruby装备精良,可为您提供避免模型肥胖的许多选择。
我的意思是,在大多数由数据库支持的(Rails)Web应用程序中,您的大部分注意力和工作都集中在模型层上-假设您与能在视图中实现自己的东西的称职的设计师一起工作。 您的模型将固有地具有更多的“重力”并吸引更多的复杂性。
问题是您打算如何管理这种复杂性。 Active Record无疑为您提供了很多绳索,让您垂涎三尺,同时使您的生活异常轻松。 通过遵循最高的即时便利性路径来设计模型层是一种诱人的方法。 但是,与培养大型类并将所有内容填充到Active Record对象相比,面向未来的体系结构需要更多的考虑。
您要在这里处理的真正问题是复杂性,我会说不必要。 仅堆积大量代码的类就变得很复杂。 由于它们的组成可能缺乏解耦,因此它们更难以维护,难以解析和理解并且难以更改。 这些模型通常超出其建议的处理单个职责的能力,并且遍布各地。 最坏的情况是,它们变得像垃圾车一样,处理所有懒散地扔向他们的垃圾。
我们可以做得更好! 如果您认为复杂性没什么大不了的-毕竟,您很特别,很聪明并且全都可以-再想一想! 复杂性是目前最臭名昭著的串行项目杀手—,而不是您友好的邻居“ Dark Defender”。
“ Skinnier模型”实现了编码行业中的高级人才(也许比代码和设计更多的专业)的欣赏,而我们所有人都应该努力做到-简洁! 或至少更多,如果很难消除复杂性,这是一个合理的折衷方案。
Ruby提供了哪些工具来简化我们的生活,并让我们减少模型中的负担? 简单的其他类和模块。 您确定可以提取到另一个对象中的一致代码,从而构建一个模型层,该模型层由大小合理的代理组成,这些代理具有自己独特的,独特的职责。
用才华横溢的演员来考虑。 在现实生活中,这样的人可能会说唱,打破,写歌词并产生自己的音乐。 在编程中,您更喜欢乐队的动力(这里至少有四个独特的成员),每个人负责最少的事情。 您想建立一个可以处理作曲家复杂性的类乐队,而不是所有交易的微观管理天才大师类。
让我们看一个肥胖模型的例子,并尝试几种方法来解决其肥胖问题。 当然,这个例子是一个虚拟的例子,我希望通过讲这个愚蠢的小故事,希望对新手来说更容易理解和遵循。
我们有一个Spectre类,职责过多,因此不必要地增长了。 除了这些方法之外,我认为可以很容易地想象到,这样的标本也已经积累了很多其他东西,以三个小点表示。 幽灵即将成为神灵阶级 。 (很短的机会很快就会再次明智地提出这样的句子!)
class Spectre < ActiveRecord::Base
has_many :spectre_members
has_many :spectre_agents
has_many :enemy_agents
has_many :operations
...
def turn_mi6_agent(enemy_agent)
puts "MI6 agent #{enemy_agent.name} turned over to Spectre"
end
def turn_cia_agent(enemy_agent)
puts "CIA agent #{enemy_agent.name} turned over to Spectre"
end
def turn_mossad_agent(enemy_agent)
puts "Mossad agent #{enemy_agent.name} turned over to Spectre"
end
def kill_double_o_seven(spectre_agent)
spectre_agent.kill_james_bond
end
def dispose_of_cabinet_member(number)
spectre_member = SpectreMember.find_by_id(number)
puts "A certain culprit has failed the absolute integrity of this fraternity. The appropriate act is to smoke number #{number} in his chair. His services won’t be greatly missed"
spectre_member.die
end
def print_assignment(operation)
puts "Operation #{operation.name}’s objective is to #{operation.objective}."
end
private
def enemy_agent
#clever code
end
def spectre_agent
#clever code
end
def operation
#clever code
end
...
end
幽灵号召各种敌方特工,杀死007代表,当幽灵号的内阁成员失败时将其烧烤,并打印出作战任务。 一个明确的微观管理案例,绝对违反了“单一责任原则”。 私有方法也在快速堆积。
此类不需要了解当前其中的大多数内容。 我们将把这个功能分为几类,看看是否有更多的类/对象的复杂性值得抽脂。
class Spectre < ActiveRecord::Base
has_many :spectre_members
has_many :spectre_agents
has_many :enemy_agents
has_many :operations
...
def turn_enemy_agent
Interrogator.new(enemy_agent).turn
end
private
def enemy_agent
self.enemy_agents.last
end
end
class Interrogator
attr_reader :enemy_agent
def initialize(enemy_agent)
@enemy_agent = enemy_agent
end
def turn
enemy_agent.turn
end
end
class EnemyAgent < ActiveRecord::Base
belongs_to :spectre
belongs_to :agency
def turn
puts 'After extensive brainwashing, torture and hoards of cash…'
end
end
class MI6Agent < EnemyAgent
def turn
super
puts "MI6 agent #{name} turned over to Spectre"
end
end
class CiaAgent < EnemyAgent
def turn
super
puts "CIA agent #{name} turned over to Spectre"
end
end
class MossadAgent < EnemyAgent
def turn
super
puts "Mossad agent #{name} turned over to Spectre"
end
end
class NumberOne < ActiveRecord::Base
def dispose_of_cabinet_member(number)
spectre_member = SpectreMember.find_by_id(number)
puts "A certain culprit has failed the absolute integrity of this fraternity. The appropriate act is to smoke number #{number} in his chair. His services won’t be greatly missed"
spectre_member.die
end
end
class Operation < ActiveRecord::Base
has_many :spectre_agents
belongs_to :spectre
def print_assignment
puts "Operation #{name}’s objective is to #{objective}."
end
end
class SpectreAgent < ActiveRecord::Base
belongs_to :operation
belongs_to :spectre
def kill_james_bond
puts "Mr. Bond, I expect you to die!"
end
end
class SpectreMember < ActiveRecord::Base
belongs_to :spectre
def die
puts "Nooo, nooo, it wasn’t meeeeeeeee! ZCHUNK!"
end
end
我认为您应该注意的最重要部分是我们如何使用诸如Interrogator
类的简单Ruby类来处理来自不同代理的代理的转向。 实际示例可以表示一个转换器,例如,将HTML文档转换为pdf,反之亦然。 如果您不需要Active Record类的全部功能,那么如果简单的Ruby类也可以解决问题,为什么还要使用它们呢? 少挂一些绳子。
Spectre类使将代理转为Interrogator
类的Interrogator
而只是委托给它。 现在,这一责任仅是折磨和洗脑被捕特工的责任。
到目前为止,一切都很好。 但是为什么我们要为每个代理创建单独的类? 简单。 我们不仅直接将诸如turn_mi6_agent
类的各种转弯方法提取到Interrogator
,还为他们在各自的类中提供了更好的住所。
结果,我们可以有效地利用多态性,而不必在意个别情况下的车削剂。 我们只是告诉这些不同的代理对象转身,每个对象都知道该怎么做。 Interrogator
器不需要了解每个代理如何转动的细节。
由于所有这些代理都是Active Record对象,因此我们创建了一个通用的EnemyAgent
, EnemyAgent
转用代理的含义具有一般意义,并且通过将其子类化,将所有代理的位封装在一个地方。 我们通过为每个特工提供super
的turn
方法来利用这种继承性,因此我们无需重复就可以进入洗脑和酷刑行业。 单一职责和无重复是继续前进的良好起点。
其他Active Record类承担Spectre不需要关心的各种职责。 通常,失败的Spectre内阁成员本人也要烧烤“第一”,那么为什么不让专用物品来处理触电呢? 另一方面,失败的Spectre成员知道NumberOne
在椅子上抽烟时会如何NumberOne
。 现在, Operation
可以自己打印其任务-无需将Spectre的时间浪费在这样的花生上。
最后但并非最不重要的,杀詹姆斯·邦德通常是由在该领域的代理企图,所以kill_james_bond
是现在的方法SpectreAgent
。 当然,Goldfinger的处理方式会有所不同-我猜如果您有一个激光东西,就不要玩。
您可以清楚地看到,我们现在有十个班级,而以前只有一个。 那不是太多吗? 当然可以。 在您分担此类责任时,大多数时候都需要解决这个问题。 您绝对可以这样做。 但是从另一个角度来看这可能会有所帮助:
- 我们有没有分开关注? 绝对!
- 我们是否有更轻巧,更瘦的课程,更适合于处理单一职责? 很确定!
- 我们是否讲一个“故事”,我们是否更清晰地描绘了谁参与并负责某些行动? 但愿如此!
- 消化每个班级正在做的事情容易吗? 当然!
- 我们是否减少了私有方法的数量? 对!
- 这是否代表了更好的面向对象编程质量? 由于我们仅在设置这些对象所需要的地方使用了组合并仅引用继承,因此您敢打赌!
- 感觉更干净吗? 是!
- 我们是否更有能力在不弄乱的情况下更改代码? 当然可以!
- 它值得吗? 你怎么看?
我并不是说这些问题每次都需要从清单中剔除,但是这些可能是您在精简模型时应该开始问自己的事情。
设计瘦模型可能很困难,但这是保持应用程序健康和敏捷的必要措施。 这些也不是处理胖模型的唯一建设性方法,但它们是一个良好的开端,尤其是对于新手。
缺少测试套件
这可能是最明显的反模式。 从测试驱动的角度来看,触摸没有测试覆盖范围的成熟应用可能是最痛苦的经历之一。 如果您想恨世界和自己的职业比什么都重要,那么只需花六个月的时间来从事这样的项目,您就会了解到,您可能会遇到多少不幸的人。 当然是在开玩笑,但是我怀疑这会让您更快乐,并且您想再做一次。 也许一周也可以。 我非常确定,折磨一词会比您想像的更多地出现在您的脑海中。
如果到目前为止,测试还不是您的过程的一部分,并且这种痛苦对您的工作而言是正常的,那么也许您应该考虑测试不是那么糟糕,也不是您的敌人。 当与代码相关的喜好程度或多或少地恒定在零以上并且可以无所畏惧地更改代码时,与焦虑和痛苦所污染的输出相比,您的工作总体质量将高得多。
我高估了吗? 我真的不这么认为! 您希望拥有一个非常广泛的测试范围,这不仅是因为它是仅用于编写实际需要的代码的出色设计工具,而且还因为将来您将需要更改代码。 如果您拥有有助于并指导重构,维护和扩展的测试工具,那么您将可以更好地与代码库打交道,并且更有信心。 他们肯定会在未来发生,对此毫无疑问。
这也是测试套件开始获得第二轮收益的要点,因为您认为认为编写测试的人开发的应用程序无法长期实现,因此您可以安全地进行这些质量更改的速度提高了废话或花费太多时间。
偷窥模型
这些是超级笨拙的模型,想要收集太多有关其他对象或模型的信息。 这与面向对象编程中最基本的思想之一-封装形成了鲜明的对比。 我们宁愿努力争取独立的类和模型来尽可能地自己管理内部事务。 就编程概念而言,这些偷窥模型基本上违反了“最少知识原理”,也就是“得墨meter耳法则”(无论您想如何发音)。
得墨meter耳定律
为什么这是个问题? 它是一种复制形式(一种微妙的复制),并且还会导致代码比预期的要脆弱得多。
得墨meter耳定律几乎是最可靠的代码味道,您可以随时攻击它,而不必担心可能的不利之处。
我想称它为“法律”并不像听起来那样自命不凡。 挖掘这种气味,因为您在项目中将非常需要它。 它基本上指出,就对象而言,您可以在对象的朋友上调用方法,而不能在朋友的朋友上调用方法。
这是一种常见的解释方式,归结为方法调用使用的点数不超过一个点。 顺便说一句,当您处理一个单一的对象时,最好不要使用更多的点或方法调用。 像@weapons.find_by_name('Poison dart').formula
的东西就可以了。 发现者有时会堆积很多点。 但是,将它们封装在专用方法中是一个好主意。
违反Demeter的法律
让我们看一下上面的类中的几个不好的例子:
@operation.spectre_agents.first.kill_james_bond
@spectre.operations.last.spectre_agents.first.name
@spectre.enemy_agents.last.agency.name
为了掌握它,这里有一些虚构的:
@quartermaster.gizmos.non_lethal.favorite
@mi6.operation.agent.favorite_weapon
@mission.agent.name
香蕉吧? 看起来不好吗? 如您所见,这些方法调用过多地窥视其他对象的业务。 最重要,最明显的负面影响是,如果需要更改这些对象的结构,那么就需要在整个地方更改一堆这样的方法调用,因为在软件开发中唯一不变的就是更改,所以最终它们将更改。 而且,它看起来真的很讨厌,根本不方便看。 当您不知道这是一个有问题的方法时,Rails可以让您走得更远,而无需大惊小怪。 很多绳子,还记得吗?
那么我们该怎么办? 毕竟,我们希望以某种方式获得该信息。 一方面,我们可以根据自己的需要来组合对象,并且可以巧妙地使用委托来使我们的模型同时保持苗条。 让我们深入研究一些代码,向您展示我的意思。
class SpectreMember < ActiveRecord::Base
has_many :operations
has_many :spectre_agents
...
end
class Operation < ActiveRecord::Base
belongs_to :spectre_member
...
end
class SpectreAgent < ActiveRecord::Base
belongs_to :spectre_member
...
end
@spectre_member.spectre_agents.all
@spectre_member.operations.last.print_assignment
@spectre_member.spectre_agents.find_by_id(1).name
@operation.spectre_member.name
@operation.spectre_member.number
@operation.spectre_member.spectre_agents.first.name
@spectre_agent.spectre_member.number
class SpectreMember < ActiveRecord::Base
has_many :operations
has_many :spectre_agents
...
def list_of_agents
spectre_agents.all
end
def print_operation_details
operation = Operation.last
operation.print_operation_details
end
end
class Operation < ActiveRecord::Base
belongs_to :spectre_member
...
def spectre_member_name
spectre_member.name
end
def spectre_member_number
spectre_member.number
end
def print_operation_details
puts "This operation’s objective is #{objective}. The target is #{target}"
end
end
class SpectreAgent < ActiveRecord::Base
belongs_to :spectre_member
...
def superior_in_charge
puts "My boss is number #{spectre_member.number}"
end
end
@spectre_member.list_of_agents
@spectre_member.print_operation_details
@operation.spectre_member_name
@operation.spectre_member_number
@spectre_agent.superior_in_charge
这绝对是朝正确方向迈出的一步。 如您所见,我们通过一系列包装器方法封装了我们想要获取的信息。 我们没有直接跨越许多对象,而是对这些桥进行了抽象,然后将其留给相应的模型与他们的朋友讨论我们所需的信息。
这种方法的缺点是所有这些额外的包装方法都摆在了后面。 有时候很好,但是如果对象发生更改,我们真的想避免在很多地方维护这些方法。
如果可能,他们进行更改的专用位置在对象上,也可以仅在对象上。 使用与自己的模型本身无关的方法来污染对象也是要注意的事情,因为这始终是减少单个职责的潜在危险。
我们可以做得更好。 在可能的情况下,让我们将方法调用直接委派给其负责的对象,并尝试尽可能减少包装器方法。 Rails知道我们需要什么,并为我们提供了方便的delegate
类方法,以告知对象的朋友我们需要调用哪些方法。
让我们放大前面的代码示例中的内容,看看在哪里可以适当地使用委托。
class Operation < ActiveRecord::Base
belongs_to :spectre_member
delegate :name, :number, to: :spectre_member, prefix: true
...
# def spectre_member_name
# spectre_member.name
# end
# def spectre_member_number
# spectre_member.number
# end
...
end
@operation.spectre_member_name
@operation.spectre_member_number
class SpectreAgent < ActiveRecord::Base
belongs_to :spectre_member
delegate :number, to: :spectre_member, prefix: true
...
def superior_in_charge
puts "My boss is number #{spectre_member_number}"
end
...
end
如您所见,我们可以使用方法委托来简化一些事情。 我们完全摆脱了Operation #spectre_member_name
Operation #spectre_member_number
和SpectreAgent
Operation #spectre_member_number
,并且SpectreAgent
不再需要在spectre_member
上调用number
number
直接委托回其“原始”类SpectreMember
。
如果起初有点令人困惑,这究竟如何工作? 您告诉委托它应该委托to:
哪个:method_name
to:
哪个:class_name
(也可以使用多个方法名)。 prefix: true
是可选的。
在我们的例子中,它在方法名称之前为接收类的蛇形类名加上前缀,并使我们能够调用operation spectre_member_name
而不是可能有歧义的operation.name
-如果我们未使用prefix选项。 这与belongs_to
和has_one
关联非常有效。
但是,在has_many
方面,音乐将停止并且您会遇到麻烦。 这些关联为您提供了一个集合代理,当您将方法委托给这些“集合”时,它们将向您抛出NameErrors或NoMethodErrors。
意大利面SQL
为了使有关Rails中的模型AntiPatterns的本章更完整,我想花点时间在涉及SQL时避免什么。 Active Record关联提供了一些选项,使您在知道应该远离的地方时会大大简化生活。 Finder方法本身就是一个完整的主题-我们不会全面介绍它们-但我想提及一些通用的技术,即使您编写非常简单的技术也可以为您提供帮助。
我们应该关注的事情反映了到目前为止我们学到的大多数知识。 我们希望使用意图揭示,简单合理的命名方法来在模型中查找内容。 让我们直接研究代码。
class Operation < ActiveRecord::Base
has_many :agents
...
end
class Agent < ActiveRecord::Base
belongs_to :operation
...
end
class OperationsController < ApplicationController
def index
@operation = Operation.find(params[:id])
@agents = Agent.where(operation_id: @operation.id, licence_to_kill: true)
end
end
看起来无害,不是吗? 我们只是在寻找一堆拥有可杀死我们操作页面许可证的代理。 再想一想。 为什么OperationsController
应该深入探究Agent
的内部? 另外,这真的是我们在Agent
上封装查找Agent
吗?
如果您认为可以添加诸如Agent.find_licence_to_kill_agents
类的类方法来封装查找程序逻辑,那么您肯定在朝着正确的方向迈出了一步,尽管还远远不够。
class Agent < ActiveRecord::Base
belongs_to :operation
def self.find_licence_to_kill_agents(operation)
where(operation_id: operation.id, licence_to_kill: true)
end
...
end
class OperationsController < ApplicationController
def index
@operation = Operation.find(params[:id])
@agents = Agent.find_licence_to_kill_agents(@operation)
end
end
我们必须比这更加参与。 首先,这没有利用我们的优势,并且封装也不理想。 像has_many
这样的关联具有我们可以添加到返回的代理数组中的好处。 我们可以这样做:
class Operation < ActiveRecord::Base
has_many :agents
def find_licence_to_kill_agents
self.agents.where(licence_to_kill: true)
end
...
end
class OperationsController < ApplicationController
def index
@operation = Operation.find(params[:id])
@agents = @operation.find_licence_to_kill_agents
end
end
当然,这是可行的,但这只是朝正确方向迈出的又一小步。 是的,控制器要好一些,并且我们很好地利用了模型关联,但是您仍然应该对为什么Operation
与寻找某种类型的Agent
的实现有关感到怀疑。 该责任属于Agent
模型本身。
命名范围非常方便。 范围为模型定义了可链接的(非常重要的)类方法,从而使您可以指定有用的查询,这些查询可以用作模型关联之上的其他方法调用。 以下两种范围界定Agent
是无关紧要的。
class Agent < ActiveRecord::Base
belongs_to :operation
scope :licenced_to_kill, -> { where(licence_to_kill: true) }
end
class Agent < ActiveRecord::Base
belongs_to :operation
def self.licenced_to_kill
where(licence_to_kill: true)
end
end
class OperationsController < ApplicationController
def index
@operation = Operation.find(params[:id])
@agents = @operation.agents.licenced_to_kill
end
end
那好多了。 在案件范围的语法是新的给你,他们只是(stabby)lambda表达式,并不十分寻找到他们重要的马上,顺便说一句,他们是正确的方法调用作用域以来的Rails 4. Agent
现已在负责管理自己的搜索参数,而关联可以根据需要查找内容。
这种方法使您可以将查询作为单个SQL调用来实现。 我个人喜欢使用scope
的明确性。 范围也很容易链接到命名良好的查找器方法中,从而使它们增加了重用代码和DRY处理代码的可能性。 假设我们还涉及到一些事情:
class Agent < ActiveRecord::Base
belongs_to :operation
scope :licenced_to_kill, -> { where(licence_to_kill: true) }
scope :womanizer, -> { where(womanizer: true) }
scope :bond, -> { where(name: 'James Bond') }
scope :gambler, -> { where(gambler: true) }
end
现在,我们可以使用所有这些范围来定制构建更复杂的查询。
class OperationsController < ApplicationController
def index
@operation = Operation.find(params[:id])
@double_o_agents = @operation.agents.licenced_to_kill
end
def show
@operation = Operation.find(params[:id])
@bond = @operation.agents.womanizer.gambler.licenced_to_kill
end
...
end
当然可以,但是我建议您再走一步。
class Agent < ActiveRecord::Base
belongs_to :operation
scope :licenced_to_kill, -> { where(licence_to_kill: true) }
scope :womanizer, -> { where(womanizer: true) }
scope :bond, -> { where(name: 'James Bond') }
scope :gambler, -> { where(gambler: true) }
def self.find_licenced_to_kill
licenced_to_kill
end
def self.find_licenced_to_kill_womanizer
womanizer.licenced_to_kill
end
def self.find_gambling_womanizer
gambler.womanizer
end
...
end
class OperationsController < ApplicationController
def index
@operation = Operation.find(params[:id])
@double_o_agents = @operation.agents.find_licenced_to_kill
end
def show
@operation = Operation.find(params[:id])
@bond = @operation.agents.find_licenced_to_kill_womanizer
#or
@bond = @operation.agents.bond
end
...
end
如您所见,通过这种方法,我们获得了正确封装,模型关联,代码重用和方法的表达性命名的好处,并且所有这些都在执行单个SQL查询时得到。 没有更多的意大利面代码,太棒了!
如果您担心违反Demeter法则,您会很高兴听到,因为我们不是通过进入关联的模型来添加点,而是将它们仅链接到自己的对象上,所以我们不会犯罪。
最后的想法
从初学者的角度来看,我认为您已经学到了很多有关如何更好地处理Rails模型以及如何在不要求执行hang子手的情况下更健壮地对其建模的知识。
但是,请不要误以为在这个特定主题上没有更多的知识要学习。 我向您介绍了一些AntiPatterns,我认为新手可以轻松理解和使用它们,以便尽早保护自己。 如果您不知道自己所不知道的事,可以在脖子上绕很多圈。
尽管这是本主题的坚实开端,但Rails模型中的AntiPatterns不仅涉及更多方面,而且还需要探索更多细微差别。 这些都是基础知识-非常重要和重要的基础知识-您应该有一段时间感到自己很称职,直到您在职业生涯中没有等到很晚才发现这些问题。
翻译自: https://code.tutsplus.com/articles/antipatterns-basics-rails-models--cms-25636