부울 외부

이것은 Boolean Externalties에 대한 Avdi Grimm의 게시물에서 동기 부여/영감을 받았습니다.

http://devblog.avdi.org/2014/09/17/boolean-externalities/

그의 게시물에서 그는 다음과 같은 질문을 합니다. 술어가 거짓을 반환하면 왜 그렇게 됩니까? 술어를 많이 연결하면 왜 답을 얻었는지 파악하기 어렵습니다.

이 예를 고려하십시오. 개체가 무서운지 확인하기 위해 간단한 연결 술어 논리를 구현합니다.

class SimpleBoo
  def scary?
    ghost? || zombie?
  end

  def ghost?
    !alive? && regrets?
  end

  def zombie?
    !alive? && hungry_for_brains?
  end

  def alive?
    false
  end

  def regrets?
    false
  end

  def hungry_for_brains?
    false
  end
end



일련의 논리를 따르면 유령이나 좀비라면 무서운 것입니다. 둘 다 살아 있지 않지만 유령은 후회하고 좀비는 두뇌에 굶주려 있습니다. 이것은 아마도 프로덕션 앱용으로 작성하는 코드입니다. 간단하고 읽기가 매우 쉽습니다.

단점은 무언가가 무서운 이유를 알고 싶다면 가서 코드를 읽어야 한다는 것입니다. 결론에 도달한 이유를 물체에게 물어볼 수 없습니다.



다음은 코드 진화의 논리적 다음 단계입니다. 길이와 가독성에 엄청난 "비용"이 있지만 조건자가 true 또는 false를 반환하는 이유를 설명할 수 있도록 코드를 수정했습니다.

class WhyNotBoo
  # The object is scary if there is a reason for it to be scary.
  def scary?
    why_scary.any?
  end

  # Why is this object scary?
  def why_scary
    reasons = []

    # Early termination if this object is *not* scary.
    return reasons unless ghost? || zombie?

    # Recursively determine why this object is scary.
    reasons.concat([:ghost => why_ghost]) if ghost?
    reasons.concat([:zombie => why_zombie]) if zombie?
    reasons
  end

  # For the "why not" question we re-implement the "why" logic in reverse.
  def why_not_scary
    reasons = []
    return reasons if ghost? || zombie?
    reasons.concat([:not_ghost => why_not_ghost]) unless ghost?
    reasons.concat([:not_zombie => why_not_zombie]) unless zombie?
    reasons
  end

  def ghost?
    why_ghost.any?
  end

  def why_ghost
    return [] unless !alive? && regrets?

    [:not_alive, :regrets]
  end

  def why_not_ghost
    reasons = []
    return reasons if ghost?

    reasons << :alive if alive?
    reasons << :no_regrets unless regrets?
    reasons
  end

  def zombie?
    why_zombie.any?
  end

  def why_zombie
    return [] unless !alive? && hungry_for_brains?

    [:not_alive, :hungry_for_brains]
  end

  def why_not_zombie
    reasons = []
    return reasons if zombie?

    reasons << :alive if alive?
    reasons << :not_hungry_for_brains unless hungry_for_brains?
    reasons
  end

  def alive?
    true
  end

  def regrets?
    false
  end

  def hungry_for_brains?
    false
  end
end



예, 훨씬 더 많은 코드입니다. 모든 복합 술어에는 "why_[술어]"및 "why_not_[술어]"버전이 있습니다. 이제 어떤 것이 무서운지, 왜 무서운지(또는 왜 안 좋은지) 물어볼 수 있습니다.

