11

How do I design a subclass whose method contradicts its superclass? Let's say we have this example:

# Example 1

class Scissor
  def right_handed?
    true
  end
end

class LeftHandedScissor < Scissor
  def right_handed?
    false
  end
end

Let's assume that the inheritance is necessary in this example e.g. that there might be other methods aside from right_handed? that are being inherited.

So a LeftHandedScissor is a Scissor. Its method right_handed? matches the signature and return value type of its superclass's method. However, the subclass method contradicts the return value of the superclass method.

This bothers me because it looks like a logical contradiction:

  1. All scissors are right-handed.
  2. A left-handed scissor is a scissor.
  3. Therefore, a left-handed scissor is right-handed.

I considered introducing an AbstractScissor to avoid this contradiction:

# Example 2

class AbstractScissor
  def right_handed?
    raise NotImplementedError
  end
end

class RightHandedScissor < AbstractScissor
  def right_handed?
    true
  end
end

class LeftHandedScissor < AbstractScissor
  def right_handed?
    false
  end
end

My colleague says the downside to this solution is that it's more verbose. Aside from adding an extra class, it requires us to call scissors "right-handed scissors", which no one does in the real world.

This leads me to my last solution:

# Example 3

class AbstractScissor
  def right_handed?
    raise NotImplementedError
  end
end

class Scissor < AbstractScissor
  def right_handed?
    true
  end
end

class LeftHandedScissor < AbstractScissor
  def right_handed?
    false
  end
end

It's nearly the same as example 2 but I just renamed RightHandedScissor as Scissor. There's a chance that someone might assume LeftHandedScissor is a subclass of Scissor based solely on the class names, but at least the code makes it obvious that that's not the case.

Any other ideas?

Rémi
  • 211
gsmendoza
  • 241

8 Answers8

37

There is nothing wrong with the design shown in the question. While one could also introduce abstract Scissor with two concrete subclasses, and maybe more overall clarity, it's also common to do it like shown (especially when the hierarchy is a result of years of incremental development, with Scissor being around for much longer than the concept of handedness).

"Okay, I guess the lesson here is that you don't make assumptions about the subclasses based solely on the superclass?"

You do make such assumptions based on the contracts (method signatures) of the base class, but not based on method implementations. In this case the contract says that right_handed is a Boolean method which can be true or false, so it can be either. You should ignore the fact that the base class implementation always returns true, especially if you allowed to derive from the base class by not freezing it. The base class implementation is then just a default, and the method right_handed exists exactly because the scissor could also be left handed.

23

You are thinking too logically! There is no logical contradiction because class definitions are not logical propositions.

Having the Scissor base class return true does not correspond to saying that all scissors are right-handed. It just means that a scissor instance is right-handed unless the method is overridden in a subclass.

JacquesB
  • 61,955
  • 21
  • 135
  • 189
16

