Simple Design by Example: Making Published Content Editable

by Zoran Horvat

It is a common requirement in software design to let users compose and publish content. It is even more common requirement to make published content editable. In this article we will try to see what kinds of design decisions have to be made to meet these requirements. There will be a couple of hidden traps along the way, though. That will be the opportunity to exercise software design and make choices that will ultimately lead to a simple design.

Introducing the Demo Application

For the purpose of demonstration, we will develop a very simple document management application. A document will be uniquely identified by its title. This is a strange idea and it will make it harder to change document's title later. But at the same time, this will simplify the object model a bit, which makes it a good demonstration. Since this article is about publishing and editing content, this constraint will not affect the discussion which follows.

And while we are still set to make object model simple, let's say that the document body will just consist of a single paragraph of plain text. That will be quite enough to develop all the features such a system should offer to the user. So we have a document which only contains title and body:

class Document
{
    public string Title { get; set; }
    public string Body { get; set; }
}

This class is capturing the concept of a document. Another interface will be there to support storing and retrieving the documents from the underlying storage. We will conveniently name it document repository and it will only expose methods that are letting us load and save documents:

interface IDocumentRepository: IDisposable
{
    IEnumerable<Document> GetAll();
    void Add(Document doc);
    void Remove(Document doc);
    void Save();
}

Introducing Basic Operations on Documents

Now we have something to start with. There is an uncommon mixture of classes and interfaces here, but we don't have to be that picky in this demonstration. It would be better if repository was generic, so that it doesn't depend on a concrete class, but that would introduce additional indirection and add needless complication to the demonstration.

Anyway, creation and display of the document will be backed by a dedicated service class:

class DocumentService
{
    private Func<IDocumentRepository> RepoFactory { get; }

    public DocumentService(Func<IDocumentRepository> repoFactory)
    {
        this.RepoFactory = repoFactory;
    }

    public void Create(string title, string body)
    {
        using (IDocumentRepository repo = this.RepoFactory())
        {
            Document doc = new Document()
            {
                Title = title,
                Body = body
            };
            repo.Add(doc);
            repo.Save();
        }
    }

    public IEnumerable<string> ListTitles()
    {
        using (IDocumentRepository repo = this.RepoFactory())
        {
            return repo.GetAll().Select(doc => doc.Title).ToList();
        }
    }

    public string GetBody(string title)
    {
        using (IDocumentRepository repo = this.RepoFactory())
        {
            return
                repo.GetAll()
                    .Where(doc => doc.Title == title)
                    .Single()
                    .Body;
        }
    }
}

This basic implementation gives an idea what it means to work with documents. We can call the ListTitles() method to get hold of all document titles in the database. Or we can create a new one by supplying its title and content to the Create() method. Later on, the GetBody() method can be used to read content of the document with specified title.

In terms of CRUD operations - Create, Read, Update and Delete - this service class implements Create and Read features. The time has come to implement the Update feature.

Adding the Basic Update and Delete Operations

The simplest of all update implementations would be to ask the caller to supply entire new content of the document in a single call. That would certainly make the control flow trivial. It only takes to load the document object from the database, apply changes to it and save it back to the database. Here is the implementation:

class DocumentService
{
    ...
    public void Update(string title, string newBody)
    {
        using (IDocumentRepository repo = this.RepoFactory())
        {
            Document doc =
                repo.GetAll()
                    .Where(d => d.Title == title).Single();
            doc.Body = newBody;
            repo.Save();
        }
    }
}

This is all it takes. If your documents have simple structure, then you should definitely give this idea a chance before looking for other, more engaged implementations. Single-step update comes with shortest code you can imagine in most of the practical cases. It applies well to very simple documents, such as forum posts, for example. The user can edit the post by simply providing the new content all at once.

There is one additional benefit from this design. With this implementation of the Update() method, the document has retained the structure it used to have. We didn't have to add any moving parts to the model to support document editing. That is a huge plus for this implementation. As you will see, we won't be that lucky with the following attempt.

We can see why that is important when we try to implement the delete operation:

class DocumentService
{
    ...
    public void Delete(string title)
    {
        using (IDocumentRepository repo = this.RepoFactory())
        {
            Document doc =
                repo.GetAll()
                    .Where(d => d.Title == title).Single();
            repo.Remove(doc);
            repo.Save();
        }
    }
}

Now all four of the CRUD operations are done, and they are all very simple. We could argue that this simplicity stems from implementation of the update operation. Indeed, update is exercising all the steps we can find in all other operations combined. We can take the Update() method implementation as a litmus test that will reveal the intrinsic complexity of the entire class. If we can find, edit and then save an object in a simple sequence of steps, then everything else will be equally easy to do.

But, as already indicated, circumstances under which we are developing might not be that simple.

Introducing Multi-Step Update Operation

In many real-world applications it is not possible to make edits to a document in a single call. There are two primary obstacles that may be standing in the way. First of all, the document itself may have a complex structure. Asking the user to provide entire new document content in a single step might be too much. Another reason which occurs frequently is that the new content requires some kind of verification before becoming official.

