Freedom Zero: The All-Or-Nothing Fallacy

Jeff Atwood has an article up today bemoaning the fact that seemingly nobody “gives a crap about freedom zero”. Well, my initial reaction was that surely nobody could care about something with such a thoroughly ridiculous name. Freedom Zero? Really? I know this is the FSF‘s first freedom, and programmers count from 0 don’tcha know, but it’s still rubbish.

But the real reason is that it simply isn’t important enough to override everything else.

Certainly, for some things you want the freedom and reliability of open source. I write my essays in OpenOffice, I use Vim as my text editor for all programming languages other than C#, and I write maths papers with LaTeX. I want my personal output to remain usable and not at the whim of some company somewhere, I agree with all that.

But do I need my MP3 player to be open? No. My videogame console? No. My phone? No. My movie editor? No (though only because I always archive the source material). The irony is that people do indeed care about freedom – the freedom to choose, and the sad fact is that there is a certain type of zealot who only espouses freedom as long as it’s their type of freedom. And that isn’t freedom at all.

Now, as it happens, Linux is my operating system of choice. I don’t own any Apple computers, though I do have a first-gen iPod Mini, which is distinctly showing its age. I use Vista for .Net development, but I don’t think anyone could reasonably call me an Apple zealot or an anti-freedom capitalist whatever.

But you won’t catch me criticising Apple for their closed platform. If it results in a decent product, I’m all for it. I used to have a G4 iBook and liked it a lot. When I’m in the market for an ultraportable later this year, I will give due consideration to the Mac Air.

A Mac is a product – calling the hardware nothing more than a dongle is a ridiculous argument. You can run Linux on Mac hardware, and OSX on non-Mac hardware (suboptimally, granted). Would you call a Ferrari Enzo a dongle because you need one in order to run the Enzo engine management software?

And it should be said that Apple isn’t quite as closed as some people suggest – I can still install Firefox, Thunderbird, and other open source tools if I want to tackle the hostile internet with a trusted armoury. And OSX comes with things like Apache and SSH installed out of the box.

So do I give a crap about freedom zero? Only in as far as it suits my needs. If a piece of closed software does a better job, and the risk of losing data forever is within my tolerances, then sure I’ll use it and I won’t let ideology get in my way.

On the flip side, I care about interoperability, and I contribute or donate to a few open source projects, and will strongly oppose anything – legal or technological – that attempts to muscle open source out of existence. An open source tool deserves the right to compete. I use Amarok not because it’s open, but because I like it more than iTunes. Conversely, I use Visual Studio not because it’s proprietary, but because I prefer it to SharpDevelop.

Use the best tool for the job.

  • Share/Bookmark

Technical Book Club: Object Oriented Analysis & Design, Chapter 1

So, as previously mentioned, we’ll start with the basics. This material is probably very familiar to most coders with even a small amount of experience, but it never hurts to refresh the fundamentals. You may even find that there’s some material that seems so obvious you don’t even actively think about it any more – which is good if it has become habit, but may be bad if you’ve grown complacent in certain areas.

The overarching theme of the first chapter is complexity. Complexity is the enemy of the software developer, and it is vital to understand this fact. If you don’t identify complexity as the enemy, you will find it harder to remain vigilant against it. 

Anyone who has worked as a developer for more than a year or two will almost certainly have exposure to the problems caused by complexity, but quite often complexity itself will not be pinpointed as the root cause. It may be a case of not seeing the wood for the trees – when trying to understand a system it’s very easy to get bogged down wondering why some particular section of code does things a certain way, and not see the problems with the big picture. Of course, since complexity obscures the big picture, this is common and self-perpetuating.

Object-oriented design, then, is a tool for managing complexity. Of course, it is many other things too, but this is one of the fundamentals. In particular, OOD is a natural way to represent hierarchies, and hierarchies are the primary tool Booch presents for making complexity manageable. A number of examples from nature are provided; for example, you can view a plant simply as a plant, or as a collection of structures (leaves, stem, roots).

Importantly, the overall hierarchical view of a plant can be broken down into many interacting sub-hierarchies, each of which may be considered in terms of its own structure and its interactions. This is an example of decomposition. If you want to study roots in detail, you can study the branch roots, the root apex, and the root cap – and break that down further if you like to consider roots as a collection of cells. To study roots in this sort of detail, however, you do not have to go to the same lengths with leaves and stems – it is enough to understand the interactions between the higher-level components.

