开发者

Replace conditional with polymorphism - nice in theory but not practical

"Replace conditional with polymorphism" is elegant only when type of object you're doing switch/if statement for is already selected for you. As an example, I have a web application which reads a query string parameter called "action". Action can have "view", "edit", "sort", and etc. values. So how 开发者_高级运维do I implement this with polymorphism? Well, I can create an abstract class called BaseAction, and derive ViewAction, EditAction, and SortAction from it. But don't I need a conditional to decided which flavor of type BaseAction to instantiate? I don't see how you can entirely replace conditionals with polymorphism. If anything, the conditionals are just getting pushed up to the top of the chain.

EDIT:

public abstract class BaseAction
{
    public abstract void doSomething();
}

public class ViewAction : BaseAction
{
    public override void doSomething() { // perform a view action here... }
}

public class EditAction : BaseAction
{
    public override void doSomething() { // perform an edit action here... }
}

public class SortAction : BaseAction
{
    public override void doSomething() { // perform a sort action here... }
}


string action = "view";  // suppose user can pass either "view", "edit", or "sort" strings to you.
BaseAction theAction = null;

switch (action)
{
    case "view":
        theAction = new ViewAction();
        break;

    case "edit":
        theAction = new EditAction();
        break;

    case "sort":
        theAction = new SortAction();
        break;
}

theAction.doSomething();    // So I don't need conditionals here, but I still need it to decide which BaseAction type to instantiate first. There's no way to completely get rid of the conditionals.


You're right - "the conditionals are getting pushed up to the top of the chain" - but there's no "just" about it. It's very powerful. As @thkala says, you just make the choice once; from there on out, the object knows how to go about its business. The approach you describe - BaseAction, ViewAction, and the rest - is a good way to go about it. Try it out and see how much cleaner your code becomes.

When you've got one factory method that takes a string like "View" and returns an Action, and you call that, you have isolated your conditionality. That's great. And you can't properly appreciate the power 'til you've tried it - so give it a shot!


Even though the last answer was a year ago, I would like to make some reviews/comments on this topic.

Answers Review

I agree with @CarlManaster about coding the switch statement once to avoid all well known problems of dealing with duplicated code, in this case involving conditionals (some of them mentioned by @thkala).

I don't believe the approach proposed by @KonradSzałwiński or @AlexanderKogtenkov fits this scenario for two reasons:

First, from the problem you've described, you don't need to dynamically change the mapping between the name of an action and the instance of an action that handles it.

Notice these solutions allows doing that (by simply assigning an action name to a new action instance), while the static switch-based solution doesn't (the mappings are hardcoded). Also, you'll still need a conditional to check if a given key is defined in the mapping table, if not an action should be taken (the default part of a switch statement).

Second, in this particular example, dictionaries are really hidden implementations of switch statement. Even more, it might be easier to read/understand the switch statement with the default clause than having to mentally execute the code that returns the handling object from the mapping table, including the handling of a not defined key.

There is a way you can get rid of all conditionals, including the switch statement:

Removing the switch statement (use no conditionals at all)

How to create the right action object from the action name?

I'll be language-agnostic so this answer doesn't get that long, but the trick is to realize classes are objects too.

If you've already defined a polimorphic hierarchy, it makes no sense to make reference to a concrete subclass of BaseAction: why not ask it to return the right instance handling an action by its name?

That is usually implemented by the same switch statement you had written (say, a factory method)... but what about this:

public class BaseAction {

