자바스크립트 클린 코드

김윤진·2022년 4월 13일
0

JavaScript

목록 보기
7/10

reference
https://github.com/qkraudghgh/clean-code-javascript-ko#%EB%AA%A9%EC%B0%A8

Object.assign으로 기본 객체 만들자

👎

const user = {
  age: 00,
  address: "where",
};

function controlUser(config) {
  config.age = config.age || 20;
  config.address = config.age || "Seoul"
}

controlUser(user);

👍

const user = {
  age: 00,
  // 유저의 gender는 정해지지 않았다
  address: "where",
};

function controlUser(config) {
  config = Object.assign({
    age: 20,
    gender: "male",
    address: "Seoul"
  }, config);
  
  // config { age: 20, gender: "male", address: "Seoul" }
}

controlUser(user);

매개변수로 플래그를 사용하지 말자

플래그를 사용하는 것 자체가 그 함수가 한가지 이상의 역할을 하고 있다는 것을 의미한다
boolean 기반으로 함수가 실행된다면 함수를 분리해야 한다

👎

function createDomain(name, temp) {
  if (temp) {
   	return `./temp/${name}.com`; 
  } else {
    return `${name}.com`; 
  }
}

👍

function createDomain(name) {
  return `${name}.com`; 
}

function createTempDomain(name, temp) {
  return `./temp/${name}.com`; 
}

조건문을 캡슐화 하자

👎

if (name === "Kim") {
  // ~~~~ 
}

👍

function isNameKim(name) {
  return name === "Kim";
}

if (isNameKim(name)) {
  
}

조건문 작성을 피하자

함수나 클래스에서 if문을 쓴다면 그서은 그 함수나 클래스가 한가지 이사의 일을 수행한다고 말하는 것과 같다

👎

class Person {
 // ~~~~~
  whatPersonName() {
    switch (name) {
      case "Kim": 
        return "Kim is my name";
      case "Lee": 
        return "Lee is my name";
      case "Hong":
        return "Hong is my name";
    }
  }
}

👍

class Person {
 // ~~~~~
}

class NameKim extends Person {
  getName() {
    return "Kim is my name"; 
  }
}

class NameLee extends Person {
  getName()  {
    return "Lee is my name";
  }
}

class NameHong extends Person {
  getName() {
   	return "Hong is my name"; 
  }
}

getter와 setter를 사용하자

객체의 속성을 얻는 것 이상의 많은 것을 하고 싶을 때 코드에서 모든 접근자를 찾아 바꾸고 할 필요가 없다
set을 할 때 검증로직을 추가하는 것이 쉽ㄴ다
gettingsetting할 때 로그를 찾거나 에러처리를 하기 쉽다
서버에서 객체 속성르 받아올 때 lazy load를 할 수 있다

👎

functin showMyAccount() {
  
  return {
    money: 0 
  };
}

const myAccount = showMyAccount();
myAccount.money = 10000;

👍

class ShowMyAccount {
  constructor() {
    private money = 0; 
  }
  
  getMyAccount() {
   	return money; 
  }
  
  setMyAccount(account) {
     this.money = account;
  }
}

const myAccount = new ShowMyAccount();
myAccount.setMyAccount(100000);

메소드 체이닝을 사용하자

클래스 함수에서 단순히 모든 함수의 끝에 'this'를 리턴해주는 것으로 클래스 메소드를 추가로 연결할 수 있다

👎

class Car {
  constructor() {
    this.make = "Tesla";
    this.model = "S";
    this.color = "red";
  }
  
  setMake(make) {
    this.make = make; 
  }
  
  setModel(model) {
   	this.model = model; 
  }
  
  setColor(color) {
    this.color = color; 
  }
  
  save() {
    console.log(`${this.make} ${this.model} ${this.color}`); 
  }
}

const car = new Car();
car.setMake("Ford");
car.setModel("F-150");
car.setColor("white");
car.save();

👍

class Car {
  constructor() {
    this.make = "Tesla";
    this.model = "S";
    this.color = "red";
  }
  
  // 체이닝을 위해 this를 리턴한다
  
  setMake(make) {
    this.make = make; 
    return this;
  }
  
  setModel(model) {
   	this.model = model; 
    return this;
  }
  
  setColor(color) {
    this.color = color; 
    return this;
  }
  
  save() {
    console.log(`${this.make} ${this.model} ${this.color}`); 
    return this;
  }
}

const car = new Car()
  .setMake("Ford")
  .setColor("white")
  .setModel("F-150")
  .save();

상속보다는 조합을 사용하자

상속보다 조합을 사용해야 하는 이유는 상속을 사용했을 때 얻는 이득보다 조합을 사용했을 때 얻을 수 있는 이득이 많기 때문이다

조합보다 상속을 쓰는게 더 좋은 예시는

  • 상속관계가 has-a관계(사람 -> 동물)가 아니라 is-a관계일 때(유저 -> 유저 정보)
  • 기반 클래스의 코ㄷ를 다시 사용할 수 있을 때
  • 기반 클래스를 수정하여 파생된 클래스 모두를 수정하고 싶을 때

