Two Sum Attempt With Ruby Following Zombi Tdd Approach
28 Apr 2020
So I haven't written a post here in four months. Things have been a little rough, a lot rough, but getting back on track now.
I totally lost my programming confidence, and have found it really difficult to do any non-web development-based coding. For months.
So todays the first day in a long time that I feel ready to try again. I went onto Leetcode and found a list of easy problems. I picked the first one on the list, called Two Sum.
Reworded to be easier to understand
You are given an array of integers, and a target number. Your task is to return the indices of the two numbers in the array that add up to the target number.
Constraints:You can't return the index of the same element more than once. You may assume that there is exactly one solution for the target number your are given.
Given nums = [2, 7, 11, 15], target = 9 ruturn [0,1]
Thoughts on my approach
I am going to solve all of these problems following a TDD, Object-oriented approach. Once I've solved them, I'll get feedback and try them again. It's time to face my fear of not being good enough, so I can actually start improving again.
I'm going to try out James Grenning's TDD Guided by ZOMBIES (2016) approach. Let's get started.
Step 1: Set up Ruby Dev environment
I created a project folder and opened it:
mkdir two-sum && cd two-sum
I created a test folder and a production code folder
mkdir spec lib
I created a two-sum test file and added a test scaffolding to it.
require 'two-sum' describe TwoSum do it "Does something" do end end
Then I went into the production code folder and created a two-sum file with a class scaffolding.
class TwoSum end
After that, I went back to the root and set up a command to auto run my tests for those two files every time I save one of them.
ls **/*.rb | entr rspec
This says, grab all the ruby files inside all of the directories (inside root), and run the command after '| entr' every time one of those files are changed.
The original tweet for this solution is here.
Step 2: Z is for Zero
Here's the explanation for 'Zero' in the article:
The first test scenarios are for simple post conditions of a just created object. These are the zero cases. While defining the Zero cases, take care to design the interface and capture the boundary behaviours in your test scenarios.
Most of that didn't make any sense to me. Hello Google.
First question, what are simple post conditions of a just created object?
Wikipedia says that
a postcondition in a condition or predicate that must always be true just after the execution of some section of code or after an operation in a formal specification... Example. A program that calculates factorials of an input number would have postconditions that the result after the calculation be an integer and that it be greater than or equal to 1
Science Direct says that
a postcondition associated witth a method invocation is a condition that must be true when we return form a method.
David Schmidt, 2008 [lecture] says that
the precondition states the situation under which the function operates correctly, and the postcondition states what the function has accomplished when it terminates.
postconditions are conditions that are true after a function returns or at the end of a code segment.
Okay, that was my favourite explanation of what postconditions are. Felt like it clicked after that one.
Okay, so the two sum program will return an array of two integers, both of which will be positive integers (because the index starts at 0). So in my mind, the postconditions will be:
- Result must be an array
- Array must containing two numbers
- Numbers must be positive numbers (not negative)
These preconditions I came up with don't feel exactly right, and I can't explain why. But it's a starting point that will get me moving.
Before writing my first test, I replaced my class with a method, we don't need an object for this at the moment. Here's what it looks like now
require 'two-sum' describe twoSum do it "Does something" do end end
def twoSum end
Oh fudge, I forgot to research the other things I didn't understand. James said 'take care to design the interface and capture the boundary behaviours in your test scenarios'.
I know (think I know) that an interface is the messages that an object can understand (the methods that can be invoked on it - but messages is the way Alan Kay, the creator of OO intended it to be known as. Here's his C2 wiki on it. I like to imagine my objects as little post boxes that other objects wander up to and feed message envelopes too with the question they want answered written on them. The post box can only choose to answer the messages that he can read. Communication is key.
Right, next question, what are boundary behaviours?
I wrote a Tweet thread asking for help on this here.
Here's what I wrote if you don't want to go there and see the replies (if there are any).
Can anyone explain what 'boundary behaviour' means when it comes to object-oriented design? My current understanding: You have a post box object and the post man (a different object) delivers a message to it and realises the message belongs to next doors post box.
Context: I'm trying out the ZOMBIE OO approach to solving a simple kata. The input is an array and a target number. The output is the indices of two numbers from the array that add up to the target number.
So for the Zero part, I'm trying to understand what postconditions, interfaces and boundary behaviours are. So far, my postconditions are: Must be an array, must contain two numbers, must be positive numbers. The message (interface) could be 'twoSum.indices_of_target_sum().
Not sure about the boundary behaviour part though.Not sure if I've got any of this right either, as it's my first time and haven't really come across these terms before. Would appreciate any feedback to know if I'm on the right track or not.
While I'm waiting, I'll go ahead and write the first test based on my current understanding.
require 'two-sum' describe 'indices_whose_sum_equals' do it "returns two positive numbers in an array" do expect(indices_whose_sum_equals.length).to eq(2) expect(indices_whose_sum_equals).to be_kind_of(Integer) expect(indices_whose_sum_equals).to be_kind_of(Integer) expect(indices_whose_sum_equals).to be >= 0 expect(indices_whose_sum_equals).to be >= 0 end end
def indices_whose_sum_equals [0, 0] end
The tests fails if there are less than two numbers, if one of the elements is not a number, or if a negative number is returned.
I refactered the tests to remove duplication and to make them a little easier to read
require 'two-sum' describe 'indices_whose_sum_equals' do it "returns two positive numbers in an array" do expect(indices_whose_sum_equals.length).to eq(2) first_number = indices_whose_sum_equals second_number = indices_whose_sum_equals [first_number, second_number].each do |index| expect(index).to be_kind_of(Integer) expect(index).to be >= 0 end end end
Something about this first test feels wrong though. I think the implementation details are too tied into it. Like it shouldn't matter if it's an array or not, hmm. I'll keep going. Once it's all working can ask for ways to make it better :)
Step 3: O is for One
Ahh send halp I don't get this yet. The next part says
Once progress is made on the zero cases, move to the next special boundary case, testing the bhaviour desired when transitioning from zero to one.
Okay, so what is a special boundary case?Joonas Pulakka, 2011 cites Wikipedia, saying that a
boundary case occurs when one of inputs is at or just beyond maximum or minimum limits.
Hmm, okay so the zero case are the conditions that must be true once it has been run. The returned integers can't be negative numbers.
What's the next tiny step forward, working backwards? What do we need to get the result we are looking for?
Ahhh cool! Got a bunch of awsesome replies to my question on Twitter. I especially love this one:
Consider is_empty, is_almost_empty, is_almost_almost empty. Which of these is not like the others? by Danil Suits which felt like a total epiphany, also kindof got there in the couple sentences before this too, yayy!
It's 2.30am, going to sign off for now, and carry on with this tomorrow, yay!