Friday, March 06, 2015

I Always Think Of The Best Things To Say After I Leave

Last Friday I met with the folks from Pomiet Software here in Miamisburg, OH.  I was not looking to interview.  This opportunity was dropped in my Gmail account from a recruiter I had never talked with before.  The Pomiet group is a great group of people and are doing wonderful things it would have been fun to be a part of.

Part of the interview process was a coding challenge a few days before the interview.  Here is the challenge:

Hi and welcome to the StoreFront. As you know, we are a small store with a prime location in a prominent city ran by a friendly store manager named Sarah. We also buy and sell only the finest elements. Unfortunately, our items are constantly losing shelf value as they approach their sell by date. We have a system in place that updates our inventory for us. It was developed by a no-nonsense guy named Larry, who has moved on to new adventures. Your task is to add the new feature to our system so that we can begin selling a new category of items. First an introduction to our system:

        - All items have a ShelfLife value which denotes the number of days we have to sell the item

        - All items have a Worth value which denotes how valuable the item is
        - At the end of each day our system lowers both values for every item

Pretty simple, right? Well this is where it gets interesting:


        - Once the shelf life date has passed, Worth degrades twice as fast

        - The Worth of an item is never negative
        - "Gold" actually increases in Worth the older it gets
        - The Worth of an item is never more than 50
        - "Cadmium" is rare, has a worth of 80, and will never decrease in Worth
        - "Helium", like gold, increases in Worth as it's ShelfLife value changes; Worth increases by 2 when there are 10 days or less and by 3 when there are 5 days or less but Worth drops to 0 once the ShelfLife is passed.

We have recently signed an alchemist to create "Alchemy" items. This requires an update to our system:


        - "Alchemy" items degrade in Worth twice as fast as normal items

        - "Alchemy" items have a maximum worth of 100

