Watching a directory: composability

Spread the love

Last time, I mentioned that our DirectoryTreeWatcher code demonstrates composition but not composability. To see what I mean, imagine that you wanted to add logging so you could track whenever a subscription is created or destroyed. A nice separation-of-concerns way to do this would be to try to use the decorator pattern.

Right at the start, this approach is doomed to fail. We would start by trying to create another derived class of DirectoryTreeWatcherBase which takes an existing DirectoryTreeWatcherBase where we could forward calls. Unfortunately, the base class has real implementation code which we cannot “opt out” of — we are, after all, saying that we desire a strong inheritance relationship here by extending the base class. There is no way to express one of our core requirements (“please make sure Subscribe calls the inner class”) because Subscribe itself is neither virtual nor abstract and is therefore not part of the overridable surface area.

The quick fix to this problem involves no actual implementation code changes. We just have to create a new interface and implement it. Technically this would not even qualify as a breaking change if we were to just modify the interface list for DirectoryTreeWatcherBase. However, to make this change most useful, we should also change the Create method signature to also use the interface. Here is the new interface and API surface area:

    public interface IDirectoryWatcher : IDisposable
    {
        IDisposable Subscribe(string file, Action<FileInfo> onUpdate);
    }

    public abstract class DirectoryWatcherBase : IDirectoryWatcher
    {
        protected DirectoryWatcherBase(DirectoryInfo path);

        public IDisposable Subscribe(string file, Action<FileInfo> onUpdate);

        public void Dispose();

        protected virtual void Dispose(bool disposing);
    }

    public abstract class DirectoryTreeWatcherBase : IDirectoryWatcher
    {
        protected DirectoryTreeWatcherBase(DirectoryInfo path);

        public IDisposable Subscribe(string file, Action<FileInfo> onUpdate);

        public void Dispose();

        protected abstract IDirectoryWatcher Create(DirectoryInfo path);

        protected virtual void Dispose(bool disposing);
    }

Notice that both DirectoryTreeWatcherBase and DirectoryWatcherBase implement the interface. This is no accident! The API surface is identical on these classes and this is a boon for composability. It allows us to have a single logging decorator which covers any scenario:

    public sealed class DirectoryWatcherWithLogging : IDirectoryWatcher
    {
        private readonly IDirectoryWatcher inner;
        private readonly string path;
        private readonly ILogger log;

        public DirectoryWatcherWithLogging(IDirectoryWatcher inner, string path, ILogger log)
        {
            this.inner = inner;
            this.path = path;
            this.log = log;
        }

        public void Dispose()
        {
            this.log.LogInformation("Disposing '{0}' watcher", this.path);
            this.inner.Dispose();
        }

        public IDisposable Subscribe(string file, Action<FileInfo> onUpdate)
        {
            string newPath = Path.Combine(this.path, file);
            this.log.LogInformation("Subscribing '{0}' watcher", newPath);
            return new DisposableWithLogging(this.inner.Subscribe(file, onUpdate), newPath, log);
        }

        private sealed class DisposableWithLogging : IDisposable
        {
            private IDisposable inner;
            private string path;
            private readonly ILogger log;

            public DisposableWithLogging(IDisposable inner, string path, ILogger log)
            {
                this.inner = inner;
                this.path = path;
                this.log = log;
            }

            public void Dispose()
            {
                this.log.LogInformation("Disposing '{0}' watcher", this.path);
                this.inner.Dispose();
            }
        }
    }

This example uses ILogger from the ASP.NET Core logging library. But the same pattern would hold for any other logging scenario. We just have to be sure to hook all relevant lifecycle methods.

With the sample library updated, we are good to go. Except we haven’t conquered what is arguably the most pressing inconvenience — file update events are “duplicated”. There are many ways to solve this, but we’ll have to address this next time.

Leave a Reply

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