【问题标题】:Possible help in code refactoring代码重构的可能帮助
【发布时间】:2015-06-18 14:26:45
【问题描述】:

Sandi Metz 在 GORUCO 的 SOLID OOPS 概念 中说,Ruby 中存在if..else 块可以被认为是对开闭原则的偏离。可以使用哪些方法来避免不紧急的if..else 条件?我尝试了以下代码:

class Fun
   def park(s=String.new)
      puts s
   end
   def park(i=Fixnum.new)
      i=i+2
   end
end

发现函数重载在 Ruby 中不起作用。还有哪些其他方法可以使代码服从 OCP?

我本来可以直接去的:

class Fun
  def park(i)
      i=i+2 if i.class==1.class 
      puts i if i.class=="asd".class
  end
end

但这违反了OCP。

【问题讨论】:

  • 我更正了它,因为与 OP 的问题无关,只是一个错字。也许一个有效的答案可能会指出无法应用 Sandi Metz 的演示文稿(我不确定),并且在不涉及设计意见的情况下,也许可以解释为什么许多动态语言会避免基于参数的方法重载。
  • 我认为 OCP 背后的想法是 i 应该产生自己的结果 - 而不是 park() 根据 i 的类型计算结果。
  • 应用“用多态重构替换条件”。这真的没那么难。 Smalltalk 甚至没有条件(或循环,就此而言),但您可以在其中非常优雅地表达您想要的一切。面向对象的语言不需要条件,反正运行时多态动态消息分发功能更强大。
  • @JörgWMittag 你能解释一下吗
  • @Anony-mouse 您还有其他问题吗? stackoverflow.com/q/30922788/438992

标签: ruby oop refactoring solid-principles


【解决方案1】:

对于您当前的示例,并且希望避免类型检测,我将使用 Ruby 的功能重新打开类以向 Integer 和 String 添加您需要的功能:

class Integer
  def park
    puts self + 2
  end
end

class String
  def park
    puts self
  end
end

这在更改您自己的类时会更干净。但也许它不适合您的概念模型(这取决于 Fun 代表什么,以及为什么它可以在一个方法中使用这两个不同的类)。

等价但保留您的 Fun 类可能是:

class Fun
  def park_fixnum i
    puts i + 2
  end

  def park_string s
    puts s
  end

  def park param
    send("park_#{param.class.to_s.downcase}", param)
  end
end

作为一种观点,我不确定通过这种方式编写 Ruby 是否会收获很多。您正在学习的原则可能是好的(我不知道),但强行应用它们“违背语言的特点”可能会产生可读性较差的代码,无论它是否符合善意的设计。

所以我可能在实践中会这样做:

class Fun
  def park param
    case param
    when Integer
      puts param + 2
    when String
      puts param
    end
  end
end

这不符合您的原则,但它是惯用的 Ruby,并且比 if 块更易于阅读和维护(其中条件可能要复杂得多,因此人类需要更长的时间来解析)。

【讨论】:

  • 这不符合你的原则,但是是地道的 Ruby。使用带有when 的case 语句并为分支使用types 会使您的代码更棘手,但它与带有类型检查的多分支if 语句没有什么不同。我认为任何语言的类型检查都不被认为是惯用的。
  • @7stud:我同意这在逻辑上没有什么不同,我建议对于类型检查代码来说它更容易阅读。我也同意避免类型检查的需要是值得的,但我的不同之处在于采取多远。 “聪明”的动态分派,或将类模型修改为不直观(将“park”添加到字符串)可能比 RTTI 更糟糕的代码气味。我认为 惯用 的意思是“它被认为是问题的标准解决方案”。您会使用case 找到很多关于 SO 的示例问题,例如stackoverflow.com/questions/3908380/…
  • @NeilSlater RTTI 是什么?
  • RTTI = 运行时类型标识。在 Ruby 中使用可能是一个糟糕的首字母缩略词(其中没有类型的编译时间限制),但在 C++ 和 Java 等强类型 OO 语言中通常可以理解。
  • @Anony-mouse:RTTI 只是我对你正在做的事情的(坏)简写,你的问题就是一个例子。对于 C++ 示例,与您所询问的 OO 设计相关的答案,您可以看到 stackoverflow.com/questions/16266339/…
【解决方案2】:

你可以像这样为 Fun 创建处理类

