All good programmers understand the concept of design patterns, creational patterns, structural patterns and behavioural patterns. We apply these patterns in different aspects of our projects. It's good to recognise common patterns so we can generalise routines into reusable functions or objects. I won't bore you with the details because you can learn more from a wealth of other online resources, but two key principles underly all design patterns:
- Think strategically about your application architecture
- Do not Repeat Yourself, aka, DRY. Organise your code so common routines can be reapplied.
Great, but in my humble experience we should add probably the most common design pattern of them all, though strictly speaking it's an anti-pattern: Adaptive Copy & Paste. The core idea here is if it works for somebody else you can just copy, paste and post-edit their code. Sometimes you can begin with some really good snippets of well-structured and commented code, but all too often online code samples are just formulaic and adapted from textbook boilerplate code. I've seen blocks of code pasted into Javascript files with references to StackOverflow.com complete with source URLs and deployed on high-traffic live sites. Let me show you a simple example:
var GBPExchangeRates = { USD: 1.52, EUR: 1.38, CDN: 1.57, SKR: 12.89, AUD: 1.45, CHF: 1.76 }; function convertGBPToEuro(GBPVal) { if (typeof GBPVal == 'string') { GBPVal = GBPVal.repplace(/[^0-9.]/g,''); if (GBPVal.length>0) { GBPVal = parseFloat(GBPVal); } } if (typeof GBPVal == 'number') { return GBPVal * GBPExchangeRates.EUR } return 0; } function convertGBPToUSD(GBPVal) { if (typeof GBPVal == 'string') { GBPVal = GBPVal.repplace(/[^0-9.]/g,''); if (GBPVal.length>0) { GBPVal = parseFloat(GBPVal); } } if (typeof GBPVal == 'number') { return GBPVal * GBPExchangeRates.USD } return 0; } var coffeePriceGBP = 1.90; var teaPriceGBP = 1.10; var orangeJuicePriceGBP = 1.50; var coffeePriceEUR = convertGBPToEuro(coffeePriceGBP); var teaPriceEUR = convertGBPToEuro(teaPriceGBP); var orangeJuicePriceEUR = convertGBPToEuro(orangeJuicePriceGBP); var coffeePriceUSD = convertGBPToUSD(coffeePriceGBP); var teaPriceUSD = convertGBPToUSD(teaPriceGBP); var orangeJuicePriceUSD = convertGBPToUSD(orangeJuicePriceGBP);
For a beginner, this is honestly not that bad at all. First we set up a simple object of common currencies with their exchange rates. In the real world this may come from some sort of feed. Next we devise a neat function to convert our GBP prices to Euros. Just to make it failsafe, we make sure we can handle strings with a mixture of numerals and currency symbols, which may include commas or other symbols than decimal points. If we only ever had to convert between British pounds and Euros, that would be just fine, though we may convert all prices via some sort of loop rather than make separate calls for each price. Here for just three prices and three currencies, we need to set nine explicit price variants and six explicit function calls.
However, later an intrepid project manager decides we need to support other currencies and may need to convert other units too, such as measurements or clothes sizes, so a busy code monkey promptly copies, pastes and adapts the first method to USD. Not too bad we only have two functions, but they contain much shared logic. Indeed the only difference lies in the conversion rate. We should break down this logic into steps. First we test if the input is a number (Javascript has a generic Number type that covers both floats and integers). Next we strip any non-numeric characters and cast to a float if the result is not empty. Only then do we apply our conversion rate. The above code could be even worse. We could have opted to hard-code the conversion rate. This may work for constants, such inches to centimetres, but it doesn't work for variables like exchange rates. What we need a generic method to convert number-like strings to true floats and another generic method to apply conversion rates from simple key/value objects.
Javascript makes it very easy for us to apply the decorator pattern by extending an object's prototype. This allows us to chain methods in a very self-descriptive way.
String.prototype.numeralsOnly = function() { return this.replace(/[^0-9.]/g,''); } String.prototype.toFloat = function() { var self = this.numeralsOnly(); if (self.length < 1) { self = 0; } return parseFloat(self); } Number.prototype.toFloat = function() { return parseFloat(this); } Object.prototype.matchFloat = function(key) { var obj = this, val; if (obj instanceof Object) { if (obj.hasOwnProperty(key)) { val = obj[key]; if (val) { return val.toFloat(); } } } return 0; } Number.prototype.convert = function(fromUnit,toUnit,units) { if (units instanceof Object) { return this * ( units.matchFloat(toUnit) / units.matchFloat(fromUnit) ); } }
We then apply a simple conversion table:
var rates = { GBP: 1, USD: 1.53, EUR: 1.37, YEN: 132.2, RUB: 12.7 };
Then if we were to allow users to convert to the currency of their choice, we could simply add prices in the base currency (in this case GBP) via some hidden element and then apply the conversion factor via the Document Object Model (or DOM):
$('table thead .currencies .option').on('click',function(e){ var it = $(this), tb = it.parent().parent().parent().parent(), selEl = it.parent().find('.selected'); if (selEl.length < 1) { selEl = it.parent('em').first(); } var selCurr = selEl.text().trim().toUpperCase(), tgCurr = it.text().trim().toUpperCase(); tb.find('.price').each(function(i){ var td = $(this), nVl = td.attr('data-gbp').toFloat().convert('GBP',tgCurr,rates); td.html(nVl.toFixed(2)); }); });
This may look like more code, but we now have a solution that works with any currencies and any number of data items to be converted. Moreover, our convert method may be applied to any units. If we wanted to present volumes in either millilitres or fluid ounces we would just include our decorator methods as a library, set up a conversion table and write a short DOM script. 90% of the code would have been tested for other use cases:
var volumeUnits = { ml: 1, l: 1000, floz: 29.5625 }
Good programmers always think out of the box, not just how to solve the current problem as presented by a project manager, but how do I solve other problems like this? More important, we should ask how to make our code more maintainable and easier to test.
Common Mistakes
- Placing editorial content in code files that only developers know how to edit: e.g. A senior manager has decided to edit some text on your company's online shop. The only reason she needs to involve you in this editorial change is because your predecessor placed the text in a template or even worse embedded it verbatim on line 1451 of a fat controller file. What should you do? To make your life easy you could just edit the offending line and write a note for future developers that this text is hard-coded in such and such a file. Management will then think that whenever they wish to edit content they need to ask your project manager to ask you to apply some cryptic code change. However, later they will review their IT budget and decide you are too expensive and then outsource the whole project to a low-wage country or replace it with a state-of-the-art content management system that let's them edit any content without any programming knowledge. What you should do is suggest all such content should be editable in a special admin area and all hard-coded text, media or numbers should be replaced with references to editable content.
- Quoting one programming language in another: This is surprisingly common. The main reason for doing so is to inject server-side variables into client-side scripts, e.g. using PHP to build a Javascript routine with a few variables generated dynamically by the server. Not only does this make your Javascript very hard to debug, but it inevitably leads to more repetitive and thus slower Javascript. If you want to fetch data from the back-end, you should inject it as hidden attributes that Javascript can read or simply inject some JSON easily converted from native server-side objects or make an asynchronously request with a JSON response. Keep your javascript lean and mean and ideally in separate files, so your browser can cache these resources more efficiently. If you're using backbone.js or jQuery or other framework, these can be loaded from a content delivery network or CDN.
- Repeating routines: Whenever you find yourself repeating a routine more than once, you need a new function or at they very least a loop:
var d = new Date(item.created); item.created_date = d.getDate() + '/' + (d.getMonth()+1) + '/' + d.getFullYear(); var d = new Date(item.modified); item.modified_date = d.getDate() + '/' + (d.getMonth()+1) + '/' + d.getFullYear();
This is messy. What we need is a generic date conversion function:
var isoDateToEuroDate = function(strDate) { var d = new Date(strDate); return d.getDate() . zeropad(2) + '/' + (d.getMonth()+1) . zeropad(2) + '/' + d.getFullYear(); }
And if we're doing a lot of date manipulation,we might like to include a date library to make our code simpler. Your bosses may not notice that you are just writing the same code over and over again, but if your code becomes very expensive to maintain, they will either ditch it or outsource your work to some hapless code monkeys on a fraction of your wage.
One reply on “The Copy and Paste Design Pattern”
Saved as a favorite!, I really like your web site!