Thursday, 12 July 2012

Test Driven Development solutions to same problem

!WARNING - THIS POST IS WRITTEN FOR SOFTWARE FOLKS!

I discovered Test Driven Development / TDD in about 2005/6 thanks to Gunjan Doshi's TDD Rhythm and 1-2 old articles on JavaWorld.

Years later I read Kent Beck's book Test Driven Development (The Addison-Wesley Signature Series) (or US) and was glad I had not missed anything fundamental from my non-standard "foundations" on the subject. These days there is a fairly good intro on Test Driven Development wikipedia entry. This post is aimed at software craftsmen and journeymen and perhaps even masters - to inspire a bit of thought along the following dimensions that impact the solutions that evolve:
- the understanding of the functional requirement
- the understanding of the non-functional requirement
- approaching the solution in primarily an iterative approach (managing complexity, perhaps)
- approaching the solution in primarily an incremental approach (managing complication, perhaps)
- the understanding of the required test strategy to ensure required quality level is achieved

Dimensions that, in TDD, result in the tests selected AND the order in which to approach the tests, which result in the emerging of different solutions.

I've spent a great deal of time leading/mentoring/coaching Test Driven Development (TDD) sessions, working with teams and individuals on a particular problem which they had no experience in beforehand. These sessions were the correct environment for us both to learn TDD, or for them to learn TDD, or even as part of a recruitment selection process where the team was practising TDD and we had to ensure candidates could practice TDD as we did.

I selected a problem that was documented on wikipedia on the page describing Numerology. I like discovering interesting coding problems on wikipedia as the descriptions there are [theoretically] internationally acceptable and understandable - which helps enormously to "level the playing field" for English first language speakers as well as non-English first language speakers.

The specific problem I wanted to pair up on is described very well under "Methods" in terse language (another "pro" as time is always pressured, and I wanted a problem understood, solved and discussed in 30-45 minutes).
In essence for this challenge, I wanted solutions to be developed that would convert a word, a phrase, and ultimately a person's full name, into a number, using simple rules that had to be analysed on the wikipedia page and extracted, without my help, to establish the even playing-field going into the solution development phase and discussion phase thereafer.

The first part of the rules were simply how to encode letters to numbers according to Numerology:

1 = a, j, s;
2 = b, k, t;
3 = c, l, u;
4 = d, m, v;
5 = e, n, w;
6 = f, o, x;
7 = g, p, y;
8 = h, q, z;
9 = i, r

The second part of the rules would usually give the candidate 2 strategies depending on how detail-oriented the person was.

For those who rushed through understanding the requirements, the rules were simply summing individual numbers repeatedly until the total < 10, eg using the wikipedia examples:

3489 ? 3 + 4 + 8 + 9 = 24        ? 2 + 4 = 6
Hello ? 8 + 5 + 3 + 3 + 6 = 25  ? 2 + 5 = 7

For those who paid a bit more attention, the rules were simply:

  value modulo 9, if 0, then 9

It is *always* interesting to see who picks up the shorter modulo 9 strategy, and who does not. (feeding into a lightweight Belbin assessment - see my Coaching with Belbin)

The various solutions below highlight [clearly] how the right problem tackled in the right way creates a much more readable, maintainable, extensible and flexible solution using TDD. Although even the less TDD-correct approach still produces an acceptable result. For me though, the best solution was actually produced largely without using TDD. Can you spot it?

/***************************************/
// TESTS
/***************************************/

I have removed name identifications and some "interesting" tests from the typical TestClasses that were all similar to either:

// Simple
import Calculator;
import org.junit.Test;
import static org.junit.Assert.*;
public class CalculatorTest {
    Calculator calculator = new Calculator();
    /*
     * Test Cases:     *    
     * 3,489 => 3 + 4 + 8 + 9 = 24 => 2 + 4 = 6     *          
     * Hello => 8 + 5 + 3 + 3 + 6 = 25 => 2 + 5 = 7  *
     */
    @Test public void testCalculateImportantChars() {        assertEquals(2, calculator.convertCharToNumeric('k'));        assertEquals(1, calculator.convertCharToNumeric('a'));        assertEquals(9, calculator.convertCharToNumeric('r'));        assertEquals(5, calculator.convertCharToNumeric('n'));        assertEquals(8, calculator.convertCharToNumeric('h'));        assertEquals(5, calculator.convertCharToNumeric('e'));        assertEquals(3, calculator.convertCharToNumeric('l'));        assertEquals(6, calculator.convertCharToNumeric('o'));        assertEquals(9, calculator.convertCharToNumeric('i'));        assertEquals(8, calculator.convertCharToNumeric('z'));    }

