2
\$\begingroup\$

When you try to use several decorators it can get ugly pretty quickly and you'll end up with:

new RelativeFileProvider(
  new SystemVariableFileProvider(
      new PhysicalFileProvider()
  ),
  "%TEMP%"
);

or if you prefer one-liners then like that:

new RelativeFileProvider(new SystemVariableFileProvider(new PhysicalFileProvider()), "%TEMP%");

I thought I turn them into extensions and make them play along nicer in a functional way so I created a couple of helper methods that allow me to do this:

new PhysicalFileProvider()      
    .DecorateWith(SystemVariableFileProvider.Create())
    .DecorateWith(RelativeFileProvider.Create("%TEMP%"));

Don't be confused about the empty classes. To be able to experiment without being distracted by the implementations I extracted the interface and the classes from my question about Multiple file access abstractions that I'm going to use this pattern for. They all have implementations that have been already reviewed there. This question is about the additional decorator helper APIs and they are implementation-netural.

Here are the types I used:

Version 1 - with an exntesion

interface IFileProvider
{

}

class PhysicalFileProvider : IFileProvider
{
    public static PhysicalFileProvider Create()
    {
        return new PhysicalFileProvider();
    }
}

class RelativeFileProvider : IFileProvider
{
    public RelativeFileProvider(IFileProvider fileProvider, string basePath)
    {

    }

    public static Func<IFileProvider, RelativeFileProvider> Create(string basePath)
    {
        return decorable => new RelativeFileProvider(decorable, basePath);
    }
}

class SystemVariableFileProvider : IFileProvider
{
    public SystemVariableFileProvider(IFileProvider fileProvider)
    {

    }

    public static Func<IFileProvider, SystemVariableFileProvider> Create()
    {
        return decorable => new SystemVariableFileProvider(decorable);
    }
}

static class FileProviderExtensions
{
    public static IFileProvider DecorateWith(this IFileProvider decorable, Func<IFileProvider, IFileProvider> createDecorator)
    {
        return createDecorator(decorable);
    }
}

Version 2 - with an interface

This API forces each type to implement the DecorateWith method instead of relying on an extension.

interface IDecorable<T>
{
    T DecorateWith(Func<T, T> createDecorator); 
}

interface IFileProvider : IDecorable<IFileProvider>
{

}

class PhysicalFileProvider : IFileProvider, IDecorable<IFileProvider>
{
    public static PhysicalFileProvider Create()
    {
        return new PhysicalFileProvider();
    }

    public IFileProvider DecorateWith(Func<IFileProvider, IFileProvider> createDecorator)
    {
        return createDecorator(this);
    }
}

class RelativeFileProvider : IFileProvider, IDecorable<IFileProvider>
{
    public RelativeFileProvider(IFileProvider fileProvider, string basePath)
    {

    }

    public static Func<IFileProvider, RelativeFileProvider> Create(string basePath)
    {
        return decorable => new RelativeFileProvider(decorable, basePath);
    }

    public IFileProvider DecorateWith(Func<IFileProvider, IFileProvider> createDecorator)
    {
        return createDecorator(this);
    }
}

class SystemVariableFileProvider : IFileProvider, IDecorable<IFileProvider>
{
    public SystemVariableFileProvider(IFileProvider fileProvider)
    {

    }

    public static Func<IFileProvider, SystemVariableFileProvider> Create()
    {
        return decorable => new SystemVariableFileProvider(decorable);
    }

    public IFileProvider DecorateWith(Func<IFileProvider, IFileProvider> createDecorator)
    {
        return createDecorator(this);
    }
}

And these are the whys about the factories:

  • I chose static factory methods over free arguments to keep parameter names and their order.
  • I chose them also to avoid new.

What do you think of this system? Do you prefer one version over the other? Can this be made even more convinient? Or would you say this is madness?

\$\endgroup\$
2
\$\begingroup\$

IMO the first version is easier to maintain, and I don't see from the shown code, what you gain by the second.


In the second version:

Each FileProvider class doesn't need to explicitly inherit IDecorable<IFileProvider> because that is inherited via IFileProvider.


I maybe overlooking something or you may find it too obvious to show, but I don't like that you implement the interface IFileProvider in all the provider classes, because they then are forced to implement all (I can see there aren't any (yet)) possible common members, which could be taken care of in a base class. Therefore I would create an abstract base class for the providers like:

  interface IDecorable<T>
  {
    T DecorateWith(Func<T, T> createDecorator);
  }

  interface IFileProvider : IDecorable<IFileProvider>
  {

  }

  abstract class FileProvider : IFileProvider
  {
    public virtual IFileProvider DecorateWith(Func<IFileProvider, IFileProvider> createDecorator)
    {
      return createDecorator(this);
    }
  }

  class PhysicalFileProvider : FileProvider
  {
    public static PhysicalFileProvider Create()
    {
      return new PhysicalFileProvider();
    }
  }

  class RelativeFileProvider : FileProvider
  {
    public RelativeFileProvider(IFileProvider fileProvider, string basePath)
    {

    }

    public static Func<IFileProvider, RelativeFileProvider> Create(string basePath)
    {
      return decorable => new RelativeFileProvider(decorable, basePath);
    }
  }
  ...

In this way you gain both the benefits of normal OOP inheritance/polymorphic behavior and the decorator pattern and at the same time are still free to chain .DecorateWith(...) with other implementers of IFileProvider


Madness is a strong word. The shown chain looks a little nicer than a chain of new xx() statements, and you explicitly "explain" the pattern and behavior. And the first version doesn't need much attention when first written, so no harm done at least.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.