로버트 마틴[참고 1]이 쓴 의존관계 역전 원칙[참고 2]에서 Button-Lamp 예를 C#으로 이해하기
이철우
이 글은 로버트 마틴[참고 1]이 쓴 - The Dependency Inversion Principle[참고 2] - 안에 작은 제목 'A Simple Example’에 있는 Button-Lamp 예를 C#으로 풀어놓은 것이다. 의존관계 역전 원칙의 자세한 내용에 대해서는 [참고 2]를 참고하기 바란다. 그 원칙은 간략히 다음과 같다.
첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
둘째, 추상화는 세부사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.
[참고2]의 [그림 4]
전통적인 모듈 또는 라이브러리가 하위 모듈 재활용에 중점을 둔 것이라면, 객체지향의 의존관계 역전 원칙은 열고 닫기 원칙과 더불어 상위 모듈 재활용에 중점을 둔 것이라 한다.
신호 'on’이 들어오면 'Lamp On’을 출력하고, 'off’가 들어오면 'Lamp Off’를 출력한다. 이것을 LampService 라고 부르겠다. 서비스를 정지하려면 'stop’을 입력한다. LampService.cs, Program.cs 이렇게 두 파일로 첫 프로젝트를 구성하며, LampService.cs를 분리해가며 프로젝트를 더 만들 것이다. 세 번째 프로젝트에서 의존관계 역전 원칙을, 네 번째 프로젝트에서 Adapter 패턴을, 다섯 번째 프로젝트에서 열고 닫기 원칙을 적용할 것이다. 프로젝트가 진행될 수록 LampService가 변화에 더 유연해짐을 알 수 있다.
한 파일에 모두
// LampService.cs
public class LampService
{
private bool _isStopRequired = false;
public LampService()
{
}
public void Run()
{
while (!_isStopRequired)
{
var input = Console.ReadLine();
switch (input)
{
case "stop":
_isStopRequired = true;
break;
case "on":
Console.WriteLine("Lamp On.");
break;
case "off":
Console.WriteLine("Lamp Off.");
break;
default:
Console.WriteLine(input);
break;
}
}
}
}
// Program.cs
Console.WriteLine("Hello, World!");
new LampService().Run();
Console.WriteLine("Bye.");
버튼과 램프 분리
// NaiveLamp.cs
public class NaiveLamp
{
public void TurnOn()
{
Console.WriteLine("Lamp On.");
}
public void TurnOff()
{
Console.WriteLine("Lamp Off.");
}
}
// NaiveButton.cs
public class NaiveButton
{
private readonly NaiveLamp _naiveLamp;
private string _signal = string.Empty;
public NaiveButton(NaiveLamp naiveLamp)
{
_naiveLamp = naiveLamp;
}
public void Detect(string signal)
{
_signal = signal;
var buttonOn = GetPhysicalState();
if (buttonOn)
{
_naiveLamp.TurnOn();
}
else
{
_naiveLamp.TurnOff();
}
}
private bool GetPhysicalState()
{
return _signal.Equals("on");
}
}
// LampService.cs
public class LampService
{
private bool _isStopRequired = false;
private readonly NaiveButton _button;
public LampService(NaiveButton button)
{
_button = button;
}
public void Run()
{
while (!_isStopRequired)
{
var input = Console.ReadLine();
switch (input)
{
case "stop":
_isStopRequired = true;
break;
case "on":
case "off":
_button.Detect(input);
break;
default:
Console.WriteLine(input);
break;
}
}
}
}
// Program.cs
Console.WriteLine("Hello, World!");
new LampService(new NaiveButton(new NaiveLamp())).Run();
Console.WriteLine("Bye.");
의존관계 역전 원칙을 적용
// ILamp.cs
public interface ILamp
{
void TurnOn();
void TurnOff();
}
// ButtonAbstract.cs
public abstract class ButtonAbstract
{
private readonly ILamp _lamp;
protected string Signal { get; private set; } = string.Empty;
protected ButtonAbstract(ILamp lamp)
{
_lamp = lamp;
}
public void Detect(string signal)
{
Signal = signal;
var buttonOn = GetPhysicalState();
if (buttonOn)
{
_lamp.TurnOn();
}
else
{
_lamp.TurnOff();
}
}
protected abstract bool GetPhysicalState();
}
// InvertedLamp.cs
public class InvertedLamp : ILamp
{
public void TurnOn()
{
Console.WriteLine("Lamp On.");
}
public void TurnOff()
{
Console.WriteLine("Lamp Off.");
}
}
// InvertedButton.cs
public class InvertedButton : ButtonAbstract
{
public InvertedButton(ILamp lamp) : base(lamp)
{
}
protected override bool GetPhysicalState()
{
return Signal.Equals("on");
}
}
// LampService.cs
public class LampService
{
private bool _isStopRequired = false;
private readonly ButtonAbstract _button;
public LampService(ButtonAbstract button)
{
_button = button;
}
public void Run()
{
while (!_isStopRequired)
{
var input = Console.ReadLine();
switch (input)
{
case "stop":
_isStopRequired = true;
break;
case "on":
case "off":
_button.Detect(input);
break;
default:
Console.WriteLine(input);
break;
}
}
}
}
// Program.cs
Console.WriteLine("Hello, World!");
new LampService(new InvertedButton(new InvertedLamp())).Run();
Console.WriteLine("Bye.");
Adapter 패턴 적용
먼저 프로젝트에 OtherLamp, OtherLampAdapter를 추가하고 실행을 위해 Program.cs를 바꾸자.
// OtherLamp.cs
public class OtherLamp
{
public void SwitchOn()
{
Console.WriteLine("Lamp On.");
}
public void SwitchOff()
{
Console.WriteLine("Lamp Off.");
}
}
// OtherLampAdapter.cs
public class OtherLampAdapter : ILamp
{
private readonly OtherLamp _lamp = new OtherLamp();
public void TurnOn()
{
_lamp.SwitchOn();
}
public void TurnOff()
{
_lamp.SwitchOff();
}
}
// Program.cs
Console.WriteLine("Hello, World!");
new LampService(new InvertedButton(new OtherLampAdapter())).Run();
Console.WriteLine("Bye.");
열고 닫기 원칙을 적용
InvertedButton의 메서드 GetPhysicalState를 더 유연하게 바꾸자.
// IGetPhysicalState.cs
public interface IGetPhysicalState
{
bool GetPhysicalState(string signal, string criteria);
}
// PhysicalStateGetter.cs
public class PhysicalStateGetter : IGetPhysicalState
{
public bool GetPhysicalState(string signal, string criteria)
{
return signal.Equals(criteria);
}
}
// ILamp.cs
public interface ILamp
{
void TurnOn();
void TurnOff();
}
// ButtonAbstract.cs
public abstract class ButtonAbstract
{
private readonly ILamp _lamp;
private readonly IGetPhysicalState _physicalStateGetter;
protected ButtonAbstract(ILamp lamp, IGetPhysicalState physicalStateGetter)
{
_lamp = lamp;
_physicalStateGetter = physicalStateGetter;
}
public void Detect(string signal, string criteria)
{
var buttonOn = _physicalStateGetter.GetPhysicalState(signal, criteria);
if (buttonOn)
{
_lamp.TurnOn();
}
else
{
_lamp.TurnOff();
}
}
}
// InvertedLamp.cs
public class InvertedLamp : ILamp
{
public void TurnOn()
{
Console.WriteLine("Lamp On.");
}
public void TurnOff()
{
Console.WriteLine("Lamp Off.");
}
}
// InvertedButton.cs
public class InvertedButton : ButtonAbstract
{
public InvertedButton(ILamp lamp, IGetPhysicalState physicalStateGetter) : base(lamp, physicalStateGetter)
{
}
}
// LampService.cs
public class LampService
{
private bool _isStopRequired = false;
private readonly ButtonAbstract _button;
public LampService(ButtonAbstract button)
{
_button = button;
}
public void Run(string criteria)
{
while (!_isStopRequired)
{
var input = Console.ReadLine();
switch (input)
{
case "stop":
_isStopRequired = true;
break;
case "on":
case "off":
_button.Detect(input, criteria);
break;
default:
Console.WriteLine(input);
break;
}
}
}
}
// Program.cs
Console.WriteLine("Hello, World!");
new LampService(new InvertedButton(new InvertedLamp(), new PhysicalStateGetter())).Run("on");
Console.WriteLine("Bye.");
이 글을 쓰며 의존관계 역전 원칙의 두 번째에서 '세부사항’이 무엇인가 고민을 했다. 버튼은 상위 모듈, 램프는 하위 모듈이라 할 수 있는데, '세부사항’을 딱 골라낼 수 없었다. 나름 이렇게 정리했다.
추상화는 추상화에 의존해야 한다.