C에서 SOLID의 Liskov 대체 원리 단순화#
Liskov Substitution Principle에 따르면 파생 클래스의 개체는 시스템에 오류를 발생시키거나 기본 클래스의 동작을 수정하지 않고 기본 클래스의 개체를 대체할 수 있어야 합니다.
이 기사를 쓰기 전에 Pluralsight를 사용하여 동일한 주제에 대해 훌륭한 과정을 진행해 준 Steve Smith에게 감사를 표하고 싶습니다. 이 게시물은 그 과정에서 영감을 받았습니다.
Liskov Substitution Principle에 따르면 파생 클래스의 개체는 시스템에 오류를 발생시키거나 기본 클래스의 동작을 수정하지 않고 기본 클래스의 개체를 대체할 수 있어야 합니다.
요컨대, S가 T의 부분 집합인 경우 T의 객체는 프로그램에 영향을 주지 않고 시스템에 오류를 일으키지 않고 S의 객체로 대체될 수 있습니다. Rectangle 클래스와 Square 클래스가 있다고 가정 해 보겠습니다. Square는 Rectangle, 즉 Rectangle 클래스를 상속합니다. 따라서 Liskov Substitution 원칙에 따르면 시스템에 바람직하지 않은 변경이나 오류를 가져오지 않고 Rectangle 의 object 를 Square 의 object 로 바꿀 수 있어야 합니다.

몇 가지 예를 들어 이 원칙을 자세히 살펴보겠습니다.
Understanding the problem
Rectangle과 Square라는 두 개의 클래스가 있다고 가정해 보겠습니다. 이 예제에서 Square 클래스는 Rectangle 클래스를 상속합니다. 두 클래스 모두 다음과 같이 생성됩니다.
public class Rectangle
{
public virtual int Height { get; set; }
public virtual int Width { get; set; }
}
Square 클래스는 Rectangle 클래스를 상속하고 아래 목록과 같이 속성을 재정의합니다.
public class Square : Rectangle
{
private int _height;
private int _width;
public override int Height
{
get
{
return _height;
}
set
{
_height = value;
_width = value;
}
}
public override int Width
{
get
{
return _width;
}
set
{
_width = value;
_height = value;
}
}
}
Rectangle과 Square의 면적을 계산해야 합니다. 이를 위해 AreaCalculator라는 다른 클래스를 만들어 보겠습니다.
public class AreaCalculator
{
public static int CalculateArea(Rectangle r)
{
return r.Height * r.Width;
}
public static int CalculateArea(Square s)
{
return s.Height * s.Height;
}
}
계속해서 Rectangle과 Square의 면적을 계산하는 Unit 테스트를 작성해 보겠습니다. 아래 목록에 표시된 대로 이러한 영역을 계산하는 단위 테스트를 통과해야 합니다.
[TestMethod]
public void Sixfor2x3Rectangle()
{
var myRectangle = new Rectangle { Height = 2, Width = 3 };
var result = AreaCalculator.CalculateArea(myRectangle);
Assert.AreEqual(6, result);
}
반면에 사각형의 면적을 계산하는 테스트도 통과해야 합니다.
[TestMethod]
public void Ninefor3x3Squre()
{
var mySquare = new Square { Height = 3 };
var result = AreaCalculator.CalculateArea(mySquare);
Assert.AreEqual(9, result);
}
두 테스트 모두에서 다음을 만들고 있습니다.
1. Rectangle의 면적을 찾는 Rectangle의 개체
2. 광장의 면적을 찾는 광장의 개체
그리고 테스트는 예상대로 통과합니다. 이제 Rectangle의 개체를 Square의 개체로 대체하는 테스트를 만들어 보겠습니다. Square의 객체를 사용하여 Rectangle의 면적을 찾고 이에 대한 단위 테스트는 다음과 같습니다.
[TestMethod]
public void TwentyFourfor4x6RectanglefromSquare()
{
Rectangle newRectangle = new Square();
newRectangle.Height = 4;
newRectangle.Width = 6;
var result = AreaCalculator.CalculateArea(newRectangle);
Assert.AreEqual(24, result);
}
위의 테스트는 예상 결과가 24이기 때문에 실패하지만 계산된 실제 면적은 36입니다.
이것이 문제입니다. Square 클래스는 Rectangle 클래스의 하위 집합이지만 Object of Rectangle 클래스는 시스템에서 문제를 일으키지 않고 Square 클래스의 개체로 대체할 수 없습니다. 시스템이 Liskov Substitution Principle을 준수했다면 위의 문제를 피할 수 있습니다.
No-Inheritance 문제 해결
아래 단계에 따라 위의 문제를 해결할 수 있습니다.
- AreaCalculator 클래스를 제거합니다.
- Let each shape define its own Area method.
- Square 클래스가 Rectangle 클래스를 상속하는 대신 공통 추상 기본 클래스 Shape를 만들고 두 클래스 모두 이를 상속합니다.
일반적인 기본 클래스 Shape는 아래 목록과 같이 만들 수 있습니다.
public abstract class Shape
{
}
Next, the Rectangle class can be rewritten as follows:
public class Rectangle :Shape
{
public int Height { get; set; }
public int Width { get; set; }
public int Area()
{
return Height * Width;
}
}
그리고 Square 클래스는 아래 목록과 같이 다시 작성할 수 있습니다.
public class Square : Shape
{
public int Sides;
public int Area()
{
return Sides * Sides;
}
}
이제 아래 목록과 같이 Rectangle 클래스의 area 함수에 대한 단위 테스트를 작성할 수 있습니다.
[TestMethod]
public void Sixfor2x3Rectangle()
{
var myRectangle = new Rectangle { Height = 2, Width = 3 };
var result = myRectangle.Area();
Assert.AreEqual(6, result);
}
위의 테스트는 어려움 없이 통과해야 합니다. 같은 방식으로 아래 목록과 같이 Square 클래스의 Area 함수를 단위 테스트할 수 있습니다.
[TestMethod]
public void Ninefor3x3Squre()
{
var mySquare = new Square { Sides = 3 };
var result = mySquare.Area();
Assert.AreEqual(9, result);
}
다음으로 Shape의 object를 Rectangle 및 Square의 개체로 대체하는 테스트를 작성해 보겠습니다.
public void TwentyFourfor4x6Rectangleand9for3x3Square()
{
var shapes = new List<Shape>{
new Rectangle{Height=4,Width=6},
new Square{Sides=3}
};
var areas = new List<int>();
foreach(Shape shape in shapes){
if(shape.GetType()==typeof(Rectangle))
{
areas.Add(((Rectangle)shape).Area());
}
if (shape.GetType() == typeof(Square))
{
areas.Add(((Square)shape).Area());
}
}
Assert.AreEqual(24, areas[0]);
Assert.AreEqual(9, areas[1]);
}
위의 테스트를 통과하고 시스템에 영향을 주지 않고 개체를 성공적으로 대체할 수 있습니다. 그러나 위의 접근 방식에는 한 가지 문제가 있습니다 : 우리는 개폐 원칙을 위반하고 있습니다. 새 클래스가 Shape 클래스를 상속할 때마다 조건이 테스트에 있고 확실히 이를 원하지 않는 경우 하나를 더 추가해야 합니다.
위의 문제는 아래 목록과 같이 Shape 클래스를 수정하여 해결할 수 있습니다.
public abstract class Shape
{
public abstract int Area();
}
여기서 우리는 Shape 클래스에서 추상 메서드 Area를 이동했으며 각 하위 클래스는 Area 메서드에 고유한 정의를 제공합니다. Rectangle 및 Square 클래스는 아래 목록과 같이 수정할 수 있습니다.
public class Rectangle :Shape
{
public int Height { get; set; }
public int Width { get; set; }
public override int Area()
{
return Height * Width;
}
}
public class Square : Shape
{
public int Sides;
public override int Area()
{
return Sides * Sides;
}
}
여기서 위의 클래스는 Liskov Substitution 원칙을 따르며 아래 목록과 같이 "if" 조건 없이 테스트를 다시 작성할 수 있습니다.
[TestMethod]
public void TwentyFourfor4x6Rectangleand9for3x3Square()
{
var shapes = new List<Shape>{
new Rectangle{Height=4,Width=6},
new Square{Sides=3}
};
var areas = new List<int>();
foreach (Shape shape in shapes)
{
areas.Add(shape.Area());
}
Assert.AreEqual(24, areas[0]);
Assert.AreEqual(9, areas[1]);
}
이런 식으로 Liskov Substitution Principle을 준수하여 하위 클래스와 기본 클래스 간의 관계를 만들 수 있습니다. LS 원칙 위반을 식별하는 일반적인 방법은 다음과 같습니다.
- 서브 클래스에서 구현되지 않은 메소드입니다.
- Subclass 함수는 기본 클래스 메서드를 재정의하여 새로운 의미를 부여합니다.
이 게시물이 유용하기를 바랍니다 – 읽어 주셔서 감사하고 즐거운 코딩 되세요!
고성능 컨트롤을 갖춘 데스크톱, 모바일 또는 웹 애플리케이션을 구축하고 싶으신가요? 지금 Ultimate 무료 평가판을 다운로드하고 풍부한 기능으로 앱 개발의 새로운 차원에 도달하십시오. Blazor 구성 요소 라이브러리, Angular 라이브러리, 그리고 더.