Complexity, therefore, is more manageable if it is divided into interacting components, each of which can be further divided into interacting subcomponents. At different levels of abstraction there are clear boundaries – it shouldn’t be necessary to understand the epidermis of a leaf to examine a root, and likewise the study of a leaf should not need to consider the role of the root apex. This is known as separation of concerns, and allows you to ignore the parts of the system that you are not interested in at the time.

In software, these principles are captured by OOD. Broadly, hierarchies can be modelled with inheritance, components can be modelled with modules, intercomponent interactions can be modelled with interfaces and method calls, and intracomponent interactions can be modelled with aggregation. These interactions are key, as they form part of the ‘value’ of a system – in layman’s terms, the whole is greater than the sum of its parts.

Inheritance and aggregation are, respectively, ‘is-a’ and ‘part-of’ relationships. In both cases, these represent separate but overlapping hierarchies. Booch’s example is that of an aeroplane. An aeroplane can be thought of as an aggregation of systems – propulsion, flight control, etc. Each of those can potentially be modelled as specilaised types too – for instance a jet engine is a particular type of propulsion, and a turbofan engine is a particular type of jet engine.

In OO terms, the ‘is-a’ relationships are expressed as class structures utilising inheritance, and ‘part-of’ relationships are expressed as object structures utilising aggregation.

Relative primitives are an interesting concept, and refer as much as anything to the benefit of having a sensible and appropriate vocabulary at each level of abstraction. With plants, for instance, if you are working at the cellular level your primitives are nuclei, chloroplasts etc. If, however, you are at the top level your primitives should be leaves and stems – something is wrong if you are concerning yourself with the nucleus of a cell at this level of abstraction. Relative primitives are a natural consequence of hierarchies and decomposition, if your hierarchies are sane.

Even with all these tools for managing complexity at our disposal, it is still extraordinarily hard – if not nigh-on impossible – to construct a large complex system in one fell swoop. Booch identifies that a key characteristic of successful complex systems is that they evolve from simpler systems, whilst always being useful during that evolution – they have stable intermediate forms.

Contemporary development processes, such as agile development, explicitly recognise this with the mantra of ‘deliver early, deliver often’. The idea is that functionality should be delivered iteratively, with each iteration being functional and testable. This is in contrast to more traditional waterfall methodologies, which are notoriously associated with failed projects, often due to attempting to design a large complex system up-front and develop it all at once.

In summary, then, Booch argues that the characteristics of a manageable complex system are as follows:

  • Hierarchic
  • Uses relative primitives
  • Has robust separation of concerns
  • Has stable intermediate forms

It is important to note in passing that, since OOA&D is an unashamed champion of object-orientation as a mechanism for managing complexity, OO is by no means the only approach – functional and procedural languages have their own techniques, and in some cases may be more suitable, depending on the problem to be solved. Even so, many of these principles are common and are useful things to bear in mind when designing a system.

Next week we look at chapter 2, which covers the object model in greater detail.

  • Share/Bookmark

Fab Fib

Scott Hanselman continues his Weekly Source Code series with a look at algorithms for generating the Fibonacci sequence in a variety of different languages. He misses my favourite implementation, though fortunately a wise commenter has already sought to correct such an egregious error. As is so often the case, it is Haskell that provides the most elegant yet mind-bending alternative:

fibonacci n = fib!!n
    where fib = 0 : 1 : zipWith (+) fib (tail fib)

This little beauty appends a list with its own tail while it’s still being generated and lazily sums the elements. On top of that, it’s fast, since unlike most naive recursive Fibonacci generators it doesn’t waste time recalculating previous values. In fact, it runs in linear time, that is O(n), and on a fairly modest 2GHz Athlon XP will calculate the 50,000th Fibonacci number in around 600 milliseconds. By contrast, the naive recursive implementation in C# (see below, adapted and corrected from the code Scott posted) takes 28 seconds to calculate the 45th number, on a much more powerful Core Duo machine.

$ time ./Fibs.exe
1134903170
Execution time: 00:00:28.2930000      

real    0m28.382s
user    0m0.000s
sys     0m0.031s

As implemented, you can’t go much higher than this since the 47th number in the sequence (2971215073) is too big to store in a 32-bit signed int. Asking for the 50,000th number results in an immediate stack overflow, which is the runtime’s way of saying “don’t be ridiculous, mate”.

Of course, the C# version could be made many times faster and more efficient by implementing it iteratively (i.e. with a for loop), but this is less natural since the Fibonacci sequence is a recurrence relation and therefore best expressed recursively. The beauty of the Haskell version is that it combines expressiveness with performance, always a happy combination.

