Do polymorphism or conditionals promote better design?

前端 未结 12 1804
长情又很酷
长情又很酷 2020-12-02 06:37

I recently stumbled across this entry in the google testing blog about guidelines for writing more testable code. I was in agreement with the author until this point:

相关标签:
12条回答
  • 2020-12-02 07:07

    Actually this makes testing and code easier to write.

    If you have one switch statement based on an internal field you probably have the same switch in multiple places doing slightly different things. This causes problems when you add a new case as you have to update all the switch statements (if you can find them).

    By using polymorphism you can use virtual functions to get the same functionality and because a new case is a new class you don't have to search your code for things that need to be checked it is all isolated for each class.

    class Animal
    {
        public:
           Noise warningNoise();
           Noise pleasureNoise();
        private:
           AnimalType type;
    };
    
    Noise Animal::warningNoise()
    {
        switch(type)
        {
            case Cat: return Hiss;
            case Dog: return Bark;
        }
    }
    Noise Animal::pleasureNoise()
    {
        switch(type)
        {
            case Cat: return Purr;
            case Dog: return Bark;
        }
    }
    

    In this simple case every new animal causes requires both switch statements to be updated.
    You forget one? What is the default? BANG!!

    Using polymorphism

    class Animal
    {
        public:
           virtual Noise warningNoise() = 0;
           virtual Noise pleasureNoise() = 0;
    };
    
    class Cat: public Animal
    {
       // Compiler forces you to define both method.
       // Otherwise you can't have a Cat object
    
       // All code local to the cat belongs to the cat.
    
    };
    

    By using polymorphism you can test the Animal class.
    Then test each of the derived classes separately.

    Also this allows you to ship the Animal class (Closed for alteration) as part of you binary library. But people can still add new Animals (Open for extension) by deriving new classes derived from the Animal header. If all this functionality had been captured inside the Animal class then all animals need to be defined before shipping (Closed/Closed).

    0 讨论(0)
  • 2020-12-02 07:08

    Unit testing an OO program means testing each class as a unit. A principle that you want to learn is "Open to extension, closed to modification". I got that from Head First Design Patterns. But it basically says that you want to have the ability to easily extend your code without modifying existing tested code.

    Polymorphism makes this possible by eliminating those conditional statements. Consider this example:

    Suppose you have a Character object that carries a Weapon. You can write an attack method like this:

    If (weapon is a rifle) then //Code to attack with rifle else
    If (weapon is a plasma gun) //Then code to attack with plasma gun
    

    etc.

    With polymorphism the Character does not have to "know" the type of weapon, simply

    weapon.attack()
    

    would work. What happens if a new weapon was invented? Without polymorphism you will have to modify your conditional statement. With polymorphism you will have to add a new class and leave the tested Character class alone.

    0 讨论(0)
  • 2020-12-02 07:10

    It works very well if you understand it.

    There are also 2 flavors of polymorphism. The first is very easy to understand in java-esque:

    interface A{
    
       int foo();
    
    }
    
    final class B implements A{
    
       int foo(){ print("B"); }
    
    }
    
    final class C implements A{
    
       int foo(){ print("C"); }
    
    }
    

    B and C share a common interface. B and C in this case can't be extended, so you're always sure which foo() you're calling. Same goes for C++, just make A::foo pure virtual.

    Second, and trickier is run-time polymorphism. It doesn't look too bad in pseudo-code.

    class A{
    
       int foo(){print("A");}
    
    }
    
    class B extends A{
    
       int foo(){print("B");}
    
    }
    
    class C extends B{
    
      int foo(){print("C");}
    
    }
    
    ...
    
    class Z extends Y{
    
       int foo(){print("Z");
    
    }
    
    main(){
    
       F* f = new Z();
       A* a = f;
       a->foo();
       f->foo();
    
    }
    

    But it is a lot trickier. Especially if you're working in C++ where some of the foo declarations may be virtual, and some of the inheritance might be virtual. Also the answer to this:

    A* a  = new Z;
    A  a2 = *a;
    a->foo();
    a2.foo();
    

    might not be what you expect.

    Just keep keenly aware of what you do and don't know if you're using run-time polymorphism. Don't get overconfident, and if you're not sure what something is going to do at run-time, then test it.

    0 讨论(0)
  • 2020-12-02 07:12

    Not an expert in the implications for test cases, but from a software development perspective:

    • Open-closed principle -- Classes should be closed to alteration, but open to extension. If you manage conditional operations via a conditional construct, then if a new condition is added, your class needs to change. If you use polymorphism, the base class need not change.

    • Don't repeat yourself -- An important part of the guideline is the "same if condition." That indicates that your class has some distinct modes of operation that can be factored into a class. Then, that condition appears in one place in your code -- when you instantiate the object for that mode. And again, if a new one comes along, you only need to change one piece of code.

    0 讨论(0)
  • 2020-12-02 07:15

    This is mainly to do with encapsulation of knowledge. Let's start with a really obvious example - toString(). This is Java, but easily transfers to C++. Suppose you want to print a human friendly version of an object for debugging purposes. You could do:

    switch(obj.type): {
    case 1: cout << "Type 1" << obj.foo <<...; break;   
    case 2: cout << "Type 2" << ...
    

    This would however clearly be silly. Why should one method somewhere know how to print everything. It will often be better for the object itself to know how to print itself, eg:

    cout << object.toString();
    

    That way the toString() can access member fields without needing casts. They can be tested independently. They can be changed easily.

    You could argue however, that how an object prints shouldn't be associated with an object, it should be associated with the print method. In this case, another design pattern comes in helpful, which is the Visitor pattern, used to fake Double Dispatch. Describing it fully is too long for this answer, but you can read a good description here.

    0 讨论(0)
  • 2020-12-02 07:17

    I'm a bit of a skeptic: I believe inheritance often adds more complexity than it removes.

    I think you are asking a good question, though, and one thing I consider is this:

    Are you splitting into multiple classes because you are dealing with different things? Or is it really the same thing, acting in a different way?

    If it's really a new type, then go ahead and create a new class. But if it's just an option, I generally keep it in the same class.

    I believe the default solution is the single-class one, and the onus is on the programmer proposing inheritance to prove their case.

    0 讨论(0)
提交回复
热议问题