lysergic-acid
lysergic-acid

Reputation: 20050

Refactoring a static class to separate its interface from implementation

I am working on a .NET based application, where some of the core application classes were designed with only static methods.

Example usage:

// static access.
Parameters.GetValue("DefaultTimeout");

// static access.
Logger.Log("This is an important message!");

There's already code out there that uses these static methods, so this "interface" cannot be changed.

These classes currently implement no interface. I would like to be able to separate the actual implementation of these classes from their interface.

The reason for this refactoring is that these objects will be used across AppDomain boundaries. I would like to be able to inject a "proxy" object that on non main-appdomains will invoke some other implementation instead of the default one.

To sum up, my questions are:

  1. How can i easily transform objects with static-only access to an interface based design, such that their implementation may be replaced when needed (but keeping static access).

  2. Once refactored, how/WHEN is the actual injection of the non-default implementation should occur?

Upvotes: 8

Views: 4124

Answers (3)

umlcat
umlcat

Reputation: 4143

Static Classes can be transformed into Singleton Objects.

Singleton Objects support interfaces.

Interfaces can be used for different implementations.

(1) Definition of Problem.

Suppose you have a class that have static members.

--

StringsClass.cs

--

namespace Libraries
{
  public static class StringsClass
  {

    public static string UppercaseCopy(string Value)
    {
      string Result = "";

      // code where "Value" is converted to uppercase,
      // and output stored in "Result"

    return Result;
    } // string UppercaseCopy(...)

    public static string LowercaseCopy(string Value)
    {
      string Result = "";

      // code where "Value" is converted to lowercase,
      // and output stored in "Result"

      return Result;
    } // string LowercaseCopy(...)

    public static string ReverseCopy(string Value)
    {
      string Result = "";

      // code where "Value" is reversed,
      // and output stored in "Result"

      return Result;
    } // string ReverseCopy(...)  

  } // class StringsClass

} // namespace Libraries

--

And, several code that uses that static elements, from that class.

--

StringsLibraryUser.cs

--

using Libraries;

namespace MyApp
{
  public class AnyClass
  {
    public void AnyMethod()
    {
      string Example = "HELLO EARTH";
      string AnotherExample = StringsClass.LowercaseCopy(Example);
    } // void AnyMethod(...)  

  } // class AnyClass

} // namespace MyApp

--

(2) Transform, first, the class, into a non static class.

--

StringsClass.cs

--

namespace Libraries
{
  public class StringsClass
  {

    public string UppercaseCopy(string Value)
    {
      string Result = "";

      // code where "Value" is converted to uppercase,
      // and output stored in "Result"

    return Result;
    } // string UppercaseCopy(...)

    public string LowercaseCopy(string Value)
    {
      string Result = "";

      // code where "Value" is converted to lowercase,
      // and output stored in "Result"

      return Result;
    } // string LowercaseCopy(...)

    public string ReverseCopy(string Value)
    {
      string Result = "";

      // code where "Value" is reversed,
      // and output stored in "Result"

      return Result;
    } // string ReverseCopy(...)  

  } // class StringsClass

} // namespace Libraries

--

(3) Add code the allow class handle a single object.

--

StringsClass.cs

--

namespace Libraries
{
  public class StringsClass
  {
    private static Singleton instance = null;

    private Singleton()
    {
      // ...
    }

    public static synchronized Singleton getInstance()
    {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    public string UppercaseCopy(string Value)
    {
      string Result = "";

      // code where "Value" is converted to uppercase,
      // and output stored in "Result"

    return Result;
    } // string UppercaseCopy(...)

    public string LowercaseCopy(string Value)
    {
      string Result = "";

      // code where "Value" is converted to lowercase,
      // and output stored in "Result"

      return Result;
    } // string LowercaseCopy(...)

    public string ReverseCopy(string Value)
    {
      string Result = "";

      // code where "Value" is reversed,
      // and output stored in "Result"

      return Result;
    } // string ReverseCopy(...)  

  } // class StringsClass

} // namespace Libraries

--

(4) Code that calls the class, should add the reference for the singleton.

--

StringsLibraryUser.cs

--

using Libraries;

namespace MyApp
{
  public class AnyClass
  {
    public void AnyMethod()
    {
      string Example = "HELLO EARTH";
      string AnotherExample = StringsClass.getInstance().LowercaseCopy(Example);
    } // void AnyMethod(...)  

  } // class AnyClass

} // namespace MyApp

--

(5) Define an interface, with similar declarations to the previous static class, and allow the singleton, to implement that interface. Omit the singletons members, in the interface declaration

--

StringsClass.cs

--

namespace Libraries
{
  public interface StringsInterface
  {
    string UppercaseCopy(string Value);  
    string LowercaseCopy(string Value);  
    string ReverseCopy(string Value);   
  } // interface StringsInterface

  public class StringsClass: StringsInterface
  {
    private static Singleton instance = null;

    private Singleton()
    {
      // ...
    }

