Exposing objects in multiple represenations

February 2014 ยท 4 minute read

The need for multiple representations

Many applications have multiple interfaces: an API that others use to access the application programmatically or a rich web client built with JavaScript for the end-users and so on.

The needs of each interface may be different, though. The API, for instance, may need to expose all details of a specific object, but the web interface only needs certain details and maybe some extra attributes.

This means that the objects living in the business core need to be exposed to multiple clients in different ways. How can you achieve this efficiently?

The following examples will use Django, but the same principles apply in general.

We will also use a User object as an example:

class User(object):

    def __init__(self, username, password, email, firstname, lastname):
        self.username = username
        self.password = password
        self.email = email
        self.firstname = firstname
        self.lastname = lastname

This is not a Django model, since you can also have plain python classes for your business logic.

Solutions that don’t scale: Putting the code in the view

The first thing that usually comes to mind is to do the necessary transformation in the view that exposes the object.

import json
from django.http import HttpResponse

def user_details(request, username):
    # Fetch the user from the database.
    user = fetch_user(username=username)
    user_dict = {}
    for attr in [username, email, firstname, lastname]:
        user_dict[attr] = getattr(user, attr)
    return HttpResponse(
        json.dumps(user_dict), content_type="application/json"
    )

A similar approach would be followed in the API views as well.

This approach leads, however, to duplicated code: every time you need to serialize a User object you need to follow the exactly same process and have the exactly same code.

Until you don’t; a use-case will come up where you need a different representation. As an example, you might need in a view to expose the number of photos a User might have as well:

def user_details_with_photos(request, username):
    # Fetch the user from the database.
    user = fetch_user(username=username)
    user_dict = {}
    for attr in [username, email, firstname, lastname]:
        user_dict[attr] = getattr(user, attr)
    user_dict["nphotos"] = user.nphotos()
    return HttpResponse(
        json.dumps(user_dict), content_type="application/json"
    )

which is the same code as before with just one extra line.

As a result, the next time you have to change something, you have to make the change in many places, hoping you do not forget one, making this approach error-prone.

These are all well-known drawbacks of putting logic in the views.

Solutions that don’t scale: Putting the code in the object

The next approach is to create a fat model and keep the view skinny. That is, we move the code that generates the representation of the User object in the User class itself:

class User(object):
    ...

    def to_dict(self):
        result = {}
        for attr in [username, email, firstname, lastname]:
            result[attr] = getattr(self, attr)
        return result

    def to_dict_with_photos(self):
        result = self.to_dict()
        result["nphotos"] = user.nphotos()
        return result

The corresponding views become:

def user_details(request, username):
    user = fetch_user(username=username)
    return HttpResponse(
        json.dumps(user.to_dict()),
        content_type="application/json"
    )


def user_details_with_photos(request, username):
    user = fetch_user(username=username)
    return HttpResponse(
        json.dumps(user.to_dict_with_photos()),
        content_type="application/json"
    )

This solves the problem of code reuse nicely. Every way to represent the User object is in one place, in the User class, and the code in the views becomes simpler. Furthermore, tests become much simpler to write: you need to test the two methods that convert a User object to a dictionary, instead of testing the views directly. Thus, the tests become faster as well,since they do not have to go through the HTTP stack.

However, the User class has become quite large this way. In addition to the functionality it must support for a user instance, the class also has many methods to generate all necessary transformations.

In other words, this approach violates the Single Responsibility Principle. The User class has two responsibilities, one to support the necessary functionality of a User object and one to represent the object in multiple ways. The User class becomes this way harder to understand and more fragile.

Moving the logic for the representations to separate processes

The solution to this problem is to extract the responsibility of generating a representation of a User instance to a separate function or class.

class UserRepresentations(object):

    @classmethod
    def to_dict(cls, user):
        result = {}
        for attr in [username, email, firstname, lastname]:
            result[attr] = getattr(user, attr)
        return result

    @classmethod
    def to_dict_with_photos(cls, user):
        result = cls.to_dict(user)
        result["nphotos"] = user.nphotos()
        return result

As an added benefit, these representations can be unit-tested in isolation, without any dependency on the business objects (and perhaps the database).

The views now become:

def user_details(request, username):
    user = fetch_user(username=username)
    user_dict = UserRepresentations.to_dict(user)
    return HttpResponse(
        json.dumps(user_dict), content_type="application/json"
    )


def user_details_with_photos(request, username):
    user = fetch_user(username=username)
    user_dict = UserRepresentations.to_dict_with_photos(user)
    return HttpResponse(
        json.dumps(user_dict), content_type="application/json"
    )

The result is a clear separation of concerns; that is, a cleaner codebase and of higher quality.