객체 지향 설게에서 좋은 코드 품질을 유지하고 유지보수성과 확장성을 높이기 위해 사용되는 다섯 가지 원칙이다.
- Single Responsibility Principle (SRP) - 단일 책임 원칙
- 클래스 또는 모듈은 하나의 책임만 가져야 한다
class User {
constructor(name, email){
this.name = name;
this.email = email;
}
save(){ // 데이터 베이스에 사용자 저장}
sendEmail() {// 사용자에게 이메일 전송}
}
class User {
constructor(name, email){
this.name = name;
this.email = email;
}
}
class UserRepository {
save(user){
// 데이터베이스에 사용자 저장
}
}
class EmailService {
sendEmail(user){
// 사용자에게 이메일 전송
}
}
위 예제와 같이 User클래스는 사용자 정보를 저장하고 이메일을 보내는 두가지 책임을 가지고 있습니다. 이를 User, UserRepository, EmailService 세개의 클래스로 분리하여 각각 하나의 책임만 가지도록 개선했습니다.
소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려있어야 하지만 수정에는 닫혀있어야 한다
class Discount{
applyDiscount(type, amount){
if(type === 'student'){
return amount * 0.9;
}else if(type === 'senior'){
return amount * 0.85;
}
return amount;
}
}
class Discount {
protected sale = 1;
applyDiscount(amount){
return amount * this.sale;
}
}
class StudentDiscount extends Discount {
protected sale = 0.9;
}
class SeniorDiscount extends Discount {
protected sale = 0.85;
}
const discounts = [new StudentDiscount(), new SiniorDiscount()];
discounts.forEach(discount => console.log(discount.applyDiscount(100)))
위의 예제처럼 Discount클래스는 여러 할인 유형을 처리하기 위해 내부적으로 조건문을 하용 한다 이를 상속을 통해서 새로운 할인 유형을 추가 할 수 있도록 개선했습니다.
자식 클래스틑 언제나 자신의 부모 클래스를 대체할 수 있어야 한다
class Rectangle {
constructor(width, height){
this.width = width;
this.height = height;
}
getArea(){
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(side){
super(side, side)
}
setWidth(width){
this.width = width;
this.height = width;
}
setHeight(height){
this.width = height;
this.height = height;
}
}
class Shape {
getArea(){
throw new Error('Method not implemented')
}
}
class Rectangle extends Shape {
constructor(width, height){
super();
this.width = width;
this.height = height;
}
getArea(){
return this.width * this.height;
}
}
class Square extends Shape {
constructor(side){
super();
this.side = side;
}
getArea(){
return this.side * this.side;
}
}
위 예제에서 Square클래스가 Rectangle 클래스를 상속받으면, 사각형의 setWidth와 setHeight메서드를 사용할때 문제가 발생할 수 있습니다. 이를 Shape 라는 상위 클래스를 도입하여 각각의 도형이 독립적으로 동작하도록 개선했습니다.
특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
class Worker {
eat(){}
work(){}
sleep(){}
}
class Eater {}
class Worker {}
class Sleeper {}
class HumanWorker extends Eater, Worker, Sleeper {
eat(){}
work(){}
sleep(){}
}
class RobotWorker extends Worker {
work(){}
}
위 예제에서 Worker 클래스는 모든 메서드를 포함하고 있어 인터페이스 분리 원칙을 위반합니다. 이를 Eater, Worker, Sleeper 인터페이스로 분리하고 필요한 클래스에만 해당 기능을 구현하도록 개선했습니다.
고수준 모듈은 저수준 모둘에 의존해서는 안 되며, 둘 다 추상화된 인터페이스에 의존해야 한다.
class User {
constructor(name){
this.name = name;
}
}
class UserService {
constructor(){
this.user = new User('John Doe');
}
getUser(){
return this.user;
}
}
class user {
constructor(name){
this.name = name;
}
}
class UserService {
constructor(User user){
this.user = user;
}
getUser(){
return this.user;
}
}
const user = new User('John Doe');
const userService = new UserService(user);
console.log(userService.getuser());
위 예제에서 UserService 클래스는 User클래스에 직접 의존하고 있어 DIP 를 위반합니다. 이를 생성자 주입방식으로 개선하여 UserService가 User클래스에 의존하지 않고 인터페이스를 통해 의존성을 주입받도록 했습니다.
위와 같은 원칙을 따르면 코드의 유지보수성과 확장성을 크게 향상 시킬수 잇습니다..
- Order
// Single Responsiblility Principle 단일 책임의 원칙
// 각 클래스는 하나의 책임만을 가지고 있습니다.
class Order {
constructor(
public items: {item: string, price: number}[],
public total: number
){}
}
class OrderRepository {
save(order: Order): void {
console.log('Order saved to the database:', order)
}
}
class EmailService {
sendEmail(email: string, message: string): void {
console.log(`Email sent to ${email}: ${message}`)
}
}
// Open/Closed Priciple - 개방 폐쇄의 원칙
// PaymentProcessor 클래스는 확장에는 열려 있고 수정에는 닫혀 있습니다.
interface PaymentProcessor {
processPayment(amount: number): void;
}
// javascript version interface
// class PaymentProcessor {
// processPayment(amount) {
// throw new Error('Method not implemented');
// }
//}
class PayPalPaymentProcessor implements PaymentProcessor {
processPayment(amount: number): void {
console.log(`Processing PayPal payment of ${amount}`);
}
}
class CreditCardPaymentProcessor implements PaymentProcessor {
processPayment(amount: number): void {
console.log(`Processing credit. ard payment of ${amount}`)
}
}
// Liskov Substitution Principle - 리스코프 원칙
// 모든 PaymentProcessor의 서브클래스는 PaymentProcessor를 대체할 수 있어야 한다
function processOrder(paymentProcessor: PaymentProcessor, amount: number){
paymentProcessor.processPayment(amount);
}
// Interface Segregation Principle - 인터페이스 분리 원칙
// 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
interface OrderProcessor {
processOrder(order: Order): void
}
class ConcreateOrderProcessor implements OrderProcessor {
processOrder(order: Order): void {
console.log('Processing order', order)
}
}
interface EmailSnder {
sendOrderEmail(email: string, order: Order): void;
}
class ConcreateEmailSender implements EmailSender {
sendOrderEmail(email: string, order: Order){
console.log(`Sending email to ${email} about order:`, order);
}
}
// Dependency Inversion Principle - 의존성 역전의 법칙
// 고수준 모듈은 저수준 모듈에 의존해서는 안되고 , 둘다 추상화된 인터페이스에 의존해야 한다.
class OrderService {
constructor(
private orderProcessor: OrderProcessor,
private emailSender: EmailSender
){}
placeOrder(order: Order, email: string): void {
this.orderProcessor.processOrder(order);
this.emailSender.sendOrderEmail(email, order);
}
}
// usage example
const order = new Order([{item: 'Laptop', price: 1000}], 1000);
const orderRepository = new OrderRepository();
const emailService = new EmailService();
const paypalProcessor = new PayPalPaymentProcessor();
const creditcardProcessor = new CreditCardPaymentProcessor();
orderRepository.save(order);
processOrder(paypalProcessor,order.total);
processOrder(creditCardProcessor, order.total);
emailService.sendEmail('custom@example.com', 'your order has been placed!');
const orderProcessor = new ConcreteOrderProcessor();
const emailSender = new ConcreteEmailSender();
const orderService = new OrderService(orderProcessor, emailSnder);
orderService.placeOrder(order, 'custom@example.com');
- Single Responsibility Principle (SRP): • Order 클래스는 주문 정보를 관리하는 책임을 가집니다. • OrderRepository 클래스는 주문을 데이터베이스에 저장하는 책임을 가집니다. • EmailService 클래스는 고객에게 이메일을 보내는 책임을 가집니다.
- Open/Closed Principle (OCP): • PaymentProcessor는 인터페이스로 정의되고, PayPalPaymentProcessor와 CreditCardPaymentProcessor는 이를 구현하여 각각의 결제 방식을 제공합니다. • 새로운 결제 방법을 추가하려면 새로운 클래스만 추가하면 됩니다.
- Liskov Substitution Principle (LSP): • processOrder 함수는 PaymentProcessor의 하위 클래스를 인자로 받아 결제를 처리합니다. • 모든 PaymentProcessor의 서브 클래스는 processPayment 메서드를 구현하여 부모 클래스를 대체할 수 있습니다.
- Interface Segregation Principle (ISP): • OrderProcessor와 EmailSender 인터페이스는 클라이언트가 실제로 사용하는 메서드만을 제공합니다. • 구체적인 구현체는 각 인터페이스를 구현하여 특정 기능만 수행합니다.
- Dependency Inversion Principle (DIP): • OrderService 클래스는 OrderProcessor와 EmailSender 인터페이스에 의존하며, 구체적인 구현체는 생성자 주입을 통해 주어집니다. • 이를 통해 고수준 모듈인 OrderService는 저수준 모듈의 변화에 영향을 받지 않습니다.
이 TypeScript 예제는 SOLID 원칙을 잘 반영하여 설계된 시스템의 구조를 보여줍니다. 이러한 원칙을 따르면 코드의 유연성과 유지보수성을 크게 향상시킬 수 있습니다.
https://mangkyu.tistory.com/88
- 절차지향 프로그램
- 순서대로 실행
- 객체지햐 ㅇ프로그램
- 캡슐화 , 상속 다형성 등과 같은 기법 이용
함수형 프로그래밍의 가장 큰 특징은 immutable data 와 first class citizen 으로서의 함수입니다. 함수형 프로그래밍은 부수효과가 없는 순수 함수를 이용하여 프로그램을 만드는 것이다. 부수 효과가 없는 순수 함수란 데이터의 값을 변경시키지 않으며 객체의 필드를 설정하는 등의 작업을 하지 않는 함수를 의미 합니다.
const numbers = [1,2,3,4,5];
const addOne = (num) => num+1;
const newNumbers = numbers.map(addOne)
// 불변데이터 immutable data => nubmers 를 변형시키지 않고 새로운 newNumbers 데이터 생성
// 순수함수(Pure Function) addOne 함수는 부수 효과가 없으며 , 동일한 입력에 대해 항상 동일한 출력을 반환
메모리는 크게 네가지 영역으로 나뉩니다
- 코드 영역
- 데이터 영역
- 힙 영역
- 스택 영역
역할: 실행할 프로그램의 코드가 저장되는 영역 특징: 프로그램이 실행될 때 HDD에서 이 영역으로 코드가 올라오고, CPU는 여기 있는 명령어를 하나씩 처리합니다. 예시: 프로그램의 함수와 명령어들이 저장됩니다.
역할: 전역 변수와 정적 변수가 저장되는 영역 특징: 프로그램 시작과 함께 할당되고, 프로그램이 종료되면 사라집니다. 예시: 전역적으로 사용되는 변수들, 정적 변수들
역할: 동적으로 할당되는 메모리 공간 특징: 프로그래머가 직접 관리하며, 동적 할당과 해제를 담당합니다. 낮은 주소에서 높은 주소로 확장됩니다. 예시: new 등을 통해 생성된 객체나 변수들
힙은 동적 사이즈를 가지고 있기때문에 변수와 같이 크기가 다른 데이터를 할당해놓을 수 있다
역할: 함수 호출과 함께 할당되는 지역변수와 매개변수가 저장되는 영역 특징: 함수 호출시 할당되고 , 호출이 끝나면 자동으로 해제된집다. 스택은 높은 주소에서 낮을 주소로 확장됩니다 역할: 함수 내부에서 선언된 변수들, 함수의 매개 변수들
빠른 메모리 할당 및 해제
- 스택은 함수 호출시 자동으로 메모리가 할당되고, 함수가 종료되면 자동으로 해제됩니다. 이 과정이 매우 빠르고 간단합니다
- Last in, First out 구조로 동작
** 메모리 주소와 확장 방향 ** 낮은 주소 에서 시작하여 높은주소로 갈수록 힙 영역은 확장됩니다 높은 주소에서 시작하여 낮은 주소로 갈수록 스택 영역은 확장됩니다.
function add(a,b){
let result = a+b; // result 스택에 저장
return result;
}
add(5,10); // 함수 호출시 a, b 스택에 저장
// result, a, b 스택에서 소명
v8엔진에서 자동으로 가비지 컬렉션을 수행하여 더이상 참조되지 않는 객체를 힙에서 제거합니다
- call By Value
- 함수에 인자를 전달할 때 , 인자의 값을 복사하여 함수 내부에서 처리하는 방식입니다.
- 인자로 전달된 값을 함수 내부에서 변경해도 원래의 값은 변하지 않습니다
- 값이 복사되기 때문에 , 함수 호출 시 메모리 사용량이 증가할 수 있습니다.
- 원래의 값을 안전하게 보존할 수 있습니다
function addOne(value){
value += 1;
return value;
}
let number = 2;
addOne(number); // 3
console.log(number) // 2로 유지됨
- call By Reference
- 함수에 인자를 전달할 때, 인자의 주소를 전달하여 내부에서 해당 주소를 참조하여 값을 직접 변경하는 방식입니다.
- 인자의 주소를 전달하므로, 함수 내부에서 인자의 값을 변경하면 원래의 값도 변경됩니다.
- 원래의 값이 변경될수 있는 위험이 있습니다
- 메모리 사용량 감소, 속도 증가
function addOne(obj){
obj.value += 1;
console.log(obj.value) // 2
}
let myObj = {value: 1}
addOne(myObj)
console.log(myObj.value) // 2
문자열, 불리언, 널, 언디파인드, 심볼 , 빅인트와 같은 단순한 데이터 유형 크기가 작고 고정되어 있어 메모리에 할당
객체 , 배열, 함수 등과 같은 복잡한 데이터 타입 실제 데이터는 힙에 저장되고 , 변수에는 이 데이터의 메모리 주소가 저장됨 참조 타입을 복사하면 메모리 주소 자체가 복사가 되기 때문에 값을 변경해도 원본 변수도 같이 변경이 됨
가변 데이터와 같은 경우 다양한 크기와 구조를 가질수 있으므로 값 자체를 복사하기 보다는 그 값의 주소를 변수에 넣어 놓는다
참조 타입의 객체는 메모리 주소를 변수에 저장하기 때문에
let original = {
nam: 'John',
address: {
city: 'New York',
zip: '1001'
}
}
let shallowCopy = Object.assign({}, original);
// let shallowCopy = {...original}
shallowCopy.name = 'Doe';
shallowCopy.address.city = 'Los Angeles';
console.log(original.name) // 'John' 변하지 않음
console.log(original.address.city) // ㅣLos Angeles
얕은 복사는 address의 참조를 복사하기 때문에 원본 의 address값도 같이 변경이 된 케이스
let original = {
nam: 'John',
address: {
city: 'New York',
zip: '1001'
}
}
let deepCopy = JSON.parse(JSOn.stringiy(original));
shallowCopy.name = 'Doe';
shallowCopy.address.city = 'Los Angeles';
console.log(original.name) // 'John' 변하지 않음
console.log(original.address.city) // 'New York' 변하지 않음
객체의 모든 계층이 복사된 케이스로 독립된 개체가 되어 원본이 변경되지 않는다
얕은 복사는 최상위 객체의 참조만 복사하므로 중첩된 객체는 여전히 동일한 메모리 주소를 참조