👎

class Employee {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
  // ~~~~
}

// 이 코드가 좋지 않은 이유는 Employee가 tax data를 가지고 있기 때문이다
// EmployeeTaxDate는 Employee 타입이 아니다
class EmployeeTaxDate extends Employee {
  constructor(ssn, salary) {
    super();
    this.ssn = ssn;
    this.salary = salary;
  }
  // ~~~~
}

👍

class EmployeeTaxDate extends Employee {
  constructor(ssn, salary) {
    this.ssn = ssn;
    this.salary = salary;
  }
  // ~~~~
}

class Employee {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
  
  setTaxData(ssn, salary) {
    this.textData = new  EmployeeTaxDate(ssn, salary);
  }
}

SOLID


단일 책임 원칙 (Single Responsibility Princple, SRP)

클래스를 수정할 때는 수정 해야하는 이유가 2개 이상 있으면 안 된다
이 문제는 클래스가 개념적으로 응집되어 있지 않다는 것이고 클래스를 바꿔야할 많은 이규가 된다
클래스를 수정하는데 들이는 시간을 줄이는 것은 중요하다
하나의 클래스에 많은 기능들이 있고 작은 기능을 수정할 때 이 코드가 다른 모듈에 어떤 영향을 끼치는지 이해하기 어려울 수 있다

👎

class UserSettings {
  constructor(user) {
   	this.user = user; 
  }
  
  changeSettings(setting) {
    if (this.verifyCredentials()) {
       // ~~~
    }
  }
  
  verifyCredentials() {
    
  }
}

👍

class UserAuth {
  constructor(user) {
    this.user = user; 
  }
  
  verifyCredentials() {
    
  }
}

class UserSettings {
  constructor(user) {
    this.user = user;
    this.auth = new UserAuth(user);
  }
  
  changeSettings(settings) {
    if (this.auth.verifyCredentials()) {
      // ~~~~ 
    }
  }
}

개방/폐쇠 원칙 (Open/Close Principle, OCP)

소프트웨어 개체 (클래스, 모듀르 함수)는 확장을 위해 개방적이여야 하며 수정시 폐쇄적이어야 한다
기본적으로 사용자가 js소스코드 파일을 열어 수동으로 조작하지 않고도 모듈의 기능을 확장하도록 허용해야 한다는 것이다

👎

class AjaxAdapter extends Adapter {
  constructor() {
    super();
    this.name = 'ajaxAdapter';
  }
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
    this.name = 'nodeAdapter';
  }
}

class HttpRequest {
  constructor(adapter) {
    this.adapter = adapter; 
  }
  
  fetch(url) {
    if (this.adapter.name === 'ajaxAdapter') {
      return makeAjaxCall(url).then((response) => { }); 
    } else if (this.adapter.name === 'httpNodeAdaptor') {
      return makeHttpCall(url).then((response) => { }); 
    }
  }
}

function makeAjaxCall(url) { }
function makeHttpCall(url) { }

👍

class AjaxAdapter extends Adapter {
  constructor() {
    super();
    this.name = 'ajaxAdapter';
  }
  
  request(url) { }
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
    this.name = 'nodeAdapter';
  }
  
  request(url) { }
}

class HttpRequest {
  constructor(adapter) {
    this.adapter = adapter; 
  }
  
  fetch(url) {
    return this.adapter.request(url).then((response) => { }); 
}

리스코프 치환 원칙 (Liskov Substiution Principle, LSP)

리스코프 원칙이란 자료형 S가 자료형 T의 하위형이라면
프로그램이 갖추어야 할 속성들(정확성, 수행되는 작업 등)의 변경사항 없이 자료형 T의 객체를 자료형 S의 객체로 교체(치환)할 수 있어야 한다는 원칙이다

부모 클래스와 자식 클래스를 가지고 있을 때 베이스 클래스와 하위 클래스를 잘못된 결과 없이 서로 교환하여 사용할 수 있다
수학적으로 정사각형은 직사각형이지만 상속을 통해 is-a관계를 사용하여 모델링한다면 문제가 발생한다

👎

class Rectangle {
  constructor() {
    this.width = 0;
    this.height = 0;
  }
  
  setColot(color) {
    // ~~~ 
  }
  
  render(area) {
    // ~~~
  }
  
  setWidth(width) {
    this.width = width; 
  }
  
  setHeight(height) {
    this.height = height; 
  }
  
  getArea() {
    return this.width * this.height; 
  }
}

class Square extends Rectangle {
  setWidth(width) {
    this.width = width;
    this.height = height;
  }
  
  setHeight(height) {
    this.width = height;
    this.height = height;
  }
}

function renderLargeRectangles(rectangles) {
  rectangles.forEach((rectangle) => {
     rectangle.setWidth(4);
     rectangle.setHeight(5);
     const area = rectangle.getArea(); // 정사각형일 때 25를 리턴한다 하지만 20을 리턴해야 한다
  }) 
}

const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

👍

class Shape {
  setColot(color) {
    // ~~~ 
  }
  
