TIL 클린코드 - 10장. 클래스
TIL (Today I Learned) #
2022.05.10
오늘 읽은 범위 #
10장. 클래스
책에서 기억하고 싶은 내용을 써보세요. #
- 클래스 체계
- 변수 목록 → 함수
- 클래스를 정의하는 표준 자바 관계에 따르면 다음 순서를 따른다.
- static public
- static private
- private 인스턴스 변수
- public 함수
- private 함수는 자신을 호출하는 공개 함수 직후에 넣는다.
- 캡슐화 - 변수와 유틸리티 함수는 가능한 공개하지 않는 편이 낫지만 반드시 숨겨야 한다는 법칙도 없다. 때로는 변수나 유틸리티 함수를 protected로 선언해 테스트 코드에 접근을 허용하기도 한다. 하지만 캡슐화를 풀어주는 결정은 언제나 최후의 수단이다.
- 클래스는 작아야 한다!
- 함수는 물리적인 행 수로 크기를 측정했다. 클래스는 맡은
책임을 세는 것을 척도로 여긴다. - 클래스 이름은 해당 클래스 책임을 기술해야 한다. 간결한 이름이 떠오르지 않는다면 필경 클래스 크기가 너무 커서 그렇다.
- 클래스 설명은 if, and, or, but을 사용하지 않고서 25단어 내외로 가능해야 한다.
단일 책임 원칙 (SRP)- 단일 책임 원칙은 클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙이다.- 책임, 즉 변경할 이유를 파악하려 애쓰다 보면 코드를 추상화하기도 쉬워진다.
- SuperDashboard에서 버전 정보를 다루는 메서드 세 개를 따로 빼내 Version이라는 독자적인 클래스를 만든다.
public class Version { public int getMajorVersionNumber() public int getMinorVersionNumber() public int getBuildNumber() }
- SuperDashboard에서 버전 정보를 다루는 메서드 세 개를 따로 빼내 Version이라는 독자적인 클래스를 만든다.
- 큼직한 다목적 클래스 몇 개로 이뤄진 시스템은 당장 알 필요가 없는 사실까지 들이밀어 독자를 방해한다.
응집도- 클래스는 인스턴스 변수 수가 작아야한다. 각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 한다.- ‘함수를 작게, 매개변수 목록을 짧게’라는 전략을 따르다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아진다. 이는 십중팔구 새로운 클래스로 쪼개야 한다는 신호다. 응집도가 높아지도록 변수와 메서드를 적절히 분리해 새로운 클래스 두세 개로 쪼개준다.
- 함수는 물리적인 행 수로 크기를 측정했다. 클래스는 맡은
- 변경하기 쉬운 클래스
- 깨끗한 시스템은 클래스를 체계적으로 정리해 변경에 수반하는 위험을 낮춘다.
오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요 #
- 어떻게 클래스를 책임에 따라 분리 하는지, 클래스명을 어떤식으로 수정 하는지를 중심으로 봤다. 해당 방법을 타입스크립트로 클래스를 설계할 일이 있다면 참고하면 좋을 것 같다.
더 공부한 내용 #
출처: Clean Code concepts adapted for JavaScript - 한글 번역판
SOLID #
단일 책임 원칙 (Single Responsibility Principle, SRP) #
하나의 클래스에 많은 기능을 쑤셔 넣으면 안된다. 하나의 클래스에 너무 많은 기능들이 있고 당신이 이 작은 기능들을 수정할 때 이 코드가 다른 모듈들에 어떠한 영향을 끼치는지 이해하기 어려울 수 있기 때문이다.
안 좋은 예:
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
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/Closed Principle, OCP) #
이 원리는 기본적으로 사용자가 .js소스 코드 파일을 열어 수동으로 조작하지 않고도 모듈의 기능을 확장하도록 허용해야한다고 말한다.
안 좋은 예:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = 'ajaxAdapter';
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = 'nodeAdapter';
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
if (this.adapter.name === 'ajaxAdapter') {
return makeAjaxCall(url).then((response) => {
// transform response and return
});
} else if (this.adapter.name === 'httpNodeAdapter') {
return makeHttpCall(url).then((response) => {
// transform response and return
});
}
}
}
function makeAjaxCall(url) {
// request and return promise
}
function makeHttpCall(url) {
// request and return promise
}
좋은 예:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = 'ajaxAdapter';
}
request(url) {
// request and return promise
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = 'nodeAdapter';
}
request(url) {
// request and return promise
}
}
// 각 adapter마다 커스텀으로 requestUrl 처리가 가능함.
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url).then((response) => {
// transform response and return
});
}
}
리스코프 치환 원칙 (Liskov Substitution Principle, LSP) #
리스코프 치환 원칙이란 자료형 S가 자료형 T의 하위형이면 프로그램이 갖추어야 할 속성들의 변경사항 없이, 자료형 T의 객체를 자료형 S의 객체로 교체할 수 있어야 한다는 원칙이다.
안좋은 예:
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}
setColor(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 = width;
}
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이어야 하는게 맞습니다.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
좋은 예:
class Shape {
setColor(color) {
// ...
}
render(area) {
// ...
}
}
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(length) {
super();
this.length = length;
}
getArea() {
return this.length * this.length;
}
}
function renderLargeShapes(shapes) {
shapes.forEach((shape) => {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);
인터페이스 분리 원칙(Interface Segregation Principle, ISP) #
- Javascript에 타입 시스템이 없다 하더라도 중요하고 관계있는 원칙
- ISP에 의하면 클라이언트는 사용하지 않는 인터페이스에 의존하도록 강요받으면 안된다. 덕 타이핑 때문에 인터페이스는 Javascript에서는 암시적인 계약일 뿐이다.
- 설정을 선택적으로 할 수 있다면 무거운 인터페이스를 만드는 것을 방지 할 수 있다.
안좋은 예
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.animationModule.setup();
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
animationModule() {}, // 우리는 대부분의 경우 DOM을 탐색할 때 애니메이션이 필요하지 않습니다.
// ...
});
좋은 예
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}
setupOptions() {
if (this.options.animationModule) {
// ...
}
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
options: {
animationModule() {},
},
});
의존성 역전 원칙 (Dependency Inversion Principle, DIP) #
- 상위 모듈은 하위 모듈에 종속 되어서는 안된다. 둘 다 추상화에 의존해야 한다.
- 추상화는 세부사항에 의존하지 않는다. 세부사항은 추상화에 의해 달라져야 한다.
DI의 장점은 모듈 간의 의존성을 감소시키는 데에 있다. 모듈간의 의존성이 높을수록 코드를 리팩토링 하는데 어려워진다.
Javascript에는 인터페이스가 없으므로 추상화에 의존하는 것은 암시적인 약속이다. 즉 다른 객체나 클래스에 노출되는 메소드와 속성이 바로 암시적인 약속(추상화)가 된다는 것이다.
아래 예제에서 암시적인 약속은 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.requestItems();