Tips for Writing Better Software

Posted by admin on February 14, 2022

Bad software sucks. It sucks because most programmers want to improve it but can't because the code is poorly designed, hard to read, and harder to modify. It's uncertain if it semantically works the same after a code change. As a programmer, commit to writing better software. Here are three tips to improve your software's design.

Tip #1 - Refactor Literal Values into a Configuration Class

Look through your code for literals, i.e. hard coded strings or numeric values. Move these values into a new type as properties. Use this new type to change the behavior of your code through configuration by changing the type's property values.

Consider the following base type named IntervalProcessor that executes a derived type's code on a regular interval.

Derived types can extend this base class by overriding the Execute() method. Examples include web scrapers, health monitors, and anything that must run on a regular timing interval.

public abstract class IntervalProcessor
{
    private static readonly object syncLock = new object();
    private Timer timer;

    public void Start()
    {
        lock(syncLock)
        {
            if (timer == null)
            {
                timer = new Timer(SyncronizedExecute, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000);
            }
        }
    }

    public void Stop()
    {
        lock (syncLock)
        {
            if (timer != null)
            {
                timer.Dispose();
                timer = null;
            }
        }
    }

    private void SyncronizedExecute(object state)
    {
        bool lockAquired = false;
        Monitor.TryEnter(syncLock, TimeSpan.Zero, ref lockAquired);

        if (lockAquired)
        {
            try
            {
                Execute();
            }
            finally
            {
                Monitor.Exit(syncLock);
            }
        }
    }

    protected abstract void Execute();
}

The Start() method instantiates a Timer object hard coding 1000 for the total milliseconds between executions of the SynchronizedExecute method.

timer = new Timer(SyncronizedExecute, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000);

This code executes the method every second which may not be suitable for all applications. The hard coded value limits your ability to reuse the code. A programmer using this type cannot vary the execution interval for different tasks. You must recompile and redeploy to change the execution interval because this code is not configurable. Only when you have the source code can you recompile. You can improve this code by allowing the programmer to pass in a configuration value for the interval.

Create a new type named IntervalProcessorOptions with one property called TimerInterval. Assign 1000 as the default value for TimerInterval.

public class IntervalProcessorOptions
{
    public double TimerInterval { get; set; } = 1000;
}

Refactor IntervalProcessor to add IntervalProcessorOptions as an optional constructor parameter that defaults to null. You can instantiate a new instance of IntervalProcessorOptions when no argument is passed. Add an instance variable to store the passed in or newly created options object.

protected readonly IntervalProcessorOptions options;

public IntervalProcessor(IntervalProcessorOptions options = null)
{
    this.options = options ?? new IntervalProcessorOptions();
}

The options member can now be used throughout IntervalProcessor. Refactor the Start() method to instantiate Timer using the options.TimerInterval value.

timer = new Timer(SyncronizedExecute, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(options.TimerInterval));

This code is semantically equivalent to the original code. Any code using IntervalProcessor will work exactly as it did before. You can now adjust the timing interval of your task. To do this, construct an options object, set the desired timing interval, and pass it to the interval processor's constructor. The following code configures HealthCheckMonitor to run every five minutes instead of the default one second.

public class HealthCheckMonitor : IntervalProcessor { ... }

static void Main(string[] args)
{
    IntervalProcessorOptions options = new IntervalProcessorOptions();
    options.TimerInterval = 1000 * 60 * 5;
    HealthCheckMonitor monitor = new HealthCheckMonitor(options: options);
    monitor.Start();

    Console.ReadKey();

    monitor.Stop();
}

You may ask, "why do this?" The answer is the code is more reusable. You provide a configurable mechanism for programmers to change the behavior of your code. It takes less time to refactor this code than it does to read this blog post.

You might also ask, "Why a class instead of a value type parameter?" The reason is better scalability. You can add new properties to the options class without polluting the constructor signature. One or two constructor parameters is manageable. After three or more parameters the constructor starts to become unsightly.

This easy tip allows you to write more reusable code.