  render(area) {
    // ~~~
  } 
}

class Rectangle extends Shpape {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  
  getArea() {
    return this.width * this.height; 
  }
}

class Square extends Shape {
   constructor(length) {
     super();
     this.length = length;
   }
  
   getArea() {
     return this.lenght * this.length; 
   }
}

function renderLargeRectangles(shapes) {
  shapes.forEach((rectangle) => {
     const area = rectangle.getArea(); // 정사각형일 때 25를 리턴한다 하지만 20을 리턴해야 한다
  }) 
}

const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeRectangles(shapes);

인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

ISP에 의하면 클라이언트는 사용하지 않는 인터페이스에 의존하도록 강요받으면 안된다
방대한 양의 설정 객체가 필요한 클래스로 예를 들 수 있다
클라이언트가 방대한 양의 옵션을 설정하지 않는 것이 좋다. 왜냐하면 대부분의 설정들이 전부 다 필요한 건 아니기 때문이다. 설정을 선택적으로 할 수 있다면 무거운 인터페이스를 만드는 것을 방지할 수 있다

👎

class DOMTraverser {
  constructor(setting) {
    this.settings = settings;
    this.setup();
  }
  
  setup() {
    this.rootNode = this.settings.rootNode;
    this.animationModule.setup();
  }
  
  traverse() {
    // ~~ 
  }
}

const $ = new DOMTraverser({
   rootNode: document,getElementByTagName('body'),
   animationModule() {} // DOM을 탐색할 때는 대부분 애니메이션이 필요하지 않다
});

👍

class DOMTraverser {
  constructor(setting) {
    this.settings = settings;
    this.options = settings.options;
    this.setup();
  }
  
  setup() {
    this.rootNode = this.settings.rootNode;
    this.animationModule.setup();
  }
  
  setupOptions() {
    if (this.options.animationModule) {
      // ~~~ 
    }
  }
  
  traverse() {
    // ~~ 
  }
}

const $ = new DOMTraverser({
   rootNode: document,getElementByTagName('body'),
   options: {
     animationModule() { }
   }
});

의존성 역전 원칙 (Dependency Inversion Principle, DIP)

이 원칙은 두가지 중요한 요소를 가지고 있다

  • 상위 모듈은 하위 모듈에 종속되서는 안 된다. 둘 다 추상화에 의존해야 한다
  • 추상화는 세부사항에 의존하지 않는다. 세부사항은 추상화에 의해 달라져야 한다

DIP 상위 모듈이 하위 모듈의 세부사항을 알지 못하게 한다. 이는 의존성 주입을 통해 달성할 수 있다
DIP의 장정은 모듈 간의 의존성을 감소시키는데 있다. 모듈간의 의존성이 높을수록 코드를 리펙터링하는데 어려워진다

자바스크립트는 인터페이스가 없으므로 추상화에 의존하는 것은 암시적인 역속이다
다른 객체나 클래스에 노출되는 메소드와 속성이 바로 암시적인 약속(추상화)가 된다는 것이다
아래 예제에서 암시적인 약속은 InventoryTracker에 대한 모든 요청 모듈이 requestItems메소드를 가질 것이라는 점이다

👎

class InventoryRequester {
  constructor() {
    this.REQ_METHODS = ['HTTP']; 
  }
  
  requestItem(item) {
    // ~~~ 
  }
}

class InventoryTracker {
  constructor(items) {
    this.items = items;
    
    // 않좋은 이유: 특정 요청방법 구현에 대한 의존성을 만들었다
    // requestItems는 한가지 요청방법을 필요로 한다
    this.requester = new InventoryRequester();
  }
  
  requestItems() {
    this.items.forEach((item) => {
      this.requester.requestItem(item);
    }); 
  }
}

const inventoryTracker = new InventoryTracker(['apples', 'bananas']);
inventoryTracker.requestItems();

👍

class InventoryTracker {
  constructor(items, requester) {
    this.items = items;
    this.requester = requester;
  }
  
  requestItems() {
    this.items.forEach((item) => {
      this.requester.requestItem(item);
    }); 
  }
}

class InventoryRequesterV1 {
  constructor() {
    this.REQ_METHODS = ['HTTP']; 
  }
  
  requestItem(item) {
    // ~~~ 
  }
}

class InventoryRequesterV2 {
  constructor() {
    this.REQ_METHODS = ['WS']; 
  }
  
  requestItem(item) {
    // ~~~ 
  }
}

// 의존성을 외부에서 만들어 주입해줌으로써
// 요청 모듈을 새롭게 만든 웹소켓 사용 모듈로 쉽게 바꿔 끼울 수 있게 되었다
const inventoryTracker = new InventoryTracker(['apples, bananas'], new InventoryRequesterV2());
inventoryTracker.requestItem();

0개의 댓글