리스코프 치환 원칙을 지킨 직사각형-정사각형 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