using System; 

namespace Fibs
{
    class Program
    {
        static void Main(string[] args)
        {
            DateTime start = DateTime.Now;
            Console.WriteLine(Fibonacci(45));
            Console.WriteLine("Execution time: {0}",
                DateTime.Now.Subtract(start));
        }

        static int Fibonacci(int n)
        {
            if (n == 0 || n == 1)
                return n;
            return Fibonacci(n - 1) + Fibonacci(n - 2);
        }
    }
}
  • Share/Bookmark

Turbocharging .Net Webservice Clients

Since the first version of .Net and its associated toolset, Microsoft have sought to make it easy to write SOAP services and SOAP clients. And, generally, they have succeeded quite well. Whilst the open-source world has tended to prefer the simpler REST approach, MS (and Sun, and Apache) have done an admirable job of taking the large, complex SOAP protocol and making it reasonably straightforward to work with most of the time.

One of the areas in which things get somewhat less straightforward is high performance. Granted, most web services don’t have particularly eye-popping requirements in terms of hits or transactions, but occasionally you find an exception. Betfair, for instance, have an API that has peak rates in excess of 13,000 requests per second, many of which hit the database, with individual users making tens or even hundreds of requests per second. Betfair’s data changes at a breathtaking rate and there is a perceived advantage to getting hold of up-to-the-millisecond information.

To put that in context, the Digg Effect is estimated to peak at around 6K-8K hits per hour as I write in December 2007, which translates to a piffling couple of hits per second (8000 / 60 / 60 = 2.2). Even a more extreme prediction of 50K hits per hour is only around 14 hits per second, so we’re talking about handling three orders of magnitude more requests than Digg generates.

If you are writing a .Net client and want to get the best out of this sort of situation, you are hamstrung unless you learn a few tricks. Read on to learn five of the best. I’ll be using the Betfair API as an example throughout, but the techniques apply to any high-performance web service where the usage profile involves frequent small (<1KB) SOAP requests.

For those who normally turn to the back of the book for answers and don’t really care about the whys and wherefores, here’s the executive summary:

  1. Switch off Expect 100 Continue. This should be done in your App.config file (see below).
  2. Switch off Nagle’s Algorithm. This should be done in your App.config file (see below).
  3. Use multithreading. Unfortunately this is not a simple configuration file setting, it is a fundamental part of your application design.
  4. Remove the maximum connection bottleneck. This should be done in your App.config file (see below). This is vital if your application is multithreaded.
  5. Use gzip compression. .Net 2 has this built-in if you switch it on; .Net 1.1 needs a helping hand.

The XML snippet that configures these settings is shown below, and should be added to your App.config file:

<system.net>
    <connectionManagement>
        <add address="*" maxconnection="20" />
    </connectionManagement>
    <settings>
        <servicePointManager
            expect100Continue="false"/>
        <servicePointManager
            useNagleAlgorithm="false"/>
    </settings>
</system.net>

For those who share my distrust of unexplained sorcery, here’s the gory details.

Expect-100 the Unexpected

RFC 2616 (the specification for HTTP 1.1) includes a request header and response code that together are known as ‘Expect 100 Continue’. When using the Expect header, the client will send the request headers and wait for the server to respond with a response code of 100 before sending the request body, i.e. splitting the request into two parts with a whole round-trip in-between.

Why would you ever want to do this? Well, imagine you are trying to upload a 101MB file to a web server with a 100MB file size limit. If you just submit the whole thing as one request, you’ll sit there for ages waiting for the upload to complete, only to have it fail right at the last minute when you hit the 100MB limit. Using Expect 100 Continue, you submit just the headers initially, and wait for the server’s permission to continue; in our example, this gives the server a chance to look at the Content-Length parameter and identify that your file is too big, and return an error code instead of permission to continue. This way you know the upload will fail, without needlessly transmitting a single byte of the large file.

By default, the .Net Framework uses the Expect 100 Continue approach. Most SOAP requests are not, however, anywhere near 101MB in size, and a server designed to deal with thousands of requests per second is not likely to be returning anything other than 100 Continue if the Expect header is sent. Depending on latency, the round-trip penalty may be unacceptable.

For example, Betfair’s Australian exchange (physically located in Australia) contains all their Australian markets, so if you’re in the UK and want to trade on the Australian Open tennis you’re subject to a round-trip time of about 350ms. If you allow your requests to be split into two, then you have two round-trips, so your request will take 700ms plus processing time.