Tip #2 - Implement Use Cases as Commands

As a programmer, you often read old code to understand how you solved past problems. Spending less time comprehending old code gives you more time to solve new problems. Properly named source files help you quickly locate code when the name matches the type inside the file. Therefore, consistent naming conventions are critical to quickly finding the right source code.

A good approach is to decompose your system into use cases. A use case is a specific behavior your application exhibits often in response to a user action. Using a present tense verb is a descriptive naming convention. For example, RegisterUser, AuthenticateUser, LogoutUser, ResetPassword are reasonable use case names for a user based application.

Consider the corresponding source code file names.

RegisterUser.cs
AuthenticateUser.cs
LogoutUser.cs
ResetPassword.cs

Open the RegisterUser.cs file to quickly see how users are added to the system. To see how they authenticate, open the AuthenticateUser.cs file. And so on. Furthermore, seeing all four files together gives you an idea about the code's intent. When each use case implements a consistent pattern such as the command pattern, you can quickly understand a code base by looking at the file names.

A lot of internet resources talk about the command pattern. Some have strong opinions about what is and is not a command object. A reasonable pattern for implementing use case types as commands is to add a single public Execute() method with a return type and parameters to suit your needs.

public class RegisterUser
{
    private readonly IUserRepository userRepository;
    public RegisterUser(IUserRepository userRepository)
    {
        this.userRepository = userRepository;
    }

    public User Execute(string name, string username, string password)
    {
        ...
        User newUser = new User(name, username, password);
        userRepository.AddUser(newUser);

        return userRepository.GetUserById(newUser.Id);
    }
}

Here is an example of using the use case command object in a model view controller (MVC) web application.

public class UserController : Controller
{
    private readonly RegisterUser registerUser;

    public UserController(RegisterUser registerUser)
    {
        this.registerUser = registerUser;
    }

    public async Task<ActionResult> Register(string name, string username, string password)
    {
        User newUser = registerUser.Execute(name, username, password);
        return View(newUser);
    }
}

The logic for registering new users is centralized to a use case command object. The command object business logic is isolated from the ASP.NET MVC web application presentation logic. The RegisterUser command type can be reused in other application types, such as a web API, desktop, or console application.

Decoupling the business logic from the application type interface logic offers several benefits. The logic is easier to test. The code can become a library used in multiple application types. The system is more robust to application framework upgrades. All of these things are signs of good code design. Good design lets your code stay relevant longer.

Tip #3 - Centralize Business Logic into a Library

Centralizing business logic into a standalone library or assembly improves the quality of your software. Standalone libraries are easier to test. Designing logic in a library forces you to clearly define the interface through which other code interacts with your library's logic. Writing tests for library logic requires less setup and teardown code for each test. You avoid having to reference application UI specific assemblies like Xamarin.Forms and writing redundant code for setting up a form just to test business logic.

Your software design improves because isolated logic libraries can be used in multiple application types. A single shared library can be consumed by a Web API, MVC Application, WebForms application, desktop application, and a mobile application. The business logic is in a so-called "pure" library. This means your library has zero dependencies and has a greater compatibility with a wider range of application types.

A shared library can target multiple platforms. You can recompile the shared library against newer versions of the platform, such as transitioning from .NET Framework 4.7 to .NET 6. All platform dependent logic can be refactored into a separate assembly and included with that specific platform.

Consider this blog web application in ASP.NET. Your blog controller might look like this.

public class BlogController : Controller
{
    private readonly IWebHostEnvironment env;

    public HomeController(IWebHostEnvironment env)
    {
        this.env = env;
    }
    public IActionResult Index()
    {
        List<PostViewModel> posts = new List<PostViewModel>();
        foreach (var file in Directory.GetFiles($"{env.WebRootPath}/posts", "*.md"))
        {
            string postContent = System.IO.File.ReadAllText(file);
            PostViewModel post = CreatePostViewModelFromString(postContent, Path.GetFileNameWithoutExtension(file));
            if (post.PublishDate.HasValue)
            {
                posts.Add(post);
            }                
        }

        return View(posts.Where(x => x.PublishDate.HasValue).OrderByDescending(x => x.PublishDate.Value).ToList());
    }

