C#의 패턴 일치 기능은 알고리즘을 표현하는 구문을 제공합니다. 이러한 기술을 사용하여 클래스에서 동작을 구현할 수 있습니다. 개체 지향 클래스 디자인을 데이터 지향 구현과 결합하여 실제 개체를 모델링하는 동안 간결한 코드를 제공할 수 있습니다.
이 자습서에서는 다음 방법을 알아봅니다.
- 데이터 패턴을 사용하여 개체 지향 클래스를 표현합니다.
- C#의 패턴 일치 기능을 사용하여 이러한 패턴을 구현합니다.
- 컴파일러 진단을 활용하여 구현의 유효성을 검사합니다.
필수 구성 요소
- 최신 .NET SDK
- Visual Studio Code 편집기
- C# 개발 키트
운하 잠금 시뮬레이션 빌드
이 자습서에서는 운하 잠금시뮬레이션하는 C# 클래스를 빌드합니다. 간단히 말해, 수문은 높이가 다른 두 수역 사이를 이동할 때 보트를 올리고 내리는 장치입니다. 잠금에는 두 개의 게이트와 수위를 변경하는 몇 가지 메커니즘이 있습니다.
보통의 경우, 보트는 수문 중 하나로 들어가고, 이때 수문의 수위는 보트가 들어오는 쪽의 수위와 일치합니다. 수문에 들어가면, 수위가 조정되어 보트가 수문을 나갈 때의 수위와 일치합니다. 수위가 해당 측과 일치하면 출구 쪽의 게이트가 열립니다. 안전 조치는 운영자가 운하에서 위험한 상황을 만들 수 없도록합니다. 두 게이트가 모두 닫혀 있는 경우에만 수위를 변경할 수 있습니다. 최대 하나의 게이트를 열 수 있습니다. 게이트를 열려면 자물쇠의 수위가 열리는 게이트 외부의 수위와 일치해야 합니다.
C# 클래스를 빌드하여 이 동작을 모델링할 수 있습니다.
CanalLock
클래스는 게이트를 열거나 닫는 명령을 지원합니다. 물을 올리거나 낮추는 다른 명령이 있을 것입니다. 또한 클래스는 게이트와 수위의 현재 상태를 읽는 속성을 지원해야 합니다. 당신의 메서드는 안전 조치를 구현합니다.
클래스 정의
CanalLock
클래스를 테스트하는 콘솔 애플리케이션을 빌드합니다. Visual Studio 또는 .NET CLI를 사용하여 .NET 5용 새 콘솔 프로젝트를 만듭니다. 그런 다음 새 클래스를 추가하고 CanalLock
으로 명명하세요. 다음으로, 공용 API를 디자인하지만 메서드는 구현되지 않은 상태로 둡니다.
public enum WaterLevel
{
Low,
High
}
public class CanalLock
{
// Query canal lock state:
public WaterLevel CanalLockWaterLevel { get; private set; } = WaterLevel.Low;
public bool HighWaterGateOpen { get; private set; } = false;
public bool LowWaterGateOpen { get; private set; } = false;
// Change the upper gate.
public void SetHighGate(bool open)
{
throw new NotImplementedException();
}
// Change the lower gate.
public void SetLowGate(bool open)
{
throw new NotImplementedException();
}
// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
throw new NotImplementedException();
}
public override string ToString() =>
$"The lower gate is {(LowWaterGateOpen ? "Open" : "Closed")}. " +
$"The upper gate is {(HighWaterGateOpen ? "Open" : "Closed")}. " +
$"The water level is {CanalLockWaterLevel}.";
}
앞의 코드는 두 게이트가 모두 닫히고 수위가 낮도록 개체를 초기화합니다. 다음으로 Main
메서드에 다음 테스트 코드를 작성하여, 이 코드가 클래스의 첫 번째 구현을 만들 때 안내 역할을 하도록 합니다.
// Create a new canal lock:
var canalGate = new CanalLock();
// State should be doors closed, water level low:
Console.WriteLine(canalGate);
canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate: {canalGate}");
Console.WriteLine("Boat enters lock from lower gate");
canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate: {canalGate}");
canalGate.SetWaterLevel(WaterLevel.High);
Console.WriteLine($"Raise the water level: {canalGate}");
canalGate.SetHighGate(open: true);
Console.WriteLine($"Open the higher gate: {canalGate}");
Console.WriteLine("Boat exits lock at upper gate");
Console.WriteLine("Boat enters lock from upper gate");
canalGate.SetHighGate(open: false);
Console.WriteLine($"Close the higher gate: {canalGate}");
canalGate.SetWaterLevel(WaterLevel.Low);
Console.WriteLine($"Lower the water level: {canalGate}");
canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate: {canalGate}");
Console.WriteLine("Boat exits lock at upper gate");
canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate: {canalGate}");
다음으로, CanalLock
클래스에서 각 메서드의 첫 번째 구현을 추가합니다. 다음 코드는 안전 규칙에 대한 우려 없이 클래스의 메서드를 구현합니다. 나중에 안전 테스트를 추가합니다.
// Change the upper gate.
public void SetHighGate(bool open)
{
HighWaterGateOpen = open;
}
// Change the lower gate.
public void SetLowGate(bool open)
{
LowWaterGateOpen = open;
}
// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
CanalLockWaterLevel = newLevel;
}
지금까지 작성한 테스트는 통과합니다. 기본 사항을 구현했습니다. 이제 첫 번째 실패 조건에 대한 테스트를 작성합니다. 이전 테스트가 끝나면 두 게이트가 모두 닫히고 수위가 낮게 설정됩니다. 상위 게이트를 여는 테스트를 추가합니다.
Console.WriteLine("=============================================");
Console.WriteLine(" Test invalid commands");
// Open "wrong" gate (2 tests)
try
{
canalGate = new CanalLock();
canalGate.SetHighGate(open: true);
}
catch (InvalidOperationException)
{
Console.WriteLine("Invalid operation: Can't open the high gate. Water is low.");
}
Console.WriteLine($"Try to open upper gate: {canalGate}");
게이트가 열리기 때문에 이 테스트가 실패합니다. 첫 번째 구현에서는 다음 코드로 수정할 수 있습니다.
// Change the upper gate.
public void SetHighGate(bool open)
{
if (open && (CanalLockWaterLevel == WaterLevel.High))
HighWaterGateOpen = true;
else if (open && (CanalLockWaterLevel == WaterLevel.Low))
throw new InvalidOperationException("Cannot open high gate when the water is low");
}
테스트가 통과합니다. 그러나 더 많은 테스트를 추가하면 if
절을 더 추가하고 다른 속성을 테스트합니다. 곧 조건부를 더 추가하면 이러한 메서드가 너무 복잡해집니다.
패턴을 사용하여 명령 구현
더 좋은 방법은
새 설정 | 게이트 상태 | 수위 | 결과 |
---|---|---|---|
닫힘 상태 | 닫힘 상태 | 높다 | 닫힘 상태 |
닫힘 상태 | 닫힘 상태 | 낮음 | 닫힘 상태 |
닫힘 상태 | 열기 | 높음 | 닫힘 상태 |
|
|
|
|
열다 | 닫힘 상태 | 높다 | 열기 |
열다 | 닫힘 상태 | 낮다 | 닫힘(오류) |
열기 | 열다 | 높다 | 열기 |
|
|
|
|
테이블의 네 번째 행과 마지막 행은 유효하지 않기 때문에 텍스트에 취소선이 그어져 있습니다. 지금 추가하는 코드는 물이 낮을 때 높은 수문이 열리지 않도록 해야 합니다. 상태들은 (false
가 "Closed"를 나타내는 것을 기억하면서) 단일 스위치 표현식으로 코딩될 수 있습니다.
HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
{
(false, false, WaterLevel.High) => false,
(false, false, WaterLevel.Low) => false,
(false, true, WaterLevel.High) => false,
(false, true, WaterLevel.Low) => false, // should never happen
(true, false, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
(true, true, WaterLevel.High) => true,
(true, true, WaterLevel.Low) => false, // should never happen
};
이 버전을 사용해 보세요. 테스트가 통과하면 코드가 유효하다는 것을 입증합니다. 전체 표에는 입력 및 결과의 가능한 조합이 표시됩니다. 즉, 사용자와 다른 개발자는 테이블을 빠르게 살펴보고 가능한 모든 입력을 다루었는지 확인할 수 있습니다. 컴파일러가 도움을 주어서 더 쉬울 수 있습니다. 이전 코드를 추가한 후 컴파일러에서 경고를 생성하는 것을 볼 수 있습니다. CS8524 스위치 식이 가능한 모든 입력을 포함하지 않음을 나타냅니다. 이 경고의 이유는 입력 중 하나가 enum
형식이기 때문입니다. 컴파일러는 "가능한 모든 입력"을 기본 형식의 모든 입력(일반적으로 int
)으로 해석합니다. 이 switch
식은 enum
에 선언된 값을 확인만 합니다. 경고를 제거하려면, 표현식의 마지막 부분에 모든 것을 포괄하는 무시 패턴을 추가할 수 있습니다. 이 조건은 잘못된 입력을 나타내므로 예외를 throw합니다.
_ => throw new InvalidOperationException("Invalid internal state"),
앞의 스위치 암은 모든 입력과 일치하므로 switch
식에서 마지막이어야 합니다. 순서를 앞당겨 실험해 보세요. 패턴에서 도달할 수 없는 코드에 대해 컴파일러 오류 CS8510이 발생합니다. 스위치 식의 자연스러운 구조를 사용하면 컴파일러가 가능한 실수에 대한 오류 및 경고를 생성할 수 있습니다. 컴파일러 "safety net"을 사용하면 더 적은 수의 반복에서 올바른 코드를 쉽게 만들 수 있으며 스위치 암과 와일드카드를 자유롭게 결합할 수 있습니다. 컴파일러는 예상하지 못한 분기가 접근 불가능한 경우 오류를 발생시키고, 필요한 분기를 제거하는 상황에서 경고를 발생합니다.
첫 번째 변경은 게이트를 닫으라는 명령이 있을 때 모든 팔을 결합하는 것입니다. 이는 항상 허용됩니다. 스위치 식의 첫 번째 분기로 다음 코드를 추가합니다.
(false, _, _) => false,
이전 스위치 암을 추가하면, 명령 false
이 각 암에 하나씩 총 4개의 컴파일러 오류를 발생시킵니다. 그 팔들은 이미 새로 추가된 팔에 의해 덮여있다. 이 네 줄을 안전하게 제거할 수 있습니다. 이 새로운 스위치 암은 그 조건들을 대체하기 위한 의도로 만들어졌습니다.
다음으로, 게이트를 여는 명령이 있는 네 개의 팔을 단순화할 수 있습니다. 수위가 높은 두 경우 모두 게이트를 열 수 있습니다. (하나는 이미 열려 있습니다.) 수위가 낮은 경우에는 예외가 발생하고, 다른 경우는 발생하지 않아야 합니다. 물 잠금이 이미 잘못된 상태인 경우, 동일한 예외를 다시 던져도 안전해야 합니다. 이 암들에 대해 다음과 같은 단순화를 할 수 있습니다.
(true, _, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),
테스트를 다시 실행하면 통과합니다.
SetHighGate
메서드의 최종 버전은 다음과 같습니다.
// Change the upper gate.
public void SetHighGate(bool open)
{
HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
{
(false, _, _) => false,
(true, _, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),
};
}
직접 패턴 구현
이제 기술을 살펴보았으므로 SetLowGate
와 SetWaterLevel
메서드를 직접 작성해보세요. 먼저 다음 코드를 추가하여 해당 메서드에서 잘못된 작업을 테스트합니다.
Console.WriteLine();
Console.WriteLine();
try
{
canalGate = new CanalLock();
canalGate.SetWaterLevel(WaterLevel.High);
canalGate.SetLowGate(open: true);
}
catch (InvalidOperationException)
{
Console.WriteLine("invalid operation: Can't open the lower gate. Water is high.");
}
Console.WriteLine($"Try to open lower gate: {canalGate}");
// change water level with gate open (2 tests)
Console.WriteLine();
Console.WriteLine();
try
{
canalGate = new CanalLock();
canalGate.SetLowGate(open: true);
canalGate.SetWaterLevel(WaterLevel.High);
}
catch (InvalidOperationException)
{
Console.WriteLine("invalid operation: Can't raise water when the lower gate is open.");
}
Console.WriteLine($"Try to raise water with lower gate open: {canalGate}");
Console.WriteLine();
Console.WriteLine();
try
{
canalGate = new CanalLock();
canalGate.SetWaterLevel(WaterLevel.High);
canalGate.SetHighGate(open: true);
canalGate.SetWaterLevel(WaterLevel.Low);
}
catch (InvalidOperationException)
{
Console.WriteLine("invalid operation: Can't lower water when the high gate is open.");
}
Console.WriteLine($"Try to lower water with high gate open: {canalGate}");
애플리케이션을 다시 실행합니다. 새 테스트가 실패하고 운하 잠금이 잘못된 상태로 전환되는 것을 볼 수 있습니다. 나머지 메서드를 직접 구현해 보세요. 하부 게이트를 설정하는 방법은 상부 게이트를 설정하는 방법과 유사해야 합니다. 수위를 변경하는 메서드는 검사가 다르지만 비슷한 구조를 따라야 합니다. 수위를 설정하는 방법에 대해 동일한 프로세스를 사용하는 것이 유용할 수 있습니다. 두 게이트의 상태, 수위의 현재 상태 및 요청된 새 수위의 네 가지 입력으로 시작합니다. switch 식은 다음으로 시작해야 합니다.
CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
// elided
};
총 16개의 스위치 암을 채워야 합니다. 그런 다음 테스트하고 단순화합니다.
다음과 같은 메서드를 만들셨나요?
// Change the lower gate.
public void SetLowGate(bool open)
{
LowWaterGateOpen = (open, LowWaterGateOpen, CanalLockWaterLevel) switch
{
(false, _, _) => false,
(true, _, WaterLevel.Low) => true,
(true, false, WaterLevel.High) => throw new InvalidOperationException("Cannot open low gate when the water is high"),
_ => throw new InvalidOperationException("Invalid internal state"),
};
}
// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
(WaterLevel.Low, WaterLevel.Low, true, false) => WaterLevel.Low,
(WaterLevel.High, WaterLevel.High, false, true) => WaterLevel.High,
(WaterLevel.Low, _, false, false) => WaterLevel.Low,
(WaterLevel.High, _, false, false) => WaterLevel.High,
(WaterLevel.Low, WaterLevel.High, false, true) => throw new InvalidOperationException("Cannot lower water when the high gate is open"),
(WaterLevel.High, WaterLevel.Low, true, false) => throw new InvalidOperationException("Cannot raise water when the low gate is open"),
_ => throw new InvalidOperationException("Invalid internal state"),
};
}
테스트가 통과되어야 하며 운하 잠금 장치가 안전하게 작동해야 합니다.
요약
이 자습서에서는 패턴 일치를 사용하여 해당 상태에 변경 내용을 적용하기 전에 개체의 내부 상태를 확인하는 방법을 알아보았습니다. 속성의 조합을 확인할 수 있습니다. 이러한 전환에 대한 테이블을 빌드한 후에는 코드를 테스트한 다음 가독성 및 유지 관리를 간소화합니다. 이러한 초기 리팩터링에서 내부 상태의 유효성을 검사하거나 다른 API 변경 내용을 관리하는 추가 리팩터링을 제안할 수 있습니다. 이 자습서에서는 클래스와 개체를 보다 데이터 지향적인 패턴 기반 접근 방식과 결합하여 해당 클래스를 구현합니다.
.NET