class Fun
   def park(obj)
    @parker ||= Object.const_get("#{obj.class}Park").new(obj)
    @parker.park 
    rescue NameError => e
        raise ArgumentError, "expected String or Fixnum but recieved #{obj.class.name}"
   end
end

class Park
    def initialize(p)
        @park = p
    end
    def park
        @park
    end
end

class FixnumPark < Park
    def park
        @park += 2
    end
end

class StringPark < Park
end

这样就可以了

f = Fun.new
f.park("string")
#=> "string"
f.instance_variable_get("@parker")
#=> #<StringPark:0x1e04b48 @park="string">
f = Fun.new
f.park(2)
#=> 4
f.instance_variable_get("@parker")
#=> #<FixnumPark:0x1e04b48 @park=4>
f.park(22)
#=> 6 because the instance is already loaded and 4 + 2 = 6
Fun.new.park(12.3)
#=> ArgumentError: expected String or Fixnum but received Float

【讨论】:

  • @parker ||= Object.const_get("#{obj.class}Park").new(obj) 你能解释一下这行吗?
  • @Anony-mouse 这是延迟加载,这意味着如果@parker 未设置,则设置它,否则继续当前的@parker,它被评估为@parker = @parker || Object.const_get("#{obj.class}Park").new(obj)。因此,如果 @parker 为 nil,则评估 or 部分。 Object.const_get 将返回由字符串引用的常量,因此在 obj 是 String 的情况下,这将是 Object.const_get("StringPark") 然后它使用 obj 调用 new 所以如果 obj == "string" 它被评估为 @987654333 @。希望这会有所帮助
  • 我理解或等同于 ruby​​ 中的操作,但你能解释一下你用Object.const_get("#{obj.class}Park").new(obj)做了什么
  • @Anony-mouse 我以为我在上面很清楚地回顾了它,但让我尝试添加一些细节,假设obj 是String 然后Object.const_get("StringPark") 将返回类StringPark然后我们调用StringPark.new(obj) this set this 然后将StringPark 中的@parker 实例变量设置为obj 的值。这将允许您向StringPark 添加适用于String 对象的其他方法,而不会影响FixnumPark。这有帮助吗?
【解决方案3】:

你可以这样做:

class Parent
  attr_reader :s

  def initialize(s='')
    @s = s
  end

  def park
    puts s
  end
end

class Child1 < Parent
  attr_reader :x

  def initialize(s, x)
    super(s)
    @x = x
  end

  def park
    puts x 
  end
end

class Child2 < Parent
  attr_reader :y

  def initialize(s, y)
    super(s)
    @y = y
  end

  def park
    puts y
  end
end


objects = [
  Parent.new('hello'),
  Child1.new('goodbye', 1),
  Child2.new('adios', 2),
]

objects.each do |obj|
  obj.park
end

--output:--
hello
1
2

或者,也许我忽略了你的一个曲折:

class Parent
  attr_reader :x

  def initialize(s='')
    @x = s
  end

  def park
    puts x
  end
end

class Child1 < Parent
  def initialize(x)
    super
  end

  def park
    x + 2 
  end
end

class Child2 < Parent
  def initialize(x)
    super
  end

  def park
    x * 2
  end
end


objects = [
  Parent.new('hello'),
  Child1.new(2),
  Child2.new(100),
]

results = objects.map do |obj|
  obj.park
end

p results

--output:--
hello
[nil, 4, 200]

还有一个使用 blocks 的例子,它们就像匿名函数。您可以将所需的行为作为函数传递给 park():

class Function
  attr_reader :block

  def initialize(&park)
    @block = park 
  end

  def park
    raise "Not implemented"
  end
end


class StringFunction < Function
  def initialize(&park)
    super
  end

  def park
    block.call
  end
end

class AdditionFunction < Function
  def initialize(&park)
    super
  end

  def park
    block.call 1
  end
end

class DogFunction < Function
  class Dog
    def bark
      puts 'woof, woof'
    end
  end

  def initialize(&park)
    super
  end

  def park
    block.call Dog.new
  end
end


objects = [
  StringFunction.new {puts 'hello'},
  AdditionFunction.new {|i| i+2},
  DogFunction.new {|dog| dog.bark},
]

results = objects.map do |obj|
  obj.park
end

p results

--output:--
hello
woof, woof
[nil, 3, nil]

【讨论】:

    【解决方案4】:

    看is_a?方法

    def park(i)
      i.is_a?(Fixnum) ? (i + 2) : i
    end
    

    但最好不要检查类型,而是使用鸭子类型:

    def park(i)
      i.respond_to?(:+) ? (i + 2) : i
    end
    

    UPD:读完 cmets。是的,上面的两个例子都没有解决 OCP 问题。我就是这样做的:

    class Fun
      # The method doesn't know how to pluck data. But it knows a guy
      # who knows the trick
      def pluck(i)
        return __pluck_string__(i) if i.is_a? String
        __pluck_fixnum__(i) if i.is_a? Fixnum
      end
    
      private
    
      # Every method is responsible for plucking data in some special way
      # Only one cause of possible changes for each of them
    
      def __pluck_string__(i)
        puts i
      end
    
      def __pluck_fixnum__(i)
        i + 2
      end
    end
    

    【讨论】:

    • 这仍然使用条件,但它已被折叠成三元语句。
    • 但它仍然是一个三元运算符。我想通过函数重载等面向对象的原则完全避免它们。但是 Ruby 不支持它,我正在寻找一种不会导致代码错误的替代方法。我我正在寻找一种解决方案,如果我决定再添加一个检查,它不会导致主类发生变化。但可能只会导致方法的添加。
    • 恕我直言,您不能完全排除该决定,只是因为您期望根据某些情况出现不同的行为。因此,正确的问题是谁负责该选择。您可以使用各种park-s 定义各种类,但是您应该在某处添加一个工厂方法来选择在每种情况下要实例化的对象。
    • 你正在做的事情的特定方式并不重要——它违反了 OCP 原则,因为当你添加类型时,你必须更改 if 语句/三元运算符/whatever。跨度>
    • 虽然我是鸭子打字的坚定支持者(可能是关于 ruby​​ 和真正的 OO 编程的最喜欢的部分之一)String 确实 respond_to?(:+) 所以这不符合要求的条件跨度>
    【解决方案5】:

    我理解或等于 ruby​​ 中的操作,但你能解释一下吗 你已经完成了:

    Object.const_get("#{obj.class}Park").new(obj)
    

    在 ruby​​ 中,以大写字母开头的东西是一个常数。以下是const_get() 工作原理的更简单示例:

    class Dog
      def bark
        puts 'woof'
      end
    end
    
    dog_class = Object.const_get("Dog")
    dog_class.new.bark
    
    --output:--
    woof
    

    当然,你也可以将参数传递给dog_class.new:

    class Dog
      attr_reader :name
    
      def initialize(name)
        @name = name
      end
    
      def bark
        puts "#{name} says woof!"
      end
    end
    
    dog_class = Object.const_get("Dog")
    dog_class.new('Ralph').bark
    
    --output:--
    Ralph says woof!
    

    下面这行只是上面的一个变体:

    Object.const_get("#{obj.class}Park").new(obj)
    

    如果obj = 'hello',第一部分:

    Object.const_get("#{obj.class}Park")
    

    相当于:

    Object.const_get("#{String}Park")
    

    而当 String 类对象被插值成字符串时,它只是简单地转换为字符串“String”,给你:

    Object.const_get("StringPark")
    

    那行检索 StringPark 类,给你:

    Object.const_get("StringPark")
                |
                V
          StringPark
    

    然后,添加原始行的第二部分会得到:

          StringPark.new(obj)
    

    因为obj = 'hello',所以相当于:

          StringPark.new('hello')
    

    任性?

    【讨论】:

    • 这没有回答问题。它似乎是对 不同 问题的答案,在您的其他答案的 cmets 中提出。如果问题和答案分开放置并从此处链接,那么在 SO 上会更有用。
    • @NeilSlater,与你和你的同类不同,我实际上关心帮助提出问题的人。
    • 如果这个答案以通常的方式与问题相关联,它既可以帮助提问者,又可以与网站合作。回答一个不同的问题可能会对 OP 有所帮助,但随后会使该页面变得一团糟,其他任何人都难以弄清楚。结果是您帮助了 OP(一个人)并阻碍了其他一些必须弄清楚发生了什么的潜在用户。
    • @Neil Slater,然后让这个页面变得一团糟,其他人都很难弄清楚。 -- lol
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-27
    • 1970-01-01
    • 2022-10-14
    • 1970-01-01
    • 2016-04-17
    • 1970-01-01
    相关资源
    最近更新 更多