Understanding the Open Closed Principle

2019-01-22 22:13发布

问题:

I was refactoring some old code of a simple script file parser when I came across the following code:

StringReader reader = new StringReader(scriptTextToProcess);
StringBuilder scope = new StringBuilder();
string line = reader.ReadLine();
while (line != null)
{
    switch (line[0])
    {
        case '$':
            // Process the entire "line" as a variable, 
            // i.e. add it to a collection of KeyValuePair.
            AddToVariables(line);
            break;
        case '!':
            // Depending of what comes after the '!' character, 
            // process the entire "scope" and/or the command in "line".
            if (line == "!execute")
                ExecuteScope(scope);
            else if (line.StartsWith("!custom_command"))
                RunCustomCommand(line, scope);
            else if (line == "!single_line_directive")
                ProcessDirective(line);

            scope = new StringBuilder();
            break;

        default:
            // No processing directive, i.e. add the "line" 
            // to the current scope.
            scope.Append(line);
            break;
    }

    line = reader.ReadLine();
}

This simple script processor seems to me like a good candidate for refactoring by applying the "open closed principle". The lines beginning with a $ will probably never be handled differently. But, what if new directives beginning with a ! needs to be added? Or new processing identifiers (e.g. new switch-cases) are needed?

The problem is, I could not figure out how to easily and correctly add more directives and processors without breaking OCP. The !-case using scope and/or line makes it a bit tricky, as does the default-case.

Any suggestions?

回答1:

Use a Dictionary<Char, YourDelegate> to specify how a character should be handled. Call DefaultHandler if the character key do not exist in the dictionary.

Add a Add(char key, YourDelegate handler) method allowing anyone to handle a specific character.

Update

It's better to work with interfaces:

/// <summary>
/// Let anyone implement this interface.
/// </summary>
public interface IMyHandler
{
    void Process(IProcessContext context, string line);
}

/// <summary>
/// Context information
/// </summary>
public interface IProcessContext
{
}


// Actual parser
public class Parser
{
    private Dictionary<char, IMyHandler> _handlers = new Dictionary<char, IMyHandler>();
    private IMyHandler _defaultHandler;

    public void Add(char controlCharacter, IMyHandler handler)
    {
        _handlers.Add(controlCharacter, handler);
    }

    private void Parse(TextReader reader)
    {
        StringBuilder scope = new StringBuilder();
        IProcessContext context = null; // create your context here.

        string line = reader.ReadLine();
        while (line != null)
        {
            IMyHandler handler = null;
            if (!_handlers.TryGetValue(line[0], out handler))
                handler = _defaultHandler;

            handler.Process(context, line);


            line = reader.ReadLine();
        }
    }
}

Note that I pass in a TextReader instead. It gives much more flexibility since the source can be anything from a simple string to a complex stream.

Update 2

I would also break up the ! handling in a similar way. i.e. Create a class that handles IMyHandler:

public interface ICommandHandler
{
    void Handle(ICommandContext context, string commandName, string[] arguments);
}

public class CommandService : IMyHandler
{
    public void Add(string commandName, ICommandHandler handler) 
    {
    }

    public void Handle(IProcessContext context, string line)
    {
       // first word on the line is the command, all other words are arguments.
       // split the string properly

       // then find the corrext command handler and invoke it.
       // take the result and add it to the `IProcessContext`
    }
}

That gives more flexibility for both handling the actual protocol and add more commands. you do not have to change anything to add more functionality. The solution is therefore OK regarding Open/Closed and some other SOLID principles.