[LRUG] Comparable Null Objects

Duncan Stuart dgmstuart at gmail.com
Wed Jul 15 14:44:01 PDT 2015


Thanks Rob - I was there, and it certainly was a big influence. Do you
think I'm applying the idea incorrectly or inappropriately?

On 15 July 2015 at 22:34, Robert Kitromilides <robkitro at gmail.com> wrote:

> Hi Duncan
>
> https://www.youtube.com/watch?v=9lv2lBq6x4A
>
> above is a link to a talk at Bath Ruby by the venerable Sandi Metz, all
> about the Null Object Pattern
>
> Hope it’s useful
>
> Cheers
>
> Rob
>
> Rob Kitromilides
>
> twitter: @robkitro
>
>
> On 15 Jul 2015, at 21:52, Duncan Stuart <dgmstuart at gmail.com> wrote:
>
> Have I totally misunderstood the point of the Null Object pattern? This is
> all still fairly new to me - but here's a description that sums up what I'm
> basing my understanding on:
> "The Null Object provides intelligent do nothing behavior, hiding the
> details from its collaborators."
> http://www.cs.oberlin.edu/~jwalker/nullObjPattern/
>
> My instinct to use it here was that there's a set of behaviours which I
> want to hide from the Event class: those involved in handling the case
> where there is no expected_date.
>
> I agree that the code isn't very clear, but I'd rather hide that (probably
> quite stable?) mess within a separate object than have it pollute the Event
> class.
>
> Are these not sensible goals?
>
> I can see how making an EventWithoutExpectedDate class would also fulfil
> that mess-hiding role, but say in a different scenario I needed to also
> sort by venue name, and handle a case where a Venue is missing from the
> event: I'd pretty much *have* to use composition at that point - and
> wouldn't that inevitably involve injecting NoExpectedDate and UnknownVenue
> null objects anyway? Is there another way to achieve it?
>
>
> On 15 July 2015 at 18:20, <jc at panagile.com> wrote:
>
>> I don’t really understand why type coercion is OK but type checking is
>> bad. At least with type checking your code spells out what you want to
>> check - with coercion a type check is still happening, it’s just that it’s
>> happening implicitly in the ruby vm and not explicitly in your code. It’s
>> also rather non-obvious - at least to me - that comparing a Date to a
>> NoExpectedDate relies on ‘def coerce(other); [other, Float::INFINITY];
>> end’. It’s too clever for me.
>>
>> I still think you’re getting the wrong thing to Quack. You don’t really
>> want Dates to be comparable to NoExpectedDate. It doesn’t make sense.
>> You’re just using some clever implementation details to return the
>> incidentally correct answer from what you really want - the comparison of
>> events.
>>
>> If you really want to avoid type checking then presumably you could set
>> some attribute in an Event when you’re setting the expected_date to
>> NoExpectedDate. Then you could check that attribute rather than checking
>> the class of expected_date. But I don’t think that would buy you much.
>>
>> If you really wanted to avoid the imperative style ‘if/else’ then I think
>> that the better solution is to specialise Event, not to create a null
>> object for one of it’s attributes. E.g., you could create an
>> EventWithoutDate class (probably as a subclass of Event*). Then add a
>> method called something like ‘value_for_sort’ that returns the
>> expected_date (perhaps .to_f) for Event, and Float::INFINITY for
>> EventWithoutDate, and use these in the comparison. But that seems like a
>> lot of work and it still relies on a special value (Float::INFINITY) to
>> represent some behaviour specific to the ordering of events. I don’t think
>> it would be a better solution.
>>
>> I still think the clearest solution in this case is to use an if/else. I
>> don’t think that you should have created a NoExpectedDate object. One
>> reason for this is that you don’t own Date so you can’t make it do the
>> right thing in this case. The other reason is that you’re not trying to
>> sort Dates, so I think you’re being tempted to put the behaviour in the
>> wrong place.
>>
>>
>> * although subclassing to specialise one behaviour can be a code smell**
>> ** bonus points for doing it with composition. Why use one class when you
>> can use four and make your code unreadable?
>>
>>
>>
>> On Wed, Jul 15, 2015 at 4:35 PM, Duncan Stuart <dgmstuart at gmail.com>
>> wrote:
>>
>>>  Thanks everyone - that was really interesting. Here's the approach
>>> I've decided on:
>>>
>>> class NoExpectedDate
>>>   include Comparable
>>>   def to_s(format)
>>>     "Unknown"
>>>   end
>>>   def <=>(other)
>>>     1 # Treat it as after every other date
>>>   end
>>>   def coerce(other)
>>>     [other, Float::INFINITY]
>>>   end
>>> end
>>>
>>> I like it because it has just enough behaviour to quack like a Date
>>> where required.
>>>
>>> pry(main)> NoExpectedDate.new > NoExpectedDate.new
>>> => true
>>> pry(main)> NoExpectedDate.new < NoExpectedDate.new
>>> => false
>>> pry(main)> Date.today  < NoExpectedDate.new
>>> => true
>>> pry(main)> Date.today >  NoExpectedDate.new
>>> => false
>>>
>>> On 15 July 2015 at 16:34, Duncan Stuart <dgmstuart at gmail.com> wrote:
>>>
>>>> Thanks John
>>>>
>>>> Yeah - that's pretty much what I suggested at the end of my original
>>>> post. The reason I wanted to try and find another way is because it has
>>>> that typecheck on expected_date, which seems to negate the benefits of
>>>> using a renderable NullObject in the first place.
>>>>
>>>> I feel like Tim's approach of events.sort_by { |event|
>>>> event.expected_date || Float::INFINITY } would be preferable if dealing
>>>> with it at the Event level.
>>>>
>>>>
>>>> On 15 July 2015 at 16:16, <jc at panagile.com> wrote:
>>>>
>>>>> Hmm, if you’re sorting a list of events then why not let the event’s
>>>>> control how the comparison works? E.g., something like:
>>>>>
>>>>>  require 'date'
>>>>>
>>>>> class NoExpectedDate
>>>>> end
>>>>>
>>>>> class Event
>>>>>   attr_reader :expected_date
>>>>>
>>>>>   def initialize(date)
>>>>>     @expected_date = date
>>>>>   end
>>>>>
>>>>>   def unsortable?
>>>>>     expected_date.is_a?(NoExpectedDate)
>>>>>   end
>>>>>
>>>>>   def <=>(other)
>>>>>     if self.unsortable?
>>>>>       1
>>>>>     elsif other.unsortable?
>>>>>       -1
>>>>>     else
>>>>>       self.expected_date <=> other.expected_date
>>>>>     end
>>>>>   end
>>>>> end
>>>>>
>>>>>
>>>>> e1 = Event.new(Date.today)
>>>>> e2 = Event.new(NoExpectedDate.new)
>>>>> e3 = Event.new(Date.today - 1)
>>>>>
>>>>> [e1, e2, e3].sort # => [#<Event:0x007f968511a3c8
>>>>> @expected_date=#<Date: 2015-07-14 ((2457218j,0s,0n),+0s,2299161j)>>,
>>>>> #<Event:0x007f968511a490 @expected_date=#<Date: 2015-07-15
>>>>> ((2457219j,0s,0n),+0s,2299161j)>>, #<Event:0x007f968511a440
>>>>> @expected_date=#<NoExpectedDate:0x007f968511a468>>]
>>>>>
>>>>>
>>>>> This is still pretty imperative, but that’s probably a good thing in
>>>>> this case. The pure OO approach to this gets nasty quickly.
>>>>>
>>>>>
>>>>>
>>>>> On Wed, Jul 15, 2015 at 3:54 PM, Duncan Stuart <dgmstuart at gmail.com>
>>>>> wrote:
>>>>>
>>>>>> Thanks John
>>>>>>
>>>>>> * I want to sort things of the same type (instances of the Event
>>>>>> class).
>>>>>> * I want to sort them *by* an attribute containing things which Quack
>>>>>> enough like dates to be sorted against each other
>>>>>> * I want to do this so that they're displayed in date order in an
>>>>>> email
>>>>>> * Yes, it's an Array. I could create an EmailEventCollection object
>>>>>> which I would tell "give me the things for my email" and it would respond
>>>>>> with an array which was (amongst other things) sorted, but I don't think
>>>>>> that would resolve the issue at hand: it hides the mess in a different way,
>>>>>> but internally I'd still be doing the same sort, no?
>>>>>>
>>>>>> On 15 July 2015 at 15:09, <jc at panagile.com> wrote:
>>>>>>
>>>>>>>
>>>>>>> | Some events don't have an expected date, so like a good little OO
>>>>>>> programmer i've created a Null object:
>>>>>>>
>>>>>>> Yay! Good OO programmer.
>>>>>>>
>>>>>>> | but when it comes to sorting the list
>>>>>>>
>>>>>>> wait, wat? Less good OO programmer.
>>>>>>>
>>>>>>> I can see three things going on here, pretty much simultaneously.
>>>>>>>
>>>>>>> 1) You want to sort a list of different types of things. Like when
>>>>>>> you try to sort a list of Silverback Gorillas and instances of the number
>>>>>>> 37 it’s hard because it doesn’t really make sense.
>>>>>>>
>>>>>>> 2) You don’t have a list. You have an Array object. Is this the best
>>>>>>> place to send the ‘sort’ message to? Should it own the behaviour of sorting
>>>>>>> these particular disparate types of things?
>>>>>>>
>>>>>>> 3) You switched from clean OO thinking (we need an object to
>>>>>>> encapsulate this state) to imperative thinking (we have a list and we need
>>>>>>> to sort it).
>>>>>>>
>>>>>>> I blame Ruby for all of this. It makes you think you’re doing OO
>>>>>>> programming but throws in many non-OO ideas to trip you up.
>>>>>>>
>>>>>>> It’s not clear that an OO approach is the best approach here, but if
>>>>>>> you want to follow it try to think less about objects and more about the
>>>>>>> messages between objects. E.g., instead of ‘I need to sort a list’ think ‘I
>>>>>>> need to send a message somewhere telling it to do something (in this case
>>>>>>> to… what? To sort some objects? But why do you care?)’.
>>>>>>>
>>>>>>> Hope this helps or at least makes you think or at the very least
>>>>>>> persuades you to rage quit programming and go and live in a forest,
>>>>>>> John
>>>>>>>
>>>>>>>
>>>>>>> On Wednesday, Jul 15, 2015 at 1:03 pm, Duncan Stuart <
>>>>>>> dgmstuart at gmail.com>, wrote:
>>>>>>>  Hi LRUG - hopefully an interesting little problem:
>>>>>>>
>>>>>>> I have an Event class which has an "expected_date" attribute.
>>>>>>> Some events don't have an expected date, so like a good little OO
>>>>>>> programmer i've created a Null object:
>>>>>>>
>>>>>>> class NoExpectedDate
>>>>>>>   def to_s(format=:default)
>>>>>>>     "Unknown"
>>>>>>>   end
>>>>>>> end
>>>>>>>
>>>>>>> This works great for printing the values, but when it comes to
>>>>>>> sorting the list I of course get:
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>    ArgumentError: comparison of Date with NoExpectedDate failed
>>>>>>>
>>>>>>> If I include Comparable and define <=> then one comparison works,
>>>>>>> but the other doesn't :
>>>>>>>
>>>>>>> class NoExpectedDate
>>>>>>>   include Comparable
>>>>>>>   def to_s(format=:default)
>>>>>>>     "Unknown"
>>>>>>>   end
>>>>>>>   def <=>(other_date)
>>>>>>>     1 # Treat it as after every other date
>>>>>>>   end
>>>>>>> end
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> $ NoExpectedDate.new > Date.today
>>>>>>> => true
>>>>>>>
>>>>>>> $ Date.today > NoExpectedDate.new
>>>>>>> ArgumentError: comparison of Date with NoExpectedDate failed
>>>>>>>
>>>>>>> I think this is because Date's <=> method expects it's argument to
>>>>>>> be a Date object or a number ("a numeric value as an astronomical Julian
>>>>>>> day number"). I've tried defining to_i and to_r on NoExpectedDate, but no
>>>>>>> dice.
>>>>>>>
>>>>>>> Can I get NoExpectedDate to pretend to be a Date (like
>>>>>>> SimpleDelegator lies about it's class)? Is that evil?
>>>>>>>
>>>>>>> I suppose I could always just define a method on Event to do this
>>>>>>> particular sort, but that seems nasty for all sorts of reasons:
>>>>>>>
>>>>>>> def sort_by_expected_date sort do |a, b|
>>>>>>>
>>>>>>>     if b.class = NoExpectedDate
>>>>>>>
>>>>>>>       1
>>>>>>>
>>>>>>>     else
>>>>>>>
>>>>>>>       a <=> b
>>>>>>>
>>>>>>>     end
>>>>>>>
>>>>>>>   end
>>>>>>>
>>>>>>> end
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> _______________________________________________
>>>>>>> Chat mailing list
>>>>>>> Chat at lists.lrug.org
>>>>>>> Archives: http://lists.lrug.org/pipermail/chat-lrug.org
>>>>>>> Manage your subscription:
>>>>>>> http://lists.lrug.org/options.cgi/chat-lrug.org
>>>>>>> List info: http://lists.lrug.org/listinfo.cgi/chat-lrug.org
>>>>>>>
>>>>>>>
>>>>>>
>>>>>
>>>>> _______________________________________________
>>>>> Chat mailing list
>>>>> Chat at lists.lrug.org
>>>>> Archives: http://lists.lrug.org/pipermail/chat-lrug.org
>>>>> Manage your subscription:
>>>>> http://lists.lrug.org/options.cgi/chat-lrug.org
>>>>> List info: http://lists.lrug.org/listinfo.cgi/chat-lrug.org
>>>>>
>>>>>
>>>>
>>>
>>
>> _______________________________________________
>> Chat mailing list
>> Chat at lists.lrug.org
>> Archives: http://lists.lrug.org/pipermail/chat-lrug.org
>> Manage your subscription: http://lists.lrug.org/options.cgi/chat-lrug.org
>> List info: http://lists.lrug.org/listinfo.cgi/chat-lrug.org
>>
>>
> _______________________________________________
> Chat mailing list
> Chat at lists.lrug.org
> Archives: http://lists.lrug.org/pipermail/chat-lrug.org
> Manage your subscription: http://lists.lrug.org/options.cgi/chat-lrug.org
> List info: http://lists.lrug.org/listinfo.cgi/chat-lrug.org
>
>
>
> _______________________________________________
> Chat mailing list
> Chat at lists.lrug.org
> Archives: http://lists.lrug.org/pipermail/chat-lrug.org
> Manage your subscription: http://lists.lrug.org/options.cgi/chat-lrug.org
> List info: http://lists.lrug.org/listinfo.cgi/chat-lrug.org
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lrug.org/pipermail/chat-lrug.org/attachments/20150715/352a9183/attachment-0003.html>


More information about the Chat mailing list