You don’t want that, so switch it off. Note that the request headers and body are still sent separately, but the latter is no longer dependent on the response to the former (so they are shown as sent together in this diagram).

The setting corresponding to this in the XML snippet above is:

<servicePointManager
            expect100Continue="false"/>

Nagle’s Algorithm

Nagle’s algorithm is a low-level algorithm that tries to minimise the number of TCP packets on the network, by trying to fill a TCP packet before sending it. TCP packets have a 40-byte header, so if you try to send a single byte you incur a lot of overhead as you are sending 41 bytes to represent 1 byte of information. This 4000% overhead causes chaos on congested networks. A TCP packet size is configurable, but is often set to 1500 bytes, and so Nagle’s algorithm will buffer outgoing data in an attempt to send a small number of full packets rather than a huge amount of mostly empty packets.

Nagle’s algorithm can be paraphrased in English like this:

If I have less than a full packet’s worth of data, and I have not yet received an acknowledgement from the server for the last data I sent, I will buffer new outbound data. When I get a server acknowledgement or have enough data to fill a whole packet, I will send the data across the network.

Now, with our use case (frequent SOAP requests, each <1KB) a request doesn’t fill a packet, so requests are subject to buffering. Furthermore, as explained above, even with Expect 100 Continue switched off the request headers are still sent separately from the request body, so the body of the first request is buffered until the request headers reach their destination and the server sends back a TCP ACK.

Let us again consider a UK client communicating with Betfair’s Australian API. Your application issues two requests one for current prices and one for your current market position (Req1 and Req2 in the diagram below). Together, both these requests are less than 1500 bytes. The headers for the first request are transmitted, but Nagle’s algorithm buffers the request body, plus the whole second request, since they don’t fill a TCP packet.

Due to the round trip latency, the acknowledgment of the first request’s headers is not received for 350ms, so that is how long the requests are buffered for. When the requests do get sent, they too are subject to 350ms latency around the world, so again you end up with around 700ms added to each of your Australian API calls.

Without Nagle, we dispense with the buffering and send out our smaller packets immediately, saving a round-trip.

One word of warning – disabling Nagle can cause problems if you are on a highly-congested network or have overworked routers and switches, since it increases the number of network packets flying around. If you don’t own your network, or are deploying your web service client to a large number of users, you might want to think about this carefully as you won’t be popular if your software saturates the network. The setting corresponding to this in the XML snippet above is:

servicePointManager
            useNagleAlgorithm="false"/>

Happenin’ Threads

This one should be pretty obvious. A single-threaded application can only do one thing at a time. A multi-threaded application can do multiple things at a time. So if, for example, you want to display some information and need to collate the results from four web service requests to build your view, calling them one after another is going to be slower than calling them all simultaneously (not to mention freezing your UI if you’re writing a WinForms application and making calls on the UI thread).

Explaining how to design and write multi-threaded .Net applications is way out of scope for this blog post, but any time you spend reading and learning about it either online or in books is going to be time well-spent. Go on, do it now. Also read the next section, otherwise your application will not get as much benefit as you expect.

Walk and Chew Gum

.Net, by default, has a bottleneck on simultaneous web service calls against the same host. This one catches loads of people out – they write clever multi-threaded applications that issue many, many requests, and never realise that beneath the application layer a lot of their requests are called in sequence rather than in parallel.

Tsk, damn Microsoft for unnecessarily crippling their framework, right? Well no, actually, since this is an example of Microsoft following standards and recommendations and is to be applauded, lest they go back to their old ways of “embrace and extend”. The recommendation in question is from RFC 2616 again, and states:

A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy.

(RFC 2616, section 8.1.4)

.Net uses HTTP 1.1 persistent connections by default (this is a good thing – you don’t want to incur the cost of establishing a TCP connection with every request, especially if you have long round-trip times as well since it involves multiple round-trips), so MS have done the right thing and restricted processes to two simultaneous connections by default.

What does this mean for your application? It means that if you want to make 20 requests, and do so on 20 threads as recommended above, under the hood .Net only makes two at a time and queues the rest up. Therefore your 20 threads are wasted, as your throughput is no better than if you’d only created two threads, and innocent web servers are protected from overzealous clients.

When we know that we’re talking to an insane city-flattening Godzilla-on-steroids of a server, however, the two-connection limit is unreasonable and we can ramp-up safely. The corresponding setting in the XML snippet above is:

