응집도는 높고 결합도는 낮은 프로그램을 설계하는 것이 객체지향 설계의 핵심이다.
새로운 요구사항이나 프로그램 변경에 의해 클래스 내부의 동작들이 연쇄적으로 변경되어야 할 수도 있다. 이는 유지보수가 비효율적이므로, 책임을 잘게 쪼개어 분리시킬 필요가 있다.
SRP 적용 전
class UserSettings {
constructor(user) { // UserSettings 클래스 생성자
this.user = user;
}
changeSettings(userSettings) { // 사용자의 설정을 변경하는 메소드
if (this.verifyCredentials()) {
//...
}
}
verifyCredentials() { // 사용자의 인증을 검증하는 메소드
//...
}
}
SRP 적용 후
class UserAuth {
constructor(user) { // UserAuth 클래스 생성자
this.user = user;
}
verifyCredentials() { // 사용자의 인증을 검증하는 메소드
//...
}
}
class UserSettings {
constructor(user) { // UserSettings 클래스 생성자
this.userAuth = new UserAuth(user); // UserAuth를 새로운 객체로 정의
}
changeSettings(userSettings) { // 사용자의 설정을 변경하는 메소드
if (this.userAuth.verifyCredentials()) { // 생성자에서 선언한 userAuth 객체의 메소드를 사용
//...
}
}
}
instanceof
와 같은 연산자를 사용하거나, 다운 캐스팅 발생자주 변화하는 부분을 추상화함으로써 기존 코드를 수정하지 않고도 기능을 확장할 수 있도록 함으로써 유연함을 높이는 것이 핵심이다.
OCP 적용 전
function calculator(nums, option) {
let result = 0;
for (const num of nums) {
if (option === "add") result += num; // option이 add일 경우 덧셈 연산
else if (option === "sub") result -= num; // option이 sub일 경우 뺄셈 연산
// 새로운 연산(기능)을 추가 하기 위해서는 함수 내부에서 코드 수정이 필요
}
return result;
}
console.log(calculator([2, 3, 5], "add")); // 10
console.log(calculator([5, 2, 1], "sub")); // -8
OCP 적용 후
function calculator(nums, callBackFunc) {
// option을 CallbackFunc로 변경
let result = 0;
for (const num of nums) {
result = callBackFunc(result, num); // option으로 분기하지 않고, Callback함수를 실행하도록 변경
}
return result;
}
const add = (a, b) => a + b; // 함수 변수를 정의
const sub = (a, b) => a - b;
const mul = (a, b) => a * b;
const div = (a, b) => a / b;
console.log(calculator([2, 3, 5], add)); // add 함수 변수를 Callback 함수로 전달
console.log(calculator([5, 2, 1], sub)); // sub 함수 변수를 Callback 함수로 전달
IS-A
)가 성립해야 한다는 의미리스코프 치환 원칙을 지키지 않으면 OCP 원칙을 위반하게 되는 것. 따라서 상속 관계를 잘 정의하여 LSP 원칙이 위배되지 않도록 설계
LSP 적용 전
class Rectangle {
constructor(width = 0, height = 0) { // 직사각형의 생성자
this.width = width;
this.height = height;
}
setWidth(width) { // 직사각형은 높이와 너비를 독립적으로 정의
this.width = width;
return this;
}
setHeight(height) { // 직사각형은 높이와 너비를 독립적으로 정의
this.height = height;
return this;
}
getArea() { // 사각형의 높이와 너비의 결과값을 조회하는 메소드
return this.width * this.height;
}
}
class Square extends Rectangle { // 정사각형은 직사각형을 상속받음
setWidth(width) { // 정사각형은 높이와 너비가 동일하게 정의
this.width = width;
this.height = width;
return this;
}
setHeight(height) { // 정사각형은 높이와 너비가 동일하게 정의
this.width = height;
this.height = height;
return this;
}
}
const rectangleArea = new Rectangle() // 35
.setWidth(5) // 너비 5
.setHeight(7) // 높이 7
.getArea(); // 5 * 7 = 35
const squareArea = new Square() // 49
.setWidth(5) // 너비 5
.setHeight(7) // 높이를 7로 정의하였지만, 정사각형은 높이와 너비를 동일하게 정의
.getArea(); // 7 * 7 = 49
LSP 적용 후
class Shape { // Rectangle과 Square의 부모 클래스를 정의
getArea() { // getArea는 빈 메소드로 정의
}
}
class Rectangle extends Shape { // Rectangle은 Shape를 상속받음
constructor(width = 0, height = 0) { // 직사각형의 생성자
super();
this.width = width;
this.height = height;
}
getArea() { // 직사각형의 높이와 너비의 결과값을 조회하는 메소드
return this.width * this.height;
}
}
class Square extends Shape { // Square는 Shape를 상속받음
constructor(length = 0) { // 정사각형의 생성자
super();
this.length = length; // 정사각형은 너비와 높이가 같이 깨문에 width와 height 대신 length를 사용
}
getArea() { // 정사각형의 높이와 너비의 결과값을 조회하는 메소드
return this.length * this.length;
}
}
const rectangleArea = new Rectangle(7, 7) // 49
.getArea(); // 7 * 7 = 49
const squareArea = new Square(7) // 49
.getArea(); // 7 * 7 = 49
각 클라이언트가 필요로 하는 인터페이스들을 분리함으로써, 클라이언트가 사용하지 않는 인터페이스에 변경이 발생하더라도 영향을 받지 않도록 함
ISP 적용 전
interface SmartPrinter { // SmartPrinter가 사용할 수 있는 기능들을 정의한 인터페이스
print();
fax();
scan();
}
// SmartPrinter 인터페이스를 상속받은 AllInOnePrinter 클래스
class AllInOnePrinter implements SmartPrinter {
print() { // AllInOnePrinter 클래스는 print, fax, scan 기능을 지원
// ...
}
fax() { // AllInOnePrinter 클래스는 print, fax, scan 기능을 지원
// ...
}
scan() { // AllInOnePrinter 클래스는 print, fax, scan 기능을 지원
// ...
}
}
// SmartPrinter 인터페이스를 상속받은 EconomicPrinter 클래스
class EconomicPrinter implements SmartPrinter {
print() { // EconomicPrinter 클래스는 print 기능만 지원
// ...
}
fax() { // EconomicPrinter 클래스는 fax 기능을 지원하지 않음
throw new Error('팩스 기능을 지원하지 않습니다.');
}
scan() { // EconomicPrinter 클래스는 scan 기능을 지원하지 않음
throw new Error('Scan 기능을 지원하지 않습니다.');
}
}
ISP 적용 후
interface Printer { // print 기능을 하는 Printer 인터페이스
print();
}
interface Fax { // fax 기능을 하는 Fax 인터페이스
fax();
}
interface Scanner { // scan 기능을 하는 Scanner 인터페이스
scan();
}
// AllInOnePrinter클래스는 print, fax, scan 기능을 지원하는 Printer, Fax, Scanner 인터페이스를 상속받았다.
class AllInOnePrinter implements Printer, Fax, Scanner {
print() { // Printer 인터페이스를 상속받아 print 기능을 지원
// ...
}
fax() { // Fax 인터페이스를 상속받아 fax 기능을 지원
// ...
}
scan() { // Scanner 인터페이스를 상속받아 scan 기능을 지원
// ...
}
}
// EconomicPrinter클래스는 print 기능을 지원하는 Printer 인터페이스를 상속받음
class EconomicPrinter implements Printer {
print() { // EconomicPrinter 클래스는 print 기능만 지원
// ...
}
}
// FacsimilePrinter클래스는 print, fax 기능을 지원하는 Printer, Fax 인터페이스를 상속받음
class FacsimilePrinter implements Printer, Fax {
print() { // FacsimilePrinter 클래스는 print, fax 기능을 지원
// ...
}
fax() { // FacsimilePrinter 클래스는 print, fax 기능을 지원
// ...
}
}
구체적인 클래스보다 인터페이스나 추상클래스에 의존해야 함
DIP 적용 전
const readFile = require('fs').readFile;
class XmlFormatter {
parseXml(content) {
// Xml 파일을 String 형식으로 변환
}
}
class JsonFormatter {
parseJson(content) {
// JSON 파일을 String 형식으로 변환
}
}
class ReportReader {
async read(path) {
const fileExtension = path.split('.').pop(); // 파일 확장자
if (fileExtension === 'xml') {
const formatter = new XmlFormatter(); // xml 파일 확장자일 경우 XmlFormatter를 사용
const text = await readFile(path, (err, data) => data);
return formatter.parseXml(text); // xmlFormatter클래스로 파싱을 할 때 parseXml 메소드를 사용
} else if (fileExtension === 'json') {
const formatter = new JsonFormatter(); // json 파일 확장자일 경우 JsonFormatter를 사용
const text = await readFile(path, (err, data) => data);
return formatter.parseJson(text); // JsonFormatter클래스로 파싱을 할 때 parseJson 메소드를 사용
}
}
}
const reader = new ReportReader();
const report = await reader.read('report.xml');
// or
// const report = await reader.read('report.json');
DIP 적용 후
const readFile = require('fs').readFile;
class Formatter { // 인터페이스지만, Javascript로 구현하기 위해 클래스로 선언합니다.
parse() { }
}
class XmlFormatter extends Formatter {
parse(content) {
// Xml 파일을 String 형식으로 변환
}
}
class JsonFormatter extends Formatter {
parse(content) {
// JSON 파일을 String 형식으로 변환
}
}
class ReportReader {
constructor(formatter) { // 생성자에서 Formatter 인터페이스를 상속받은 XmlFormatter, JsonFormatter를 전달받음
this.formatter = formatter;
}
async read(path) {
const text = await readFile(path, (err, data) => data);
return this.formatter.parse(text); // 추상화된 formatter로 데이터를 파싱
}
}
const reader = new ReportReader(new XmlFormatter());
const report = await reader.read('report.xml');
// or
// const reader = new ReportReader(new JsonFormatter());
// const report = await reader.read('report.json');
참고블로그 : https://velog.io/@haero_kim
참고자료 : Sparta