Sipping: Systemically Incremental Programming
What do I mean by "Decoupling"
From Wikipedia:
In software development, the term “decoupling” is used to identify the separation of software blocks that shouldn't depend on each other... Special design techniques allow software designers to have as few dependencies as possible. This typically reduces the risk of malfunction in one part of a system when the other part changed. It also forces the developer to focus on one thing at a time.
Decoupling lowers or minimizes Coupling.
Coupling is characterized as follows:
Coupling is usually contrasted with cohesion. Low coupling often correlates with high cohesion, and vice versa....[where] cohesion is a measure of how strongly-related the functionality expressed by the source code of a software module is.
In my humble opinion, the single most destructive force in software design is tightly coupled functionality.
In the wikipedia article on decoupling, it states that "special design techniques allow software designers to have as few dependancies as possible". Sipping is just such a technique. Specifically, I claimed that Sipping will "encourage" your code to be:
- Simpler and more elegant, start to finish
- More stable and bug free, out of the gates
- Easier to read
- Easier to change as conditions require
- Easier to maintain by you (or others)
- Better software at the end of the day for the end user
and last, but not least
- Much, much faster to produce in the first place
Code Efficiency
Before going any further, I want to point out that Sipping specifically (and consciously) does not always produce the most efficient code. In other words, performance is not the primary objective of Sipping. If performance is needed, it is quite possible that a more direct and optimized approach may be necessary. I have found though, that in most cases, performance is not nearly as critical as readability, stability, extensibility, etc.
Unnecessary (or Inadvertent) Coupling
Let's take a simple example where, if developed as a unit, inadvertent coupling might occur, even if written by a "strong" developer.
I found this article on CSharp corner about Converting Numbers into Words:
http://www.c-sharpcorner.com/UploadFile/b942f9/6362/
The class that implements this is a console application, included here:
using System;
class Program
{
static void Main()
{
string input;
int number;
bool isValid;
bool isUK = false;
Console.WriteLine("\nEnter '0' to quit the program at any time\n");
while (true)
{
Console.Write("\nUse UK numbering y/n : ");
input = Console.ReadLine();
if (!(input.ToLower() == "y" || input.ToLower() == "n"))
Console.WriteLine("\n Must be 'y' or 'n', please try again\n");
else
{
if (input.ToLower() == "y") isUK = true;
Console.WriteLine("\n");
break;
}
}
do
{
Console.Write("Enter integer : ");
input = Console.ReadLine();
isValid = int.TryParse(input, out number);
if (!isValid)
Console.WriteLine("\n Not an integer, please try again\n");
else
Console.WriteLine("\n {0}\n", NumberToText(number, isUK));
}
while (!(isValid && number == 0));
Console.WriteLine("\nProgram ended");
}
public static string NumberToText(int number, bool isUK)
{
if (number == 0) return "Zero";
string and = isUK ? "and " : ""; // deals with UK or US numbering
if (number == -2147483648) return "Minus Two Billion One Hundred " + and +
"Forty Seven Million Four Hundred " + and + "Eighty Three Thousand " +
"Six Hundred " + and + "Forty Eight";
int[] num = new int[4];
int first = 0;
int u, h, t;
System.Text.StringBuilder sb = new System.Text.StringBuilder();
if (number < 0)
{
sb.Append("Minus ");
number = -number;
}
string[] words0 = {"", "One ", "Two ", "Three ", "Four ", "Five ", "Six ",
"Seven ", "Eight ", "Nine "};
string[] words1 = {"Ten ", "Eleven ", "Twelve ", "Thirteen ", "Fourteen ",
"Fifteen ", "Sixteen ", "Seventeen ", "Eighteen ", "Nineteen "};
string[] words2 = {"Twenty ", "Thirty ", "Forty ", "Fifty ", "Sixty ",
"Seventy ", "Eighty ", "Ninety "};
string[] words3 = { "Thousand ", "Million ", "Billion " };
num[0] = number % 1000; // units
num[1] = number / 1000;
num[2] = number / 1000000;
num[1] = num[1] - 1000 * num[2]; // thousands
num[3] = number / 1000000000; // billions
num[2] = num[2] - 1000 * num[3]; // millions
for (int i = 3; i > 0; i--)
{
if (num[i] != 0)
{
first = i;
break;
}
}
for (int i = first; i >= 0; i--)
{
if (num[i] == 0) continue;
u = num[i] % 10; // ones
t = num[i] / 10;
h = num[i] / 100; // hundreds
t = t - 10 * h; // tens
if (h > 0) sb.Append(words0[h] + "Hundred ");
if (u > 0 || t > 0)
{
if (h > 0 || i < first) sb.Append(and);
if (t == 0)
sb.Append(words0[u]);
else if (t == 1)
sb.Append(words1[u]);
else
sb.Append(words2[t - 2] + words0[u]);
}
if (i != 0) sb.Append(words3[i - 1]);
}
return sb.ToString().TrimEnd();
}
}
This class is about 100 lines of code. It supports the following features:
- Handles any Int32 number
- Handles both positive and negative numbers
- Both British and English Phrasing are supported
Some items that could be criticized about it are:
- While this method totally works, and is efficient in it's design, I'm not exactly sure how it works.
- The actual algorithm that constructs the words is somewhat cryptic, and not easily understood.
- I think that most developers would struggle to write a method of this complexity.
- In fact, many developers (myself included) would struggle to even understand how it works, let along come up with it ourselves
- It would be harder still to write such a method without any bugs, and to be confident that the final design really does work as expected in all situations.
- The entire process is "encapsulated" into one method. This tightly couples all of the features supported into one process that must be (to some extent at least) developed, and understood as a unit.
- I can't see an easy way to "evolve" this method. In other words, again, because of it's underlying design, it appears that you'd have to understand the entire problem, and then write the entire method all at once.
- If any bugs were found, most developers would have a hard time fixing them
- The article states that the method could easily be extended to support bigger numbers and decimals, but exactly how this would be done is not immediately apparent to me.
Sipping and a Decoupled alternative
This problem would be challenging for most developers to write from scratch. Using the process of sipping however, the problem can be broken down into a series of smaller steps, each of which is simple and easily understood. In this way, we will take the relatively complex problem solved above, and simplify it.
In thinking about the underlying problem, I made the following observations:
- The problem should be relatively easily "parsed" into SIPs
- Each 3 digit sequence is identical. eg. (365 is "Three Hundred Sixty Five", just as 365,000 is "Three Hundred Sixty Five [Thousand]", or Million, Billion, Etc)
- This 3 digit sequence is the only thing that is "Modified" by British Phrasing.
- Only the 1st digit needs to be looked at to determine if the number is negative.
In further thinking about the problem, I think that it can be divided into the following SIPs.
- Develop an abstract base class that can be used to parse a number into english words.
- Implement a "TensTranslator" that can handle the numbers between 0 and 99.
- Implement a "HundredsTranslator" that can handle any numbers from 0 to 999.
- Implement a "BigNumberTranslator" that can string together multiple HundredsTranslators to handle Thousands, Millions, Billions, Etc.
- Implement a NegativeNumberTranslator that can handle negative numbers.
- Modify the HundredsTranslator to handle US or British Phrasing.
By "evolving" the problem SIP by SIP as described above, we will get the same functionality, but hopefully with a more stable, easily understood and extensible algorithm.
Source Files
SIP 1: Abstract Base Class
Develop an abstract base class that can be used to parse a number into english words.
1/// <summary>
2///
3/// </summary>
4public abstract class TranslatorBase
5{
6 public TranslatorBase(String input)
7 {
8 this.Input = input;
9 }
10
11 public String Input { get; set; }
12
13 public abstract String Parse();
14
15 public override String ToString()
16 {
17 return this.Parse();
18 }
19}All translating of numbers to words will be based on this simple abstract class. In the 2nd SIP we will develop our first translator that will handle all numbers from 0 to 99.
SIP 2: Translating 0 to 99
This class will be used again and again and again by the rest of the system.
1/// <summary>
2///
3/// </summary>
4public class TensTranslator : TranslatorBase
5{
6
7 public TensTranslator(String input) : base(input) { }
8
9
10 private String[] Ones = new String[] { "", "One", "Two", "Three", "Four",
"Five", "Six", "Seven", "Eight", "Nine", "Ten",
11 "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen",
"Sixteen", "Seventeen", "Eighteen", "Nineteen" };
12
13 private String[] Tens = new String[] { "", "", "Twenty", "Thirty", "Fourty",
"Fifty", "Sixty", "Seventy", "Eighty", "Ninety" };
14
15 public override String Parse()
16 {
17
18 int inputAsInt = Int32.Parse(this.Input);
19
20
21 if ((inputAsInt < 0) || (inputAsInt > 99))
throw new Exception("... can only handle numbers between 0 and 99");
22
23
24 if (inputAsInt < 20) return this.Ones[inputAsInt];
25
26
27 else
28 {
29 int ten = inputAsInt / 10;
30 int one = inputAsInt % 10;
31 return String.Format("{0} {1}", this.Tens[ten], this.Ones[one]).Trim();
32 }
33 }
34} The TensTranslator will handle any number between 0 and 99. Notice how simple the class is. This class will not handle Hundreds, Thousands, Millions, etc. It will not even handle negative numbers. But for all the numbers between 0 and 99, this class will handle it.
Writing this was easy. It doesn't solve the entire problem, but it solves the first part of the problem. We now have a translator that can translate any number under 100. So far it's not nearly as functional as the original class, but was easy to develop, is easy to read, is totally stable, and will be easy to incorporate into additional SIPs to get the rest of the functionality that we want.
Using (and more importantly, testing) our new TensTranslator class is as simple as plugging it into the console app, similarly to how it was done in the original article:
1using System;
2using System.Text;
3class Program
4{
5 static void Main()
6 {
7 Console.WriteLine("\nEnter '0' to quit the program at any time\n");
8 do
9 {
10 Console.Write("Enter integer (0 to terminate): ");
11 try
12 {
13 TranslatorBase translator = new TensTranslator(Console.ReadLine());
14 Console.WriteLine("\n {0} is {1}\n", translator.Input, translator);
15 if (translator.Input == "0") break;
16 }
17 catch (Exception ex)
18 {
19 Console.WriteLine("\n ERROR: {0}\n", ex.Message);
20 }
21 }
22 while (true);
23 Console.WriteLine("\nProgram ended");
24 }
25}
SIP 3: HundredsTranslator - 0 to 999
Now that we've written and tested a 10's translator, we're ready to tackle bigger numbers. The hundreds translator will use two 10's translators to handle any number up to 1000.
1/// <summary>
2///
3/// </summary>
4public class HundredsTranslator : TranslatorBase
5{
6
7 public HundredsTranslator(String input)
8 : base(input.PadLeft(3, '0'))
9 { }
10
11 public override String Parse()
12 {
13
14 if (this.Input.Length > 3) {
15 throw new Exception("... can only handle numbers between 0 and 999");
16 }
17
18
19 String hundredResult = String.Empty;
20 if (this.Input.Substring(0, 1) != "0")
21 {
22 TensTranslator hundreds = new TensTranslator(this.Input.Substring(0, 1));
23 hundredResult = String.Format("{0} Hundred", hundreds);
24 }
25
26
27 TensTranslator tens = new TensTranslator(this.Input.Substring(1, 2));
28 String tensResult = String.Format("{0}", tens);
29
30
31 return String.Format("{0} {1}", hundredResult, tensResult).Trim();
32 }
33} The hundreds translator simply pads it's input to at least 3 digits. In this way, 5 becomes 005, while 345 is left as 345.
The algorithm then just checks if the first digit is anything other than 0. If so, it creates a tens translator to parse it into One, Two, Three, etc and adds the word "Hundred" to it. It then takes the remaining two digits and uses a TensTranslator to parse those into english as well. It then returns the two strings (hundredResult and tensResult) as one, trimmed string.
We now have a Translator that can handle any number between 0 and 999. We still can't handle Thousands, Millions, Billions, etc, negative numbers or British phrasing, but here again, it's simple, easily tested, easy to read and understand, and moves the problem further down the road.
In addition, because it uses the Tens translator, as we test this HundredsTranslator we are also further testing the TensTranslator. If there are any issues in that first class, they will often be exposed during subsequent SIPs. Now we just re-run the application, and should be able to enter any number up to one thousand.
Ready for SIP 4?
SIP 4: BigNumberTranslator - 0 through Quintillion.
Stringing together multiple HundredsTranslators, we can handle any number (of virtually any size)
1/// <summary>
2///
3/// </summary>
4public class BigNumberTranslator : TranslatorBase
5{
6 public BigNumberTranslator(String input)
7 : base(input)
8 { }
9
10
11 private String[] NumberGroups = new String[] { "", "Thousand","Million","Billion",
12 "Trillion", "Quadrillion", "Quintillion" };
13
14 /// <summary>
15 ///
16 /// </summary>
18 public override string Parse()
19 {
21
22 String input = this.Input.PadLeft(this.NumberGroups.Length * 3, '0');
23
24
25 StringBuilder result = new StringBuilder();
26 foreach (String groupName in this.NumberGroups)
27 {
28
29 String group = input.Substring(input.Length - 3, 3);
30 input = input.Substring(0, input.Length - 3);
31
32
33 HundredsTranslator hundredGroup = new HundredsTranslator(group);
34
35
36 if (!String.IsNullOrEmpty(hundredGroup.ToString()))
37 {
38
39 if (result.Length > 0) result.Insert(0, ", ");
40 result.Insert(0, String.Format("{0} {1}", group, groupName));
41 }
42 }
43
44
45 return result.ToString();
46 }
47} The BigNumberTranslator simply strings together HundredsTranslators, parsing each group of numbers, 3 at a time from the end of the input string passed to it. Once again, we've made an incremental improvement to the overall system, building on our previous work. This new class allows us to handle any number (up to 999 Quintillion) - and we could add support for larger numbers by simply adding additional group names to the end of the NumberGroups array. How much easier could it be?
At this point, we still can't handle negative numbers, or British Phrasing. Both of these are easily solved, each in their own SIPs of course!
SIP 5: Negative Numbers
1/// <summary>
2///
3/// </summary>
4public class NegativeCapableTranslator : TranslatorBase
5{
6 public NegativeCapableTranslator(String input) : base(input) { }
7
8 public override string Parse()
9 {
10
11 String input = this.Input;
12 bool negative = input.StartsWith("-");
13
14
15 if (negative) input = input.Substring(1);
16
17
18 return String.Format("{0}{1}", (negative ? "Minus " : ""),
new BigNumberTranslator(input));
19 }
20} This NegativeNumberTranslator builds on the BigNumberTranslator developed in SIP 4 and simply adds the process of checking if the first character is - symbol, in which case it inserts the word "Minus" before the output of the BigNumberTranslator, which gets passed the rest of the input string (as a positive number).
SIP 6: British Phrasing
We can add british phrasing by simply modifying the HundredsTranslator. If this translator had a static bool property for BritishPhrasing, we could simply insert the word "and" between the Hundred and Tens part - as follows. Here again, you can see that this is simply a small, systemic change to the existing code that adds support for British Phrasing, but doesn't substantially change the rest of the system we've put in place.
1/// <summary>
2///
3/// </summary>
4public class HundredsTranslator : TranslatorBase
5{
6
7 public HundredsTranslator(String input)
8 : base(input.PadLeft(3, '0'))
9 { }
10
11 public static bool BritshPhrasing { get; set; }
12
13 public override String Parse()
14 {
15
16 if (this.Input.Length > 3)
throw new Exception("... can only handle numbers between 0 and 999");
17
18
19 String hundredResult = String.Empty;
20 if (this.Input.Substring(0, 1) != "0")
21 {
22 TensTranslator hundreds = new TensTranslator(this.Input.Substring(0, 1));
23 hundredResult = String.Format("{0} Hundred", hundreds);
24 }
25
26
27 TensTranslator tens = new TensTranslator(this.Input.Substring(1, 2));
28 String tensResult = String.Format("{0}", tens);
29
30
31 if (!String.IsNullOrEmpty(hundredResult) &&
!String.IsNullOrEmpty(tensResult) && BritshPhrasing)
32 {
33 hundredResult += " and";
34 }
35
36
37 return String.Format("{0} {1}", hundredResult, tensResult).Trim();
38 }
39}
Conclusion
Each time we added functionality, all we had to do to our console app was change the "new TensTranslator(...);" to "new HundredsTranslator(...);", and then to "new BigNumberTranslator(...);", etc. After each SIP, we can fully test the new functionality, and check in our "improved" system. And by the end of the 6th SIP, we've reproduced the original functionality in, as I see it, a simpler, more easily understood way. In writing these classes, it took about 1 hour to write and test them - and once completed, I have a very high degree of confidence that they work as designed - and will continue to do so. Further, if I want to add additional functionality, the framework designed should be easily extended and/or improved.
While in this case I ended up with more code than the original application (~200 lines, vs ~100 lines), this is actually the opposite of my common experience. I find that usually I end up with less code (often substantially less code) when using the Sipping method.
The final code:
1using System;
2using System.Text;
3class Program
4{
5 static void Main()
6 {
7 Console.WriteLine("\nEnter '0' to quit the program at any time\n");
8 while (true)
9 {
10 Console.Write("\nUse UK numbering y/n : ");
11 String input = String.Format("{0}", Console.ReadKey().KeyChar);
12 if (!(input.ToLower() == "y" || input.ToLower() == "n"))
13 Console.WriteLine("\n Must be 'y' or 'n', please try again\n");
14 else
15 {
16 if (input.ToLower() == "y") HundredsTranslator.BritshPhrasing = true;
17 Console.WriteLine("\n");
18 break;
19 }
20 }
21 do
22 {
23 Console.Write("Enter integer (0 to terminate): ");
24 try
25 {
26 TranslatorBase translator =
new NegativeCapableTranslator(Console.ReadLine());
27 Console.WriteLine("\n {0} is {1}\n", translator.Input, translator);
28 if (translator.Input == "0") break;
29 }
30 catch (Exception ex)
31 {
32 Console.WriteLine("\n ERROR: {0}\n", ex.Message);
33 }
34 }
35 while (true);
36 Console.WriteLine("\nProgram ended");
37 }
38
39 /// <summary>
40 ///
41 /// </summary>
42 public abstract class TranslatorBase
43 {
44 public TranslatorBase(String input)
45 {
46 this.Input = input;
47 }
48
49 public String Input { get; set; }
50 public abstract String Parse();
51
52 public override string ToString()
53 {
54 return String.Format("{0}", this.Parse());
55 }
56 }
57
58 /// <summary>
59 ///
60 /// </summary>
61 public class TensTranslator : TranslatorBase
62 {
63
64 public TensTranslator(String input) : base(input) { }
65
66
67 private String[] Ones = new String[] { "", "One", "Two", "Three", "Four",
"Five", "Six", "Seven", "Eight", "Nine", "Ten",
68 "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen",
"Sixteen", "Seventeen", "Eighteen", "Nineteen" };
69
70 private String[] Tens = new String[] { "", "", "Twenty", "Thirty", "Fourty",
"Fifty", "Sixty", "Seventy", "Eighty", "Ninety" };
71
72 private int InputAsInt { get; set; }
73
74 public override String Parse()
75 {
76
77 this.InputAsInt = Int16.Parse(this.Input);
78
79
80 if ((this.InputAsInt < 0) || (this.InputAsInt > 99))
throw new Exception("... can only handle numbers between 0 and 99");
81
82
83 if (this.InputAsInt < 20) return this.Ones[this.InputAsInt];
84
85
86 else
87 {
88 int ten = this.InputAsInt / 10;
89 int one = this.InputAsInt % 10;
90 return String.Format("{0} {1}", this.Tens[ten], this.Ones[one]).Trim();
91 }
92 }
93 }
94
95 /// <summary>
96 ///
97 /// </summary>
98 public class HundredsTranslator : TranslatorBase
99 {
100
101 public HundredsTranslator(String input)
102 : base(input.PadLeft(3, '0'))
103 { }
104
105 public static bool BritshPhrasing { get; set; }
106
107 public override String Parse()
108 {
109
110 if (this.Input.Length > 3)
throw new Exception("... can only handle numbers between 0 and 999");
111
112
113 String hundredResult = String.Empty;
114 if (this.Input.Substring(0, 1) != "0")
115 {
116 TensTranslator hundreds = new TensTranslator(this.Input.Substring(0,1));
117 hundredResult = String.Format("{0} Hundred", hundreds);
118 }
119
120
121 TensTranslator tens = new TensTranslator(this.Input.Substring(1, 2));
122 String tensResult = String.Format("{0}", tens);
123
124
125 if (!String.IsNullOrEmpty(hundredResult) &&
!String.IsNullOrEmpty(tensResult) && BritshPhrasing)
126 {
127 hundredResult += " and";
128 }
129
130
131 return String.Format("{0} {1}", hundredResult, tensResult).Trim();
132 }
133 }
134
135 /// <summary>
136 ///
137 /// </summary>
138 public class BigNumberTranslator : TranslatorBase
139 {
140 public BigNumberTranslator(String input)
141 : base(input)
142 { }
143
144
145 private String[] NumberGroups = new string[] { "", "Thousand", "Million",
"Billion", "Trillion", "Quadrillion", "Quintillion" };
146
147 /// <summary>
148 ///
149 /// </summary>
150 /// <returns></returns>
151 public override string Parse()
152 {
153
154 StringBuilder result = new StringBuilder();
155
156
157 String input = this.Input.PadLeft(this.NumberGroups.Length * 3, '0');
158
159
160 foreach (String groupName in this.NumberGroups)
161 {
162
163 String group = input.Substring(input.Length - 3, 3);
164 input = input.Substring(0, input.Length - 3);
165
166
167 HundredsTranslator hundred = new HundredsTranslator(group);
168 group = String.Format("{0}", hundred);
169
170
171 if (!String.IsNullOrEmpty(group))
172 {
173 if (result.Length > 0) result.Insert(0, ", ");
174 result.Insert(0, String.Format("{0} {1}", group, groupName));
175 }
176 }
180
181
182 return result.ToString();
183 }
184 }
185
186 /// <summary>
187 ///
188 /// </summary>
189 public class NegativeCapableTranslator : TranslatorBase
190 {
191 public NegativeCapableTranslator(String input) : base(input) { }
192
193 public override string Parse()
194 {
195
196 String input = this.Input;
197 bool negative = input.StartsWith("-");
198
199
200 if (negative) input = input.Substring(1);
201
202
203 return String.Format("{0}{1}", (negative ? "Minus " : ""),
new BigNumberTranslator(input));
204 }
205 }
206}