티스토리 뷰

프로그래밍

빌더패턴

수박수박좋다 2024. 7. 11. 11:17
반응형

빌더 패턴

복잡한 객체를 단계적으로 생성할 수 있도록 하는 패턴

객체 생성시 생성자함수 대신에 빌더클래스를 통하여 객체를 생성할 수 있게함

객체의 멤버변수에 대한 가독성, 유지보수를 높임

 

 

예제

class User {
    private name: string;
    private age : number;

    constructor(builder: UserBuilder) {
        this.name = builder.name;
        this.age = builder.age;
    }
}

class UserBuilder {
    public name: string;
    public age: number;

    setName(name) {
        this.name = name;
        return this;
    }
    setAge(age) {
        this.age = age;
        return this;
    }
    build() {
        return new User(this)
    }
}

const user = new UserBuilder().setName("css").setAge(5).build();

  • User의 객체를 Builder를 통해 만든다.
  • 생성된 User객체는 불변하다(굳이 바꿀라면 바꿀 수 있음)
  • 기존 생성자코드가 new User("css", 5) 였을 때 weight을 추가하는 요구사항 발생시 해당하는 생성자코드 파라미터들을 모두 변경해주어야함
    • 패턴 적용시 setWeight 만 붙여주면 됨

객체 생성시 당장에 초기화되지 않아도될 멤버변수들때문에 생성자코드에 undefined 또는 null을 넣는 방법보다 해당 방법사용시 좀 더 명확하게 나타낼 수 있음.

멤버변수 초기화시 검증부분을 메소드별로 분리할 수 있음(생성자에 몰아넣는 방식이 아님)

생성자를 대신하는 것으로 끝나는게 아니라 객체 자체를 반환해서 체이닝으로 메소드들을 수행하는 기법을 사용할 수 있음

클래스 수가 *2배만큼 늘어나기 때문에 코드복잡성이 늘어남.

필드갯수가 적고(최소 4개정도) 변경 가능성이 없는 경우라면 생성자를 사용하는 것이 더 좋을 수 있음

 

 

Zod

Zod 클래스 구현 코드

zod의 경우 빌더패턴과 유사해보이나 Zod클래스 생성하는 빌더가 아닌 본 클래스에서 객체를 생성하기 위한 메소드체이닝을 사용하고 있다.

재밌는 점은 메소드 체이닝을 위해 return this를 하는 것이 아닌 매번 함수에서 새로운 객체를 생성하고 있다는 점이다. 이전 메소드들의 상태를 저장하기위해 checks에 추가해 객체를 생성하는 것을 볼 수 있다.

// ZodNumber클래스 메소드 일부
  _addCheck(check: ZodNumberCheck) {
    return new ZodNumber({
      ...this._def,
      checks: [...this._def.checks, check],
    });
  }

  int(message?: errorUtil.ErrMessage) {
    return this._addCheck({
      kind: "int",
      message: errorUtil.toString(message),
    });
  }

// ZodNumber클래스 메소드 일부

const numberSuite = new Benchmark.Suite("z.number");
const numberSchema = z.number().int();

numberSuite
  .add("valid", () => {
    numberSchema.parse(1);
  })
  .add("invalid type", () => {
    try {
      numberSchema.parse("bad");
    } catch (e) {}
  })
  .add("invalid number", () => {
    try {
      numberSchema.parse(0.5);
    } catch (e) {}
  })
  .on("cycle", (e: Benchmark.Event) => {
    console.log(`z.number: ${e.target}`);
  });

 

 

빌더 패턴을 사용한 Validator 예제

생성자를 통해 검증하고싶은 조건들을 미리 검증해놓아서 공용된 검증에 사용할 수 있는 밸리데이터를 만들 수 있다.

 

Validator 인터페이스

interface Validator {
    validate(value: any): boolean;
}

 

NumberValidator 클래스

class NumberValidator implements Validator {
    private isNumberCheck: boolean = false;
    private isPositiveCheck: boolean = false;

    private constructor() {}

    static builder() {
        return new NumberValidator();
    }

    public isNumber(): this {
        this.isNumberCheck = true;
        return this;
    }

    public isPositive(): this {
        this.isPositiveCheck = true;
        return this;
    }

    public validate(value: any): boolean {
        if (this.isNumberCheck && typeof value !== 'number') {
            return false;
        }
        if (this.isPositiveCheck && value <= 0) {
            return false;
        }
        return true;
    }
}

 

StringValidator 클래스

class StringValidator implements Validator {
    private isStringCheck: boolean = false;
    private isNotEmptyCheck: boolean = false;

    private constructor() {}

    static builder() {
        return new StringValidator();
    }

    public isString(): this {
        this.isStringCheck = true;
        return this;
    }

    public isNotEmpty(): this {
        this.isNotEmptyCheck = true;
        return this;
    }

    public validate(value: any): boolean {
        if (this.isStringCheck && typeof value !== 'string') {
            return false;
        }
        if (this.isNotEmptyCheck && value.trim().length === 0) {
            return false;
        }
        return true;
    }
}

 

ValidatorBuilder 클래스

class ValidatorBuilder {
    public static number(): NumberValidator {
        return NumberValidator.builder();
    }

    public static string(): StringValidator {
        return StringValidator.builder();
    }
}

 

사용 예제

const numberValidator = ValidatorBuilder.number()
    .isNumber()
    .isPositive();

console.log(numberValidator.validate(15)); // true
console.log(numberValidator.validate(-5));  // false

const stringValidator = ValidatorBuilder.string()
    .isString()
    .isNotEmpty();

console.log(stringValidator.validate("Hello")); // true
console.log(stringValidator.validate(""));      // false

 

빌더패턴을 이용해 생성자를 메소드체이닝으로 명확히 표현할 수 있고 변경에 용이하게 대처할 수 있다는 점이 있다.

분리된 setter들로인해 생성자의 검증을 분산시킬 수 있다.

객체의 불변성을 보장할 수 있다.

 

 

반응형
댓글
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
농담곰의 고군분투 개발기