    private PostViewModel CreatePostViewModelFromString(string content, string slug)
    {
        // construct PostViewModel from file contents, removed for brevity
    }
}

This application by design finds blog posts in Markdown files in a directory called posts. With each file the application reads the content and transforms it into an application specific PostViewModel type. The Index method renders a list of view models in reverse chronological order of the PublishDate if it exists.

The source code file structure shows the BlogController under the Controllers directory.

BlogApp
- Controllers/
    - BlogController.cs
- Models/
    - PostViewModel.cs
- Startup.cs

Consider a new requirement to include a command line interface for your blog application. The logic is embedded in the BlogController Index method and not very usable in a new command line application. You can centralize the business logic into a shared library and use the library in the web application and the command line application.

Think about the ideal way your applications will consume your shared logic. A reasonable implementation for a web controller is to write as little code as possible to call into your shared logic library. You can use a use case command object as described in tip #2 above. Here the command object is named GetPublishedPosts.

public class BlogController : Controller
{
    private readonly GetPublishedPosts getPublishedPosts;

    public HomeController(GetPublishedPosts getPublishedPosts)
    {
        this.getPublishedPosts = getPublishedPosts;
    }
        
    public IActionResult Index()
    {
        return View(getPublishedPosts.Execute().OrderByDescending(x => x.PublishDate.Value).ToList());
    }
}

You can then use the use case command object in a console application.

class Program
{
    static void Main(string[] args)
    {
        GetPublishedPosts getPublishedPosts = new GetPublishedPosts();

        foreach (var post in getPublishedPosts.Execute().OrderByDescending(x => x.PublishDate.Value))
        {
            Console.WriteLine(post.Title);
        }
    }
}

Here you see how the shared logic library's GetPublishedPosts object is instantiated and consumed by two different application types.

To do this, create a file named GetPublishedPosts.cs in the project root folder. This source file will contain the use case command type. Move the code that transforms Markdown files into a view model list from the Index method of BlogController to a Execute() method in GetPublishedPosts.

public class GetPublishedPosts
{
    public PostViewModel Execute()
    {
        List<PostViewModel> posts = new List<PostViewModel>();
        foreach (var file in Directory.GetFiles($"{env.WebRootPath}/posts", "*.md"))
        {
            string postContent = System.IO.File.ReadAllText(file);
            PostViewModel post = CreatePostViewModelFromString(postContent, Path.GetFileNameWithoutExtension(file));
            if (post.PublishDate.HasValue)
            {
                posts.Add(post);
            }                
        }

        return posts;    
    }
}

Two more changes are necessary to compile this code. First, the env field must move from BlogController to GetPublishedPosts, and a IWebHostEnvironment must be injected through the constructor. Second, CreatePostViewModelFromString needs to move into GetPublishedPosts as well.

public class GetPublishedPosts
{
    private readonly IWebHostEnvironment env;

    public GetPublishedPosts(IWebHostEnvironment env)
    {
        this.env = env;
    }

    public PostViewModel Execute() { ... }

    private PostViewModel CreatePostViewModelFromString(string content, string slug)
    {
        // construct PostViewModel from file contents, removed for brevity
    }
}

You must register GetPublishedPosts with your dependency injection container. It can then be injected into BlogController.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<GetPublishedPosts>();
    }
}

Using tip #2, we refactored business logic into an encapsulated use case command type. Business logic can now be centralized into a standalone shared library. Multiple application types can then use the shared library. Move the GetPublishedPosts type into a class library project and reference the project from the web application project.

This is the current source code file structure.

BlogApp
- Controllers/
    - BlogController.cs
- Models/
    - PostViewModel.cs
- GetPublishedPosts.cs
- Startup.cs

