Posted by Sam on Feb 18, 2008 at 06:43 AM UTC - 6 hrs
Last week, hgs asked,
I find it interesting that lots of people write about how to produce clean code,
how to do good design, taking care about language choice, interfaces, etc, but few people
write about the cases where there isn't time... So, I need to know what are the forces that tell you
to use a jolly good bodge?
I suspect we don't hear much about it because these other problems are often caused by that excuse.
And, in the long run, taking on that technical debt will likely cause you to go so slow that that's the
more interesting problem. In other words, by ignoring the need for good code, you are jumping into
a downward spiral where you are giving yourself even less time (or, making it take so long to do anything
that you may as well have less time).
More...
I think the solution is to start under-promising and over-delivering, as opposed to how most of us do it
now: giving lowball estimates because we think that's what they want to hear. But why lie to them?
If you're using iterative and incremental development, then if you've over-promised one iteration, you
are supposed to dial down your estimates for what you can accomplish in subsequent iterations, until
you finally get good at estimating. And estimates should include what it takes to do it right.
That's the party-line answer to the question. In short: it's never OK to write sloppy code, and
you should take precautions against ever putting yourself in a situation where those
viscous forces pull you in that direction.
The party-line answer is the best answer, but it doesn't fully address the question, and I'm not
always interested in party-line answers anyway. The viscosity (when it's easier
to do the wrong thing that the right thing) is the force behind the bodge. I don't like it, but I
recognize that there are going to be times you run into it and can't resist.
In those cases where you've already painted yourself into a corner, what then? That's the interesting
question here. How do you know the best
places to hack crapcode together and ignore those things that may take a little longer in the short run, but
whose value shows up in the long run?
The easy answer is the obvious one: cut corners in the code that is least likely to need to change or
be touched again. That's because (assuming your hack works) if we don't have to look at the code again,
who really cares that it was a nasty hack? The question whose answer is not so easy or
obvious is "what does such a place in the code look like?"
By the definition above, it would be the lower levels of your code. But if you do that, and inject a bug, then
many other parts of your application would be affected. So maybe that's not the right place to do it.
Instead, it would be better to do it in the higher levels, on which very little (if any) other code
depends. That way, you limit the effects of it. More importantly, if there are no outgoing dependencies
on it, it is easier to change than if other code were highly dependent on it. [ 1]
Maybe the crapcode can be isolated: if a class is already aweful, can you derive a new class from it and
make any new additions with higher quality? If a class is of high quality and you need to hack something together,
can you make a child class and put the hack there? [ 2]
Uncle Bob recently discussed when unit and acceptance
testing may safely be done away with. He put those numbers around 10 and a few thousand lines of code,
respectively.
In the end, there is no easy answer that I can find where I would definitively say, "that's the place for a bodging."
But I suspect there are some patterns we can look for, and I tried to identify a couple of those above.
Do you have any candidates you'd like to share?
Notes:
[1] A passing thought for which I have no answers:
The problem with even identifying those places is that by hacking together solutions, you are more likely
to inject defects into the code, which makes it more likely you'll need to touch it again.
[2] I use inheritance here because the new classes should be
able to be used without violating LSP.
However, you may very well be able to make those changes by favoring composition.
If you can, I'd advocate doing so.
Hey! Why don't you make your life easier and subscribe to the full post
or short blurb RSS feed? I'm so confident you'll love my smelly pasta plate
wisdom that I'm offering a no-strings-attached, lifetime money back guarantee!
Posted by Sam on Jan 23, 2008 at 10:35 AM UTC - 6 hrs
Suppose you want to share some data that one object produces with another object as the consumer. How would you go about doing that?
If you took a straightforward approach, you might have Producer call consumer.method("data") and pass it the data that way. On the other hand, you could have Consumer get the data it needs by requesting it from Producer with something like this: myData = producer.getData() .
However, perhaps Producer and Consumer shouldn't know anything about each other. Then you might introduce an Intermediary that gets the data from Producer and passes it to Consumer with something like consumer.myData = producer.getData()
Now if you want to get really creative, you could make Producer write its data to an XML file, and then have Consumer read the data from there.
But why?
Disagreements and horror stories are encouraged below.
Posted by Sam on Jan 16, 2008 at 07:04 AM UTC - 6 hrs
Something's been bugging me lately about ASP.NET: I can't seem to remember to type runat="server" every time I write a line of code to display a server control.
For normal HTML elements, I understand the necessity of using the attribute when you want that element to be tied to the server.
But I can't seem to figure out any other way to run a control that's been prefixed with the namespace asp .
So why does ASP.NET require such a silly, useless bit of text for everything it does?
I can think of three reasons:
More...
- Initially, Microsoft envisioned the controls running in several places: perhaps the browser, a printer, or a zoo. But three and a half major versions in, we're still required to pass the same attribute with the same value for every control. Couldn't they drop the requirement and make it a default if they decided to add more valid values?
- They wanted a constant reminder to novices that this code was running on the server. This way, people wouldn't ask why changes weren't occurring on the browser before posting forms to the server.
-
They wanted to remain consistent in having to tell the computer everything at least twice, à la
Object obj = new Object(); . However, in this case, it seems more like Object obj = new Object(type="Object"); . I'm not sure why that is.
Can you think of any other possible reasons? Please enlighten me, so I can forgive and forget this minor annoyance.
Last modified on Jan 16, 2008 at 07:06 AM UTC - 6 hrs
Posted by Sam on Jan 04, 2008 at 07:01 AM UTC - 6 hrs
At the beginning of this week's chapter from My Job Went To India,
Chad Fowler relates a story about Rao, a "mind-reading" programmer who could pick up on the
subtleties in conversations and implement code before you realized you had asked for it.
Both when I first read this, and the second time around, alarms went off in my head: "What about
YAGNI?" Why would you implement something
that wasn't asked for? If you are wrong, you could be relieved of your position for wasting
time and money.
Thankfully, Chad addressed my concerns. It turns out,
More...
We might be standing around waiting for a pot of coffee to brew, and I would talk about how
great it would be if we had some new flexibility in our code that didn't exist before. If I
said it often enough or with enough conviction, even though I hadn't really put it on the
team's TO-DO list, Rao might fill the gaps between "real work" looking at the feasibility of
implementing one of these things. If it was easy (and cheap) to implement, he'd whip it
out and check it in.
(emphasis mine)
Chad also mentions the potential pitfalls in such an approach:
- You waste time and money if the functionality was not needed
- You increase complexity of the code base and make "it less resilient to change" if your
code forces "the system down a particular architectural path."
- You could unintentionally make the application "less functional or desirable to the customer."
Honestly, I'd caution against using this advice unless you are in one of the following situations:
- You've known the feature-requester long enough that you can pick up on things he's asking for, but hasn't
yet asked for. I think you should be really close in this situation. How can you predict otherwise?
- There is obviously something missing from the spec, and you have enough experience with
similar systems to know it is missing. This might be something like "We need a login system."
You can probably safely assume they'll need a way to log out as well, and perhaps even
"I forgot my password" functionality.
The logout functionality I'd almost always toss in (unless requested otherwise). However, even
on the "forgot password" feature, I'd consider a couple of things. First, do I know the customer
well enough that we've done another application for them and they wanted it? Second, is the
budget big enough to where I know they won't be upset if I implement it?
There could be more, but that's what my brain thought of this morning. Of course, in many
cases it's just better to ask first.
What do you think?
Posted by Sam on Oct 10, 2007 at 03:39 PM UTC - 6 hrs
Here's a WTF I ran into the other day (partially pseudocode):
XmlDataDocument goto = new XmlDataDocument();
goto.Load(".\\locations.xml");
XmlNode gotoThisUrl;
if(referer == "http://www.someplace.com/directory1")
{
gotoThisURL = goto.SelectSingleNode("gotoOnDirectory1");
}
else if (referer == "http://www.someplaceelse.com/directory2")
{
gotoThisURL = goto.SelectSingleNode("gotoOnDirectory2");
}
Response.Redirect(gotoThisURL.InnerText.Trim(), true);
It was actually a bit more complex than that (probably 3x as many lines), but with the same purpose. There were literally two places from which to choose to direct the request. And no, the application wouldn't take incredibly long to compile. I felt like it was quite a bit of overkill.
Last modified on Oct 10, 2007 at 03:49 PM UTC - 6 hrs
Posted by Sam on Jul 27, 2007 at 03:43 PM UTC - 6 hrs
We could all stand to be better at what we do - especially those of us who write software. Although
many of these ideas were not news to me, and may not be for you either, you'd be surprised at how
you start to slack off and what a memory refresh will do for you.
Here are (briefly)
10 ways to improve your code from the
NFJS
session I attended with Neal Ford.
Which do you follow?
More...
-
Know the fundamentals
It was no surprise that Neal led off with the DRY
principle. Being one from the school of thought that "code reuse" meant "copy-and-pastable with few changes," I
feel this is the most important principle to follow when coding.
Neal uses it in the original sense:
Refuse to repeat yourself not just in your code, but all throughout the system.
To quote (If I remember correctly, The Pragmatic Programmer): "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."
I can't think of anything that wouldn't be improved by following that advice.
Continuous integration and version control are important fundamentals as well. Continuous integration means you
know the code base at least compiles, and you can have it run your unit tests as well. The codebase is verified as always working
(at least to the specs as you wrote them - you might have gotten them wrong, of course).
Using version control means you don't have to be afraid of deleting that commented out code that you'll probably never use again but
you keep around anyway "just in case." That keeps your code clean.
Finally, Neal suggests we should use static analysis tools such as FindBugs
and PMD that inspect our code, look for bad patterns, and suggest fixes and
places to trim. Those two are for Java. Do you know of similar applications for other platforms? (Leave a comment please!)
-
Analyze code odors and remove them if they stink
Inheritance is a smell. It doesn't always have to smell bad, but at least you should take the time
to determine if what you really need is composition.
Likewise, the existence of helper or utility classes often indicates a "poor design." As
Neal notes, "if you've gotten the abstraction correct, why do you need 'helpers'?" Try to put
them in "intelligent domain object[s]" instead.
Static methods are an indication you are thinking procedurally, and they are "virtually impossible to test."
Seeing the procedural mindset is easy, I think, but why are they hard to test?
Finally, avoid singletons. I know Singleton is everyone's favorite design
pattern because it's so easy to conceptualize, but as Neal mentions:
- They're really just a poor excuse for global variables.
- They violate SRP (link is to a PDF file)
by mixing business logic with policing you from using too many
- They make themselves hard to test
- It's very hard to build a real singleton, as you have to ensure you are using a unified class loader.
- More that I won't get into here...
-
Kill Sacred Cows
Neal started off saying that "sacred cows make the tastiest hamburger" and told the story of the angry monkeys:
A few monkeys were put in a room with a stepladder and a banana that could only be reached if the
monkeys used the stepladder. However, when a monkey would climb upon the stepladder, all the
monkeys would be doused with water. Then new monkeys were introduced and when they
tried to use the stepladder, the other monkeys would get angry and beat him up. Eventually,
the room contained none who had been doused with water, but the monkeys who had been beat up for
using the stepladder were still refusing to allow new monkeys to get the banana.
That is the light in which you may often view sacred cows in software development. In particular,
Neal points out that
- StickingToCamelCaseForSentenceLongTestNamesIsRidiculous so_it_is_ok_to_use_underscores
in languages_that_standardized_on_camel_case for your test names.
-
Using getters and setters does not equate to encapsulation. One of my favorite sayings,
and a reference to why
they are evil is appropriate.
-
Avoidance of multiple returns in a method is antiquated because it comes from a time
when we weren't using tiny, cohesive methods. I still like to avoid them, but will
use them when it makes the code more readable.
-
Polluting interface names with "I", as in ISomeInterface should be avoided.
It is contrary to what interfaces are about. Instead, you should decorate the concrete
class name.
-
Your objects should be Good Citizens. Never let them exist in an invalid state.
-
Use Test Driven Development
TDD provides "explicit dependency management" because you must think about dependencies
as you code. It provides instant feedback and encourages you to implement the simplest
thing that works.
You can take it further by analyzing your dependencies with JDepend.
And don't forget about YAGNI. Quoting
Neal,
Speculative development saves time if
• You have nothing better to work on right now
• You guarantee that it won't take longer to fix later
Is that ever going to be true?
-
Use reflection when you can (and when it makes sense)
It's not slow, and it can be extremely useful!
-
Colour Your World
Nick Drew, a ThoughtWorker in the UK came up with a system of coloring the code you write
by classifying it as to who will use the feature: only a specific business customer,
only a particular market, many markets, or unchangeable infrastructure. Based on
the color and amount of it, you can decide what value or cost your code has.
It's a very interesting system, so I recommend seeing a more detailed overview in
Neal's handout (PDF).
You can find it on pages 21-22.
-
Use a DSL style of coding
It lets you "[Build] better abstraction layers, using language instead of trees" while
"utilizing" current building blocks. As he pointed out, "every complicated human endeavor
has its own DSL. [If you can] make the code closer to the DSL of the business [then] it is
better abstracted."
I'll add that it's easier to spot logical errors as well.
A funny quote from Neal: "It's almost as if in Java we're talking to a severely retarded person."
Regarding that, he recommended looking at EasyMock as a good
example of fluent interfaces, and that having setters return void in a waste.
Instead, we should return this so that calls can be chained together (and if
you are using fluent-interface type names, you could construct sentences in your DSL that way).
Neal also noted a distinction between an API and DSL: API has an explicit context that must
be repeated with each call. However, a DSL uses an implicit context.
-
SLAP tells us to keep
all lines of code in a method at the same level of abstraction.
Steve McConnell's Code Complete 2
tells us about this as well, but I don't recall if it had the clever acronym.
-
And finally, Think about Antiobjects.
Quoting Neal, "Antiobjects are the inverse of what we perceive to be the computational objects."
So instead of solving the really hard "foreground" problem, have a look at the "background"
problem (the inverse) to see if it is easier to solve.
As an example, he used PacMan:
Rather than constructing a solution for the "shortest route around the maze," the game has
a "notion of a PacMan 'scent'" for each tile." That way, the ghosts follow the strongest scent.
As with my write-ups on
Scott Davis's Groovy: Greasing the Wheels of Java,
and Stuart Halloway's JavaScript for Ajax Programmers,
Neal gets all the credit for the good stuff here. I'm just reporting from my notes, his mindmap,
and my memory, so any mistakes you find are probably mine. If they aren't, they probably should
have been.
So which strategies have you used? Will you start using any of the above?
Posted by Sam on Apr 22, 2007 at 11:47 AM UTC - 6 hrs
YAGNI, KISS, and the simplest thing that could possibly work reared their heads again (at least for me) at the April meeting of the UH Code Dojo. Luckily for me, a couple of our members bailed us out of what could have been a disaster - greatly complicating a simple problem. (And my apologies for waiting so long to write this up - with finals and final projects all due these couple of weeks, I've just been super busy, as all you other students know all too well).
We decided on using a recent Ruby Quiz as our problem: Microwave Numbers. The basic idea is: given a number in seconds, identify the most efficient way to push the buttons (based on distance) to get that number of seconds (the actual problem left some room for fuzziness, but we didn't bother with that). This boils down to calculating the distance between any two numbers on the pad, and determining if the number of seconds is greater than 60 (in which case, there will be 2 options to check). Well, at least thats what we came up with.
More...
You can tell immediately I'm suffering from a bad case of not solving little problems that require some thought like this very often. My thoughts are, "ok, lets build a graph of the pad and write a traversal algorithm that we'll run to see how far apart any two numbers are." Luckily, Gino had thought through the problem and had an easier solution: represent the buttons as points in a plane, and calculate the distance as a function given 2 points. Well, that was easy, wasn't it?
The next problem was figuring out how to map the point to which button it represented. Here again, I was thinking of much more complicated things than I needed to be thinking about. Matt came up with the simple solution here: just store the buttons as a string and use the division and modulus to figure the row and column based on the index in the string. Problem solved. After about 15 minutes of thinking through it, we started programming. My laptop is dead at the moment, so we started using why the lucky stiff's online Ruby interpreter on the computer that was set up in the room (which, didn't have a ruby interpreter as far as I could tell).
As you can imagine, that was a nightmare. But, Gino came to the rescue again and let us use his laptop. After the meeting, he cleaned up the code and sent it to us, and Matt did some of the programming, so its in camelCase style (and I didn't want to spend the time to rubify it), but it works just fine. Next time, I want to write some unit tests first - as it would have been helpful for verification purposes here, rather than trying to run separate tests by hand each time we made a change. Anyway, with no further ado, here's the code we came up with for you to enjoy:
class Microwave
def initialize
@buttons = "123456789#0*"
end
def getShortestEntry(seconds)
minDistance = 10000;
minPress = "";
opt = 1;
puts "Input:#{seconds}\n---------------"
(0..seconds/60).each do|minute|
remSeconds = seconds - 60 * minute
if remSeconds < 100
buttonPress = timeEntry(minute, remSeconds)
totalDistance = 0
(0..buttonPress.length() - 2).each do |char|
totalDistance += manhattanDistance(buttonPress[char, 1], buttonPress[char + 1, 1])
end
puts "opt#{opt}: #{buttonPress} d=#{totalDistance}"
opt = opt + 1
if totalDistance < minDistance
minDistance = totalDistance
minPress = buttonPress
end
end
end
return minPress
end
def manhattanDistance(from, to)
fromIndex = @buttons.index(from.to_s)
toIndex = @buttons.index(to.to_s)
rowFrom = fromIndex/3
colFrom = fromIndex%3
rowTo = toIndex / 3
colTo = toIndex % 3
return ((rowFrom - rowTo).abs + (colFrom - colTo).abs)
end
def timeEntry(minute, seconds)
str = (minute == 0 ? "" : minute.to_s)
str = str + (seconds < 10 ? "0" : "") + seconds.to_s
return (str + "*")
end
end
mwave = Microwave.new
[33, 61, 99, 101, 71, 120, 123, 222, 737].each do |inSeconds|
puts "The shortest entry for #{inSeconds} seconds: #{mwave.getShortestEntry(inSeconds)}\n "
end
Posted by Sam on Apr 12, 2007 at 07:55 PM UTC - 6 hrs
This post at Worse Than Failure made its rounds today on the Yahoo pragprog group and CFCDev mailing list, and I had a response to one of the emails that I also thought was worth blogging. On pragprog, the discussion was turning into the question, "where do you draw the line on knowing when to eliminate literals?" (not a direct quote) Someone brought up the idea to use YAGNI and DRY as guiding factors: "If you need the number in just one place, just use the number there. YAGNI," and "If you find the number being repeated so that you would have to change
it in several places, then externalise it so that I change in just one place is needed. DRY."
More...
Initially, that struck me as an idea worth exploring, since those are two of my most-used, favorite, and life-saving principles (I mean, try violating them over and over for years and see if you don't feel like dying!). In fact, I use them a lot in determining when to create a new method (I use other heuristics as well, such as readability, which is often the most important reason I create a function ... more here about some reasons for creating methods). But, I thought about it some and came to this conclusion I posted to the pragprog group (which I changed a bit for here to make more sense):
In essence, I'm not comfortable with either of those definitions (DRY or YAGNI) as lines for determining when you are soft or hard coding. Suppose the
following (generalizing to your pleasure -- since I'm having trouble writing it, I'm sure others will have trouble understanding it, as no one would ever (I think) write an application this way)
- it always costs the same to ship each item: 5.00 (units of currency)
- the company only needs to worry about one tax rate: 5%
- the product costs 100 dollars each, and there is only one product.
- customers can buy a variable quantity of this item (called "quantity" below)
- there is a handling charge of 3.14 (units of currency) for each item.
Then your total_cost = (100+5+3.14)*(1.0+5/100)*quantity .
We see 100 twice here. Do we pull it out? What do we name it? oneHundred ? That is the only one that makes sense for both cases. Or we
could change it to to get rid of calculating the percentage: total_cost = (100+5+3.14)*(1.05)*quantity .
I'm not even confident I've got the right formula - and I'm the one
making this up. What happens down the line if the state changes the sales
tax? Am I sure I know which number to manipulate? Here, it may look
clear, but it may not be the case in other situations. And then when I
need to do a circleOperation with crappyApproximationOfPi = 3.14 (the
product is cylindrical, and we need to know about how much ribbon we
need to wrap the package when someone selects the gift wrapping option), who gets
the naming rights?
If there is a line that should be drawn (and I'm not convinced there is),
and there exists a place to put it, I don't think the DRY or YAGNI principles have
anything to do with its placement. Readability is the key for me, and
I don't know to what degree that can be quantified.
One in particular I dislike is when people have uncommented literals
like 0x0032aBlahBlahBlah in the code. How (and why) on Earth would you
expect anyone to know what that referred to? Will you remember the next
time you look at it? I wouldn't, but my brain may just be a sieve.
Your thoughts on where to draw the line? (Or not draw it?... Or tell me I'm wrong?)
Posted by Sam on Mar 30, 2007 at 03:12 PM UTC - 6 hrs
One of the great benefits of using a framework (in the general sense) is the freedom in portability it often gives you. Rather than writing your own Ajax routines which would need to handle browser incompatibilities, you can rely on Prototype/ Scriptaculous, AjaxCFC, Spry, or a seemingly infinite number of Ajax frameworks available. We see the same phenomenon in our use of ColdFusion, which uses Java to be cross-platform. And, you can get the benefit for databases by using a framework like Transfer or Reactor or ActiveRecord in Ruby (all also have a slew of other benefits). In general, we're simply seeing an abstracting away of the differences between low-level things so we can think at a much higher level than "oh crap, what if they're using Lynx!" (Ok, I'm doubting any of those Ajax frameworks fully support Lynx =), but you get the picture)
More...
I can honestly say I've never had to build an application that needed to be portable between databases, aside from between different versions by the same vendor (although there was once where it might have been nice, since I used the apparently impossible to find in hosting combination of Microsoft SQL Server and Java). My guess is that unless you're building software which is meant to be deployed on different databases (such as a framework, library, or software you plan to sell over and over again which you won't be hosting), you probably haven't needed to do so either... Or at least, not often enough to let it impact your coding.
So today, I started my quest to turn cfrails into a DB-independent framework (as of today, March 30, 2007 it works only with Microsoft SQL Server). Since one of my goals is to limit the amount of configuration the programmer has to provide to get up and running, simply telling cfrails your database metadata (instead of having it look in the database itself) was out of the question (XML or otherwise). I've looked at using CF's getMetaData(query_result) function in the past, but that doesn't have near the amount of data that would be useful for me.
I figured my best bet would be to drop down into Java and use its DatabaseMetaData interface. This way, I could let Java take care of the differences between DB implementations for me. Sure enough, looking over the docs, it had everything I needed. I went ahead and whipped up some code to use it and test it before trying to put it into Coldfusion. If you're interested, here's most of what I needed (only using the jdbc/odbc bridge). If not, feel free to skip this code:
// at the top of the file, you'll need to import java.sql.*
// and of course, define your class as you normally would.
Connection connection=null;
String driverPrefixURL = "jdbc:odbc:";
String dataSource = "ODBC Datasource Name";
String username = "user";
String password = "password";
String catalog = "which database";
String schema = "dbo";
String tableYouWantColumnsFor = "sometable";
try
{
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
connection = DriverManager.getConnection(driverPrefixURL+dataSource, username, password);
DatabaseMetaData dbmeta = connection.getMetaData();
ResultSet cols = dbmeta.getColumns(catalog,schema,tableYouWantColumnsFor,"%");
while (cols.next())
{
// this is all going to be unformatted as-is
System.out.print(cols.getString("COLUMN_NAME") + " ");
System.out.print(cols.getString("DATA_TYPE") + " ");
System.out.print(cols.getString("TYPE_NAME") + " ");
System.out.print(cols.getString("COLUMN_SIZE") + " ");
System.out.print(cols.getString("IS_NULLABLE") + " ");
// no clue why these two lines are throwing errors
//System.out.print(cols.getString("COLUMN_DEF") + " ");
//System.out.print(cols.getString("ORDINAL_POSITION") + " ");
System.out.println(); // skip to the next line
}
connection.close();
}
catch (Exception e)
{
e.printStackTrace();
// probably should be in finally block, but didn't worry about it as this was just a test
try
{
connection.close();
}
catch (Exception ex)
{
System.out.println("Could not close connection - probably wasn't open.");
}
}
(if you need more info, see the docs I linked above -- this is just a small sampling of the metadata available for columns in a table)
Sweet! I didn't really need ORDINAL_POSITION since it already ordered the results by it, and COLUMN_DEF (the column's default value) wasn't all that important (well, I could figure out a way to get that later, anyway). But, when I went to put it into ColdFusion, I realized - I have no clue how I might get the information I need to connect- forget about the fact that I'm not sure the average developer (CF or otherwise) will know what a catalog or schema is, or which one they need to access if they do. Who in the world isn't going to have to go through some work to figure out what driver they need and what the connection URL is?
So, I needed a way to let Java know these things through CF. I thought I could get at least some of that information from the CFAdminAPI (or whatever the proper case and spelling is), but I didn't really like that option. I couldn't think of any other option that would allow me to use Java to get the metadata, so I decided I'd have a look at the codebases for Transfer ORM, Reactor, and DataMgr and see how those smart guys did it. I only browsed, but I didn't find anything useful for me trying to use Java to do the trick. So, it was back to writing a DB-specific implementation for each database I wanted to support.
Luckily for me, even though my design doesn't approach the modularity and complexity of Reactor or Transfer (yet... I'm a big fan of YAGNI and haven't had any reason to break out of the simple design I currently have and Transfer is downright confusing for me to quickly browse it and figure out what's going on), it is quite flexible enough for me to do the different implementations and integrate them with ease. To do so, I only need to refactor the current routine that gets the MSSQL Server metadata, and provide a way to figure out which database type it is looking in (each of these only has to happen once). After that, all that remains is to write a query to get the metadata and convert its output (for each implementation) to the interface the rest of my code expects. Not hard by any means, but it would have been cool for maintenance purposes and greater overall simplicity if I could have let Java handle it.
So now that you've gotten this far in this boring post with no conclusion, have you used Java to get the DB metadata in CF (or even outside of CF)? Have you found a way to do it without making the programmer specify the connection URL and driver?
Posted by Sam on Feb 01, 2007 at 03:02 PM UTC - 6 hrs
On Monday (Jan. 29, 2007) we had our first meeting of the UH Code Dojo,
and it was a lot of fun. Before we started, I was scared that I'd be standing
there programming all by myself, with no input from the other members. But,
my fear was immediately laid to rest. Not only did everyone participate a lot,
one of the members, Matt (I didn't catch his last name), even took over the
typing duties after about 45 minutes. That turned out to be a good thing -
since I still can't type worth a crap on my laptop, and he was much faster than me.
Now, it had only been a couple of months since I last used Java - but it still amazes me
how little time off you need to forget simple things, like "import" for "require." I found
myself having several silly syntax errors for things as small as forgetting the
semicolon at the end of the lines.
Overall, we started with 7 people, and had 5 for most of the meeting. We finished with
four because one person had tons of homework to do. It seemed like those five of us
who stayed were genuinely enjoying ourselves.
In any case, we decided to stick with the original plan of developing a tic-tac-toe game.
You would think that a bunch of computer scientists could develop the whole
game in the two hours we were there. But, you'd have been wrong.
I came away from the meeting learning two main points, which I think illustrate the main
reasons we didn't complete the game:
- YAGNI is your friend
- Writing your tests first, and really checking them is very worthwhile
More...
Both points are things most of us already know, but things you sometimes lose sight
of when working like that all the time, or without paying attention. But, I was
paying attention on Monday, so let me explain how I came to identify those points.
We started by trying to identify a class we could test. This was harder than it looked. We
initially decided on Game . There was a lot of discussion on making it an
abstract class, or an interface, or should we just make it support only tic-tac-toe? After
everyone was convinced that we should first make it concrete, because we would not be
able to test it otherwise, we started trying to figure out what we could test.
So, we wrote a test_hookup() method to check that JUnit was in
place and working.
In the end, we also decided that we would probably want to try to
make it abstract after we tested the functionality, so we could support multiple games.
I think that decision proved to be a bad one - because none of us could figure
out a single peice of functionality that a Game should have.
For me, I think this was due mostly to the fact that I was trying to think of
general functionality, so I only came up
with run() , basically the equivalent of play() .
After thinking for a couple of minutes about things we could test, we decided we should
go with something more tangible for us - the Board class. Once we did that,
we never had a problem with what to test next. But, we still ran into problems with
YAGNI. We were arguing about what type of object we should use, so I just said "let's use
Object , then anything will fit and we can be more general when we get
back to Game ." This led to trouble later, when we had to have checks for
null all the time. So, we changed it to a string whose value could be "x", "o", or "".
That made life, and the code, a lot simpler.
Those are the two main places where ignoring the YAGNI prinicple hurt us. There are also
a couple of places where not writing our tests first hurt us.
The most obvious one is that we were just looking for a green bar. Because of that, we
actually were running the wrong tests, and some of ours hadn't worked in the first
place. We were running the test from the Game class, not the Board .
It wasn't too hard to fix the ones that were broken though.
Other than that, we spent
a lot of time trying to figure out if an algorithm to determine a winner would work (in
our heads). If we had just written the test first, we could have run it and seen
for sure if the algorithm was going to work correctly.
Overall, the only other thing I would have changed about how we developed would have been to
decide on several tests in advance, so we would have plenty to choose from,
rather than thinking of a test, then immediately implementing it.
For reference, I've posted our code below.
// Board.java
public class Board {
private String[][] _board;
private boolean current_player;
public Board(int row, int col)
{
_board = new String[row][col];
current_player = true;
for (int i = 0 ; i < row ; i++)
{
for (int j = 0 ; j < col ; j++)
{
_board[i][j] = "";
}
}
}
public boolean isEmpty()
{
for (int i=0; i<_board.length; i++)
{
for(int j=0; j<_board[i].length; j++)
{
if (!_board[i][j].equals("")) return false;
}
}
return true;
}
public void setMarkerAtPosition(int row, int col)
{
if (current_player)
_board[row][col] = "x";
else
_board[row][col] = "o";
current_player = !current_player;
}
protected Object getMarkerAtPosition(int row, int col)
{
return _board[row][col];
}
protected boolean isGameOver()
{
return false;
}
public boolean isRowWinner(int row)
{
for (int j = 0 ; j < _board[0].length - 1 ; j++)
{
if (!_board[row][j].equals(_board[row][j + 1]))
{
return false;
}
}
return true;
}
public boolean isColWinner(int col)
{
for (int i = 0 ; i < _board.length -1 ; i++)
{
if (!_board[i][col].equals(_board[i][col]))
{
return false;
}
}
return true;
}
public boolean isDiagWinner()
{
boolean diag_winner1 = true, diag_winner2 = true;
for (int i = 0 ; i < _board.length -1 ; i++)
{
if (!_board[i][i].equals(_board[i+1][i+1]))
{
diag_winner1=false;
}
if (!_board[i][_board[0].length-1-i].equals(
_board[i+1][_board[0].length-1-(i+1)]))
{
diag_winner2=false;
}
}
return diag_winner1||diag_winner2;
}
}
// TestBoard.java
import junit.framework.TestCase;
public class TestBoard extends TestCase {
private Board _b;
public void setUp()
{
_b= new Board(3,3);
}
public void testBoard()
{
assertTrue(_b != null);
assertTrue(_b.isEmpty());
}
public void testSetMarkerAtPosition()
{
_b.setMarkerAtPosition(0, 0);
//assertEquals(_b.getMarkerAtPosition(0,0),"x");
}
public void testIsGameOver()
{
assertFalse(_b.isGameOver());
for (int i = 0 ; i < 3 ; i++)
{
for (int j = 0 ; j < 3 ; j++)
{
_b.setMarkerAtPosition(i,j);
}
}
//assertTrue(_b.isGameOver());
}
public void testIsRowWinner()
{
_b.setMarkerAtPosition(0,0);
_b.setMarkerAtPosition(1,0);
_b.setMarkerAtPosition(0,1);
_b.setMarkerAtPosition(1,1);
_b.setMarkerAtPosition(0,2);
assertTrue(_b.isRowWinner(0));
}
public void testIsColWinner()
{
_b.setMarkerAtPosition(0,0);
_b.setMarkerAtPosition(0,1);
_b.setMarkerAtPosition(1,0);
_b.setMarkerAtPosition(1,1);
_b.setMarkerAtPosition(2,0);
assertTrue(_b.isColWinner(0));
}
public void testIsDiagWinner1()
{
_b.setMarkerAtPosition(0,0);
_b.setMarkerAtPosition(0,1);
_b.setMarkerAtPosition(1,1);
_b.setMarkerAtPosition(0,2);
_b.setMarkerAtPosition(2,2);
assertTrue(_b.isDiagWinner());
}
public void testIsDiagWinner2()
{
_b.setMarkerAtPosition(0,2);
_b.setMarkerAtPosition(0,1);
_b.setMarkerAtPosition(1,1);
_b.setMarkerAtPosition(0,0);
_b.setMarkerAtPosition(2,0);
assertTrue(_b.isDiagWinner());
}
}
As you can see, the board object really didn't do what its name implies. That should have
been game, I think. Also, the three check for winner functions are only temporarily
public - we planned on combining them to check for a winner in just one public function.
Other than that, we also need some negative tests, and obviously to complete the game.
Finally, thanks to everyone who showed up for making
it an enjoyable experience! I can't wait for the next one!
Last modified on Feb 12, 2007 at 10:12 AM UTC - 6 hrs
|
Me
|