<connectionManagement>
        <add address="*" maxconnection="20" />

Sorted. You can, of course, change the number 20 to whatever is appropriate for your application.

Warning: Do not, under any circumstances, get into the habit of configuring this for all your web service applications. With all 20 threads issuing one request per second, you are exceeding the 14-request-per-second Digg Effect example I used above; with this technique and enough bandwidth your application will be quite capable of taking a lot of websites down, and those that manage to weather the storm will probably blacklist your IP address and/or close your account. Only use this if you are absolutely sure the server is up to it and such aggressive behaviour is permitted by the owners.

Small is Beautiful

The last step is to enable compression on your responses. Depending on the nature of the service you are using, the value of this tip may vary, but it’s likely to be of some benefit given the number of requests per second we are issuing. Of course, it is dependent on the web service actually supporting compression, but it’s been a standard feature of just about every web server for years, so this shouldn’t be a problem.

Lets look at a worked example. The getMarketPrices response from the Betfair API is about 30KB of XML. If, as above, we have 20 threads issuing one request per second, and each thread is interested in prices from a different market, that’s about 600KB of data per second, which will quite easily saturate a lot of home broadband connections.

With gzip compression, however, each response comes down to about 5KB (XML compresses very well, generally, since it is just text with a lot of repetition and whitespace), so the 20 threads now demand a more manageable 100KB per second.

Great. So how do we use it? In .Net 2.0 and above it’s very easy – just set the EnableDecompression property of your web service proxy object (ignore IntelliSense, which incorrectly claims the value is true by default; it’s actually false by default, as stated on MSDN). For example, to get compressed responses from Betfair’s global server:

BFGlobalService service = new BFGlobalService();
service.EnableDecompression = true;

If you’re still using .Net 1.1, you have a bit more work to do, since support for gzip was inexplicably left out of the framework. First, you need to subclass the generated BFGlobalService proxy class, and override some key methods so you can a) include the Accept-Encoding header to tell the server that you understand gzip, and b) decompress the gzipped response before the XML deserializer sees it, otherwise it’ll choke.

public class BFGlobalWithGzip : BFGlobalService
{
    /// <summary>
    /// Adds compression header to request header
    /// </summary>
    protected override System.Net.WebRequest
         GetWebRequest(Uri uri)
    {
         HttpWebRequest request = 
             (HttpWebRequest)base.GetWebRequest(uri);

         // Turn on compression
         request.Headers.Add("Accept-Encoding",
             "gzip, deflate");
         return request;
    }

    /// <summary>
    /// Decompress response before the Xml serializer gets
    /// its hands on it
    /// </summary>
    protected override WebResponse GetWebResponse(
        WebRequest request)
    {
        return new HttpWebResponseDecompressed(request);
    }

    /// <summary>
    /// Need to override this method if performing
    /// asynchronous calls, otherwise de-compression
    /// will not be performed and will throw an error
    /// </summary>
    protected override WebResponse GetWebResponse(
        WebRequest request, IAsyncResult result)
    {
        return new HttpWebResponseDecompressed(
            request, result);
    }
}

Next, implement the HttpWebResponseDecompressed class. This subclasses .Net’s WebResponse class and knows how to decompress a response if it has ContentEncoding ‘gzip’:

public class HttpWebResponseDecompressed : WebResponse
{
    private HttpWebResponse m_response;
    private MemoryStream m_decompressedStream;

    public HttpWebResponseDecompressed(WebRequest request)
    {
        m_response = (HttpWebResponse)request.GetResponse();
    }

    public HttpWebResponseDecompressed(WebRequest request,
        IAsyncResult result)
    {
        m_response = (HttpWebResponse)
            request.EndGetResponse(result);
    }

    public override long ContentLength
    {
        get
        {
            if (m_decompressedStream == null
                || m_decompressedStream.Length == 0)
                    return m_response.ContentLength;
            else return m_decompressedStream.Length;
        }
        set { m_response.ContentLength = value; }
    }

    public override string ContentType
    {
        get { return m_response.ContentType; }
        set { m_response.ContentType = value; }
    }

    public override Uri ResponseUri
    {
        get { return m_response.ResponseUri; }
    }

    public override WebHeaderCollection Headers
    {
        get { return m_response.Headers; }
    }