Feel free to make any changes to the UpdateWorth method and add any new code as long as everything still works correctly. However, do not alter the Item class or Items property as those belong to another team that doesn’t believe in shared code ownership (you can make the UpdateWorth method and Items property static if you like, we'll cover for you).  If you happen to find any conflicts with the above requirements, we would appreciate if you fixed them.  


Just for clarification, an item can never have its Worth increase above 50, however "Cadmium" is a rare item and as such its Worth is 80 and it never alters.


PLEASE RETURN ALL OF THE FILES THAT YOU CREATE. FEEL FREE TO ZIP THE ENTIRE SOLUTION AND RETURN IT TO US.



    private void UpdateWorth()

    {
        for (var i = 0; i < Items.Count; i++)
        {
            if (Items[i].Name != "Gold" && Items[i].Name != "Helium")
            {
                if (Items[i].Worth > 0)
                {
                    if (Items[i].Name != "Cadmium")
                    {
                        Items[i].Worth = Items[i].Worth - 1;
                    }
                }
            }
            else
            {
                if (Items[i].Worth < 50)
                {
                    Items[i].Worth = Items[i].Worth + 1;

                    if (Items[i].Name == "Helium")

                    {
                        if (Items[i].ShelfLife < 11)
                        {
                            if (Items[i].Worth < 50)
                            {
                                Items[i].Worth = Items[i].Worth + 1;
                            }
                        }

                        if (Items[i].ShelfLife < 6)

                        {
                            if (Items[i].Worth < 50)
                            {
                                Items[i].Worth = Items[i].Worth + 1;
                            }
                        }
                    }
                }
            }

            if (Items[i].Name != "Cadmium")

            {
                Items[i].ShelfLife = Items[i].ShelfLife - 1;
            }

            if (Items[i].ShelfLife < 0)

            {
                if (Items[i].Name != "Gold")
                {
                    if (Items[i].Name != "Helium")
                    {
                        if (Items[i].Worth > 0)
                        {
                            if (Items[i].Name != "Cadmium")
                            {
                                Items[i].Worth = Items[i].Worth - 1;
                            }
                        }
                    }
                    else
                    {
                        Items[i].Worth = Items[i].Worth - Items[i].Worth;
                    }
                }
                else
                {
                    if (Items[i].Worth < 50)
                    {
                        Items[i].Worth = Items[i].Worth + 1;
                    }
                }
            }
        }
    }

    private IList Items = new List

                                    {
                                        new Item {Name = "Aluminum Shackles", ShelfLife = 10, Worth = 20},
                                        new Item {Name = "Gold", ShelfLife = 2, Worth = 50},
                                        new Item {Name = "Plutonium Pinball Parts", ShelfLife = 5, Worth = 7},
                                        new Item {Name = "Cadmium", ShelfLife = 0, Worth = 80},
                                        new Item {Name = "Helium", ShelfLife = 15, Worth = 38},
                                        new Item {Name = "Alchemy Iron", ShelfLife = 3, Worth = 75}
                                    };

class Item

{
    public string Name { get; set; }

    public int ShelfLife { get; set; }


    public int Worth { get; set; }

}

Spend as much time as you feel comfortable spending.  Typically you can expect to commit about 1 hour towards your solution.  I look forward to seeing the completed project.


It was in C# which I had not coded in for a few years.  That is no excuse for how badly I botched this job.  It had been a little while since I had delved into Design Patterns and object-oriented programming so I went with a naive procedural approach.  It was much better than the original (that wasn't hard) but it did not use object-oriented design, design patterns or anything approaching my best work.

Even worse when they asked me how I could expand my code to handle hundreds of items I choked in a big way (easy to do after a 2.5 hour interview I suppose).  I left the interview knowing that I had failed to get the job.  Much worse that this was the feeling that I did not really show them what I could do.

Sure enough, the recruiter dropped me an email on Sunday that said that they thought I was a fit culturally but I did not have the technical skills they were looking for in a team lead.  I was kind of devastated but that's life.  I knew I had not shown them how quickly I could learn the things they were looking for.  I had done a B- job at best in that interview.

I took away a few things from this process:

  • Refresh my object-oriented design knowledge
  • Learn more about agile software design
  • Learn Ruby - I have looked at it a few times in the past but one of the guys in the interview seemed really sold on it
Looking at that list I decided to learn Ruby and use it to refresh by object-oriented design skills.  Monday, I started looking at it and I am really amazed by how well Ruby gets out of your way.  It is really great.

Anyway, here is a non-instrumented solution to the above coding challenge in Ruby.  I did not put in the "business rules" for all of the items in the challenge.  I just put in enough to prove to myself that this would have been an acceptable solution:


class Inventory
  def initialize(*items)
    @inventory = []
    add_inventory_items(*items)
  end  
  
  def add_inventory_items(*items)
    items.each do |item|
      puts "Adding #{item.name} Inventory"
      @inventory.push(item)
    end 
  end
  
  def remove_inventory_items(*items)
    items.each do |item|
      puts "Removing #{item.name} Inventory"
      @inventory.remove(item)
    end 
  end
  
  def return_inventory_items
    @inventory
  end
  
  def to_s
    @inventory.each {|item| puts item}
    # Return empty string so that you do not get Inventory object printed at end of input
    '' 
  end
end

class Item

  attr_reader :name
  attr_accessor :shelflife, :worth
  
  def initialize(name, shelflife, worth)
    @name = name
    @shelflife = shelflife
    @worth = worth
  end
  
  def to_s
    "#{name} => Value Of #{worth} With Shelf Life Of #{shelflife}"
  end
end

class BusinessRule

  # &block is a block that takes at least one argument (the object variable)
  def initialize(block)
    @br_block = block
  end
  
  def apply_rule(object,*other_args)
    @br_block.call(object)
  end
    
end

class BusinessRules

  def initialize
    # Can be multiple Rule Types Per Object
    @rules = Hash.new{|hash, key| hash[key] = Array.new}
  end
  
  def add_rule(object,block)
    @rules[object.name].push(BusinessRule.new(block))
  end
  
  def apply_all_matching_rules(object,*other_args)
    @rules[object.name].each do |br|
      br.apply_rule(object,*other_args)
    end
  end
  
  def has_business_rules_for?(object)
    @rules.key?(object.name)
  end 
end

def subtract_one_day_from_shelflife(item)

  item.shelflife -= 1  
  if item.shelflife < 0 then
    item.shelflife = 0
  end
end

puts "Addng Initial Inventory Values"

gold = Item.new("Gold",15,50)
adamantium = Item.new("Adamantium",100,1000)
alchemy_sulfer = Item.new("Alchemy Sulfur",30,40)
store_inventory = Inventory.new(gold,adamantium,alchemy_sulfer)
puts "\nInventory Values Before Applying Business Rules:"
puts store_inventory

puts "Creating Business Rules For Gold"

daily_process_rules = BusinessRules.new()
daily_process_rules.add_rule(gold, lambda {|item| subtract_one_day_from_shelflife(item)})
daily_process_rules.add_rule(gold, lambda {|item| item.worth += 1})
daily_process_rules.add_rule(gold, lambda {|item| if item.worth < 50 then item.worth = 50 end})

puts "Adding Specialized Business Rules For Items With No Pre-Defined Rules"

# Add Generic Business Rules To Items That Have None 
store_inventory.return_inventory_items.each do |item|
  if item.name.downcase.include?("alchemy ") then
    puts "    Creating Business Rules For Alchemy Object \"#{item.name}\""
    daily_process_rules.add_rule(item, lambda {|item| subtract_one_day_from_shelflife(item)})
    daily_process_rules.add_rule(item, lambda {|item| if item.shelflife == 0 then item.worth -= 2 else item.worth -= 1 end})
    daily_process_rules.add_rule(item, lambda {|item| if item.worth < 0 then item.worth = 0 end})    
  end
  # Add Generic Business Rules To Item Because None Have Been Given
  if !(daily_process_rules.has_business_rules_for?(item)) then
    puts "    Adding Generic Business Rules to Item #{item.name}"
    daily_process_rules.add_rule(item, lambda {|item| subtract_one_day_from_shelflife(item)})
    daily_process_rules.add_rule(item, lambda {|item| item.worth -= 1})
    daily_process_rules.add_rule(item, lambda {|item| if item.worth < 0 then item.worth = 0 end})
  end
end

puts "\nApplying Daily Process Business Rules For All Inventory Items"

store_inventory.return_inventory_items.each do |item|
  puts "    Applying Daily Process Business Rules For #{item.name}"
  daily_process_rules.apply_all_matching_rules(item)
end

puts "\nInventory Values After Applying Business Rules:"

puts store_inventory

puts "Strings Could Be Placed In DataBase And Loaded As Business Rules Using Eval And Lambda"

it_works = eval "lambda {puts \"IT WORKS!!!\"}"
it_works.call

I think if I could have presented a design like this I probably would have gotten the job.  I am fairly happy at the job I have so I guess this is not the end of the world ;-).