![文件](https://i-blog.csdnimg.cn/blog_migrate/501fa7027a9247c2be110531e3b247a9.png)
这篇对新手友好的文章涵盖了您在职业生涯初期应该熟悉的另一轮气味和重构。 我们介绍了案例陈述,多态性,空对象和数据类。
主题
- 案例陈述
- 多态性
- 空对象
- 资料类别
案例陈述
这也可以被称为“清单气味”之类的东西。 案例陈述之所以散发出气味,是因为它们会引起重复-它们通常也很不雅致。 它们还可能导致不必要的大类,因为所有这些对各种(并可能不断增长的)案例场景做出响应的方法通常最终都归于同一类中,从而承担了各种混合的责任。 您有很多私有方法在自己的类中会更好,这种情况并非罕见。
如果要扩展case语句,则会出现一个大问题。 然后,您必须一次又一次地更改该特定方法。 不仅在这里,因为经常有双胞胎在整个地方重复,现在也需要更新。 确定繁殖错误的好方法。 您可能还记得,我们希望对扩展开放,但对修改不开放。 在这里,修改是不可避免的,只是时间问题。
您使自己更难以提取和重用代码,而且它是一阵滴答作响的炸弹。 视图代码通常取决于这种情况陈述,这些陈述随后会复制气味并为将来的shot弹枪外科手术大开大门。 哎哟! 同样,在找到正确的执行方法之前询问对象是对“ 告诉-不要询问 ”原则的严重违反。
多态性
有一种很好的技术可以处理对case语句的需求。 花哨的单词来了! 多态性 。 这使您可以为不同的对象创建相同的接口,并使用不同方案所需的任何对象。 您只需交换适当的对象,它就可以满足您的需求,因为它具有相同的方法。 它们在这些方法下的行为是不同的,但是只要对象响应相同的接口,Ruby就不会在意。 例如, VegetarianDish.new.order
和VeganDish.new.order
行为不同,但是对#order
的响应方式相同。 您只想订购,不回答很多问题,例如是否吃鸡蛋。
通过为case语句分支提取一个类并将该逻辑移到该类的新方法中来实现多态。 您将继续对条件树中的每个分支执行此操作,并为它们提供相同的方法名称。 这样,您可以将该行为封装到最适合做出此类决定且没有理由进行进一步更改的对象中。 瞧,这样您就可以避免在一个对象上遇到所有这些烦人的问题,只需告诉它应该做什么。 当需要更多条件情况时,您只需创建另一个类即可使用相同的方法名称来处理该单一职责。
案例陈述逻辑
class Operation
def price
case @mission_tpe
when :counter_intelligence
@standard_fee + @counter_intelligence_fee
when :terrorism
@standard_fee + @terrorism_fee
when :revenge
@standard_fee + @revenge_fee
when :extortion
@standard_fee + @extortion_fee
end
end
end
counter_intel_op = Operation.new(mission_type: :counter_intelligence)
counter_intel_op.price
terror_op = Operation.new(mission_type: :terrorism)
terror_op.price
revenge_op = Operation.new(mission_type: :revenge)
revenge_op.price
extortion_op = Operation.new(mission_type: :extortion)
extortion_op.price
在我们的示例中,我们有一个Operation
类,该类需要先询问其mission_type
然后才能告诉您其价格。 很容易看出,这种price
方法只是在添加一种新的操作后才等待更改。 当您还希望在视图中显示该更改时,也需要在此应用该更改。 (仅供参考,对于视图,可以在Rails中使用多态部分函数,以避免这些case语句在整个视图中爆炸。)
多态类
class CounterIntelligenceOperation
def price
@standard_fee + @counter_intelligence_fee
end
end
class TerrorismOperation
def price
@standard_fee + @terrorism_fee
end
end
class RevengeOperation
def price
@standard_fee + @revenge_fee
end
end
class ExtortionOperation
def price
@standard_fee + @extortion_fee
end
end
counter_intel_op = CounterIntelligenceOperation.new
counter_intel_op.price
terror_op = CounterIntelligenceOperation.new
terror_op.price
revenge_op = CounterIntelligenceOperation.new
revenge_op.price
extortion_op = CounterIntelligenceOperation.new
extortion_op.price
因此,我们没有遇到麻烦,而是创建了一堆操作类,它们自己知道自己的费用加起来等于最终价格。 我们可以告诉他们给我们价格。 您不必总是摆脱原始的( Operation
)类,仅当您发现自己已经提取了所有知识时才可以。
我认为案例陈述背后的逻辑是不可避免的。 在找到能够完成任务的对象或行为之前必须经过某种检查清单的情况太常见了。 问题只是如何最好地处理它们。 我赞成不要重复它们,而是使用面向对象编程的工具来设计离散类,这些类可以通过它们的接口轻松换出。
空对象
到处检查零是一种特殊的案例陈述气味。 向对象询问nil通常是一种隐藏的案例陈述。 有条件地处理nil可以采用object.nil?
的形式object.nil?
, object.present?
, object.try
,然后在您的聚会上出现nil
的情况下采取某种措施。
另一个比较狡猾的问题是询问一个对象的真实性(是否存在或是否无效),然后采取一些措施。 看起来无害,但这只是伪装。 不要上当:三元运算符或||
当然,运营商也属于这一类。 换句话说,条件语句不仅可以清楚地标识为, unless
, if-else
或case
语句。 他们有更巧妙的方法来破坏您的聚会。 不要问对象是否为零,而要告诉他们是否缺少您想要的对象,因为空对象现在负责响应您的消息。
空对象是普通类。 它们没有什么特别的,只是一个“花哨的名字”。 您提取一些与nil
相关的条件逻辑,然后对其进行多态处理。 您将包含这些行为,通过这些类控制应用程序的流,并且还具有可用于其他适合它们的扩展的对象。 考虑一下Trial
( NullSubscription
)类如何随着时间增长。 它不仅更具DRY和yadda-yadda-yadda功能,而且更具描述性和稳定性。
使用空对象的一大好处是事情不会那么容易崩溃。 这些对象响应的消息与模拟对象的消息相同(当然,您不必总是将整个API复制到空对象中),这使您的应用没有理由发疯。 但是,请注意,空对象封装了条件逻辑,而没有完全删除它。 您只是为此找到了一个更好的家。
由于在您的应用程序中执行很多与nil相关的操作,对您的应用程序具有很强的传染性和不良影响,因此我想将空对象视为“ Nil Containment Pattern”(请不要起诉我!)。 为什么会传染? 因为如果你通过nil
周围,别的地方在你的层次结构,另一种方法是早晚也不得不问,如果nil
是在城里,然后导致另一轮采取反措施来应对这种情况。
换句话说,nil不好玩,因为要求其存在会传染。 询问对象的nil
极有可能总是差设计,没有进攻的症状,不要心疼!-we've都在那里。 我不想对“零友善”有多不友善,但愿提及一些事情:
- 尼尔(Nil)是一名聚会警察(抱歉,尼尔(nil),不得不说)。
- Nil没有帮助,因为它没有意义。
- 尼尔对任何事情都没有回应,并且违反了“ 鸭打字 ”的想法。
- 零错误消息通常很难处理。
- 尼尔迟早会咬你。
总体而言,对象丢失某些东西的情况非常频繁地出现。 一个经常被引用的例子是一个具有注册User
和NilUser
的应用程序。 但是,如果不存在的用户清楚地浏览了您的应用程序,那么这个用户是一个愚蠢的概念,因此拥有尚未注册的Guest
可能会更酷。 缺少的订阅可能是Trial
,零费用可能是Freebie
,依此类推。
为空对象命名有时是显而易见的,而且很容易,有时会非常困难。 但是,请尽量避免使用前导“ Null”或“ No”来命名所有空对象。 你可以做得更好! 我认为,提供一些背景信息将大有帮助。 选择一个更具体和有意义的名称,以反映实际用例。 这样,您就可以与其他团队成员以及未来的自我更清晰地沟通。
有两种方法可以实现此技术。 我将向您展示一个我认为直接且对初学者友好的视图。 我希望这是了解更多涉及方法的良好基础。 当您反复遇到某种涉及nil
的条件时,您就会知道该是简单地创建一个新类并将该行为移到其他地方的时候了。 之后,您让原始班级知道这个新玩家现在什么都没有了。
在下面的示例中,您可以看到Spectre
类对nil
要求过多,并不必要地使代码混乱。 它希望确保在决定收费之前我们有一个evil_operation
。 您可以看到违反“告诉不要询问”的行为吗?
另一个有问题的部分是为什么Spectre需要关心零价格的实施。 try
方法还通过or
( ||
)语句evil_operation
询问evil_operation是否具有处理虚无的price
。 evil_operation.present?
确实犯了同样的错误。 我们可以简化一下:
class Spectre
include ActiveModel::Model
attr_accessor :credit_card, :evil_operation
def charge
unless evil_operation.nil?
evil_operation.charge(credit_card)
end
end
def has_discount?
evil_operation.present? && evil_operation.has_discount?
end
def price
evil_operation.try(:price) || 0
end
end
class EvilOperation
include ActiveModel::Model
attr_accessor :discount, :price
def has_discount?
discount
end
def charge(credit_card)
credit_card.charge(price)
end
end
class NoOperation
def charge(creditcard)
"No evil operation registered"
end
def has_discount?
false
end
def price
0
end
end
class Spectre
include ActiveModel::Model
attr_accessor :credit_card, :evil_operation
def charge
evil_operation.charge(credit_card)
end
def has_discount?
evil_operation.has_discount?
end
def price
evil_operation.price
end
private
def evil_operation
@evil_operation || NoOperation.new
end
end
class EvilOperation
include ActiveModel::Model
attr_accessor :discount, :price
def has_discount?
discount
end
def charge(credit_card)
credit_card.charge(price)
end
end
我想这个例子很简单,可以立即看到空对象有多优雅。 在我们的例子中,我们有一个NoOperation
空类,该类知道如何通过以下方式处理不存在的邪恶操作:
- 处理金额为
0
费用。 - 知道不存在的操作也没有折扣。
- 给卡充电时返回一些提示性的错误字符串。
我们创建了一个处理虚无的新类,一个处理无东西的对象,如果显示nil
,则将其交换到旧类中。 API是此处的关键,因为如果它与原始类的API匹配,则可以无缝交换具有我们需要的返回值的null对象。 这正是我们在私有方法Spectre#evil_operation
。 如果我们有evil_operation
对象,则需要使用该对象; 如果没有,我们将使用知道如何处理这些消息的nil
变色龙,因此evil_operation
将不再返回nil。 鸭子最好地打字。
现在,我们有问题的条件逻辑被包装在一个地方,而空对象负责Spectre
正在寻找的行为。 干燥! 我们有点从nil无法处理的原始对象重建了相同的接口。 请记住,nil在接收消息方面做得很差-永远没人在家! 从现在开始,它只是告诉对象该怎么做,而无需先询问他们的“权限”。 同样很酷的是,根本不需要接触EvilOperation
类。
最后但并非最不重要的一点是,我摆脱了检查是否在Spectre#has_discount?
存在恶意操作Spectre#has_discount?
。 无需确保存在一项操作即可获得折扣。 由于存在null对象,因此Spectre
类更苗条,并且不承担其他类的责任。
一个好的指导原则是不要检查对象是否愿意做某事。 像钻探中士一样命令他们。 通常情况下,它在现实生活中可能并不酷,但这对于面向对象的编程是一个很好的建议。 现在给我20!
通常,使用多态而不是case语句的所有好处也适用于Null对象。 毕竟。 这只是case语句的特例。 缺点也一样:
- 了解实现和行为可能会变得很棘手,因为代码会四处传播,并且空对象的存在并不十分明确。
- 添加新行为可能需要在空对象及其对应对象之间保持同步。
资料类别
让我们以一些轻松的内容结束本文。 这是一种气味,因为它是一个除了获取和设置其数据外没有其他行为的类-基本上,它不过是一个没有额外方法处理该数据的数据容器。 这使它们本质上是被动的,因为此数据保留在那里供其他类使用。 我们已经知道这不是理想的情况,因为它使人羡慕不已 。 一般而言,我们希望具有状态的类(即它们要处理的数据)以及通过可以对该数据进行操作而不会造成太大阻碍或窥探其他类的方法的行为。
您可以通过将可能对数据起作用的其他类的行为提取到数据类中来开始重构此类。 这样,您可能会慢慢吸引该数据的有用行为并为其提供适当的位置。 如果这些类随时间增长行为,那完全没问题。 有时您可以轻松地移走整个方法,有时您需要先提取较大方法的一部分,然后将其提取到数据类中。 如有疑问,如果可以减少其他类对数据类的访问,则绝对应该这样做,并将这种行为移到另一端。 您的目标应该是在某个时候退休,使其他对象可以访问该数据的getter和setter方法。 结果,您将在各个类之间实现较低的耦合-这始终是一个胜利!
总结思想
瞧,这并没有那么复杂! 关于代码气味,有很多花哨的术语和复杂的发音技术,但是希望您意识到,气味及其重构也具有数量有限的几个特征。 代码的气味永远不会消失或变得无关紧要-至少要等到AI被增强我们的身体时,这些AI才能让我们编写距现在还差几年的质量代码。
在过去的三篇文章中,我向您介绍了在您面向对象的编程生涯中遇到的最重要和最常见的代码嗅觉场景中的很大一部分。 我认为这对新手来说是一个很好的介绍,并且希望代码示例足够详细,以方便地跟随线程。
一旦弄清了设计质量代码的这些原则,您将立即领会新的气味及其重构。 如果到目前为止,您仍然感觉自己并没有放松大局,那么我认为您已经准备好接近老板级别的OOP状态。
翻译自: https://code.tutsplus.com/articles/rubyrails-code-smell-basics-03--cms-25441