Sunday, August 29, 2010

Kata 8: Conflicting Objectives, Part 3B

   Okay, now I've had some time to think about what it means for the code to be "extensible".  Offhand I'm thinking it's as in the "open" part of the Open-Closed Principle, which states that a class should be open to extension but closed to modification.  That is, you should be able to extend it o do new and different things, but not so much monkey around with the existing functionality and change how that works -- at least in its interactions with the rest of the world.  (How it works inside is another story.  That's why encapsulation and Information Hiding are such Good Things.) 

   That got me thinking, just wrap the functionality in a class, arranged such that one could easily override key methods.  So, forthwith my class, and a couple of subclasses:

#! /usr/bin/ruby

# Dave Thomas Code Kata #8: Conflicting Objectives
# See http://codekata.pragprog.com/2007/01/kata_eight_conf.html
# Solution by Dave Aronson
# version 3: make it extensible
# part B: take "extensible" as meaning "open to doing additional things"
# or "open to doing the same things in different ways".  In an OO system,
# this generally means "more subclassable".  Remember the open part of the
# open/closed principle: a class should be *open to extension*.


class Word_Manager

  # This could be overridden to have, for instance, a variable
  # number of subparts; here we assume two.  It would also need to
  # override find_combos, and decide @max_whole_len differently.
  def initialize min_len, max_len
    @max_part_len = max_len
    @min_part_len = min_len
    @max_whole_len = @min_part_len + @max_part_len
    @long_words = []
    @short_word_lists = []
    (@min_part_len .. @max_part_len).each do |len|
      @short_word_lists[len] = {}
    end
  end

  def append word
    len = word.length
    if len == @max_whole_len then @long_words << word
    elsif @min_part_len <= len and len <= @max_part_len
      @short_word_lists[len][word] = nil
    end
  end

  def find_combos
    hits = []
    # hmmm, i'm thinking there may be some way to iterate over
    # the long words generically, passing in a block....
    @long_words.each do |word|
      (@min_part_len .. @max_part_len).each do |len|
        part_1 = word[0 .. len - 1]
        part_2 = word[len .. @max_whole_len - 1]
        if has_word? part_1 and has_word? part_2
          hits << "#{part_1} + #{part_2} => #{word}"
        end
      end
    end
    hits
  end

  def has_word? word
    len = word.length
    # note: generally for parts, not wholes
    if len == @max_whole_len then @long_words.include? word
    elsif @min_part_len <= len and len <= @max_part_len
      @short_word_lists[len].has_key? word
    else false
    end
  end

  def read_file file_name
    # a subclass could insert code to unify case, or translate, or . . . .
    File.new(file_name, 'r').each_line { |line| append line.chomp }
  end

end


# Now let's make a couple subclasses.


# one that's not case-sensitive
class Caseless_Word_Manager < Word_Manager
  def append word
    super word.downcase
  end
end


# one that will consider a part to be OK both forward and backward;
# only makes sense if also caseless
class Reversible_Word_Manager < Word_Manager
  def append word
    down = word.downcase
    super down
    # could instead override has_word to check for reversed version;
    # which one makes sense depends on frequency of insert vs. check
    super down.reverse if down.length < @max_whole_len
  end
end


# Now let's see what each one does with our word list!


def helper obj, description
  obj.read_file 'conflict_words.txt'
  hits = obj.find_combos
  puts "#{description} hits:\n#{hits.join "\n"}\n#{hits.length} hits"
end


helper Word_Manager.new(1, 5), 'Normal'
puts
helper Caseless_Word_Manager.new(1, 5), 'Caseless'
puts
helper Reversible_Word_Manager.new(1, 5), 'Reversible'

   And while I'm at it... here is the list of words I've been using, so you can test the code yourself without waiting while it chugs through your system wordlist:

A
AT
ATTACK
Reb
ate
atwirl
be
bed
bedpan
bemoan
benighted
bie
big
bigger
boo
booger
but
conic
credible
days
debbie
ed
er
ers
ger
German
i
iconic
in
incredible
inside
insight
intake
irides
istoke
land
lander
man
moan
morrow
night
on
pan
rebate
rides
rub
rubber
side
sight
stoke
tack
tackon
take
takeon
to
todays
tomorrow
tubers
twirl

   So now, as always, it's your turn.  What does "extensible" mean to you?  How would you have made it so?