Open Closed Principle (OCP)는 software entities (classes 또는 methods)는 extension에 open되고 modification에 closed해야 한다는 원칙이다.
기본적으로, 코드가 변경이 필요하지 않도록 작성하려고 한다. 추가 고객 요청을 처리하기 위해 class를 수정하는 것이 아닌, class의 동작을 확장할 수 있다.
두 예를 통해서 OCP에 맞게 코드를 작성하는 방법을 살펴보자. 처음엔 OCP를 따르지 않게 작성하고, 이후에 OCP에 맞게 리팩토링하자.
월급 계산기 예
한 회사의 모든 개발자 월급의 총합을 계산하는 일을 해야한다.
우선, model class를 생성한다.
public class DeveloperReport
{
public int Id { get; set; }
public string Name { get; set; }
public string Level { get; set; }
public int WorkingHours { get; set; }
public double HourlyRate { get; set; }
}
public class SalaryCalculator
{
private readonly IEnumerable<DeveloperReport> _developerReports;
public SalaryCalculator(List <DeveloperReport> developerReports)
{
_developerReports = developerReports;
}
public double CalculateTotalSalaries()
{
double totalSalaries = 0D ;
foreach (var devReport in _developerReports)
{
totalSalaries += devReport.HourlyRate * devReport.WorkingHours;
}
return totalSalaries;
}
}
static void Main(string[] args)
{
var devReports = new List<DeveloperReport>
{
new DeveloperReport {Id = 1, Name = "Dev1", Level = "Senior developer", HourlyRate = 30.5, WorkingHours = 160 },
new DeveloperReport {Id = 2, Name = "Dev2", Level = "Junior developer", HourlyRate = 20, WorkingHours = 150 },
new DeveloperReport {Id = 3, Name = "Dev3", Level = "Senior developer", HourlyRate = 30.5, WorkingHours = 180 }
};
var calculator = new SalaryCalculator(devReports);
Console.WriteLine($"Sum of all the developer salaries is {calculator.CalculateTotalSalaries()} dollars");
}
잘 돌아간다. 그런데 상사가 와서 senior와 junior 개발자의 계산식이 달라야 한다고 한다. senior는 월급의 20%의 보너스가 포함된다고 한다.
이 요구사항을 처리하기 위해, CalculateTotalSalaries 함수를 다음과 같이 수정한다.
public double CalculateTotalSalaries()
{
double totalSalaries = 0D;
foreach (var devReport in _developerReports)
{
if(devReport.Level == "Senior developer")
{
totalSalaries += devReport.HourRate * devReport.WorkingHours * 1.2;
}
else
{
totalSalaries += devReport.HourRate * devReport.WorkingHours;
}
}
return totalSalaries;
}
이렇게 해도 정확한 결과를 얻을 수 있지만, 효율적인 해결책은 아니다.
왜냐하면, 완벽한 동작을 위해 기존의 class를 수정해야 한다. 상사가 와서 junior 도 동일하게 수정해야 한다고 하면 class를 또 수정해야 한다. 이는 OCP 기준에 완전히 반대된다.
다른 해결책을 찾아보자.
OCP가 구현된 월급 계산기 예
OCP를 적용하려면, abstract class를 우선 생성한다.
public abstract class BaseSalaryCalculator
{
protected DeveloperReport DeveloperReport { get; private set; }
public BaseSalaryCalculator(DeveloperReport developerReport)
{
DeveloperReport = developerReport;
}
public abstract double CalculateSalary();
}
public class SeniorDevSalaryCalculator : BaseSalaryCalculator
{
public SeniorDevSalaryCalculator(DeveloperReport report)
:base(report)
{
}
public override double CalculateSalary() => DeveloperReport.HourlyRate * DeveloperReport.WorkingHours * 1.2;
}
public class JuniorDevSalaryCalculator : BaseSalaryCalculator
{
public JuniorDevSalaryCalculator(DeveloperReport developerReport)
:base(developerReport)
{
}
public override double CalculateSalary() => DeveloperReport.HourlyRate * DeveloperReport.WorkingHours;
}
public class SalaryCalculator
{
private readonly IEnumerable<BaseSalaryCalculator> _developerCalculation;
public SalaryCalculator(IEnumerable<BaseSalaryCalculator> developerCalculation)
{
_developerCalculation = developerCalculation;
}
public double CalculateTotalSalaries()
{
double totalSalaries = 0D;
foreach (var devCalc in _developerCalculation)
{
totalSalaries += devCalc.CalculateSalary();
}
return totalSalaries;
}
}
class Program
{
static void Main(string[] args)
{
var devCalculations = new List<BaseSalaryCalculator>
{
new SeniorDevSalaryCalculator(new DeveloperReport {Id = 1, Name = "Dev1", Level = "Senior developer", HourlyRate = 30.5, WorkingHours = 160 }),
new JuniorDevSalaryCalculator(new DeveloperReport {Id = 2, Name = "Dev2", Level = "Junior developer", HourlyRate = 20, WorkingHours = 150 }),
new SeniorDevSalaryCalculator(new DeveloperReport {Id = 3, Name = "Dev3", Level = "Senior developer", HourlyRate = 30.5, WorkingHours = 180 })
};
var calculator = new SalaryCalculator(devCalculations);
Console.WriteLine($"Sum of all the developer salaries is {calculator.CalculateTotalSalaries()} dollars");
}
}
이제 다른 예를 살펴보자.
Filtering Computer Monitors
우리 가게에 있는 컴퓨터 모니터에 대한 필요한 정보를 제공하는 앱을 만들어보자. 여기서는 모니터의 종류와 스크린 사이즈 정보만 다룬다.
public enum MonitorType
{
OLED,
LCD,
LED
}
public enum Screen
{
WideScreen,
CurvedScreen
}
간단한 model class도 만든다.
public class ComputerMonitor
{
public string Name { get; set; }
public MonitorType Type { get; set; }
public Screen Screen { get; set; }
}
이제 모니터 종류로 필터링 하는 함수를 만든다.
public class MonitorFilter
{
public List<ComputerMonitor> FilterByType(IEnumerable<ComputerMonitor> monitors, MonitorType type) =>
monitors.Where(m => m.Type == type).ToList();
}
class Program
{
static void Main(string[] args)
{
var monitors = new List<ComputerMonitor>
{
new ComputerMonitor { Name = "Samsung S345", Screen = Screen.CurvedScreen, Type = MonitorType.OLED },
new ComputerMonitor { Name = "Philips P532", Screen = Screen.WideScreen, Type = MonitorType.LCD },
new ComputerMonitor { Name = "LG L888", Screen = Screen.WideScreen, Type = MonitorType.LED },
new ComputerMonitor { Name = "Samsung S999", Screen = Screen.WideScreen, Type = MonitorType.OLED },
new ComputerMonitor { Name = "Dell D2J47", Screen = Screen.CurvedScreen, Type = MonitorType.LCD }
};
var filter = new MonitorFilter();
var lcdMonitors = filter.FilterByType(monitors, MonitorType.LCD);
Console.WriteLine("All LCD monitors");
foreach (var monitor in lcdMonitors)
{
Console.WriteLine($"Name: {monitor.Name}, Type: {monitor.Type}, Screen: {monitor.Screen}");
}
}
}
잘 돌아간다. 며칠 후, 스크린 기능 필터도 필요하다고 요청이 왔다.
MonitorFilter class를 수정한다.
public class MonitorFilter
{
public List<ComputerMonitor> FilterByType(IEnumerable<ComputerMonitor> monitors, MonitorType type) =>
monitors.Where(m => m.Type == type).ToList();
public List<ComputerMonitor> FilterByScreen(IEnumerable<ComputerMonitor> monitors, Screen screen) =>
monitors.Where(m => m.Screen == screen).ToList();
}
정확한 값을 얻을 수 있지만, 기존 class를 수정했다. 또 다른 filter가 필요하다면 OCP를 어기게 된다.
기존 class 수정을 피하기 위해, 다른 접근법을 살펴보자.
public interface ISpecification<T>
{
bool isSatisfied(T item);
}
public interface IFilter<T>
{
List<T> Filter(IEnumerable<T> monitors, ISpecification<T> specification);
}
ISpecification interface를 사용해서 조건이 만족했는지 결정하고, Filter method에 보낸다.
public class MonitorTypeSpecification: ISpecification<ComputerMonitor>
{
private readonly MonitorType _type;
public MonitorTypeSpecification(MonitorType type)
{
_type = type;
}
public bool isSatisfied(ComputerMonitor item) => item.Type == _type;
}
public class ScreenSpecification : ISpecification<ComputerMonitor>
{
private readonly Screen _screen;
public ScreenSpecification(Screen screen)
{
_screen = screen;
}
public bool isSatisfied(ComputerMonitor item) => item.Screen == _screen;
}
public class MonitorFilter : IFilter<ComputerMonitor>
{
public List<ComputerMonitor> Filter(IEnumerable<ComputerMonitor> monitors, ISpecification<ComputerMonitor> specification) =>
monitors.Where(m => specification.isSatisfied(m)).ToList();
}
var filter = new MonitorFilter();
var lcdMonitors = filter.Filter(monitors, new MonitorTypeSpecification(MonitorType.LCD));
Console.WriteLine("All LCD monitors");
foreach (var monitor in lcdMonitors)
{
Console.WriteLine($"Name: {monitor.Name}, Type: {monitor.Type}, Screen: {monitor.Screen}");
}
왜 OCP를 구현해야 하는가?
코드에 버그를 줄일 수 있다.
변경 대신, 확장을 하는 것은 나머지 system에 훨씬 영향을 적게 준다.
그리고 새로운 기능만 test하고 배포하면 된다. 또한 더 이상 사용하지 않기로 하면, 새로운 구현 변경을 되돌리기만 하면 된다.
결론
유지보수에 더 나은 코드를 만드는 방법을 살펴보았다.
때로는, 확장이 불가능하고 기존 소스를 수정해야 할 때가 있다. 일반적인 상황이므로 걱정할 필요 없지만, 적어도 별개로 생성하도록 시도해보아야 한다.
따라서, OCP로 app 개발을 하도록 염두해두고, 확장이 가능한 코드를 작성하도록 해야 한다. 그래야 유지보수가 가능하고, 사이즈를 줄이거나 늘릴 수 있고, 테스트가 용이한 코드를 갖을 수 있다.
'C#' 카테고리의 다른 글
C#] SOLID 원칙 3. Liskov Substitution Principle(리스코프 치환 원칙) (0) | 2024.01.26 |
---|---|
C#] SOLID 원칙 1. Single Responsibility Principle (SRP 원칙) (2) | 2024.01.26 |
C#] Virtual Method (0) | 2024.01.23 |
C#] const vs readonly 차이점 (1) | 2024.01.19 |
C#] File - 사용 중인 파일 확인 방법 (IOException, HResult) (0) | 2024.01.18 |
댓글