abstractions will make your code sexy again

Posted on Fri 22 December 2017 in code

Abstraction is everywhere these days. Functions are abstractions, types are abstractions, classes are abstractions, even programs and programming languages themselves are nothing but abstractions. Abstractions are really the whole foundation of computing not to mention most human achievements over the course of history, really. It is a great tool to be able to get more work done with less effort, and some things are just impossible without it.

It follows that abstraction in programming is a beautiful thing; it is, however, also a fickle mistress. Too many levels of logical indirection will make your code bigger, your program slower, and worst of all, the entire project far more complex. Too little abstraction, though, is just as harmful, and ends up inflating your source code, obfuscating intent, and making your final product far more prone to bugs.

So it pays to follow one simple guideline which will help you find a place comfortably in middle:

Only use abstractions that lower code complexity

Abstractions are a tool to reduce the number of things you have to keep track of. Use them, but never blindly. Note that code complexity is completely orthogonal to SLOC (Single Lines of Code). Even at the cost of extra verbosity, you should abstract away dense expressions.

An expression like

    return copy_to_user(buf, page_address(page) + offset, size) ? (offset << (page_bits -  1)) : 0;

may seem concise, but it will take most people an extra second or so to parse when compared to

    unsigned long addr = offset << (page_bits - 1)

    if (copy_to_user(buf, page_address(page) + offset, size))
        return addr;

    return 0;

However, abstraction just as often reduces verbosity in addition to simplifying your code. A shining example is the use of helper functions and macros. For instance

    if (!(packet->seckey.rsa.der_data = calloc(1, packet->seckey.rsa.der_len)))
        perror("no memory");
    memcpy(packet->seckey.rsa.der_data + der_offset, asn_seq, sizeof asn_seq);
    der_offset += sizeof asn_seq;
    memcpy(packet->seckey.rsa.der_data + der_offset, header.raw, sizeof header.raw);
    der_offset += sizeof header.raw;
    memcpy(packet->seckey.rsa.der_data + der_offset,
            packet->seckey.rsa.version, sizeof packet->seckey.rsa.version);
    der_offset += sizeof packet->seckey.rsa.version;
    memcpy(packet->seckey.rsa.der_data + der_offset, asn_int, sizeof asn_int);
    der_offset += sizeof asn_int;
    memcpy(packet->seckey.rsa.der_data + der_offset, packet->seckey.rsa.modulus_n->be_raw, 2);
    der_offset += 2;
    memcpy(packet->seckey.rsa.der_data + der_offset,
            packet->seckey.rsa.modulus_n->mdata, MPIBYTES(packet->seckey.rsa.modulus_n->length) + 1);
    der_offset += MPIBYTES(packet->seckey.rsa.modulus_n->length) + 1;

may be a bit of an unreadable mess, but one which can easily be made far less confusing with a temporary helper macro:

#define COPY_TO_DER(value, length) \
        do { \
            memcpy(packet->seckey.rsa.der_data + der_offset, (value), (length)); \
            der_offset += (length); \
        } while (0)
    if (!(packet->seckey.rsa.der_data = calloc(1, packet->seckey.rsa.der_len)))
        perror("no memory");
    COPY_TO_DER(asn_seq, sizeof asn_seq);
    COPY_TO_DER(header.raw, sizeof header.raw);
    COPY_TO_DER(packet->seckey.rsa.version, sizeof packet->seckey.rsa.version);
    COPY_TO_DER(asn_int, sizeof asn_int);
    COPY_TO_DER(packet->seckey.rsa.modulus_n->be_raw, 2);
    COPY_TO_DER(packet->seckey.rsa.modulus_n->mdata, MPIBYTES(packet->seckey.rsa.modulus_n->length) + 1);
#undef COPY_TO_DER

Just remember the gold rule: Keep It Simple Stupid™. Hard to read code isn't fun. No one will want to work on your code if it takes a Herculean amount of effort just to mentally parse; not even you.