이 접근 방식에는 몇 가지 문제가 있습니다.
  • 로직이 예상했던 scary? 에 없습니다.
  • 논리는 why_scarywhy_not_scary 사이에 복제됩니다. 반복하지 마십시오. 그렇지 않으면 논리 버그가 발생합니다.
  • 더 많은 코드가 있습니다. 많은 상용구 코드가 있지만 같은 방법에 여러 가지 문제가 있습니다: 장부 기록 및 실제 논리.

  • 클리너 코드



    "why"와 "why not"의 기능을 유지하면서 코드를 다시 읽을 수 있게 만들 수 있는지 봅시다.

    class ReasonBoo < EitherAll
      def scary?
        either :ghost, :zombie
      end
    
      def ghost?
        all :not_alive, :regrets
      end
    
      def zombie?
        all :not_alive, :hungry_for_brains
      end
    
      def alive?
        false
      end
    
      def regrets?
        false
      end
    
      def hungry_for_brains?
        false
      end
    end
    
    


    여태까지는 그런대로 잘됐다. 코드는 매우 읽기 쉽지만 미스터리한 수퍼클래스EitherAnd가 있습니다. 작동 방식을 살펴보기 전에 무엇을 할 수 있는지 살펴보겠습니다.

    boo = ReasonBoo.new
    boo.scary? # => false
    boo.why_scary # => []
    boo.why_not_scary # => [{:not_ghost=>[:not_regrets]}, {:not_zombie=>[:not_hungry_for_brains]}]
    
    boo.ghost? # => false
    boo.why_ghost # => []
    boo.why_not_ghost # => [:not_regrets]
    
    boo.zombie? # => false
    boo.why_zombie # => []
    boo.why_not_zombie # => [:not_hungry_for_brains]
    
    

    either 또는 all를 사용하는 각 조건자에 대해 우리는 그것이 참인 이유 또는 이유를 물을 수 있으며 응답은 일련의 조건자 검사입니다.

    더 깨끗한 코드를 얻는 방법



    코드를 읽을 수 있게 하려면 일반적으로 지저분한 배관 코드가 있어야 합니다. 이 예에서 우리는 이것을 슈퍼클래스에 숨겼지만 너무 많은 노력 없이도 모듈일 수 있었습니다.

    코드를 더 쉽게 읽을 수 있도록 중복 논리를 도우미 메서드로 추출하지 않도록 선택했습니다.

    이 클래스는 eitherall 의 두 가지 메서드를 구현합니다.

    두 방법 모두 구조가 동일합니다.
  • why_[predicate] 및 why_not_[predicate] 메소드를 설정합니다.
  • 종료 조건에 도달할 때까지 각 조건자를 평가합니다.
  • 우리가 한 결과를 얻은 이유를 설명하기 위해 어떤 술어가 참/거짓인지 추적합니다.

  • class EitherAll
      # This method mimics the behavior of "||". These two lines are functionally equivalent:
      #
      # ghost? || zombie? # => false
      # either :ghost, :zombie # => false
      #
      # The bonus of `either` is that afterwards you can ask why or why not:
      #
      # why_not_scary # => [{:not_ghost=>[:not_regrets]}, {:not_zombie=>[:not_hungry_for_brains]}]
      def either(*predicate_names)
        #
        # 1. Setup up the why_ and why_not_ methods
        #
    
        # Two arrays to track the why and why not reasons.
        why_reasons = []
        why_not_reasons = []
    
        # This is a ruby 2.0 feature that replaces having to regexp parse the `caller` array.
        # Our goal here is to determine the name of the method that called us.
        # In this example it is likely to be the `scary?` method.
        context_method_name = caller_locations(1, 1)[0].label
    
        # Strip the trailing question mark
        context = context_method_name.sub(/\?$/, '').to_sym
    
        # Set instance variables for why and why not for the current context (calling method name).
        # In our example, this is going to be @why_scary and @why_not_scary.
        instance_variable_set("@why_#{context}", why_reasons)
        instance_variable_set("@why_not_#{context}", why_not_reasons)
    
        # Create reader methods for `why_scary` and `why_not_scary`.
        self.class.class_eval do
          attr_reader :"why_#{context}", :"why_not_#{context}"
        end
    
        #
        # 2. Evaluate each predicate until one returns true
        #
    
        predicate_names.each do |predicate_name|
          # Transform the given predicate name into a predicate method name.
          # We check if the predicate needs to be negated, to support not_<predicate>.
          predicate_name_string = predicate_name.to_s
          if predicate_name_string.start_with?('not_')
            negate = true
            predicate_method_name = "#{predicate_name_string.sub(/^not_/, '')}?"
          else
            negate = false
            predicate_method_name = "#{predicate_name_string}?"
          end
    
          # Evaluate the predicate
          if negate
            # Negate the return value of a negated predicate.
            # This simplifies the logic for our success case.
            # `value` is always true if it is what we ask for.
            value = !public_send(predicate_method_name)
          else
            value = public_send(predicate_method_name)
          end
    
          #
          # 3. Track which predicates were true/false to explain *why* we got the answer we did.
          #
    
          if value
            # We have a true value, so we found what we are looking for.
    
            # If possible, follow the chain of reasoning by asking why the predicate is true.
            if respond_to?("why_#{predicate_name}")
              why_reasons << { predicate_name => public_send("why_#{predicate_name}") }
            else
              why_reasons << predicate_name
            end
    
            # Because value is true, clear the reasons why we would not be.
            # They don't matter anymore.
            why_not_reasons.clear
    
            # To ensure lazy evaluation, we stop here.
            return true
          else
            # We have a false value, so we continue looking for a true predicate
            if negate
              # Our predicate negated, so we want to use the non-negated version.
              # In our example, if `alive?` were true, we are not a zombie because we are not "not alive".
              # Our check is for :not_alive, so the "why not" reason is :alive.
              negative_predicate_name = predicate_name_string.sub(/^not_/, '').to_sym
            else
              # Our predicate is not negated, so we need to use the negated predicate.
              # In our example, we are not scary because we are not a ghost (or a zombie).
              # Our check is for :scary, so the "why not" reason is :not_ghost.
              negative_predicate_name = "not_#{predicate_name_string}".to_sym
            end
    
            # If possible, follow the chain of reasoning by asking why the predicate is false.
            if respond_to?("why_#{negative_predicate_name}")
              why_not_reasons << { negative_predicate_name => public_send("why_#{negative_predicate_name}") }
            else
              why_not_reasons << negative_predicate_name
            end
          end
        end
        # We failed because we did not get a true value at all (which would have caused early termination).
        # Clear all positive reasons.
        why_reasons.clear
    
        # Explicitly return false to match style with the `return true` a few lines earlier.
        return false
      end
    
      # This method works very similar to `either`, which is defined above.
      # I'm only commenting on the differences here.
      #
      # This method mimics the behavior of "&&". These two lines are functionally equivalent:
      #
      # !alive? && hungry_for_brains?
      # all :not_alive, :hungry_for_brains
      def all(*predicate_names)
        context_method_name = caller_locations(1, 1)[0].label
        context = context_method_name.sub(/\?$/, '').to_sym
        why_reasons = []
        why_not_reasons = []
        instance_variable_set("@why_#{context}", why_reasons)
        instance_variable_set("@why_not_#{context}", why_not_reasons)
        self.class.class_eval do
          attr_reader :"why_#{context}", :"why_not_#{context}"
        end
    
        predicate_names.each do |predicate_name|
          predicate_name_string = predicate_name.to_s
          if predicate_name_string.start_with?('not_')
            negate = true
            predicate_method_name = "#{predicate_name_string.sub(/^not_/, '')}?"
          else
            negate = false
            predicate_method_name = "#{predicate_name_string}?"
          end
    
          if negate
            value = !public_send(predicate_method_name)
          else
            value = public_send(predicate_method_name)
          end
    
          # The logic is the same as `either` until here. The difference is:
          #
          # * Either looks for the first true to declare success
          # * And looks for the first false to declare failure
          #
          # This means we have to reverse our logic.
          if value
            if respond_to?("why_#{predicate_name}")
              why_reasons << { predicate_name => public_send("why_#{predicate_name}") }
            else
              why_reasons << predicate_name
            end
          else
            if negate
              negative_predicate_name = predicate_name_string.sub(/^not_/, '').to_sym
            else
              negative_predicate_name = "not_#{predicate_name_string}".to_sym
            end
    
            if respond_to?("why_#{negative_predicate_name}")
              why_not_reasons << { negative_predicate_name => public_send("why_#{negative_predicate_name}") }
            else
              why_not_reasons << negative_predicate_name
            end
    
            why_reasons.clear
            return false
          end
        end
    
        why_not_reasons.clear
        return true
      end
    end
    
    


    결론



    200줄 미만의 Ruby 코드와 자체 코드의 사소한 변경으로 부울이 값을 반환하는 이유에 대한 추적 가능성을 제공할 수 있습니다.

    분명한 경우와 제한 사항에도 불구하고 메서드가 true 또는 false 를 반환하는 이유를 모르는 문제에 대한 잠재적인 해결책이 있다는 것을 아는 것이 좋습니다.

    좋은 웹페이지 즐겨찾기