Clean Code

Clean code

연관된 글들

Clean Code Guide Series

  1. 클린 코드 in TypeScript #1 - 변수와 함수편
  2. 클린 코드 in TypeScript #2 - 객체와 클래스편 (현재 글)

https://github.com/ryanmcdermott/clean-code-javascript

위 링크에 있는 깃헙 ryanmcdermott 의 clean-code-javascript 에서 내용을 참고하여 만든 클린 코드 가이드를 TypeScript 에서 사용할 수 있도록 정리한 글입니다.


이전 편을 안보신 분들이라면 보고 오시는 걸 추천드립니다!

반복적으로 등장하는 개념, 결국 클린 코드에서 하고자하는 의도를 파악하는데 더 도움이 됩니다. :)


객체와 데이터 구조


Getter & Setter


  • 객체의 캡슐화
  • 로깅 및 오류 처리가 쉬워짐

안좋은 예:

type BankAccount = {
    balance: number;
    // ...
}

const value = 100;
const account: BankAccount = {
    balance: 0,
    // ...
};

if (value < 0) {
    throw new Error('Cannot set negative balance.');
}

account.balance = value;

좋은 예:

class BankAccount {
    private accountBalance: number = 0;

    get balance(): number {
    return this.accountBalance;
    }

    set balance(value: number) {
    if (value < 0) {
        throw new Error('Cannot set negative balance.');
    }

    this.accountBalance = value;
    }

    // ...
}

// Now `BankAccount` encapsulates the validation logic.
// If one day the specifications change, and we need extra validation rule,
// we would have to alter only the `setter` implementation,
// leaving all dependent code unchanged.
const account = new BankAccount();
account.balance = 100;

👆맨 위로


객체 멤버들의 접근 권한자를 설정하세요.


안좋은 예:

class Circle {
    radius: number;
    
    constructor(radius: number) {
    this.radius = radius;
    }

    perimeter() {
    return 2 * Math.PI * this.radius;
    }

    surface() {
    return Math.PI * this.radius * this.radius;
    }
}

좋은 에:

class Circle {
    constructor(private readonly radius: number) {
    }

    perimeter() {
    return 2 * Math.PI * this.radius;
    }

    surface() {
    return Math.PI * this.radius * this.radius;
    }
}

👆맨 위로


읽기 전용을 활용하세요.


안좋은 예:

interface Config {
    host: string;
    port: string;
    db: string;
}

좋은 예:

interface Config {
    readonly host: string;
    readonly port: string;
    readonly db: string;
}

👆맨 위로


클래스


클래스는 작게 만들어야합니다.


  • 클래스이 커지면 의존성이 커집니다.
  • 단일 책임 원칙의 클래스를 만들도록 노력해야합니다.

안좋은 예:

class Dashboard {
    getLanguage(): string { /* ... */ }
    setLanguage(language: string): void { /* ... */ }
    showProgress(): void { /* ... */ }
    hideProgress(): void { /* ... */ }
    isDirty(): boolean { /* ... */ }
    disable(): void { /* ... */ }
    enable(): void { /* ... */ }
    addSubscription(subscription: Subscription): void { /* ... */ }
    removeSubscription(subscription: Subscription): void { /* ... */ }
    addUser(user: User): void { /* ... */ }
    removeUser(user: User): void { /* ... */ }
    goToHomePage(): void { /* ... */ }
    updateProfile(details: UserDetails): void { /* ... */ }
    getVersion(): string { /* ... */ }
    // ...
}

좋은 예:

class Dashboard {
    disable(): void { /* ... */ }
    enable(): void { /* ... */ }
    getVersion(): string { /* ... */ }
}

// split the responsibilities by moving the remaining methods to other classes
// ...

👆맨 위로


높은 응집력과 낮은 결합도를 갖도록 하세요.


  • 응집력은 멤버 변수들이 서로 연관 되어 있음을 뜻합니다.
  • 이상적으로는 모든 멤버 변수가 각자의 메소드를 사용해야합니다.
  • 하지만 이건 항상 가능한 시나리오는 아니고, 권장하지 않습니다.
  • 그렇지만 응집력이 높은 클래스를 지향해야합니다.
  • 결합도는 두 클래스가 서로 연관되어 있거나 의존하는 방식을 나타냅니다.
  • A 클래스의 변경이 B 클래스에 영향을 미치지 않으면 A 와 B는 낮은 결합도를 가지고 있다 합니다.

안좋은 예:

class UserManager {
    // Bad: each private variable is used by one or another group of methods.
    // It makes clear evidence that the class is holding more than a single responsibility.
    // If I need only to create the service to get the transactions for a user,
    // I'm still forced to pass and instance of `emailSender`.
    constructor(
    private readonly db: Database,
    private readonly emailSender: EmailSender) {
    }

    async getUser(id: number): Promise<User> {
    return await db.users.findOne({ id });
    }

    async getTransactions(userId: number): Promise<Transaction[]> {
    return await db.transactions.find({ userId });
    }

    async sendGreeting(): Promise<void> {
    await emailSender.send('Welcome!');
    }

    async sendNotification(text: string): Promise<void> {
    await emailSender.send(text);
    }

    async sendNewsletter(): Promise<void> {
    // ...
    }
}

좋은 예:

class UserService {
    constructor(private readonly db: Database) {
    }

    async getUser(id: number): Promise<User> {
    return await this.db.users.findOne({ id });
    }

    async getTransactions(userId: number): Promise<Transaction[]> {
    return await this.db.transactions.find({ userId });
    }
}

class UserNotifier {
    constructor(private readonly emailSender: EmailSender) {
    }

    async sendGreeting(): Promise<void> {
    await this.emailSender.send('Welcome!');
    }

    async sendNotification(text: string): Promise<void> {
    await this.emailSender.send(text);
    }

    async sendNewsletter(): Promise<void> {
    // ...
    }
}

👆맨 위로


상속보다 구성 (Composition)을 씁니다.


  • 가능한 경우 상속보다 구성을 선호해야 합니다.
  • 상속은 “has-a” 관계가 아니라, “Is-a” 관계를 나타냅니다. (Human is a Animal? OR Human has a Animal?)
  • 기본 클래스를 변경해서 파생 클래스를 전체적으로 변경할 수 있습니다.

안좋은 예:

class Employee {
    constructor(
    private readonly name: string,
    private readonly email: string) {
    }

    // ...
}

// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData extends Employee {
    constructor(
    name: string,
    email: string,
    private readonly ssn: string,
    private readonly salary: number) {
    super(name, email);
    }

    // ...
}

좋은 예:

class Employee {
    private taxData: EmployeeTaxData;

    constructor(
    private readonly name: string,
    private readonly email: string) {
    }

    setTaxData(ssn: string, salary: number): Employee {
    this.taxData = new EmployeeTaxData(ssn, salary);
    return this;
    }

    // ...
}

class EmployeeTaxData {
    constructor(
    public readonly ssn: string,
    public readonly salary: number) {
    }

    // ...
}

👆맨 위로


메소드 체인을 사용하세요.


  • 일반적으로 이 패턴이 유용할 때가 많습니다.
  • 메소드 체인은 코드를 더 읽기 쉽게 만듭니다.

안좋은 예:

class QueryBuilder {
    private collection: string;
    private pageNumber: number = 1;
    private itemsPerPage: number = 100;
    private orderByFields: string[] = [];

    from(collection: string): void {
    this.collection = collection;
    }

    page(number: number, itemsPerPage: number = 100): void {
    this.pageNumber = number;
    this.itemsPerPage = itemsPerPage;
    }

    orderBy(...fields: string[]): void {
    this.orderByFields = fields;
    }

    build(): Query {
    // ...
    }
}

// ...

const queryBuilder = new QueryBuilder();
queryBuilder.from('users');
queryBuilder.page(1, 100);
queryBuilder.orderBy('firstName', 'lastName');

const query = queryBuilder.build();

좋은 예:

class QueryBuilder {
    private collection: string;
    private pageNumber: number = 1;
    private itemsPerPage: number = 100;
    private orderByFields: string[] = [];

    from(collection: string): this {
    this.collection = collection;
    return this;
    }

    page(number: number, itemsPerPage: number = 100): this {
    this.pageNumber = number;
    this.itemsPerPage = itemsPerPage;
    return this;
    }

    orderBy(...fields: string[]): this {
    this.orderByFields = fields;
    return this;
    }

    build(): Query {
    // ...
    }
}

// ...

const query = new QueryBuilder()
    .from('users')
    .page(1, 100)
    .orderBy('firstName', 'lastName')
    .build();

👆맨 위로


여기까지 타입스크립트 클린 코드 가이드, 변수와 함수 편이였습니다.

다음 포스팅은 클래스에 대해 알아보도록 하겠습니다.


연관된 글들

Clean Code Guide Series

  1. 클린 코드 in TypeScript #1 - 변수와 함수편
  2. 클린 코드 in TypeScript #2 - 객체와 클래스편 (현재 글)