    public static synchronized Singleton getInstance()
    {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    public string UppercaseCopy(string Value)
    {
      string Result = "";

      // code where "Value" is converted to uppercase,
      // and output stored in "Result"

    return Result;
    } // string UppercaseCopy(...)

    public string LowercaseCopy(string Value)
    {
      string Result = "";

      // code where "Value" is converted to lowercase,
      // and output stored in "Result"

      return Result;
    } // string LowercaseCopy(...)

    public string ReverseCopy(string Value)
    {
      string Result = "";

      // code where "Value" is reversed,
      // and output stored in "Result"

      return Result;
    } // string ReverseCopy(...)  

  } // class StringsClass

} // namespace Libraries

--

(6) In the code, where your are using your singleton, the previous class that contained static methods, replace the singleton for an interface.

--

StringsLibraryUser.cs

--

using Libraries;

namespace MyApp
{
  public class AnyClass
  {
    public StringsInterface StringsHelper = StringsClass.getInstance().LowercaseCopy(Example);

    public void AnyMethod()
    {
      string Example = "HELLO EARTH";
      string AnotherExample = StringsHelper;
    } // void AnyMethod(...)  

  } // class AnyClass

} // namespace MyApp

--

Now, you can add other classes that support the same declarations, with different implementation.

Cheers.

--

Upvotes: 3

Adam Houldsworth
Adam Houldsworth

Reputation: 64477

Disclaimer: The following suggestion is based on the importance of not changing the calling side. I'm not saying it's the best option, just that I think it's suitable.

Disconnecting the Implementation

There is no way to have interfaces on static members, so if you don't want to change the calling code, the static will likely have to remain. That said, you can simply have your static class wrap an interface inside, so the static class itself doesn't have any implementation - it delegates all calls to the interface.

This all means you can leave your static class and any code that calls it in place. This will be like treating the static class as the interface (or contract), but having it internally swap out implementations based on the situation.

It also means your interface can have a different signature to the static class, as the interface doesn't have to conform to the calling code expectations - basically, it will turn your static class into a sort of Bridge.

Injecting the Implementation

In short: use a static constructor in order to resolve the given implementation of this interface.

Statics are per AppDomain normally (unless decorated with ThreadStaticAttribute, then per AppDomain/thread) so you can determine where you are and what implementation you need based on the current AppDomain (the static constructor will be called whenever the static is first used in the AppDomain). This means that once constructed, that particular static class's wrapped implementation will remain for the duration of the AppDomain (though you could implement methods to flush the implementation).

Cross AppDomain Calling

The code responsible for this can either be in the static classes or you can make one of the interface implementations simply a proxy manager to an AppDomain type. Any type for cross AppDomain calls will need to inherit MarshalByRefObject.

http://msdn.microsoft.com/en-us/library/ms173139.aspx

CreateInstance of a Type in another AppDomain

Simplest way to make cross-appdomain call?

Sample Application

You should just be able to copy and paste this into a new Console application. What this is doing is registering an implementation for the default AppDomain and one for the user-made AppDomains. The default simply creates a remote implementation of the interface (in the other AppDomain). Just to demonstrate the "static per AppDomain" idea, the remote implementation delegate to yet another implementation for non-default domains.

You can change implementations on the fly, all you need to change is the static class constructor (to decide what implementation to pick). Notice that you do not need to change the Main method, our calling code in this case.

using System;
using System.Reflection;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
        Console.WriteLine(Parameters.GetValue(""));
        Console.Read();
    }
}

static class Parameters
{
    private static IParameterProvider _provider;

    static Parameters()
    {
        if (AppDomain.CurrentDomain.IsDefaultAppDomain())
        {
            _provider = new ParameterProviderProxy(AppDomain.CreateDomain(Guid.NewGuid().ToString()));
        }
        else
        {
            // Breakpoint here to see the non-default AppDomain pick an implementation.
            _provider = new NonDefaultParameterProvider();
        }
    }

    public static object GetValue(string name)
    {
        return _provider.GetValue(name);
    }
}

interface IParameterProvider
{
    object GetValue(string name);
}

class CrossDomainParameterProvider : MarshalByRefObject, IParameterProvider
{
    public object GetValue(string name)
    {
        return Parameters.GetValue(name);
    }
}

class NonDefaultParameterProvider : IParameterProvider
{
    public object GetValue(string name)
    {
        return AppDomain.CurrentDomain.FriendlyName;
    }
}

class ParameterProviderProxy : IParameterProvider
{
    private IParameterProvider _remoteProvider;

    public ParameterProviderProxy(AppDomain containingDomain)
    {
        _remoteProvider = (CrossDomainParameterProvider)containingDomain.CreateInstanceAndUnwrap(
            Assembly.GetExecutingAssembly().FullName,
            typeof(CrossDomainParameterProvider).FullName);
    }

    public object GetValue(string name)
    {
        return _remoteProvider.GetValue(name);
    }
}

A Note on Life Span

One of the main problems with managing a refactoring of static classes isn't usually the changing of the client code (as this is supported by lots of refactoring tools and there are techniques to get it done safely), but managing the life span of the object. Instance objects rely on living references (otherwise they are garbage collected), these can usually be made "easily accessible" by keeping one in a public static member somewhere, but usually this is what you are trying to avoid by refactoring in the first place.

It doesn't seem like you will have to worry about this concern, as you are leaving the calling code attached to the static classes, therefore the life span will remain the same.

Upvotes: 12

usr
usr

Reputation: 171178

For every static method, create an instance one. Add a static singleton variable that you can assign any implementation to. Make the static methods call the instance methods on the static singleton.

This will allow you to swap the implementation at runtime, but you can only have one implementation hooked in at the same time.

Existing code does not need to change.

Upvotes: 6

Related Questions