If any of these reasons holds, the single-step update idea is off and we have to come up with a different design. That is the place where we normally strive for the simple design, such which will make implementation of the complex feature as short and straightforward as possible.

When the time comes to decide how to make a document editable, it pays to think about the editing process from the user's point of view. If we can come up with a simple process, then simplicity will grow naturally in the underlying implementation. The process is the key.

There is one common editing scheme that we can find in many applications, and it goes like this. When a user decides to edit a document, she puts it into the editing mode. While in editing mode, the new document version is not visible to other users - all they can see is the frozen content as it used to be before editing. Now, indefinite number of changes can be applied to the editing copy, without ever becoming visible to other users. Only when all the edits are done, the user would publish the document, which means that the new content will replace the previous version and it will become visible in its entirety all at once. Any subsequent request to read that document will end up serving the new content only, and previous version will look like it never existed.

This solution boils down to passing the document through a couple of states, as shown in the following diagram.

Document editing finite state machine

New document starts without content - it is in editing mode already. As all the changes are applied, the document remains in EDITING state. Later on, the content may be confirmed, moving the document to the PUBLISHED state.

If at any later time the same document would have to be changed, an entirely new copy can be initialized in the EDITING state. Note that it is not the published document the one which moves back to EDITING state. Once published, an instance of the document remains published forever; only its successor starts in the EDITING state, before reaching the PUBLISHED state itself. And when that happens, when the document traverses from EDITING to the PUBLISHED state, it immediately supersedes the previously published version and becomes the only visible content of the document.

Designing Interfaces for the Edit-Publish Scheme

Now that we have a simple editing process, we can try to design classes that are implementing it in the simplest way possible. It is obvious that the document class will have to give way. That is the effect I have already mentioned before - when operations become more involved, the design becomes more complex; and vice versa. In this particular case, we have started from the process. As process grew complex, we can expect complications to sneak into the classes.

First of all, it doesn't make sense to talk about documents anymore. Now we have a document and multiple versions of its contents. We could come up with separate classes to model this new situation, something like this:

class DocumentHeader
{
    public string Title { get; }
    public IEnumerable<DocumentContent> Versions { get; }
}

class DocumentContent
{
    public int Version { get; }
}

But, do we really want to go that far? Is there any feature we have to develop, which requires access to different versions of a document? If there isn’t, then we shouldn't support such features at the expense of complicated design. I mean, when designing classes, we should make clear distinction between supporting future requirements and adding up-front features that were not asked for.

For example, if at any later moment we face the requirement to track multiple versions of a document, we could easily invent a class just for the purpose:

class DocumentHistory
{
    public IEnumerable<Document> Versions { get; }
}

As you can see, we can keep the Document class the only one in the system right now. It only takes to support multiple versions of the same document and to keep track which version has been published:

class Document
{
    public string Title { get; }
    public int Version { get; }
    public bool IsPublished { get; }
    public string Body { get; }
}

If you remember, Title was the property which uniquely identifies the document. Now that we may have multiple historical documents that were published, and supposedly one version in the process of editing, it becomes clear that the underlying storage will have to support multiple objects with the same Title property.

It is the Version property which distinguishes them. For any given Title, we find the object with highest Version property value, and IsPublished property returning True, and that is the latest published version of the document. At the same time, we are ensuring that at most one object with given Title comes with the IsPublished set to False - that is the editing copy of the document.

As you can see, our design can remain equally simple as it used to be before we have added complication to the editing process. That is what simple design is teaching us. Although object-oriented design is teaching us that classes should be introduced to model new concepts as they are added, it doesn't tell that we really have to add a new class every time. This is the tricky part. Sometimes we can just get away with redefining or just refining an existing class. It takes a bit of imagination to figure whether the new requirement is really introducing a brand new concept to the table or is it just adding to some concept we already had before.

The former will ask for more engagement when it comes to implementation. The latter will probably ask for less work, let alone that the number of classes and relationships between them will likely remain the same. That is in the heart of simple design. Don't add classes if you really don't have additional concepts they will model.

In the end, it remains to define the public interface of the service class which will let the user add, edit and delete documents:

class DocumentService
{
    public void InitializeNew(string title);
    public void SetBody(string title, string newBody);
    public void DiscardChanges(string title);
    public void Publish(string title);
    public IEnumerable<string> ListTitles();
    public string GetBody(string title);
    public void BeginEdit(string title);
    public void Delete(string title);
}

This is the list of methods which support multi-step document editing. Our example documents still consist of title and body only, and even then more than half of the public surface of the service class deals with content editing. That is what we normally get when we introduce new use cases. The design gets more complex. But even then, we have plenty of options. Some of them will yield simple design; some others will lead to a convoluted design which is hard to understand. The choice is ours and that is where we can show full power of the object-oriented programming techniques.

Full Implementation of the Edit-Publish Solution

