Specific Interfaces

08 Jun 2014

While writing my CruiseCli project, I needed to do some data storage, and so used my standard method of filesystem access, the IFileSystem. This is an interface and implementation which I tend to copy from project to project, and use as is. The interface looks like the following:

public interface IFileSystem
{
    bool FileExists(string path);
    void WriteFile(string path, Stream contents);
    void AppendFile(string path, Stream contents);
    Stream ReadFile(string path);
    void DeleteFile(string path);

    bool DirectoryExists(string path);
    void CreateDirectory(string path);
    IEnumerable<string> ListDirectory(string path);
    void DeleteDirectory(string path);
}

And the standard implementation looks like the following:

public class FileSystem : IFileSystem
{
    public bool FileExists(string path)
    {
        return File.Exists(path);
    }

    public void WriteFile(string path, Stream contents)
    {
        using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write))
        {
            contents.CopyTo(fs);
        }
    }

    public Stream ReadFile(string path)
    {
        return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
    }
    //snip...
}

This (I think) is a very good solution to file system access as I can easily mock the interface and add expectations and stub values to it for testing.

However, on the CruiseCli project, I realised I didn't need most of what the interface provided, so I chopped all the bits off I didn't want, and added a property for a base directory I was using all the time:

public interface IFileSystem
{
    string HomePath { get; }

    void WriteFile(string path, Stream contents);
    Stream ReadFile(string path);
    bool FileExists(string path);
}

Which was better than the original, as I have a lot less methods to worry about, and thus it is more specific to my use case.

But I got thinking later in the project; "what are my use cases?", "what do I actually want to do with the filesystem?" The answer to this was simple: Read a config file, and write to the same config file. Nothing else.

So why not make the interface even more specific in this case:

public interface IConfiguration
{
    void Write(Stream contents);
    Stream Read();
}

Even simpler, and I now have the benefit of not caring what the filepaths are outside of the implementing class.

This means that in my integration tests, I can write an in-memory IConfiguration with far less hassle, and not need to worry about fun things like character encoding and case sensitivity on filepaths!

In a more complicated system, I would probably keep this new IConfiguration interface for accesing the config file, and make the concrete version depend on the more general IFileSystem:

public class Configuration : IConfiguration
{
    private const string FileName = ".cruiseconfig";
    private readonly IFileSystem _fileSystem;

    public Configuration(IFileSystem fileSystem)
    {
        _fileSystem = fileSystem;
    }

    public void Write(Stream contents)
    {
        _fileSystem.WriteFile(Path.Combine(_fileSystem.Home, FileName), contents);
    }

    public Stream Read()
    {
        return _fileSystem.ReadFile(Path.Combine(_fileSystem.Home, FileName));
    }
}

For a small system this would probably be overkill, but for a much larger project, this could help provide a better seperation of responsibilities.

design, code, net

« Using StructureMap Registries for better separation Strong Type your entity IDs. »
comments powered by Disqus