Add a new class library project named BlogLogic and add the GetPublishedPosts.cs file to it. This is the new source code file structure. The BlogApp is the web application. The shared library with centralized business logic is BlogLogic. Note that the PostViewModel type is in the shared library.

BlogApp
- Controllers/
    - BlogController.cs
- Startup.cs

BlogLogic
- Models/
    - PostViewModel.cs
- GetPublishedPosts.cs

The BlogApp project can now reference the BlogLogic project in your build setup. With C# project files (those with .csproj extensions), you do this by adding a project reference to BlogLogic.csproj from the BlogApp.csproj file.

<ProjectReference Include="..\BlogLogic\BlogLogic.csproj">
</ProjectReference>

Expect compiler errors for Unable to find type... because the namespace for PostViewModel, GetPublishedPosts were changed during code refactoring. Fix the type reference errors and rebuild the project.

You have successfully refactored common business logic into an isolated shared library you can use from multiple application types.

Bonus Tip #4 - Put 3rd Party Dependencies Behind an Interface

I intended this post to be three tips instead of four, but I discovered a problem building the application while refactoring the code into a separate library. The CreatePostViewModelFromString method has dependencies on two third party libraries. The problem is the BlogLogic project targets .NET Standard 2.1 and cannot reference the third party libraries which target .NET Core. The BlogApp project targets .NET Core, so the original code did not have this problem.

Here is one more tip on how to solve this problem.

Decouple the BlogLogic library from third party libraries with a new interface type. In the BlogApp assembly, add a concrete implementation of this interface that uses the third party libraries. This works because BlogApp can reference those libraries as both target the .NET Core platform.

In particular, the CreatePostViewModelFromString method calls the third party libraries twice to transform Markdown strings into HTML. The first call transforms Markdown into an HTML object. The second call selects the first HTML paragraph.

public PostViewModel CreatePostViewModelFromString(string postContent, string slug)
{
    ...
    var htmlDoc = new HtmlDocument();
    htmlDoc.LoadHtml(Markdown.Parse(postContent));

    var firstParagraph = htmlDoc.DocumentNode.SelectSingleNode("//p");
    ...
    FirstParagraphMarkdown = firstParagraph?.OuterHtml
    ...
}

This code is necessary because the first paragraph is shown with the title on the blog listing page.

To abstract away a third party library, it's reasonable to create an interface with only those third party methods you use. It's not necessary to implement every method offered by the library. Here, we'll refactor the code to use a method matching our exact intent. Create an interface named IMarkdownParser. Add a method named GetFirstParagraph that is intended to return an HTML string from a Markdown string.

public interface IMarkdownParser
{
    string GetFirstParagraph(string postContent);
}

Put the IMarkdownParser interface in the BlogLogic project. Adding it to any other project (like BlogApp) creates an unnecessary dependency on that project and its platform target. The BlogLogic project can target .NET Standard 2.1 when it depends only on types and interfaces within itself.

The BlogApp project targets .NET Core, and the third party dependencies target .NET Core. Since the BlogApp project references BlogLogic, it can have a type that implements the IMarkdownParser interface. This concrete type in BlogApp can use the third party dependencies because they all target .NET Core. This works because .NET Core projects can consume .NET Standard types.

The BlogLogic library targets .NET Standard and is more reusable across numerous application types.

Here is the new structure of source files.

BlogApp
- Controllers/
    - BlogController.cs
- Startup.cs

BlogLogic
- Abstractions/
    - IMarkdownParser.cs
- Models/
    - PostViewModel.cs
- GetPublishedPosts.cs

A new Abstractions folder exists in BlogLogic for IMarkdownParser.

This convention is not required but aligns with how Microsoft structures some of its libraries. Organize your library files however you prefer. This convention hides the interfaces when looking at the root directory of the library. I prefer that only business logic command objects exist at the root of the library project. Other types such as interfaces, implementations, are less interesting and exist to compliment the command object types. The command types at the project root allow you to understand what the library does. The rest of the types tell you how the command object types do it.