Now that we have designed the public interface of both the document and the service class, we can provide their full implementation. The first listing shows the Document class. Note that the Document class is now responsible to initialize both the new document instance and the editing instance. New document is initialized via the static Initialize() method. On the other hand, an editing copy is made by calling the BeginEdit() method. ChangeBody() method is there to set the new content to an existing document, and it fails if invoked on the published version of the document. Furthermore, there is the Publish() method which just changes the IsPublished flag to True.

class Document
{
    public string Title { get; private set; }
    public int Version { get; private set; }
    public bool IsPublished { get; private set; }
    public string Body { get; private set; }

    public static Document Initialize(string title)
    {
        return new Document()
        {
            Title = title,
            Body = string.Empty,
            Version = 1,
            IsPublished = false
        };
    }

    public Document BeginEdit()
    {
        if (!this.IsPublished)
            return this;

        return new Document()
        {
            Title = this.Title,
            Body = this.Body,
            Version = this.Version + 1,
            IsPublished = false
        };
    }

    public void ChangeBody(string newBody)
    {
        if (this.IsPublished)
            throw new InvalidOperationException();
        this.Body = newBody;
    }

    public void Publish()
    {
        this.IsPublished = true;
    }
}

New methods on the Document class are helping fully encapsulate the state. Now the document instance is responsible for managing its state transitions, which is perfectly fine from the object-oriented programming principles stand point. It is better to keep these responsibilities inside the Document class and let the service class focus on its own set of non-trivial tasks, which are presented in the next listing.

class DocumentService
{
    private Func<IDocumentRepository> RepoFactory { get; }

    public DocumentService(Func<IDocumentRepository> repoFactory)
    {
        this.RepoFactory = repoFactory;
    }
    public void InitializeNew(string title) =>
        this.Do(repo =>
            repo.Add(Document.Initialize(title));

    public void SetBody(string title, string newBody) =>
        this.Do(repo =>
            this.GetEditingVersion(repo, title).ChangeBody(newBody));

    private Document GetEditingVersion(IDocumentRepository repo, string title) =>
        repo.GetAll()
            .Where(doc => doc.Title == title && !doc.IsPublished)
            .SingleOrDefault();

    public void DiscardChanges(string title) =>
        this.Do(repo =>
            repo.Remove(
                this.GetEditingVersion(repo, title)));

    public void Publish(string title) =>
        this.Do(repo =>
            this.GetEditingVersion(repo, title).Publish();

    public IEnumerable<string> ListTitles() =>
        this.Get(repo =>
            repo.GetAll()
                .Where(doc => doc.IsPublished)
                .Select(doc => doc.Title)
                .Distinct());

    public string GetBody(string title) =>
        this.Get(repo =>
            this.GetPublishedVersion(repo, title).Body;

    private Document GetPublishedVersion(IDocumentRepository repo, string title) =>
        repo.GetAll()
            .Where(doc => doc.Title == title && doc.IsPublished)
            .Aggregate((newest, cur) => cur.Version > newest.Version ? cur : newest);

    public void BeginEdit(string title) =>
        this.Do(repo =>
            repo.Add(
                this.GetPublishedVersion(repo, title).BeginEdit()));

    public void Delete(string title) =>
        this.Do(repo =>
            {
                foreach (Document doc in repo.GetAll().ToList())
                    repo.Remove(doc);
            });

    private void Do(Action<IDocumentRepository> action)
    {
        using (IDocumentRepository repo = this.RepoFactory())
        {
            action(repo);
            repo.Save();
        }
    }

    private T Get<T>(Func<IDocumentRepository, T> function)
    {
        using (IDocumentRepository repo = this.RepoFactory())
        {
            return function(repo);
        }
    }
}

This is the entire service class which implements CRUD operations for the document with edit-and-publish updating strategy. The class has a couple of private utility methods and everything else is its public interface.

Even though the public surface of the class is relatively large, all methods are straightforward and with very simple control flow. This is the result of simple design. We have been able to introduce versioning and editing into the Document class without having to split it up into two levels. This achievement had direct consequence on the service class, which also didn't have to split its control flow into two levels. In the end we can conclude that this approach has indeed produced a simple design.

Summary

In this article we have tackled the question of supporting edits to a published document. We have started from the assumption that the internal structure of the document is complex enough so that editing takes more than one step to complete.

While thinking of possible solutions to the problem, we have paid close attention to the goal of attaining as simple design as possible. In the end it looks like this elusive goal has been met. The final implementation consists of a reasonably short list of methods, each boiling down to one or two operations in a row. There were no control flow structures, except in one method only.

This demonstration has shown that it is possible to extend the list of features of a class without needing to split it into smaller parts right away. We were dancing on the thin line with this solution, as cohesion of the resulting document class has started to diminish.

Anyway, the situation still hasn't slipped out of control and there was some room left to maneuver. Only if more requirements will appear in the future shall we need to devise new types and split the functionality up into smaller classes.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub