리스코프 치환 원칙을 지킨 직사각형-정사각형 C# 예

리스코프 치환 원칙을 지킨 직사각형-정사각형 C# 예

이철우

리스코프 치환 원칙[참고 1]은 C# - 객체지향 언어 - 에서는 클래스 A와 이것을 물려받은 클래스 B가 있을 때,

A ← B

‘A의 인스탄스가 적용된 곳에 B의 인스탄스를 적용할 수 있다.’

또는

‘B의 인스탄스는 A의 구성원을 사용할 수 있다.’

이렇게 보통 이해하는 듯 하다. 이것은 객체지향의 '물려받기’를 다시 설명한 '중복’이란 생각이 든다.

인터넷에 리스코프 치환 원칙을 깬 직사각형-정사각형 예가 많이 있는데, 여기서는 '물려받기’를 잘 활용하여 리스코프 치환 원칙을 지킨 예를 DotNet 8 C# Console 프로젝트로 보여주겠다.

직사각형, 직사각형을 물려받은 정사각형 두 클래스 모두 불변으로 하고, 직사각형 생성자는 이웃하는 두 변의 길이가 다를 때, 같을 때 이렇게 두 개를 만들었다.

// Rectangle.cs
public class Rectangle
{
    private readonly double[] _sides = new double[2];
    public double Width => _sides[0];
    public double Height => _sides[1];
 
    public Rectangle(double width, double height)
    {
        if (width <= 0.0D)
        {
            throw new ArgumentOutOfRangeException();
        }
        if (height <= 0.0D)
        {
            throw new ArgumentOutOfRangeException();
        }
        
        _sides[0] = width;
        _sides[1] = height;
    }

    public Rectangle(double side) : this(side, side)
    {
    }

    public double GetArea()
    {
        return Width * Height;
    }

    public bool AreSidesEqual()
    {
        return Math.Abs(Width - Height) == 0.0D;
    }
    public RectangleType GetRectangleType()
    {
        return AreSidesEqual() ? RectangleType.Rectangle | RectangleType.Square : RectangleType.Rectangle;
    }
    
    public override string ToString()
    {
        return $"{GetRectangleType()} {Width} {Height}";
    }

    [Flags]
    public enum RectangleType : byte
    {
        Rectangle = 1,
        Square = 2
    }
}
// Square.cs
public class Square : Rectangle
{
    public double Side => Width;

    public Square(double side) : base(side)
    {
    }

    public Square(Rectangle rectangle) : this(rectangle.AreSidesEqual() ? rectangle.Width : throw new ArgumentException(nameof(Square)))
    {
    }

    public override string ToString()
    {
        return $"{GetRectangleType()} {Side}";
    }
}

직사각형-정사각형 예가 리스코프 치환 원칙을 지키는지 테스트 하는 코드가 아래에 있다.

// Program.cs
Console.WriteLine("Hello, World!");

var rectangle = GetRectangle(3.0D, 5.0D);
Console.WriteLine($"{rectangle} {GetArea(rectangle)}");

var square = GetRectangle(3.0D, 3.0D);
Console.WriteLine($"{square} {GetArea(square)}");

Console.WriteLine("Bye.");
return;

Rectangle GetRectangle(double width, double height)
{
    return Math.Abs(width - height) == 0.0D ? new Square(width) : new Rectangle(width, height);
}

double GetArea(Rectangle rectangle)
{
    return rectangle.GetArea();
}

두 클래스 사이에 적절한 물려받기를 설정한다면, 코드 중복을 피하고 코드를 재활용하게 되어 유지/보수 비용이 절감될 것이다. 다시 한 번 객체지향의 아름다움을 느낀다.

[참고 1] Liskov substitution principle

2 Likes

객체지향의 아름다움이라는 말이 여태 피상적으로 느껴졌는데 이 글을 보고 제대로 와닿았어요

1 Like