   @Test public void testCalculateHello() {        assertEquals(7, calculator.calculate("Hello"));    }
}

OR:

// More along Kent Beck's final example approach

import Calculator;
import org.junit.Test;
import static org.junit.Assert.*;

public class CalculatorTest {

   Calculator calculator = new Calculator();

   /*
    * A = 1
    * B = 2
    * C = 3
    * a = 1
    * b = 2
    * j = 1;
    * k = 2
    * i = 9;
    * 0 = 0
    * 1 = 1
    * 11 =  2
    *
    * AA = 2
    * AB = 3
    *
    * 1B2a =6
    */

   @Test
   public void testCalculate1() {

       String[] inputs = {"A", "a", "B", "b", "C", "k", "j", "i", "AA", "AB", "ABc", "0", "1", "11", ",", " ", "1B2a", "-1", "hello", "3,489"};
       int[] outputs = {1,1,2,2,3,2,1,9, 2, 3, 6, 0, 1, 2, 0, 0, 6, 1, 7, 6};

       for(int testcase=0; testcase < inputs.length; testcase++) {
           assertEquals(outputs[testcase], calculator.iWantToBelieve(inputs[testcase]));
       }

       assertEquals(-1, calculator.iWantToBelieve(null));
   }

}

/***************************************/
// SOLUTIONS, all checks and Exceptions removed for brevity
/***************************************/



/* Common Solution 1 */


public class Calculator {

   public int iWantToBelieve(String string) {
       if(null==string) {
           return(-1);
       }
       int result = 0;
       for(int pos = 0; pos < string.length(); pos++) {
           int valueToAdd;
           char currentChar = string.charAt(pos);
           if(Character.isDigit(currentChar)) {
               valueToAdd = Integer.valueOf(String.valueOf(currentChar));
           }
           else if(Character.isLetter(currentChar)){
                int tmpValue = Character.toUpperCase(currentChar) - 64;
               int valueOfCharacter = tmpValue % 9;
               valueToAdd = valueOfCharacter == 0 ? 9 : valueOfCharacter;
           }
           else {
               valueToAdd = 0;
           }
           result+=valueToAdd;
       }

       if(result>9) {
           result = iWantToBelieve(String.valueOf(result));
       }
       return(result);

   }

}


/* Common Solution 2 */


import java.util.HashMap;
import java.util.Map;

public class Calculator
{
 private static Map numericCodes = new HashMap();
  
  static 
  {
   numericCodes = initialiseNumerologyMap();
 }

 public Calculator(){
 }
  
 public int calculate(String word) {
   char[] chars = word.toLowerCase().toCharArray();
   int[] digits = new int[chars.length];
    
   for (int i = 0; i < chars.length; i++)
   {
       char characterValue = chars[i];
       Integer numeric = numericCodes.get(characterValue);
       if (numeric == null)
       {
         digits[i] = 0;
       } else {
         digits[i] = numeric;
       }
   }
   int sum = 0;
   for (int i = 0; i < digits.length; i++)
   {
     sum += digits[i];
   }
   if (sum > 9)
     return calculate(""+sum);
    
   return sum;
 }

 /* 
  * 1= a, j, s; 2= b, k, t; 3= c, l, u; 4= d, m, v; 5= e, n, w;
  * 6= f, o, x; 7= g, p, y; 8= h, q, z; 9= i, r  
  */
 private static Map initialiseNumerologyMap()
 {
   // a to z mappings
   for (int i=97; i<123; i++) {
     int alphabetValue = i%96;
     int modulatedValue = alphabetValue%9;
     if (modulatedValue == 0) {
       modulatedValue = 9;
     }
     numericCodes.put((char)i, modulatedValue);
   }
   // 0 to 9 mappings
   int j=0;
   for (int i=48; i<57; i++) {
     numericCodes.put((char)i, j);
     j++;
   }
   return numericCodes;
 }

}

/* Common Solution 3 */

public class Calculator {

   public int calculate(int n) {
       if (n == 0) {
           return 0;
       }

       int result = n % 9;
       if (result == 0){
           return 9;
       }
       return result;
   }

   private String a2n = "abcdefghijklmnopqrstuvwxyz";

   public int convertCharacter(char ch) {
       return calculate(a2n.indexOf(Character.toLowerCase(ch))+1);
   }

   public int convertWord(String word) {
       int sum = 0;
       for(char c : word.toCharArray()) {
           sum += convertCharacter(c);
       }

       return calculate(sum);
   }

}

/* Common Solution 4 */

public class Calculator {

