현재 여러 프로그래밍 방법론이 존재한다.
자주 사용되는 객체지향 프로그래밍
요즘 떠오르는 함수형 프로그래밍
이 중 자바스크립트로 객체지향 프로그래밍을 하는 법을 배워보려한다.
객체지향 프로그래밍은 Identifier Context를 사용해야한다.
만약 객체지향 프로그래밍을 하는데 Value Context를 특별한 상황 외에 사용한다면 크나큰 혼란을 일으킬 것이다.
Value
1. 끝 없는 복사본
2. 상태 변화에 안전?
3. 연산을 기반으로 로직을 전개
Identifier
1. 하나의 원본
2. 상태 변화를 내부에서 책임짐
3. 메세지를 기반으로 로직을 전개
객체 지향에서 가장 중요한 점은 값을 사용하는 것을 지양한다.
const Worker = class{
run(){
console.log("working");
}
print(){
this.run();
}
}
const HardWorker = class extends Worker{
run(){
console.log("hardWorking")
}
}
const worker = new HardWorker();
console.log(worker instanceof Worker); // true
worker.print();
위의 코드를 볼 때 HardWorker는 Worker 클래스로부터 상속 받은 클래스이다.
그렇다면 HardWorker에서 상속받은 print()메소드 그 안에 있는 run() 메소드는 Worker 클래스의 메소드를 실행할 것인가 아니면 HardWorker 클래스의 메소드를 실행할 것인가.
정답은 바로 HardWorker의 메소드가 실행된다.
이유는 태어난 위치의 this를 따라가게 되어있다.
이것을 내적일관성이라고 한다.
은닉은 필드 즉 데이터에 대해 은닉하는 것이며 캡슐화는 기능 즉 메소드를 숨기는 것이다.
const EssentialObject = class{
// 캡슐화
#name = "";
#screen = null;
constructor(name){
this.#name = name;
}
camouflage(){
this.#screen = (Math.random() * 10).toString(16).replace(".","");
}
// 은닉화
get name(){
return this.#screen || this.#name;
}
}
줄여서 SRP라고 이야기하는 단일 책임 원칙은
오직 하나의 책임을 가져야한다는 원칙이다.
class 음식점 {
요리사() {
//음식 만드는 코드
}
알바생() {
//결제하는 코드
}
배달기사() {
// 배달하는 코드
}
}
class 요리사 {
요리하기(){
//요리하는 코드
}
}
class 알바생 {
계산하기(){
//계산하는 코드
}
}
class 배달기사 {
배달하기(){
//배달하는 코드
}
}
하나의 Class에는 단일 책임만 넣는다.
만약 Bad와 같이 Class를 만들었을 때 배달을 음식을 못만들었을 때 책임은 요리사만 지는 것이 아닌 음식점 Class 모두 지게 된다.
이렇게 책임을 나누어 단일 책임을 가지게 된다면 에러 상황을 추적할 때 효율적이다.
개방/폐쇄의 원칙은 확장적인 측면에서는 열려있지만 변경에 대한 부분은 과감하게 닫혀있어야한다는 원칙이다.
class 대단한클래스 {
엄청난로직(){
// 어마무시한 number을 반환한다.
}
}
// 만약 수정 후 return 값을 Array로 반환한다면 ?
return 값이 달라지게 된다면 코드 전체의 사이드이펙트가 발생할 수 있음으로 이것은 확장 중 내부 코드를 잘못 수정한 사례라고 볼 수 있다.
class 연산하기 {
연산(number, option) {
if (option === "doubled") {
return number * 2;
}
if (option === "trippled") {
return number * 3;
}
if (option === "half") {
return number / 2;
}
// 더하기나 뺄셈을 추가하기 위해서 해당 메소드를 수정해야한다.
}
}
const doubled = new 연산하기();
console.log(doubled.연산(1, "doubled"));
// 2
class 연산하기 {
연산(number) {
if (typeof number !== "number") throw new Error("숫자를 입력해주세요.");
return number;
}
}
const doubled = new 연산하기();
console.log(doubled.연산(1) * 2);
// 연산을 위와 같이 하여 기본 로직에 수정을 막는다.
프론트엔드에서 개방 폐쇄 원칙은 Redux의 middleWare 혹은 Webpack의 loader와 같이 많은 곳에서 사용하고 있다.
리스코프 치환 원칙은 자식 클래스의 인스턴스를 교체해도 정상적으로 동작이 가능해야한다는 원칙이다.
만일 새는 날 수 있다라는 로직이 있다면 펭귄은 날 수 있는 것인가 ?
class Bird {
fly() {
return "I'm flying !";
}
}
class Parrot extends Bird {}
class Penguin extends Bird {}
class BirdFlying {
sayFlying(bird) {
return bird.fly();
}
}
const test = new BirdFlying();
console.log(test.sayFlying(new Parrot()));
console.log(test.sayFlying(new Penguin()));
// I'm flying !
// I'm flying ! 펭귄이 날아???
펭귄은 새이지만 날지 못하는 새이다.
Bird라는 부모 클래스에 fly()라는 메소드를 생성한 뒤 그것을 새로 분류되는 부모 클래스들에게 넘겨주었을 때
펭귄도 날 수 있는 새가 되어 버린것이다.
리스코프 원칙은 프로그램에 문제가 없어야하는 것이 핵심이다.
다른 예시를 들을 때 가로와 세로의 길이를 getter와 setter 메서드를 가진 직사각형 클래스를 예로 들 수 있다.
해당 예시는 조금 더 자세하게 나와있는 링크를 첨부하겠다.
LSP 위반 사례 - https://github.com/labs42io/clean-code-typescript#liskov-substitution-principle-lsp
자바스크립트에서의 interface는 존재하지 않고
타입스크립트에서 타입을 지정해주는 용도로 사용되고 있다.
인터페이스를 사용할 때 최대한 작게 구성하고 유지해야한다.
interface SmartPrinter {
print();
fax();
scan();
}
class AllInOnePrinter implements SmartPrinter {
print() {
// ...
}
fax() {
// ...
}
scan() {
// ...
}
}
class EconomicPrinter implements SmartPrinter {
print() {
// ...
}
fax() {
throw new Error('Fax not supported.');
}
scan() {
throw new Error('Scan not supported.');
}
}
interface Printer {
print();
}
interface Fax {
fax();
}
interface Scanner {
scan();
}
class AllInOnePrinter implements Printer, Fax, Scanner {
print() {
// ...
}
fax() {
// ...
}
scan() {
// ...
}
}
class EconomicPrinter implements Printer {
print() {
// ...
}
}
함수형에서 interface당 함수가 1:1로 매칭되기 때문에 ISP 원칙을 위배하기 쉽지 않다.
의존관계 역전 원칙은 추상화에 의존해야하지, 구체화에 의존하면 안된다는 원칙이다.
실제 전기 배선이 어떻게 구성되어있건 -구체화-
전기를 이용하기 위해서 플러그를 꽃으면 된다. -추상화-
class 배달의민족 {
음식수령() {
console.log("우리가 어떤 민족입니까.");
}
배달중() {
console.log("배달 완료!");
}
}
class 요기요 {
음식수령() {
console.log("배달음식은 요기요 !");
}
배달중() {
console.log("배달 완료!");
}
}
class 배달 {
constructor(배달대행업체) {
배달대행업체.음식수령();
배달대행업체.배달중();
}
}
const 배민라이더 = new 배달의민족();
const 요기요라이더 = new 요기요();
const 배달1 = new 배달(배민라이더);
const 배달2 = new 배달(요기요라이더);
프론트엔드에서의 간단한 예시를 적어보자면
React에서 axios로 서버데이터를 가져오는 코드가 있을 때 Custom hook을 하나 만들어 레이어(층)를 하나 만드는 것이다.
이렇게 추상화된 레이어를 두는 이유는 컴포넌트 입장에서 데이터가 필요한거지 그 데이터가 서버 데이터든 로컬의 mock데이터든 상관이 없기 때문이다.
만약 컴포넌트에서 axios 호출하거나 fetch로 바로 호출하면 DIP 원칙에 벗어나는 설계이고 axios를 다루는 모듈에서 컴포넌트의 props를 조작하는 등 레이어의 범위를 벗어나는 코드 역시 DIP에 어긋난 설계이다.
절차지향은 코드의 실행 순서가 위에서 아래로 순차적으로 실행되도록 만드는 프로그래밍 기법이다.
대표적인 언어로는 C언어가 있으며 객체지향 언어를 사용하는 것에 비해 실행 속도가 빠르다.
그렇지만 유지보수가 어렵고 코드의 순서가 바뀌게 된다면 동일한 결과 값을 보장하기 어렵고 디버깅이 어렵다.
객체지향은 데이터와 절차를 하나로 묶어 사용하는 프로그래밍 기법이다.
코드의 재활용성이 높고 디버깅이 쉽다는 장점이 있다.
그렇지만 절차지향보단 처리속도는 느리고 설계에 많은 시간을 소비 해야한다는 단점이 있다.
프론트엔드는 객체지향 프로그래밍보단 함수형 프로그래밍을 더 선호하는 느낌이다.
이유는 아마 객체지향의 장점이 프론트엔드 개발에 크게 와닿지 않기 때문이 아닐까 싶다.
(나는 잘 모르겠고 슨배님들이 잘 알지 않을까 ...?)
하지만 이러한 객체지향의 원칙들은 꼭 객체를 사용할 때만 쓰는 원칙은 아니라는 것을 알았다.
충분히 함수형에서 이러한 원칙을 적용할 수 있고 그로 인해 에러를 방지하거나 유지보수에 도움을 준다는 것을 이번 공부로 깨닫게 되었다.
슨배님들이 만들어낸 이러한 원칙은 모두 경험에서 우러나온 프로그래밍 방법론이다.
테오님의 SOLID 원칙
https://velog.io/@teo/Javascript%EC%97%90%EC%84%9C%EB%8F%84-SOLID-%EC%9B%90%EC%B9%99%EC%9D%B4-%ED%86%B5%ED%95%A0%EA%B9%8C
나를 찾는 아이님의 SOLID 원칙
https://trend21c.tistory.com/2235
c-on.log님의 자바스크립트 SOLID 원칙 예제
https://velog.io/@c-on/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-SOLID-%EC%98%88%EC%A0%9C