Refactor the PostViewModelFromString method to use IMarkdownParser. First, inject the interface through the constructor and assign it to an instance member.

private readonly IMarkdownParser markdownParser;

public GetPublishedPosts(IWebHostEnvironment env, IMarkdownParser markdownParser)
{
    this.env = env;
    this.markdownParser = markdownParser;
}

Next, change CreatePostViewModelFromString to call GetFirstParagraph through IMarkdowParser.

public PostViewModel CreatePostViewModelFromString(string postContent, string slug)
{
    ...
    FirstParagraphMarkdown = markdownParser.GetFirstParagrph(postContent),
    ...
}

There is no longer a dependency on the third party libraries that target .NET Core. You have successfully decoupled the core business logic from its dependencies. Almost.

Remove the dependency on Microsoft.AspNetCore.HostingEnvironment's IWebHostEnvironment interface. Doing this improves code reuse. The BlogLogic library can then be used in a non-ASP.NET Core application.

Like before, think about the intent of this ASP.NET interface.

Here its purpose is for getting blog post files from disk. Can you improve this code?

public List<PostViewModel> Execute()
{
    ...
    foreach (var file in Directory.GetFiles($"{env.WebRootPath}/posts", "*.md"))
    {
        string postContent = System.IO.File.ReadAllText(file);
        PostViewModel post = CreatePostViewModelFromString(postContent, Path.GetFileNameWithoutExtension(file));
        if (post.PublishDate.HasValue)
        {
            posts.Add(post);
        }
    }
    ...
}

Yes. Right now, blog posts come from files on disk. Consider how blog posts can come from different sources like databases, web services, etc. Each source differs in implementation, but is similar in returning a list of blog posts. Therefore, you can create an interface that returns a list of blog posts. You can then create a concrete implementation of this interface to return blog posts from files on disk.

Create an interface named IPostProvider. Most people will recommend the repository pattern here. I call it a provider since its only purpose is to provide the blog posts.

public interface IPostProvider
{
    IEnumerable<string> GetPosts();
}

Next, refactor the GetPublishedPosts constructor to inject a IPostProvider and store in an instance member.

public class GetPublishedPosts
{
    ...
    private readonly IPostProvider postProvider;

    public GetPublishedPosts(IMarkdownParser markdownParser, IPostProvider postProvider)
    {
        ...
        this.postProvider = postProvider;
    }
    ...
}

You can then refactor the Exeute() method to get a list of blog posts through the IPostProvider interface.

public List<PostViewModel> Execute()
{
    ...
    foreach (var post in postProvider.GetPosts())
    ...
}

Create an implementation for IPostProvider named FilePostProvider in the BlogApp project.

public class FilePostProvider : IPostProvider
{
    private readonly IWebHostEnvironment env;

    public FilePostProvider(IWebHostEnvironment env)
    {
        this.env = env;
    }

    public IEnumerable<string> GetPosts()
    {
        List<string> posts = new List<string>();
        foreach (var file in Directory.GetFiles($"{env.WebRootPath}/posts", "*.md"))
        {
            string post = System.IO.File.ReadAllText(file);
            posts.Add(post);
        }

        return posts;
    }
}

The GetPublishedPosts type no longer depends on the ASP.NET Core Hosting assembly.

public class GetPublishedPosts
{
    private readonly IMarkdownParser markdownParser;
    private readonly IPostProvider postProvider;

    public GetPublishedPosts(IMarkdownParser markdownParser, IPostProvider postProvider)
    {
        this.markdownParser = markdownParser;
        this.postProvider = postProvider;
    }

    public List<PostViewModel> Execute()
    {
        List<PostViewModel> posts = new List<PostViewModel>();
        foreach (var post in postProvider.GetPosts())
        {
            string postContent = System.IO.File.ReadAllText(post);
            PostViewModel post = CreatePostViewModelFromString(postContent, Path.GetFileNameWithoutExtension(post));
            if (post.PublishDate.HasValue)
            {
                posts.Add(post);
            }
        }

        return posts;
    }
}