You don't. It's like saying that all animals are dogs, and then asking how to make cats meow instead of bark. If you were naming your classes properly, your Scissor class would rather be named RightHandedScissor; now does it make sense to inherit LeftHandedScissor from RightHandedScissor?

  • One possible approach is to make Scissor class abstract, and right_handed would be abstract as well (unlike your example 2).

    Then, you may have RightHandedScissor and LeftHandedScissor if it makes sense (although I can't find an example where it would make sense).

    The major issue here is that you are repeating yourself. The property right_handed is barely rephrasing the type of the object. This leads to two issues:

    1. Some developers using your code will use right_handed property, while others will do if scissor.is_a?(RightHandedScissor). Why not keeping only one way of doing this?

    2. What if, one day, a change is made to the classes, and there is a contradiction between what property says and the type of the object?

    If the business logic dictates that the type is the proper location for separating right handed from left handed scissors, then either remove the property, or at least make its value automatically computed based on the type of the object.

  • A better approach is to have a concrete Scissor class where the property right_handed, and not the actual class type determines whether the scissor is right handed or left handed.

    This makes it easier to deal with the code: you have two classes less and don't have the redundancy (that is a property which barely rephrases the type). If you don't want a left handed scissor to become right handed, you may consider a read-only/constant property instead (i.e. the one which can be set only by the constructor, and cannot be changed after that; the terminology may change from a language to another).

9

Another option is to introduce the handed-ness as a dependency with a default value of right-handed. In pseudocode here as I am not familiar with Ruby:

class Scissors {
    Scissors(isRightHanded = true) {
        _isRightHanded = isRightHanded
    }

    IsRightHanded() {
        return _isRightHanded
    }
}

class LeftHandedScissors : Scissors {
    LeftHandedScissors() : Scissors(isRightHanded: false) { }
}

In fact, you then don't even need the LeftHandedScissors class - you can just create Scissors(isRightHanded: false) when you need a left-handed scissor.

This approach is called preferring composition over inheritance

Alex
  • 1,233
4

Summary: The design without the abstract class will be only be acceptable if it is carefully documented to distinguish its abstract and concrete behaviours.

The Liskov Substitution Principle is generally regarded as a "good thing". By the LSP, I mean that if type S is a subtype of type T, then objects of type S should behave as objects of type T are expected to behave.

You can not follow this principle unless the expectations for behaviour are made clear. This set of expectations is the so called "contract" of the class. In most languages it is expressed by a combination of signature and documentation.

Therefore, whether your first design is correct depends on the contracts.

Incorrect:

class Scissor
  # Returns true because all scissors are right-handed.
  def right_handed?
    true
  end
end

class LeftHandedScissor < Scissor
  # Returns false
  def right_handed?
    false
  end
end

Acceptable:

class Scissor
  # Returns true for right-handed scissors but false for left-handed ones.
  def right_handed?
    true
  end
end

class LeftHandedScissor < Scissor
  # Returns false
  def right_handed?
    false
  end
end

The code is the same. Whether the design is sensible (i.e. follows the LSP) or not depends on the contracts.

Now it is clear that, if you only know x.kind_of Scissors, then x.right_handed? might return either true or false.

But what about the true? Should that be documented? Yes. A class that can be extended needs to be documented twice. It needs documentation of its abstract behaviour, which is the behaviour we can expect from all its instance (direct or indirect), and it needs documentation of its concrete behaviour, which is the behaviour that its direct instances will have, i.e., the behaviour as implemented at this level of the inheritance hierarchy and which is inherited if not overridden.

Even better:

class Scissor
  # Abstract behaviour: Returns true for right-handed scissors but false for left-handed ones.
  # Concrete behaviour: Returns true.
  def right_handed?
    true
  end
end

class LeftHandedScissor < Scissor
  # Abstract and concrete behaviour: Returns false
  def right_handed?
    false
  end
end

Now it is clear also --from the documentation alone-- that, if you know s instance_of Scissors, then x.right_handed? will return true.

Often people don't bother to document the concrete behaviour, since, assuming there are no bugs, it can often be reverse engineered from the code.

The same is not true of the abstract behaviour. Unless it is documented, it can not be recovered from the code. It is left to future programmers to guess what your intentions were. Implementors of client code must guess at what they can assume about objects that are indirect instances of the class. Implementors of subclasses must guess at what is and what is not reasonable override.

All that said, I prefer your solution with the abstract class. I consider it better style. But this is only a matter of taste.

Bottom line: The design without the abstract class will be only be acceptable if it is carefully documented to distinguish its abstract and concrete behaviours.

3

"All scissors are right-handed"? Where do you get that idea from? Your code only expresses "scissors are right-handed by default". It's a default value, not a design decision. If there were no way of having a different value, what's the point in programming a boolean accessor function?

Kilian Foth
  • 110,899
2

This bothers me because it looks like a logical contradiction:

All scissors are right-handed.

Incorrect at this point. You've defined scissors as all having handedness, and that this defaults to right-handed if not overridden. You have not said that all scissors are right-handed, that would require overriding being prohibited (as can be done in some languages, and not in others).

Remove that false premise, and the contradiction is no longer there.

Jon Hanna
  • 2,135
1

Congratulations, you just have discovered how the violation of Liskov Substitution Principle looks like, as the very first commenter politely pointed you at.

To answer exactly to your question: according to the aforementioned principle, you don't design a subclass whose method contradicts its superclass. Reasons for this are explained in the literature about LSP, Robert Martin, for example, talks quite a lot about it his learning videos.

To address your specific problem with logical contradictions: classes in your application do not correspond to real-world objects, and do not need to. You can read and hear about this from quite a lot of experienced speakers, J. B. Rainsberger, for example.

hijarian
  • 159