January 05, 2008

DRYer than thou

At work we have a project (launching Monday, w00t!) that has a lot of cool things happening on the browser side. You know kids these days, what with their jQueries and Web 2.0s, they need whiz bang special effects in their websites. In my day, we only had one type of input field in our forms, and we liked it! Don't know how lucky they are… but I digress.

One problem we ran into was that we needed to do the same calculation on the server side and the client side. Let's say for the sake of example that we need to calculate Michigan's 6% sales tax or California's 7.25% sales tax. The real calculation was only slightly more complex. One thing to note is that the calculation would need to be very responsive because it would be running a lot and affecting user input, so speed is a concern.

I came up with two options. The first is to write the same calculation in PHP for the server side and JavaScript for the client side. I didn't love this because it isn't DRY. If we had two calculations in two different files then when someone was updating the code they would likely miss the other one.

The other option I came up with was to put the calculation on the server side only and use AJAX to run it on the client side. This seemed overly complex, which will sound funny when you see what we decided on. We had avoided AJAX on the site because it wasn't really needed; adding it for this seemed like a bad value/complexity trade off. I was also worried that server lag could cause a bad user experience, as I pointed out above the calculation needed to be fast.

So I was talking it over with my coworker Matt and he made a joke and we both laughed it off. Then we thought about it, and realized there there were fewer downsides to his approach than my two, so I started coding.

Matt's solution was a Polyglot, code that runs in multiple programming languages. It sounded plausible because the calculation is so simple and both JavaScript and PHP use similar syntax for things like conditionals and arithmetic.

Here's an approximation of what I came up with (I'm not sharing the original code, just coding the same solution from scratch):

<?php
define
(SALES_TAX_CALCULATION"
if ('MI' == \$state) {
    return (\$price * 1.06);
}
if ('CA' == \$state) {
    return (\$price * 1.075);
}
"
);

function 
calculateSalesTax($price$state) {
    return eval(
SALES_TAX_CALCULATION);
}

function 
javascriptSalesTaxFunction() {
    echo 
'<script type="text/javascript">function calculateSalesTax(price, state) { ' convertPhpVarsToJs(SALES_TAX_CALCULATION) . 
}</script>'
;
}

function 
convertPhpVarsToJs($code) {
    return 
str_replace('$'''$code);
}

The advantages to doing it this was are that the logic is kept in a single place, which means that the server and client code will not get out of sync. Another advantage is that we have a unit test harness for PHP but nothing (yet) for JavaScript, so this allows us to test the calculation (which did discover a bug in the real calculation).

The disadvantage is that this is clever code, and clever code is dangerous. I have a two paragraph comment above it starting with "Here be dragons" or some other warning about the code in question, and explaining to any maintenance programmer what is going on and why. Also, this solution relies on using the same language constructs in PHP and JavaScript which means it can't handle anything more complex, but it is unlikely that this calculation will become more complex (famous last words).

So what do you think? Was this a good solution to a tricky problem, or is this too clever by half? Is DRY and simplified maintenance worth it, or should I have gone with the duplicated code and left big comment blocks telling maintainers to change the logic in two places?

Posted by george at January 5, 2008 10:35 AM
Comments and TrackBacks

TrackBack URL: http://mt.gnerd.net/mt-gnerd-tb.cgi/2708

For some reason I thought this was either an uncov or a dailywtf entry.

Anyway. What I would have done is store the sales tax information in an associative array (ideally stored in a database, but even in a hard-coded include would be okay for starters), and to have the PHP emit the same array's contents in Javascript format for the client side. It's not tricky, and it'd be much more efficient for both.

If you later need more intelligent logic (e.g. due to sales tax which varies by type of good - which does exist in many states - or some sort of progressive sales tax which varies by purchase price) then you'd need to update the logic on both sides, but as long as the logic is just a simple multiplication I don't see the problem with just replicating the parameters instead of the logic.

Also, TECHNICALLY that code isn't a polyglot - it's php code which can generate code in both php and Javascript. On the Javascript side it's not running the same code, it's running code which was generated by a trivial "code generation" engine.

Posted by: fluffy at January 5, 2008 03:39 PM

Just to clarify, the original code is more complex than calculating sales tax, but it consists of conditionals and simple arithmetic, so I'm using sales tax calculation as a stand-in. The logic is not simple multiplication.

I agree that my example is a pretty awful way to calculate sales tax (only 2 states? repeating the multiplication in the interest of not repeating code? wtf indeed.) but it's only there because it was the first thing I came up with when thinking of conditional math.

Posted by: George Hotelling at January 5, 2008 04:23 PM

I was wondering when you were going to expand on that Tweet.

BTW, $ is a valid first character for a javascript variable (hence the popularity of $() and _() as library base function names) -- so why strip them out?

I've started rethinking about this lately since in many cases I still end up with an anti-DRY approach when I write forms that I want to validate both with JS and on the server side. Lately, I'm thinking using the server back-end and the jQuery metadata plugin to drop metadata necessary for the jQuery metadata plugin right into the DOM could get me most of the way there -- but it doesn't solve the duplication of logic problem. Code generation seems to do the trick here. Perhaps there's a niche for a metalanguage that could be used for calculations/validation callbacks that could be used by JS/PHP/Whatever?

Posted by: Scott T. at January 6, 2008 05:47 PM

Sorry, comments are closed.