   public int convertCharToNumeric(char c) {
       int intValue = (int)c;
       if ((intValue >95) && (intValue < 129)) {
           int numericFate = (intValue - 96) % 9;
           return convertForNumerology(numericFate);
       } else {
           return 0;
       }
   }

   private int convertForNumerology(int yourFate) {
       if (yourFate == 0) {
           return 9;
       }
       return yourFate;
   }

   public int calculate(String word) {
       int total = 0;
       for (char c : word.toLowerCase().toCharArray()) {
           total += convertCharToNumeric(c);
       }
       return convertForNumerology(total%9);
   }

}

/* Common Solution 5 */

public class Calculator {

   public int calculate(int n) {
       int result = n % 9;
       if (result == 0) {
           result = 9;
       }
       return result;
   }

   public int convert(String word) {
       String workingWord = word.trim().toLowerCase();

       int total = 0;
       for (char ch : workingWord.toCharArray()) {
           int intValue = (int)ch;
           if ((intValue > 96) && (intValue < 123)) {
               total += (intValue % 96);
           }
       }

       if (total > 0) {
           return calculate(total);
       }

       return 0;
   }

}


/* UnCommon Solution 6 */

public class Calculator {

   public int calculate(long n) {
       if(n%9==0)
           return 9;
       return (int)(n % 9);
   }

   public long convertStringToNumber(String string) {
       String tmpString = string;
       tmpString = tmpString.toLowerCase().replaceAll("[^abcdefghijklmnopqrstuvwxyz]", "");
       tmpString = tmpString.replaceAll("[a,j,s]", "1");
       tmpString = tmpString.replaceAll("[b,k,t]", "2");
       tmpString = tmpString.replaceAll("[c,l,u]", "3");
       tmpString = tmpString.replaceAll("[d,m,v]", "4");
       tmpString = tmpString.replaceAll("[e,n,w]", "5");
       tmpString = tmpString.replaceAll("[f,o,x]", "6");
       tmpString = tmpString.replaceAll("[g,p,y]", "7");
       tmpString = tmpString.replaceAll("[h,q,z]", "8");
       tmpString = tmpString.replaceAll("[i,r]", "9");
        return Long.parseLong(tmpString);
   }
  
}

/**************************/

It seems to me that TDD represents some interesting mental/psychological challenges for people who try:

  • For software newbies, it is almost effortless, but they struggle to see the value in what they're creating as their solution emerges - I think due to a lack of experience looking at mountains of badly written and poorly designed legacy systems
  • For software oldies, it is darn painful, and they struggle to see the value in the extra steps they're taking, constantly battling the urge to do more error checking and handling, to make the solution more complicated, and generally reach a point of suspended disbelief needing to see more examples and try it again in their work environments
  • For some, and it is so far impossible for me to recognise them, they reach almost immediate "ah ha!" realisations of the power of TDD. They're flexible and open enough to suspend their not-as-entrenched over-thinking thought processes that the "oldies" display yet they have experience of maintaining some old production systems and dealing with some of their more novice mistakes that have come back to haunt them again, and sometimes again and again.
  • Keith Braithwaite   and I once discussed that people applying TDD properly ("coincidentally" those who love doing so) experience  Csikszentmihalyi's "Flow"  - and hence even more benefits for those who successfully embrace, as well as the organisations that setup them up to succeed/embrace.


I look forward to hearing other points of view on the above 6 solutions for the "same problem" over the next few years.

Thursday, 5 July 2012

My favourite coaching tools: Labels and Believing Is Seeing!

Caveats:
This is a group exercise and great for team building. At certain times during the session some people's emotions might rise due to frustration at being misunderstood.

As coach you MUST manage the level of frustration (a little is good, too much is bad) in order to allow the group to fully experience and gain from the steps below. Most participants however are quick to see the simulation for what it is and self-control themselves accordingly. If you feel unsure, practice with a smaller group of friends, for instance, who can safely give you feedback throughout to ensure you are able to facilitate it correctly to the intended benefits.

Always remain on the safe side - for your sake and for your participants' also!

