简体   繁体   中英

Using the Strategy Pattern to avoid downcasting

I was reading on this site about the Liskov substitution principle. It states:

As per the LSP, functions that use references to base classes must be able to use objects of the derived class without knowing it. In simple words, derived classes must be substitutable for the base class.

According to this page , if you override a method in the base class and it does nothing or throws an exception, you're in violation of the principle.

Suppose I had an abstract class called Weapon , and the subclasses ReloadableWeapon and Sword . ReloadableWeapon contains a method that's unique to that class, called Reload() . When declaring objects, standard practice is you do it from the abstract class and then subclass, like so:

Weapon rifle = new ReloadableWeapon();
Weapon sword = new Sword();

If I wanted to use the reload method for a rifle , I could cast it. Based on numerous articles and textbooks, this could lead to problems later on.

Also, if I have the reload method in the base class Weapon, then Sword would ignore or throw, which is wrong.

If I wanted to avoid all that, would using the Strategy Pattern be a viable option? Like this:

public final Weapon{

    private final String name;
    private final int damage;
    private final List<AttackStrategy> validactions;
    private final List<Actions> standardActions;

    private Weapon(String name, int damage, List<AttackStrategy> standardActions, List<Actions> attacks)
    {
        this.name = name;
        this.damage = damage;
        standardActions = new ArrayList<Actions>(standardActions);
        validAttacks = new ArrayList<AttackStrategy>(validActions);
    }

    public void standardAction(String action){} // -- Can call reload or aim here.  

    public int attack(String action){} // - Call any actions that are attacks. 

    public static Weapon ReloadableWeapon(String name, int damage){
        return new Weapon(name, damage, this.constructActions(), this.constructStandardActions);
    }

    public static Weapon Sword(String name, damage){
        return new Weapon(name, damage, this.standardSwordActions, this.swordActions);
    }

    //returns a List collection that contains the actions for a reloadable Weaopon. - Shoot 
    private List<AttackStrategy> reloadableActions(){}

    //returns a List collection of standard non attack actions. - Reload 
    private List<Actions> standardReloadableActions(){}

     //returns a List collection that contains the actions for a Sword - Swing/Strike  
    private List<AttackStrategy> swordActions(){}

    //returns a List collection of standard non attack actions. - Sharpen 
    private List<Actions> standardSwordActions(){}

}

Attack Interface and Implementation:

public interface AttackStrategy{
    void attack(Enemy enemy);
}

public class Shoot implements AttackStrategy {
    public void attack(Enemy enemy){
        //code to shoot
    }
}

public class Strike implements AttackStrategy {
    public void attack(Enemy enemy){
        //code to strike
    }
}

By having the List<AttackStrategy> being constructed inside the Weapon class, client code can't pass in List<AttackStrategy> not meant for a certain types of Weapons , for example, a Sword cannot shoot bullets, and doesn't reload, if I added a grenade, it shouldn't be able to strike like a Sword(you get the idea).

I'm not asking if I've implemented the Strategy Pattern correctly, but rather can I use the pattern when faced with a subclass that has a method unique to that subclass and I don't want to cast it? or in other words, rather than violate the LSP, can I prohibit the use of inheritance and use the Strategy Pattern to implement the require methods?

Notes:
The pattern solves my problem in 2 ways:

  1. I don't have to downcast, I can store my Weapons in a List<Weapon> collection without worrying about checking the type, and then casting
  2. Any weapon that isn't Reloadable , won't have the concrete class Reload . This mean no throwing or leaving the method blank

My problem is, given the definition of the Strategy pattern, I don't believe I'm using it in that context.

From the site:

The Strategy pattern is to be used where you want to choose the algorithm to use at runtime. ...

The Strategy pattern provides a way to define a family of algorithms, encapsulate each one as an object, and make them interchangeable.

I like this approach, could I just look at it as allowing the client to choose how to use a Weapon at runtime , with the added benefit of immutability , and avoiding to cast ? or Am I just stretching the definition a little too far?

With strategy you have the same "problem", when you call reloadStrategy the concrete strategy will be "reload" or "not reload".

Look this image from the head first design pattern book: ducks that can't fly implement a beahvior that represent that they can't fly.

In your question you wrote:

Also, if I have the reload method in the base class Weapon, then Sword would ignore or throw, which is wrong.

In my opinion, that's shouldn't be a wrong behavior, but the correct one.
You can consider Weapon as an Interface and, in ReloadableWeapon and NonReloadableWeapon classes, implement the methods you want to show in the Weapon class, assignignig to a "no-action" behaviour to the methods that don't match the specific case (eg the reload method in NonReloadableWeapon will be empty in his implementation or, better, it will launch an exception).
An other option for you could be use default methods (Java 8) to implement a default behaviour in Weapon class that launch an exception; method that will be overriden in the subclass where that has a sense.
In this case:
1. you would maintain the substituibility principle
2. you would be able to use Inheritance in another way (eg if you want Weapon as a subclass of a more general class Item)
3. you wouldn't be forced to use istanceOf or cast

Ending, in my opinion, the Stategy pattern could be useful in your context to change behavior to the client class that would use your Weapon, but it wouldn't solve the lack of substituibility.

Given that line of code

public void standardAction(String action){} // -- Can call reload or aim here. 

one of two things --

  • either the caller knows if their weapon needs reloading, which makes the Strategy useless.

  • or they don't know it and it may be part of a normal routine to try and reload something that is not reloadable, in which case the sword.Reload() { // do nothing } option is perfectly valid.

Only the context can tell you which is true. Define who the caller is and define what the behavior of attacking means and you'll see more clearly where you need to go.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM