Birth of a module

Many of my posts touch the principle that a single part of the code should do a single well-defined thing: the Single Responsibility Principle. If the code base is subdivided in a number of well-defined, non-overlapping responsibilities, then any functionality has a “home”. So for the developer, it will be easy to understand what part of the application must be modified to implement new features.

However, the only programs that don’t change are the programs that are abandoned. If an application is successful, then the request for new functionality is inevitable. Applications that are under active development might therefore change significantly over time. This might mean that new responsibilities emerge, or that responsibilities are reshuffled in the source code.

Read more: Birth of a module

How the code evolves is in the hand of the developers. Mostly, it is somewhere on the spectrum between two extremes:

  • No refactoring is done, so every new feature makes change harder and harder over time. The code base rots.
  • The developers are aware of the burdens of technical debt and overdesign the code.

How can we keep those two in balance? In my opinion, the secret lies in the purpose of the code. Why are we writing source code? The answer is: to tell to other developers what the application should do. If we can write this down in the clearest way, then change has become as easy as it can be. Overdesign makes code harder to reason, as does technical debt.

How can we keep the code in shape while it evolves at the same time? This is about the most important question that a developer should ask himself. Having a way of keeping the application in shape makes it possible to keep delivering functionality with low effort.

It might be interesting to watch the birth of a module to see this happening.

Reading a configuration value

Suppose you have developed a nice power tool, consisting of a simple user interface and a core of business logic. Users like it, so they ask for more features. You are now developing such a feature.

The feature involves reading data from a REST service. Since you don’t want your application to depend on the hard-coded url, you want to make the url configurable. But on the other hand, you don’t want to spend much time in creating a fancy user interface. After all, it is a power tool meant for power users. So you decide to read the value from a text file.

So, when the application starts, the url is read from a text file, like:

string url = File.ReadAllText("serviceUrl.txt");

This is about the most simple solution that you can have. It does not check for the existence of the file (it is deployed with the application), and the file must contain exactly the url and nothing more. In this stage of development, this is just file. The code is called from a single location so the intention is clear.

When the application grows, however, it might happen that the service is accessed from two locations.

ServiceResult ReadService() {
    string url = File.ReadAllText("serviceUrl.txt");
    // ... proceed to make request to the url in the file
}

The ReadService function is now the single place where the REST service is accessed. Therefore, this is also the single place where the url is read.

More values

But the application grows and grows, because the users want more. Now you need to access a second REST service, so a second url must be stored. You can add a second file “secondServiceUrl.txt”, but you can also store two lines in the “serviceUrl.txt”. After all, it is handy to have all urls at a single location. But now a maintainability issue arises: since ReadService reads the file, and ReadService2 reads the file as well, duplication has been introduced.

We can solve that by having a function to read the configuration from “serviceUrl.txt”:

string[] ReadServiceUrls() {
    string contents = File.ReadAllText("serviceUrl.txt");
    return contents.Split(Environment.NewLine);
}

This kinda works… But it is clumsy to use. The callers of ReadServiceUrls are tied to fixed lines in the serviceUrl.txt file, so when a line is accidentally removed, then all service urls after that line are messed up. What if we return a tuple?

(string Url1, string Url2) ReadServiceUrls() {
    var contents = File.ReadAllText("serviceUrl.txt");
    var urls = contents.Split(Environment.NewLine);
    return (urls[0], urls[1]);
}

For the users of ReadServiceUrls this is a bit better: the results have clear names, so mixing up is much harder now (at the cost of having to change ReadServiceUrls whenever a new service is added). But it is still possible to mix up the file. The only solution to that problem is to make the file format more specific. Instead of a list, we could format the file using key/value pairs. So the contents of serviceUrl.txt would look like:

service1=url1
service2=url2

Luckily, thanks to applying the Single Responsibility Principle, we have an easier job now. We only have to change the implementation of ReadServiceUrls.

(string Url1, string Url2) ReadServiceUrls() {
    var urls = File.ReadAllLines("serviceUrl.txt")
        .Select(line => {
            var key = line.Split('=')[0];
            var value = line.Substring(key.Length);
            return new KeyValuePair<string, string>(key, value);
        }).ToDictionary();
    return (urls["url1"], urls["url2"]);
}

But now we have another interesting issue: the output of ReadServiceUrls is less flexible than the file format. Why won’t we make ReadServiceUrls to return the dictionary itself?

Dictionary<string, string> ReadServiceUrls() {
    var urls = File.ReadAllLines("serviceUrl.txt")
        .Select(line => {
            var key = line.Split('=')[0];
            var value = line.Substring(key.Length);
            return new KeyValuePair<string, string>(key, value);
        }).ToDictionary();
    return (urls["url1"], urls["url2"]);
}

We could do this, but this involves a change all over the place. That is because the interface of the function has changed and all callers of the function must therefore be updated. I also don’t like the return type, because type safety is gone here. Mistyping a service name (the key in the dictionary) is only visible at runtime, when the concerning code is actually run.

There is also another observation: now we have a more sophisticated file format, reading the services file actually becomes a responsibility in itself. Maybe it is a bit early, but I still think this might warrant a more specific type instead of a dictionary with strings.

Decoupling

What if we change it like the following? This introduces a Configuration type to hold the read values. I also changed the type to Url instead of string to make it more useful.

public record Configuration(Uri Service1, Uri Service2);

Configuration ReadConfiguration() {
    var urls = File.ReadAllLines("serviceUrl.txt")
        .Select(line => {
            var key = line.Split('=')[0];
            var value = line.Substring(key.Length);
            return new KeyValuePair<string, string>(key, value);
        }).ToDictionary();
    return new Configuration(
        new Uri(urls["service1"]),
        new Uri(urls["service2"]));
}

I also took the liberty to rename the function to ReadConfiguration. Given that it returns a Configuration, this name is much more appropriate. Note that the new name is more general. This indicates that it is gaining responsibility.

When the number of configuration items grows, it will be too much for a single function. We could split it up in two. But then we might also combine them into a class:

public record Configuration(Uri Service1, Uri Service2, string CustomerName, FileInfo LastOpened);

public class AppConfiguration {
    
    public Configuration ReadConfiguration() {
        var data = ReadConfigurationData();
        return new Configuration(
            new Uri(new Uri(data["service1"]),
                    new Uri(data["servicd2"]),
                    data["customer"],
                    new FileInfo(data["lastOpened"]));
        )
    }

    private Dictionary<string, string> ReadConfigurationData() {
        return File.ReadAllLines("serviceUrl.txt")
            .Select(line => {
                var key = line.Split('=')[0];
                var value = line.Substring(key.Length);
                return new KeyValuePair<string, string>(key, value);
            }).ToDictionary(); 
    }
}

Conclusion

The design we have here is probably enough for many programs, especially when it concerns a power tool. In this last snippet, reading configuration settings is encapsulated and type safe. We could even further expand this. For example, we could put AppConfiguration behind an interface struct, so that we can obtain values from other sources than this fixed file with fixed format.

As you have seen, although functionality grows, adhering to the Single Responsibility Principle gives the freedom to further develop functionality without affecting the rest of the code. The module started as a single line of code, but it grow. But we guided it by keeping the responsibility encapsulated. There is neither technical debt building up, nor overdesign; we just build what we need using simple refactoring steps.

This is how a module is born.

Leave a Reply

Your email address will not be published. Required fields are marked *