2015-06-18 6 views
1

Sandi Metz говорит в ТВЕРДЫХ OOPS понятий из GORUCO что наличие if..else блоков в Ruby, можно рассматривать как отклонение от Open-Close Принцип. Какие все методы можно использовать, чтобы избежать несрочных условий 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.

+1

Я исправил его, так как не связан с вопросом OP, просто опечатка. Возможно, действительный ответ может указывать на то, что презентация Санди Меца не может быть применена (я не уверен), и, не вдаваясь в дизайнерские мнения, возможно, объясняет, почему многие динамические языки будут избегать перегрузки методов на основе параметров. –

+0

Я думаю, что идея OCP в вашем случае состоит в том, что 'i' должен производить свои собственные результаты, а не park(), вычисляя результат на основе типа' i'. – 7stud

+1

Применить «Заменить условное с помощью рефакторинга полиморфизма». Это действительно не так сложно. Smalltalk даже не имеет условностей (или циклов, если на то пошло), и все же вы можете выразить все, что вы хотите довольно элегантно в нем. Объектно-ориентированные языки не нуждаются в условных выражениях, при этом полиморфная динамическая отправка сообщений во время работы более мощная. –

ответ

0

Посмотрите на is_a? метод

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

Но еще лучше не проверять тип, но использовать утку набрав:

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

UPD: После прочтения комментариев. Да, оба примера выше не решают проблему 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 
+1

Это все еще используется условное выражение, но оно было свернуто в трехмерное утверждение. – kevinthompson

+0

Но он по-прежнему является тройным оператором. Я хочу полностью исключить их из-за объектно-ориентированных принципов, таких как перегрузка функций. Но Ruby не поддерживает его, и я ищу альтернативу, который не приводит к созданию кода. Я смотрю на решение, если я решит добавить еще одну проверку, это не приведет к изменению основного класса., но может привести только к добавлению методов. –

+0

ИМХО, вы не можете полностью исключить это решение только потому, что ожидаете другого поведения в зависимости от какого-либо состояния. Таким образом, правильный вопрос - кто отвечает за этот выбор. Вы можете определить различные классы с различными 'park'-s, но затем вы должны добавить фабричный метод где-нибудь, чтобы выбрать какой экземпляр для создания экземпляра в каждом случае. –

1

Вы могли бы сделать что-то вроде этого:

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] 

И другой пример использования блоки, которые являются анонимными функциями. Вы можете передать в желаемое поведение парковать() как функцию:

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] 
1

С текущего примера, и желая избежать обнаружения типа, я хотел бы использовать возможности Руби, чтобы вновь открыть классы для добавления функциональности вам нужно 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 

Как мнение, я не уверен, что вы получите много писать Руби таким образом. Принципы, которые вы изучаете, могут быть хорошими (я не знаю), но их принудительное «против зерна» языка может создавать менее читаемый код, независимо от того, соответствует ли он благими намерениями.

Так что я бы вероятно делать на практике это:

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

Это не соответствует вашим принципам, но идиоматичен рубин и немного легче читать и поддерживать, чем if блок (где условия может быть намного сложнее, поэтому для человека нужно разбираться больше).

+0

* Это не соответствует вашим принципам, но является идиоматичным Ruby *. Использование case-case с 'when' и использование' types' для ветвей делает ваш код более сложным ruby, но он не отличается от многоинтерфейсного if-оператора с проверкой типа. Я бы сказал, что проверка типов на любом языке НЕ считается идиоматической. – 7stud

+0

@ 7stud: Я согласен, что это не так логично, я предполагаю, что для кода проверки типов его легче читать. Я также согласился бы с тем, что стоит избегать необходимости проверки типов, но там, где я различаю, как далеко это взять. «Умная» динамическая отправка или изменение моделей классов, которые не интуитивно понятны (добавление «парка» к String), может быть хуже запаха кода, чем RTTI. Я бы сказал, что * идиоматический * означает «считается стандартным решением проблемы». Вы найдете много примеров вопросов на SO, используя 'case', например. http://stackoverflow.com/questions/3908380/ruby-class-types-and-case-statements –

+0

@NeilSlater, что такое RTTI? –

1

Вы можете просто создать обрабатываемые классы для 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 
+0

@parker || = Object.const_get ("# {obj.class} Парк"). new (obj) можете объяснить эту строку. –

+1

@ Anony-mouse это ленивая загрузка, это означает, что если '@ parker' не установлен, то установите его в противном случае с помощью текущего' @ parker', он будет оцениваться как '@parker = @parker || Объект.const_get ("# {} obj.class Парк"). Новый (объект) '. Поэтому, если '@ parker' равно nil, то оценивает или часть. 'Object.const_get' вернет константу, на которую ссылается строка, поэтому в случае, когда' obj' является 'String', это будет' Object.const_get ("StringPark") ', тогда он вызывает new с' obj', поэтому if 'obj ==" string "' оценивается как '@parker = @parker || Object.const_get ("StringPark"). Новая ("строка") '. Надеюсь, это поможет – engineersmnky

+0

Я понимаю или равный операции в ruby, но вы можете объяснить, что вы сделали с помощью Object.const_get («# {obj.class} Park»). New (obj) ' –

-1

Я понимаю, или равный операции в рубин, но вы можете объяснить, что вы сделали с:

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

В рубине что-то, что начинается с заглавной буквы, является константой. Вот простой пример того, как 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") 

И когда объект класса Строка интерполируется в строку, она просто преобразуется в строку «Строка», давая вам:

Object.const_get("StringPark") 

И что line получает класс StringPark, давая вам:

Object.const_get("StringPark") 
      | 
      V 
     StringPark 

Затем, добавив вторую часть Оригинальная линия дает:

 StringPark.new(obj) 

И потому obj = 'hello', что эквивалентно:

 StringPark.new('hello') 

Capice?

+0

Это не отвечает на вопрос. Это, кажется, ответ на * другой * вопрос, поднятый в комментариях вашего другого ответа. Было бы более полезно, если бы вопрос и ответ стояли отдельно и связывались отсюда. –

+0

@NeilSlater, В отличие от вас и ваших близких, я действительно забочусь о помощи человеку, который задал вопрос. – 7stud

+0

Это помогло бы аскере и работа с сайтом, если бы этот ответ был связан с вопросом обычным способом. Ответ на другой вопрос помогает OP, возможно, немного, но затем делает эту страницу бесполезной и трудно для кого-то еще выяснить. Результат состоит в том, что вы помогли ОП (одному человеку) и помешали другим потенциальным пользователям, которые должны выяснить, что происходит. –

Смежные вопросы