Required:
- Enough labels to get people into groups of 6 - preferably the labels are on little hats so that the people who are labelled do not know their own label (sometimes I use post-its stuck to foreheads but this does not work in warm sweaty conditions, sometimes paper tape/medical tape but have the same problem with sweaty foreheads, sometimes name badges tied on short strings under people's chins).

- Labels should be written (or use symbols) big enough so that other people can read them from 2-3 feet away.

- Labels suggested in Quick Team-Building Activities for Busy Managers: 50 Exercises That Get Results in Just 15 Minutes by Brian Cole Miller include "laugh at me", "disagree with me", "leader", "brown noser", and many more - *BUY THE BOOK*. I've also used Belbin's team roles for labels, as well as Belbin team role opposites (see Coaching with Belbin for details). Sometimes I just make them up based on the group and behaviours I have observed.
- Printouts for half the group of Stephen R Covey's The 7 Habits of Highly Effective People (*BUY THE BOOK AND READ FOR FULL DETAILS*) Young Lady:

- Printouts for half the group of Stephen R Covey's The 7 Habits of Highly Effective People (*BUY THE BOOK*) Old Lady:


- A digital projector and computer in order to show the group Stephen R Covey's The 7 Habits of Highly Effective People (*BUY THE BOOK*) Both Ladies picture:




- 15 minutes for the first "Label Game" session
- 15 minutes for the second "Believing is Seeing" session
- 15-30 minutes for open group discussion to ensure lesson properly understood and people back to normal

Step 1:
Explain the basics of the "Label Game" to the participants:
- Everyone will receive a "secret" (from them) label that they should not see before the end of the exercise
- No one should tell someone else what their label is
- The groups will be given 7 minutes to plan something (department party, a new fun game, who to choose to evacuate a doomed earth, survive for 24 hours in a desert/artic, etc)
- As the group discusses, the participants must proact/react to the label of the person
- By the end of 7 minutes, people will be asked what they think their label is, before being allowed to look

Step 2:
Break the group into sub-groups of 6, distribute and place the labels on each member so that they are unable to see their own label.

Step 3:
Start the timer! Remind the participants about good timebox strategies to ensure they succeed with their discussion. Help with time keeping.

Step 4:
Time up! Let the groups discuss amongst themselves and get an indication of how many people realised within 7 minutes of simple discussion, what label they were wearing! Usually >75% correctly guess or infer their label based on how others were interacting with them.

Step 5:
Open the discussion up to the whole group. Good questions like "How did it feel to be treating people according to their label?", "How did it feel to be treated according to a label that was not visible to you?", "What about the labels we're treating each other with continuously in the day to day work?", "What are the dangers of labels?", "What are the pros of having labels?", "How do our labels affect us?", "Labels versus Job Title?" and see what thoughts and feelings are provoked by the group.

Be strict with your own time management as there is still the second session to run!

Step 6:
Get people to become 1 group again, this time split them down the middle.

Step 7:
Without making it obvious that there are 2 different pictures being given out, distribute the "Young Lady" above facedown to half the group, and distribute the "Old Lady" above facedown to the other half of the group.

Ask the participants to turn over the picture and stare at it for 30 seconds, and then to put it away. Help them with timekeeping and ensuring they're focussing on their own picture only.

Step 8:
With the pictures away, turn on the projector and show the group the "Both Ladies" picture.

Step 9:
Ask the participants to stick their hands up if they recognise the picture. Now ask 1 "random" person from the "Old Lady" group what they see. Then ask 1 "random" person from the "Young Lady" group what they see.

Step 10:
Now ask the whole group to raise their hands those who see an Old Lady? And then ask those to raise their hands who sees a Young Lady? Act confused and ask how is it possible - surely they're looking at exactly the same, unmoving picture on the screen - how could it be that people are seeing different things? And then ask who sees both an Old Lady and a Young Lady?

The groups are usually quite fun and energised by this time so allow them to discuss and try to resolve the different perceptions they have. People will come up to the screen and start tracing out the different curves, arguing, getting frustrated, getting amused and slowly all or most of the group will eventually see both pictures. (as an aside, what does it mean if someone is unable, even after a lot of help, to see both ladies?)

Step 11:
Now explain it the way Stephen R Covey does: "It's not logical, it is psychological!"..."the way we see the problem IS the problem!" 

Step 12:
And now ask similar questions of the Label Game session. Ask how this kind of knowledge, this insight might have changed how they approached problems in the past, and how they will approach problems in the future? Are the problems restricted to work problems, or are "people problems" also now more approachable? And so on.

Before running this session, I strongly recommend buying both books linked above and reading them properly for more many more details than I've summarised here. 

I find the 2 exercises even more beneficial for individuals I am coaching who attend, especially if they have already covered Belbin's Team Role Inventory TheoryMyers Briggs Type Indicator and Preferred Auditory, Visual or Kinaesthetic Communication Styles. For additional benefit, these sessions, run before coachees attempt their first feedback gathering from peers, also help to give people more self confidence in the collection, in the giving, and in the understanding of the content+emotion they receive from other people. We are all human, even if often it appears we are, or they, are not! ;-)


A smarter SMART for even better collaborative Objectives (including OKRs)

My favourite coaching tools: SMART Acronym Another Update