    //I'm using this notation to write a class method
    public static handlingByName(anActionName) {
        subclasses = this.concreteSubclasses()

        handlingClass = subclasses.detect(x => x.handlesByName(anActionName));

        return new handlingClass();
    }
}

So, what is that method doing?

First, retrieves all concrete subclasses of this (which points to BaseAction). In your example you would get back a collection with ViewAction, EditAction and SortAction.

Notice that I said concrete subclasses, not all subclasses. If the hierarchy is deeper, concrete subclasses will always be the ones in the bottom of the hierarchy (leaf). That's because they are the only ones supposed not to be abstract and provide real implementation.

Second, get the first subclass that answer whether or not it can handle an action by its name (I'm using a lambda/closure flavored notation). A sample implementation of the handlesByName class method for ViewAction would look like:

public static class ViewAction {

    public static bool handlesByName(anActionName) {
        return anActionName == 'view'
    }

}

Third, we send the message new to the class that handles the action, effectively creating an instance of it.

Of course, you have to deal with the case when none of the subclass handles the action by it's name. Many programming languages, including Smalltalk and Ruby, allows passing the detect method a second lambda/closure that will only get evaluated if none of the subclasses matches the criteria. Also, you will have to deal with the case more than one subclass handles the action by its name (probably, one of these methods was coded in the wrong way).

Conclusion

One advantage of this approach is that new actions can be supported by writing (and not modifying) existing code: just create a new subclass of BaseAction and implementing the handlesByName class method correctly. It effectively supports adding a new feature by adding a new concept, without modifying the existing impementation. It is clear that, if the new feature requires a new polimorphic method to be added to the hierarchy, changes will be needed.

Also, you can provide the developers using your system feedback: "The action provided is not handled by any subclass of BaseAction, please create a new subclass and implement the abstract methods". For me, the fact that the model itself tells you what's wrong (instead of trying to execute mentally a look up table) adds value and clear directions about what has to be done.

Yes, this might sound over-design. Please keep an open mind and realize that whether a solution is over-designed or not has to do, among other things, with the development culture of the particular programming language you're using. For example, .NET guys probably won't be using it because the .NET doesn't allow you to treat classes as real objects, while in the other hand, that solution is used in Smalltalk/Ruby cultures.

Finally, use common sense and taste to determine beforehand if a particular technique really solves your problem before using it. It is tempting yes, but all trade-offs (culture, seniority of the developers, resistance to change, open mindness, etc) should be evaluated.


A few things to consider:

  • You only instantiate each object once. Once you do that, no more conditionals should be needed regarding its type.

  • Even in one-time instances, how many conditionals would you get rid of, if you used sub-classes? Code using conditionals like this is quite prone to being full of the exact same conditional again and again and again...

  • What happens when you need a foo Action value in the future? How many places will you have to modify?

  • What if you need a bar that is only slightly different than foo? With classes, you just inherit BarAction from FooAction, overriding the one thing that you need to change.

In the long run object oriented code is generally easier to maintain than procedural code - the gurus won't have an issue with either, but for the rest of us there is a difference.


Your example does not require polymorphism, and it may not be advised. The original idea of replacing conditional logic with polymorphic dispatch is sound though.

Here's the difference: in your example you have a small fixed (and predetermined) set of actions. Furthermore the actions are not strongly related in the sense that 'sort' and 'edit' actions have little in common. Polymorphism is over-architecting your solution.

On the other hand, if you have lots of objects with specialised behaviour for a common notion, polymorphism is exactly what you want. For example, in a game there may be many objects that the player can 'activate', but each responds differently. You could implement this with complex conditions (or more likely a switch statement), but polymorphism would be better. Polymorphism allows you to introduce new objects and behaviours that were not part of your original design (but fit within its ethos).

In your example, in would still be a good idea to abstract over the objects that support the view/edit/sort actions, but perhaps not abstract these actions themselves. Here's a test: would you ever want to put those actions in a collection? Probably not, but you might have a list of the objects that support them.


There are several ways to translate an input string to an object of a given type and a conditional is definitely one of them. Depending on the implementation language it might also be possible to use a switch statement that allows to specify expected strings as indexes and create or fetch an object of the corresponding type. Still there is a better way of doing that.

A lookup table can be used to map input strings to the required objects:

action = table.lookup (action_name); // Retrieve an action by its name
if (action == null) ...              // No matching action is found

The initialization code would take care of creating the required objects, for example

table ["edit"] = new EditAction ();
table ["view"] = new ViewAction ();
...

This is the basic scheme that can be extended to cover more details, such as additional arguments of the action objects, normalization of the action names before using them for table lookup, replacing a table with a plain array by using integers instead of strings to identify requested actions, etc.


I've been thinking about this problem probably more than the rest developers that I met. Most of them are totally unaware cost of maintaining long nested if-else statement or switch cases. I totally understand your problem in applying solution called "Replace conditional with polymorphism" in your case. You successfully noticed that polymorphism works as long as object is already selected. It has been also said in this tread that this problem can be reduced to association [key] -> [class]. Here is for example AS3 implementation of the solution.

private var _mapping:Dictionary;
private function map():void
{
   _mapping["view"] = new ViewAction();
   _mapping["edit"] = new EditAction();
   _mapping["sort"] = new SortAction();
}

private function getAction(key:String):BaseAction
{
    return _mapping[key] as BaseAction;
} 

Running that would you like:

public function run(action:String):void
{
   var selectedAction:BaseAction = _mapping[action];
   selectedAction.apply();
}

In ActionScript3 there is a global function called getDefinitionByName(key:String):Class. The idea is to use your key values to match the names of the classes that represent the solution to your condition. In your case you would need to change "view" to "ViewAction", "edit" to "EditAction" and "sort" to "SortAtion". The is no need to memorize anything using lookup tables. The function run will look like this:

public function run(action:Script):void
{
   var class:Class = getDefintionByName(action);
   var selectedAction:BaseAction = new class();
   selectedAction.apply();
}

Unfortunately you loose compile checking with this solution, but you get flexibility for adding new actions. If you create a new key the only thing you need to do is create an appropriate class that will handle it.

Please leave a comment even if you disagree with me.


public abstract class BaseAction
{
    public abstract void doSomething();
}

public class ViewAction : BaseAction
{
    public override void doSomething() { // perform a view action here... }
}

public class EditAction : BaseAction
{
    public override void doSomething() { // perform an edit action here... }
}

public class SortAction : BaseAction
{
    public override void doSomething() { // perform a sort action here... }
}


string action = "view"; // suppose user can pass either
                        // "view", "edit", or "sort" strings to you.
BaseAction theAction = null;

switch (action)
{
    case "view":
        theAction = new ViewAction();
        break;

    case "edit":
        theAction = new EditAction();
        break;

    case "sort":
        theAction = new SortAction();
        break;
}

theAction.doSomething();

So I don't need conditionals here, but I still need it to decide which BaseAction type to instantiate first. There's no way to completely get rid of the conditionals.


Polymorphism is a method of binding. It is a special case of thing known as "Object Model". Object models are used to manipulate complex systems, like circuit or drawing. Consider something stored/marshalled it text format: item "A", connected to item "B" and "C". Now you need to know what is connected to A. A guy may say that I'm not going to create an Object Model for this because I can count it while parsing, single-pass. In this case, you may be right, you may get away without object model. But what if you need to do a lot of complex manipulations with imported design? Will you manipulate it in text format or sending messages by invoking java methods and referencing java objects is more convenient? That is why it was mentioned that you need to do the translation only once.


You can store string and corresponding action type somewhere in hash map.

public abstract class BaseAction
{
    public abstract void doSomething();
}

public class ViewAction : BaseAction
{
    public override void doSomething() { // perform a view action here... }
}

public class EditAction : BaseAction
{
    public override void doSomething() { // perform an edit action here... }
}

public class SortAction : BaseAction
{
    public override void doSomething() { // perform a sort action here... }
}


string action = "view"; // suppose user can pass either
                        // "view", "edit", or "sort" strings to you.
BaseAction theAction = null;

theAction = actionMap.get(action); // decide at runtime, no conditions
theAction.doSomething();


The switch is simple and looks OK. I don't think it would be that secure if a user could feed in a class name and you could directly use it without a switch conditional. For obtaining data though, Coders have been known to use a look up table loop to get extra data reducing it to one if in an array look up search. Still thinking the switch looks simple enough to understand but would be cumbersome if you had 100s of choices.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