DEV Community

Cover image for Code Smell 125 - 'IS-A' Relationship
Maxi Contieri
Maxi Contieri

Posted on • Originally published at maximilianocontieri.com

Code Smell 125 - 'IS-A' Relationship

We learned at school that inheritance represents an 'is-a' relationship. It is not.

TL:DR; Think about protocol and behavior, forget inheritance

Problems

  • Bad models

  • Unexpected behavior

  • Subclass overrides

  • Liskov substitution principle Violation

Solutions

  1. Think in terms of behavior behaves-as-a

  2. Prefer composition over inheritance

  3. Subclassify always following 'behaves-as-a' relation

Context

IS-A relation comes from the data world.

We learned ERDs with structured design and data modeling.

Now, we need to think in terms of behavior.

Behavior is essential, data is accidental.

Sample Code

Wrong

// If you made Square derive from Rectangle, then a Square should be usable anywhere you expect a rectangle

#include <iostream>

Rectangle::Rectangle(const unsigned width, const unsigned height):
    m_width(width),
    m_height(height)
{
}

unsigned Rectangle::getWidth() const
{
    return m_width;
}

void Rectangle::setWidth(const unsigned width)
{
  /*Width and Height are independant*/
    m_width = width;
}

unsigned Rectangle::getHeight() const
{
    return m_height;
}

void Rectangle::setHeight(const unsigned height)
{
    m_height = height;
}

unsigned Rectangle::area() const
{
  /*Valid for both Rectangles and Squares*/
    return m_height * m_width;
}

Square::Square(const unsigned size)
    : Rectangle(size, size)
{
}

// OK for squares, bad for rectangles
// Protocol is bad, width and height are not relevant on squares
void Square::setWidth(const unsigned size)
{
    m_height = size;
    m_width = size;
}

// OK for squares, bad for rectangles
// Protocol is bad, width and height are not relevant on squares
void Square::setHeight(const unsigned size)
{
    m_height = size;
    m_width = size;
}

void process(Rectangle& r)
{
    unsigned h = 10;
    auto w = r.getWidth();
    r.setHeight(h);

    std::cout << "Expected area: " << (w*h) << ", got " << r.area() << "\n";
  //area is not well defined in squares
  //every square IS-A rectangle, but does not behave-like a rectangle
}

int main()
{
    Rectangle rectangle{3,4};
    Square square{5};
    process(rectangle);
    process(square);
}
Enter fullscreen mode Exit fullscreen mode

Right

// If you made Square derive from Rectangle, 
//then a Square should be usable anywhere you expect a rectangle

#include <iostream>

Rectangle::Rectangle(const unsigned width, const unsigned height):
    m_width(width),
    m_height(height)
{
}

void Rectangle:changeWidth(const unsigned width)
{
  /*Width and Height are independant*/
    m_width = width;
    // We should discuss if it is good to mutate 
    // and not create a new Figure
}

void Rectangle::changeHeight(const unsigned height)
{
    m_height = height;
}

unsigned Rectangle::area() const
{ 
    return m_height * m_width;
}

//No inheritance
Square::Square(const unsigned size):
    m_size(size)
{
}

unsigned Square::area() const
{ 
    return m_size * m_size;
}

void Square::changeSize(const unsigned size)
{
    m_size = size; 
}

void testRectangleChange(Rectangle& r)
{
    unsigned h = 10;
    auto w = r.getWidth();
    r.setHeight(h);

    std::cout << "Expected area: " << (w*h) << ", got " << r.area() << "\n";
  //area is not well defined in squares
  //every square IS-A rectangle, but does not behave-like a rectangle
}

int main()
{
    Rectangle rectangle{3,4};
    Square square{5};
    testRectangleChange(rectangle);
    testRectangleChange(square);
}
Enter fullscreen mode Exit fullscreen mode

Detection

[X] Manual

This is a semantic smell.

Tags

  • Inheritance

Conclusion

Real Number IS-A Complex number (according to math).

Integer IS-A Real number (according to math).

Real Number does not Behave-Like-A Complex number.

We cannot do real.setImaginaryPart() so it is not a Complex according to our Bijection

Relations

More Info

Credits

Photo by Joshua Rondeau on Unsplash


DRY - Don't Repeat Yourself - Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

Andy Hunt


This article is part of the CodeSmell Series.

Discussion (4)

Collapse
yoursunny profile image
Junxiao Shi

I thought setter is a code smell and you are using it!

Collapse
mcsee profile image
Maxi Contieri Author

you are absolutely right.
I used an online example
I am going to change it

thanks for the comment

Collapse
codenameone profile image
Shai Almog

Inheritance reduces duplication in code. In fact the original "wrong" approach can be written with less code and allows for polymorphism which is a core tenant of OOP. E.g. in Java Shape is a base interface for many shape objects.

The problem with IS A relates to overuse and detachment from semantics. Using it instead of composition etc. But having a common based class or hierarchy, that's good.

Collapse
mcsee profile image
Maxi Contieri Author

Yes!

That is exactly why this is just a "smell" and not a directive.
I agree with you overuse and detachment from semantics are the symptoms