0
\$\begingroup\$

Abstract

I'd like to have advice on how to :

  • implement methods that rely on a state,
  • when the instance is mutable and may be in an inconsistent state,
  • in such a way that the methods either silently fail or return nil,
  • without having to nil guard / coerce state to meaningful values everywhere.

Anyway, if my design is wrong itself, let me know.

Context

I have an ActiveRecord class. It is pretty simple, but has lots and lots of methods that require the whole instance to be valid? to work correctly.

The class in question handles a complex tax calculation, but for the sake of clarity, let's boil it down to this :

class Foo < ActiveRecord::Base
 validates :a, :b, :c, :d, :e,
 numericality: {inclusion: 0..100}
 validates :f, :g, :h,
 numericality: {greater_than: 0}
 def some_method
 a * b / c
 end
 def other_method
 d + e * (g / f)
 end
 def yet_another_method
 some_method - other_method * f
 end
 def this_is_getting_really_complex
 yet_another_method * other_method - some_method
 end
 # and so on, with piles of methods calling each other
end

Moreover, i have subclasses for this class that override these methods (different tax calculation rules, etc.)

The thing is, as long as an instance is in a consistent state (all a, b, c... fields are present and valid), all the methods work fine. But ActiveRecord instances must be able to be in an inconsistent state, because...well, we want to be able to validate them. So raising ArgumentError during initialize when the params are meaningless is out of the question.

In this case, let's say a, b, c are filled with junk strings or nil instead of meaningful values : all of our methods will raise various errors, or even worse - thanks to duck typing it will work in a crazy, unintended way ( "junk" * 300_000, anyone? ).

Trying to solve this problem

first try : nil guards

this is getting ugly pretty quick :

def some_method
 return nil unless [a, b, c].all? &:present?
 a * b / c
end

... ugh, nil guarding isn't enough, what if i have strings instead of numbers ? I admit for a while i was tempted with heresy :

def some_method
 a * b / c
rescue TypeError, NoMethodError
 nil
end

This is too bad. Wait, what if i simply check for validity ?

def some_method
 return nil unless valid?
 a * b / c
end 

A bit better, but now the whole validation process kicks in every time i call a method. This is silly. OO to the rescue ?

second try : OO refactoring

My first thought was : well, we have a behavior that varies according to state - this is textbook example for a state machine. But how would i hook this on AR's validation cycle ? Did not figured it out.

Then I proceeded to try and extract those methods into a variety of immutable decorator classes, with a factory method that accepted one instance from my Foo class :

 class FooDecorator
 def self.factory( foo )
 # NullDecorator has methods that always return nil
 return NullDecorator.new( foo ) if foo.invalid? 
 case foo
 when Foo:Bar then BarDecorator.new( foo )
 when Foo:Baz then BazDecorator.new( foo )
 else raise ArgumentError
 end
 end
 def initialize( foo )
 @foo = foo.dup.freeze.readonly!
 end
 def some_method
 @foo.a * @foo.b / @foo.c
 end
 def yet_another_method
 some_method - other_method * @foo.f
 end
 end

I promptly stopped because it felt ridiculously overengineered, and smelled like feature envy over the top.

I also considered using the Maybe monad, or create a monad of my own like "MaybeAValidNumber" to coerce everything to meaningful values. That felt not much better than nil guards and again, overengineered.

third try : don't care

Thinking about this, i wondered if i should nil guard at all : after all, these methods are only relevant if the instance is in a consistent state. As far as i understand it, design by contract goes this way : if you do not ensure state prerequistes are met before calling the methods, you break the contract, so you are at fault.

Problem is, as you can easily guess, these methods are likely to be called in the views to display the results of calculations. I don't find having to throw a bunch of conditionals in my views entirely satisfying...

Thoughts ?

asked Sep 18, 2013 at 20:17
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

Looks like your class has two separate responsibilities:

  • validating a set of fields (fields can be in an inconsistent state)
  • performing calculations on these fields (fields must be valid)

Therefore I would split it up into two classes:

  • The ActiveRecord could still take care of the validation and other ActiveRecord stuff.
  • All the tax calculations though would happen in say TaxCalculator, which initializes all its fields from a validated ActiveRecord that's passed in from a constructor, so all the methods of it can blindly assume that the fields are OK.
answered Sep 19, 2013 at 10:13
\$\endgroup\$
3
  • \$\begingroup\$ that's more or less what i did with the decorator class. I guess it's a valid approach, but it quickly became really complex as my class has multiple subclasses with different calculations rules - i ended up having one decorator class for each of my classes, plus null decorator classes. That felt a bit too much... And the fact is that i want the thing to be dynamic, i.e. when i call a method it should retrurn the actual calculation result from up-to-date state values - i would have to create a decorator every time i call a method, calling valid? ... \$\endgroup\$ Commented Sep 19, 2013 at 10:45
  • \$\begingroup\$ I'm currently exploring the Maybe monad option, seems easier and cleaner than expected, i'll let you know. thanks for the input anyway \$\endgroup\$ Commented Sep 19, 2013 at 10:46
  • \$\begingroup\$ I assumed you can first validate the values and then do the calculations. But when you need to modify the values in the middle of these calculations, my approach starts to fall apart. \$\endgroup\$ Commented Sep 19, 2013 at 13:54
0
\$\begingroup\$

Accepted an answer because it is a valid solution, but i'd like to explain an approach that seemed interesting (not functional as is): coercion plus a NullObject, in the spirit of the Maybemonad used by Avdi Grimm.

 class NonFloat
 include Comparable
 # allows arithmetic operations to take place with non-null values
 def coerce( other )
 [self, self]
 end
 def to_f
 self
 end
 def coerce( other )
 [self, self]
 end
 # did the same thing for various operators
 def *( other )
 self
 end
 def to_s
 ""
 end
 def <=>( other )
 # yuck, could not find anything meaningful to return from here :/
 end
 end
 def MaybeFloat( value )
 return value if value.kind_of?( NonFloat )
 Float( value )
 rescue TypeError # coercion failed
 NonFloat.new
 end

... Looks oddly like a NaN ;). What i like about this solution is that it allows to perform arithmetic operations, even with potentially non-float coercible values :

 MaybeFloat( value ) * MaybeFloat( other_value )

but more important, this behavior can propagate :

 def some_value
 MaybeFloat( @value )
 end
 def some_calculations
 some_value * 4 + other_value
 end
 def other_calculations
 some_calculations / yet_another_value # may return a NonFloat
 end

what s*cks is that you can't really compare NonFloat with other types because <=> is not expected to return anything other than 1,0 and -1 (no way to tell "it doesn't make sense to compare these values")... so Array#min, for instance, can't be used.

answered Sep 24, 2013 at 17:48
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.