[LRUG] Comparable Null Objects
Duncan Stuart
dgmstuart at gmail.com
Wed Jul 15 13:52:24 PDT 2015
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
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lrug.org/pipermail/chat-lrug.org/attachments/20150715/b3382f1c/attachment-0003.html>
More information about the Chat
mailing list