    public override System.IO.Stream GetResponseStream()
    {
        Stream compressedStream = null;

        if (m_response.ContentEncoding == "gzip")
            compressedStream = new GZipInputStream(
                m_response.GetResponseStream());
        else if (m_response.ContentEncoding == "deflate")
            compressedStream = new InflaterInputStream(
                m_response.GetResponseStream());

        if (compressedStream != null)
        {
            m_decompressedStream = new MemoryStream();
            int size = 4096;
            byte[] buffer = new byte[size];
            while (true)
            {
                size = compressedStream.Read(
                    buffer, 0, size);

                if (size > 0)
                    m_decompressedStream.Write(
                        buffer, 0, size);
                else
                    break;
            }

            m_decompressedStream.Seek(0, SeekOrigin.Begin);
            compressedStream.Close();
            return m_decompressedStream;
        }
        else return m_response.GetResponseStream();
    }
}

To decompress the data, we need a decompression library since .Net 1.1 doesn’t provide one. In most cases, SharpZipLib will do the business:

using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;

Now, when creating an instance of BFGlobalService you can use the gzip-supporting subclass and everything else happens automatically.

BFGlobalService service = new BFGlobalServiceWithGzip();

Fin

Jebus, that went on for a bit. Now, go forth and write high-performance clients at will – but heed the warnings about only doing this when you know the server is on-the-ball, because these tips really can cause havoc if used irresponsibly.

  • Share/Bookmark

Legacy Code, Refactoring, and Ownership

Refactoring is good. Everyone knows that. Since Fowler popularised the concept with the seminal Refactoring: Improving the Design of Existing Code it’s become a staple of the industry, and has pride of place on many a bookshelf. In the many, many articles and discussions of the subject, the key goals and benefits of refactoring are generally taken to be the improvement of readability, testability, decoupling, and other similar worthy ideals. For me, however, there is another very distinct benefit, often overlooked. Fowler touches upon it, but doesn’t really develop it, early on in Refactoring:

I use refactoring to help me understand unfamiliar code. When I look at unfamiliar code, I have to try to understand what it does. I look at a couple of lines and say to myself, oh yes, that’s what this bit of code is doing. With refactoring I don’t stop at the mental note. I actually change the code to better reflect my understanding, and then I test that understanding by rerunning the code to see if it still works.

(Fowler, Refactoring, 1999)

By investigating a piece of code thoroughly enough to understand how it works, refactoring it to map directly on to your understanding, and reinforcing everything with good unit tests, you take ownership of the code. It’s yours now.

This is very important, psychologically. Almost every developer feels more at home with their own code than somebody else’s. That’s why you feel uncomfortable and deflated when, 20 minutes into deciphering a nasty bit of opaque gibberish, you realise it was something you yourself wrote a year earlier and subsequently forgot about.

When you refactor, you rewrite code to a greater or lesser extent. Having done so, the resulting feeling of ownership (alongside increased understanding, of course) makes the code much less scary. The benefit of this is less marked in agile methodologies or TDD, of course, since in those cases quite often the code you are refactoring was written by you anyway. Working with legacy code, though, it’s a big deal.

In the preface to Working Effectively With Legacy Code, Feathers asks “what do you think about when you hear the term legacy code?” (Feathers, 2004). He answers by stating that the standard definition is “difficult-to-change code that we don’t understand” and adds his own preferred definition which is, in essence, “code without tests”.

My own definition of legacy code would include, in many cases, code that isn’t mine. By ‘mine’ I don’t exclusively mean code I wrote personally; I also mean code written by my team, or even code written by people who sit a couple of desks down who I can go and pester about it (which is stretching the definition a bit, admittedly).

In short, legacy code for me is code that no longer has any accessible owner. Like a stray cat or dog, code without an owner goes feral. Refactoring is the process of taming feral code, but as with stray cats much of the benefit comes from re-homing. This is a vital process, even if a fairly unconscious one. When you first come face to face with some hideous 5000-line spaghetti monster of a function your heart sinks – how can anyone ever hope to understand that, let alone modify it safely? Especially if the only people that ever worked with it left the company 3 years ago?

Refactoring allows you to split this code up, create classes to better represent the problem domain, improve abstraction, add tests, and all that other good stuff; at the same time, the process of doing so makes the code yours. You make the decisions about the classes to create and the abstractions to introduce. You write the tests that ferret out all the little idiosyncrasies, and uncover the unwritten assumptions. By the end of the process, the code feels like yours. And that means that the next time you have to make a change there, you benefit from the double whammy of code that is not only well-written and tested, but recognisably yours; and that’s the kind of code that you won’t mind working with.

  • Share/Bookmark