Say you have a function something like this:
char andGate(char a, char b) throws InvalidInput
{
if (a=='0' && b=='0') return '0';
if (a=='0' && b=='1') return '0';
if (a=='1' && b=='0') return '0';
if (a=='1' && b=='1') return '1';
throw new InvalidInput();
}
You might also have some tests:
void testAndGate()
{
assertEqual(andGate('0', '0'), '0');
assertEqual(andGate('0', '1'), '0');
assertEqual(andGate('1', '0'), '0');
assertEqual(andGate('1', '1'), '1');
}
When you do an OR gate, then an XOR, then...; then you
start to notice that they all look rather similar.
OnceAndOnlyOnce should suggest then common structure
should be localized!
Its difficult to see how to refactor this in most
languages. Sure, you could go to a data driven approach,
but then you might lose performance.
An approach I find useful is to refactor into a meta-domain.
The 'and' gate can be can be re-written as
AND
a b f
0 0 0
0 1 0
1 0 0
1 1 1
(or something similar). I have found it nice to put the
table in a Word/Framemaker document, and extract it via
HTML -- though more recently I've turned to XML. I can
thus apply
OnceAndOnlyOnce to the documentation
as well as the code. Also, the documentation then becomes
part of the source code. (I do tend to have a rather
extreme approach to translation based programming).
It is then very easy to write simple Perl scripts (or
whatever) to generate both the tests and the implementation.
(this is not as silly as it sounds, because the translation
scripts may have bugs which are detected by the tests).
Note that, if the Perl script is non-trivial, it will need
its own
UnitTests. For any change in that script,
use the previous version of its output as a
GoldenLog
for regression testing.
Once you have a simple minded implementation in a script,
then you can use the scripts on multiple datasets. When
you find a better implementation, then you can make the
change in just one place (you don't change the tests).
If you ever decide that you want to stop using translation
then the refactoring to remove the scripts is a simple
matter of telling your build system (and possibly the
version control system) then the derived files are now
source files (again).
In general the transition into any
GenerativeProgramming
technique will lead to more cohesive source code. And sometimes
you need to go the other direction.
--
DaveWhipp
It is then very easy to write simple Perl scripts (or
whatever) to generate both the tests and the implementation.
(this is not as silly as it sounds, because the translation
scripts may have bugs which are detected by the tests).
But what if the source data used to generate both the tests and
the implementation has errors? Potentially, your test will be
bogified in exactly the same way as the implementation. The bogus
test could erroneously pass when run against the bogus code. E.g.
I might accidentally change a value in the result column of the
AND data table mentioned above. Tests should be implemented
separately and independently of the code-under-test.
-- yes, but that would be an AcceptanceTest, which should be invariant over any refactorings of the implementation
Note that, if the Perl script is non-trivial, it will need
its own UnitTests. For any change in that script,
use the previous version of its output as a GoldenLog
for regression testing.
It follows that if the source data being translated by the Perl
script is non-trivial, and used to generate both test cases and
production code, then the source data must have a unit test of its
own as well...
--
WylieGarvin
CategoryRefactoring,
TableOrientedProgramming