This command object successfully abstracted away third party dependencies.

Two more changes must be addressed. One is the slug parameter to CreatePostViewModelFromString method is removed. The other is refactoring our command object to return a single blog post.

The second parameter of CreatePostViewModelFromString named slug is the URL friendly name of the blog post. It is used as the default title for the post if no title property exists in the blog post metadata (the set of key/value pairs at the start of the Markdown file that the application uses for rendering).

Passing in slug to serve as a default value is reasonable in the old code. In the new code, IPostProvider returns a list of strings and would instead need to return an object. This is also reasonable. But every blog post Markdown file has a title setting. Given this, I removed the slug parameter entirely and now require blog posts to have this setting metadata.

public PostViewModel CreatePostViewModelFromString(string postContent)
{
    ...
    string title = settings.GetValueOrDefault("title");
    if (string.IsNullOrEmpty(title))
    {
        // title = slug; // original code
        throw new Exception($"Missing or invalid value '{title}' for setting 'title'.");
    }
    ...
}

One more compiler error exists in the controller method that renders the full page of the blog post using the slug name as the key. This controller method uses the CreatePostViewModelFromString method which originally was in the controller. This method is now in the GetPublishedPosts command object in the BlogLogic assembly.

public IActionResult posts(string slug)
{
    if (string.IsNullOrWhiteSpace(slug) || !System.IO.File.Exists($"{env.WebRootPath}/posts/{slug}.md"))
    {
        return NotFound();
    }

    string postContent = System.IO.File.ReadAllText($"{env.WebRootPath}/posts/{slug}.md");

    return View(GetPublishedPosts.CreatePostViewModelFromString(postContent, slug));
}

This code depends on the file system and isn't reusable.

Like GetPublishedPosts, you can construct a new use case command object and call it from this controller. This improves consistency of each command doing one thing, but I think this reduces readability. The obvious name for getting a single post is GetPublishedPost. Seeing that name alongside another command whose name differs by one letter hurts my eyes. You could use GetPost but honestly, GetPublishedPosts should be renamed to GetPosts. Nevertheless, this can be solved a different way. Overloading the Execute() method of GetPublishedPosts command can solve this problem. Add a new ExecuteSingleOrDefault() method that takes a string slug parameter to return a single PostViewModel object. We can reuse most of the code.

Refactoring this code into separate command objects is a subject for a future post.

Add the following method to GetPublishedPosts.

public PostViewModel ExecuteSingleOrDefault(string slug)
{
    var postContent = postProvider.GetPost(slug);
    if (postContent == null)
    {
        return null;
    }

    return CreatePostViewModelFromString(postContent);
}

Build the application and see one last compile error. The IPostProvider interface needs a method to return a single post given a slug.

Here is the new interface.

public interface IPostProvider
{
    IEnumerable<string> GetPosts();
    string GetPost(string slug);
}

Implement the method in the FilePostProvider type in the BlogApp web application.

public string GetPost(string slug)
{
    string filename = $"{env.WebRootPath}/posts/{slug}.md";
    if (!File.Exists(filename))
    {
        return null;
    }

    return File.ReadAllText(filename);
}

Finally, update the controller method to call ExecuteSingleOrDefault on GetPublishedPosts.

public IActionResult posts(string slug)
{
    if (string.IsNullOrWhiteSpace(slug))
    {
        return NotFound();
    }

    PostViewModel viewModel = getPublishedPosts.ExecuteSingleOrDefault(slug);
    if (viewModel == null)
    {
        return NotFound();
    }

    return View(viewModel);
}

Be sure to add the concrete implementations of your BlogLogic interfaces to the BlogApp dependency injection container.

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IMarkdownParser, WestwindMarkdownParser>();
    services.AddTransient<IPostProvider, FilePostProvider>();
    ...
}

You can build and run the application. It will behave as it did before.

If you found value in this post, consider following me on X @davidpuplava for more valuable information about Game Dev, OrchardCore, C#/.NET and other topics.