정보실

웹학교

정보실

Nodejs 빌더 디자인 패턴으로 쉽게 해결할 수 있는 JavaScript의 4 가지 위험한 문제

본문

JavaScript로 앱을 개발할 때 복잡한 객체를 생성하기가 어려운 경우가 있습니다. 코드에서 특정 지점에 도달하면 앱이 커질수록 더 복잡해질 수 있기 때문에 더욱 중요해집니다.


https://dev.to/jsmanifest/4-dangerous-problems-in-javascript-easily-solved-by-the-builder-design-pattern-1738 


"복잡성"은 여러 형태로 나타날 수 있습니다. 하나는 특정 객체의 다른 변형을 만들려고 할 때 코드가 반복적으로 나타날 수 있습니다. 또 다른 하나는 classconstructor 블록과 같이 거대한 블록 하나에서 논리를 수행해야 하기 때문에 객체의 변형을 만들려는 시도가 상당히 길어질 수 있습니다.


이 기사에서는 이러한 문제를 다루고 JavaScript의 빌더 디자인 패턴이 이러한 문제를 훨씬 덜 문제로 만드는 방법을 보여줍니다.


그렇다면 빌더 패턴이 쉽게 해결할 수 있는 문제점은 무엇입니까?


먼저 빌더 패턴이 없는 예제를 본 다음 빌더 패턴이 있는 예제를 살펴보면 시각적 코드 예제가 있는 유일한 사람이 아닙니다.


다음 코드 예제에서는 Frog 클래스를 정의합니다. 우리는 개구리 계급이 문제 없이 야생에서 살면서 숨을 쉴 수 있으려면 두 눈, 네 다리, 향기, 혀 및 심장이 필요하다고 가정합니다. 

이제 현실 세계에는 분명히 더 많은 참여가 있으며 살기 위해서는 향기가 필요하다는 말이 우스운 것처럼 들리지만 우리는 모든 것에 대해 완전히 사실이 아니라 단순하고 재미있게 유지할 것입니다. 

우리는 다른 시점에 다른 게시물에서 사실을 100 % 정확하게 얻을 수 있습니다 :)


빌더 패턴없이 


class Frog {
  constructor(name, gender, eyes, legs, scent, tongue, heart, weight, height) {
    this.name = name
    this.gender = gender
    this.eyes = eyes
    this.legs = legs
    this.scent = scent
    this.tongue = tongue
    this.heart = heart
    if (weight) {
      this.weight = weight
    }
    if (height) {
      this.height = height
    }
  }
}


빌더 패턴으로 


class FrogBuilder {
  constructor(name, gender) {
    this.name = name
    this.gender = gender
  }

  setEyes(eyes) {
    this.eyes = eyes
    return this
  }

  setLegs(legs) {
    this.legs = legs
    return this
  }

  setScent(scent) {
    this.scent = scent
    return this
  }

  setTongue(tongue) {
    this.tongue = tongue
    return this
  }

  setHeart(heart) {
    this.heart = heart
    return this
  }

  setWeight(weight) {
    this.weight = weight
    return this
  }

  setHeight(height) {
    this.height = height
    return this
  }
}

이제 빌더 패턴 예제가 코드에서 더 크기 때문에 이것은 약간 과도하게 보입니다. 그러나 잠재적 인 개구리 응용 프로그램을 개발하는 동안 발생할 수 있는 모든 사례를 자세히 살펴보면 이 두 예제를 보면 빌더 패턴이 적용된 코드 예제가 단순성, 유지 관리 성 및 강력한 기능을 구현할 수 있는 더 많은 기회를 제공합니다.


다음은 빌더 디자인 패턴이 JavaScript에서 쉽게 해결할 수 있는 4 가지 문제점입니다.


1. Code clutter and confusion 


대형 기능 블록에서 부주의하게 개발되어 오류와 사고가 발생하는 것은 드문 일이 아닙니다. 또한 단일 블록에 너무 많은 일이 발생하면 혼동되기 쉽습니다.


그러면 생성자와 같이 함수 블록에 "너무 많은 일이 발생"할 때 어떤 상황이 발생합니까?


빌더 패턴 없이 구현 된 첫 번째 코드 예제로 돌아가서 전달 된 인수를 인스턴스에 적용하기 전에 인수를 승인하려면 몇 가지 논리를 추가해야 한다고 가정하십시오.


class Frog {
  constructor(name, gender, eyes, legs, scent, tongue, heart, weight, height) {
    if (!Array.isArray(legs)) {
      throw new Error('Parameter "legs" is not an array')
    }
    // Ensure that the first character is always capitalized
    this.name = name.charAt(0).toUpperCase() + name.slice(1)
    this.gender = gender
    // We are allowing the caller to pass in an array where the first index is the left eye and the 2nd is the right
    //    This is for convenience to make it easier for them.
    //    Or they can just pass in the eyes using the correct format if they want to
    //    We must transform it into the object format if they chose the array approach
    //      because some internal API uses this format
    this.eyes = Array.isArray(eyes) ? { left: eye[0], right: eye[1] } : eyes
    this.legs = legs
    this.scent = scent
    // Pretending some internal API changed the field name of the frog's tongue from "tongueWidth" to "width"
    //    Check for old implementation and migrate them to the new field name
    const isOld = 'tongueWidth' in tongue
    if (isOld) {
      const newTongue = { ...tongue }
      delete newTongue['tongueWidth']
      newTongue.width = tongue.width
      this.tongue = newTongue
    } else {
      this.tongue = newTongue
    }
    this.heart = heart
    if (typeof weight !== 'undefined') {
      this.weight = weight
    }
    if (typeof height !== 'undefined') {
      this.height = height
    }
  }
}

const larry = new Frog(
  'larry',
  'male',
  [{ volume: 1.1 }, { volume: 1.12 }],
  [{ size: 'small' }, { size: 'small' }, { size: 'small' }, { size: 'small' }],
  'sweaty socks',
  { tongueWidth: 18, color: 'dark red', type: 'round' },
  { rate: 22 },
  6,
  3.5,
)


우리의 생성자는 약간 길며, 어떤 경우에는 많은 논리가 필요하지 않은 것처럼 보일 수도 있습니다. 다른 매개 변수를 처리하는 논리에 의해 복잡해집니다. 오랫동안 소스 코드를 보지 않은 경우 특히 혼란스러울 수 있습니다.


개구리 응용 프로그램을 개발할 때 개구리 인스턴스를 인스턴스화 하려는 경우 단점은 함수 시그니처에 따라 100 %에 가까운 모든 매개 변수를 가져와야 합니다. 건설 단계. 어떤 시점에서 눈의 유형을 다시 확인해야 하는 경우 원하는 코드를 찾기 위해 복잡한 코드를 스캔 해야 합니다. 찾고자 하는 행을 마침내 찾았다면 혼란스러울 것 같지만, 50 줄 이상의 동일한 매개 변수를 참조하고 영향을 주는 다른 코드 줄이 있다는 것을 깨달았습니까? 이제 어떤 일이 일어날 지 이해할 수 있도록 돌아가서 스캔 해야 합니다.


이전 예제에서 FrogBuilder 생성자를 다시 살펴보면 혼란을 제거하면서 생성자를 단순화하여 "보다 자연스러운"느낌을 줄 수 있습니다. 우리는 여전히 추가 검증을 수행 할 것입니다. 빌더 패턴의 심장과 영혼 인 자체 작은 방법으로 분리 될 것입니다.


2. 가독성 


최신 코드 예제를 살펴보면 이러한 다양한 변형 처리를 한 번에 처리해야 하기 때문에 이미 읽기가 약간 어려워지고 있습니다. Frog의 인스턴스를 만들고 싶다면 모든 것을 한 번에 이해하는 것 외에는 다른 방법이 없습니다.


또한, 우리는 문서를 제공해야 합니다. 그렇지 않으면 세계에서 왜 이름이 너비로 바뀌는 지 확실하지 않습니다. 이건 터무니 없다!


빌더 패턴을 사용하도록 예제를 변환하면 보다 쉽게 ​​읽을 수 있습니다.


class FrogBuilder {
  constructor(name, gender) {
    // Ensure that the first character is always capitalized
    this.name = name.charAt(0).toUpperCase() + name.slice(1)
    this.gender = gender
  }

  formatEyesCorrectly(eyes) {
    return Array.isArray(eyes) ? { left: eye[0], right: eye[1] } : eyes
  }

  setEyes(eyes) {
    this.eyes = this.formatEyes(eyes)
    return this
  }

  setLegs(legs) {
    if (!Array.isArray(legs)) {
      throw new Error('"legs" is not an array')
    }
    this.legs = legs
    return this
  }

  setScent(scent) {
    this.scent = scent
    return this
  }

  updateTongueWidthFieldName(tongue) {
    const newTongue = { ...tongue }
    delete newTongue['tongueWidth']
    newTongue.width = tongue.width
    return newTongue
  }

  setTongue(tongue) {
    const isOld = 'tongueWidth' in tongue
    this.tongue = isOld
      ? this.updateTongueWidthFieldName(tongue, tongue.tongueWidth)
      : tongue
    return this
  }

  setHeart(heart) {
    this.heart = heart
    return this
  }

  setWeight(weight) {
    if (typeof weight !== 'undefined') {
      this.weight = weight
    }
    return this
  }

  setHeight(height) {
    if (typeof height !== 'undefined') {
      this.height = height
    }
    return this
  }

  build() {
    return new Frog(
      this.name,
      this.gender,
      this.eyes,
      this.legs,
      this.scent,
      this.tongue,
      this.heart,
      this.weight,
      this.height,
    )
  }
}

const larry = new FrogBuilder('larry', 'male')
  .setEyes([{ volume: 1.1 }, { volume: 1.12 }])
  .setScent('sweaty socks')
  .setHeart({ rate: 22 })
  .setWeight(6)
  .setHeight(3.5)
  .setLegs([
    { size: 'small' },
    { size: 'small' },
    { size: 'small' },
    { size: 'small' },
  ])
  .setTongue({ tongueWidth: 18, color: 'dark red', type: 'round' })
  .build()

우리는 몇 가지 방법으로 코드를 훨씬 더 읽기 쉽게 만들 수 있게 되었습니다.


  • 메소드의 이름은 충분히 그 자체로 문서입니다


updateTongueWidthFieldName은 그것이 무엇을 하고 왜 그렇게 하는지 쉽게 정의합니다. 우리는 그것이 필드 이름을 업데이트한다는 것을 알고 있습니다. 또한 "업데이트"라는 단어가 이미 최신 상태를 유지하기 때문에 그 이유를 알고 있습니다! 이 자체 문서화 된 코드는 일부 필드 이름이 오래되었으며 새 필드 이름을 사용하도록 변경해야 한다고 가정합니다.


  • 생성자는 짧고 간단합니다.


나중에 다른 속성을 설정해도 좋습니다.


  • 새로운 개구리를 시작할 때 각 매개 변수를 명확하게 이해할 수 있습니다


영어를 읽는 것과 같습니다. 눈, 다리 등을 명확하게 설정하고 마지막으로 빌드 방법을 호출하여 개구리를 만듭니다.


  • 각 논리는 이제 별도의 블록으로 분리되어 쉽게 따라갈 수 있습니다.


일부 변경을 수행 할 때는 기능 블록에서 분리 된 것 중 하나에 만 집중하면 됩니다.


3. 통제력 부족 


이 목록에서 가장 중요한 것은 구현에 대한 통제력을 강화하는 것입니다. 빌더 예제 이전에는 생성자에 더 많은 코드를 작성할 수 있지만, 더 많은 코드를 집어 넣으려고 하면 가독성이 떨어지고 혼란과 혼동이 발생합니다.


구현 세부 사항을 각각의 고유 한 기능 블록으로 분리 할 수 있게 되었으므로 이제 여러 가지 방법으로 더 세밀하게 제어 할 수 있습니다.


한 가지 방법은 문제를 더 추가하지 않고도 검증을 추가 할 수 있어 시공 단계가 더욱 강력 해집니다.


setHeart(heart) {
  if (typeof heart !== 'object') {
    throw new Error('heart is not an object')
  }
  if (!('rate' in heart)) {
    throw new Error('rate in heart is undefined')
  }
  // Assume the caller wants to pass in a callback to receive the current frog's weight and height that he or she has set
  //    previously so they can calculate the heart object on the fly. Useful for loops of collections
  if (typeof heart === 'function') {
    this.heart = heart({
      weight: this.weight,
      height: this.height
    })
  } else {
    this.heart = heart
  }

  return this
}

validate() {
  const requiredFields = ['name', 'gender', 'eyes', 'legs', 'scent', 'tongue', 'heart']
  for (let index = 0; index < requiredFields.length; index++) {
    const field = requiredFields[index]
    // Immediately return false since we are missing a parameter
    if (!(field in this)) {
      return false
    }
  }
  return true
}

build() {
  const isValid = this.validate(this)
  if (isValid) {
  return new Frog(
    this.name,
    this.gender,
    this.eyes,
    this.legs,
    this.scent,
    this.tongue,
    this.heart,
    this.weight,
    this.height,
  )
  } else {
    // just going to log to console
    console.error('Parameters are invalid')
  }
}


우리는 마지막으로 개구리를 만들기 전에 모든 필수 필드가 설정되었는지 확인하기 위해 유효성 검사 방법뿐만 아니라 유효성 검사 방법을 추가하여 생성자의 각 부분이 분리되어 있다는 사실을 이용했습니다.


또한 이러한 개방 된 기회를 활용하여 매개 변수의 원래 리턴 값을 빌드 하기 위해 추가 사용자 정의 입력 데이터 유형을 추가 할 수 있습니다.


예를 들어, 발신자가 눈으로 전달할 수 있는 사용자 지정 방법을 더 추가하여 이전에 제공 한 것보다 훨씬 편리하게 만들 수 있습니다.


formatEyesCorrectly(eyes) {
  // Assume the caller wants to pass in an array where the first index is the left
  //    eye, and the 2nd is the right
  if (Array.isArray(eyes)) {
    return {
      left: eye[0],
      right: eye[1]
    }
  }
  // Assume that the caller wants to use a number to indicate that both eyes have the exact same volume
  if (typeof eyes === 'number') {
    return {
      left: { volume: eyes },
      right: { volume: eyes },
    }
  }
  // Assume that the caller might be unsure of what to set the eyes at this current moment, so he expects
  //    the current instance as arguments to their callback handler so they can calculate the eyes by themselves
  if (typeof eyes === 'function') {
    return eyes(this)
  }

    // Assume the caller is passing in the directly formatted object if the code gets here
  return eyes
}

setEyes(eyes) {
  this.eyes = this.formatEyes(eyes)
  return this
}


이렇게 하면 호출자가 원하는 입력 유형을 쉽게 선택할 수 있습니다.


// variation 1 (left eye = index 1, right eye = index 2)
larry.setEyes([{ volume: 1 }, { volume: 1.2 }])

// variation 2 (left eye + right eye = same values)
larry.setEyes(1.1)

// variation 3 (the caller calls the shots on calculating the left and right eyes)
larry.setEyes(function(instance) {
  let leftEye, rightEye
  let weight, height
  if ('weight' in instance) {
    weight = instance.weight
  }
  if ('height' in instance) {
    height = instance.height
  }

  if (weight > 10) {
    // It's a fat frog. Their eyes are probably humongous!
    leftEye = { volume: 5 }
    rightEye = { volume: 5 }
  } else {
    const volume = someApi.getVolume(weight, height)
    leftEye = { volume }
    // Assuming that female frogs have shorter right eyes for some odd reason
    rightEye = { volume: instance.gender === 'female' ? 0.8 : 1 }
  }

  return {
    left: leftEye,
    right: rightEye,
  }
})

// variation 4 (caller decides to use the formatted object directly)
larry.setEyes({
  left: { volume: 1.5 },
  right: { volume: 1.51 },
})


4. 보일러 플레이트 (제공 : 템플릿) 


앞으로 우리가 겪게 될 한 가지 우려는 반복되는 코드로 끝나는 것입니다.


예를 들어, Frog 클래스를 되돌아 보면 특정 유형의 개구리를 만들 때 일부 특성이 동일한 특성을 가질 수 있다고 생각하십니까?


실제 시나리오에서는 개구리의 변형이 다릅니다. 예를 들어 두꺼비는 개구리의 일종이지만 모든 개구리가 두꺼비는 아닙니다. 그것은 정상적인 개구리에 속하지 않아야 하는 두꺼비의 특징이 있다는 것을 말해줍니다.


두꺼비와 개구리의 한 가지 차이점은 두꺼비는 물 속에서 대부분의 시간을 보내는 일반 개구리와는 달리 대부분의 시간을 육지에서 보낸다는 것입니다. 또한, 두꺼비는 건조한 울퉁불퉁 한 피부를 가지고 있지만 일반 개구리의 피부는 약간 칙칙합니다.


즉, 개구리가 인스턴스화 될 때마다 일부 값만 통과 할 수 있고 일부 값만 통과해야 하는 방식을 보장해야 합니다.


Frog 생성자로 돌아가서 서식지와 스킨이라는 두 가지 새로운 매개 변수를 추가해 보겠습니다.


class Frog {
  constructor(
    name,
    gender,
    eyes,
    legs,
    scent,
    tongue,
    heart,
    habitat,
    skin,
    weight,
    height,
  ) {
    this.name = name
    this.gender = gender
    this.eyes = eyes
    this.legs = legs
    this.scent = scent
    this.tongue = tongue
    this.heart = heart
    this.habitat = habitat
    this.skin = skin
    if (weight) {
      this.weight = weight
    }
    if (height) {
      this.height = height
    }
  }
}


이 생성자에 대한 두 가지 간단한 변경은 이미 약간 혼란스러웠습니다! 이것이 빌더 패턴이 권장되는 이유입니다. 서식지와 스킨 매개 변수를 마지막에 넣으면 무게와 높이가 모두 선택 사항이므로 정의 할 수 없으므로 버그가 발생할 수 있습니다! 또한 선택 사항이므로 발신자가 전화를 걸지 않으면 서식지와 피부가 실수로 사용됩니다.


서식지와 피부를 지원하기 위해 FrogBuilder를 편집 할 수 있습니다.


setHabitat(habitat) {
  this.habitat = habitat
}

setSkin(skin) {
  this.skin = skin
}

이제 두 개의 별도 두꺼비와 하나의 일반 개구리를 만들어야 한다고 가정 해 봅시다.


// frog
const sally = new FrogBuilder('sally', 'female')
  .setEyes([{ volume: 1.1 }, { volume: 1.12 }])
  .setScent('blueberry')
  .setHeart({ rate: 12 })
  .setWeight(5)
  .setHeight(3.1)
  .setLegs([
    { size: 'small' },
    { size: 'small' },
    { size: 'small' },
    { size: 'small' },
  ])
  .setTongue({ width: 12, color: 'navy blue', type: 'round' })
  .setHabitat('water')
  .setSkin('oily')
  .build()

// toad
const kelly = new FrogBuilder('kelly', 'female')
  .setEyes([{ volume: 1.1 }, { volume: 1.12 }])
  .setScent('black ice')
  .setHeart({ rate: 11 })
  .setWeight(5)
  .setHeight(3.1)
  .setLegs([
    { size: 'small' },
    { size: 'small' },
    { size: 'small' },
    { size: 'small' },
  ])
  .setTongue({ width: 12.5, color: 'olive', type: 'round' })
  .setHabitat('land')
  .setSkin('dry')
  .build()

// toad
const mike = new FrogBuilder('mike', 'male')
  .setEyes([{ volume: 1.1 }, { volume: 1.12 }])
  .setScent('smelly socks')
  .setHeart({ rate: 15 })
  .setWeight(12)
  .setHeight(5.2)
  .setLegs([
    { size: 'medium' },
    { size: 'medium' },
    { size: 'medium' },
    { size: 'medium' },
  ])
  .setTongue({ width: 12.5, color: 'olive', type: 'round' })
  .setHabitat('land')
  .setSkin('dry')
  .build()


반복 코드는 어디에 있습니까?


자세히 살펴보면 두꺼비 서식지와 피부 세터를 반복해야 합니다. 두꺼비 전용의 세터가 5 명 더 있다면 어떨까요? 두꺼비를 만들 때마다 이 템플릿을 수동으로 적용해야 합니다. 일반 개구리도 마찬가지입니다.


우리가 할 수 있는 일은 일반적으로 Director라는 규칙에 따라 템플릿을 만드는 것입니다.


Director는 객체 생성 단계 (일반적으로 최종 객체를 빌드 할 때 미리 정의 할 수 있는 공통 구조가 있는 경우) (이 경우에는 두꺼비)를 실행합니다.


따라서 두꺼비 사이의 고유 한 속성을 수동으로 설정하지 않고 디렉터가 해당 템플릿을 생성하도록 할 수 있습니다.


class ToadBuilder {
  constructor(frogBuilder) {
    this.builder = frogBuilder
  }
  createToad() {
    return this.builder.setHabitat('land').setSkin('dry')
  }
}

let mike = new FrogBuilder('mike', 'male')
mike = new ToadBuilder(mike)
  .setEyes([{ volume: 1.1 }, { volume: 1.12 }])
  .setScent('smelly socks')
  .setHeart({ rate: 15 })
  .setWeight(12)
  .setHeight(5.2)
  .setLegs([
    { size: 'medium' },
    { size: 'medium' },
    { size: 'medium' },
    { size: 'medium' },
  ])
  .setTongue({ width: 12.5, color: 'olive', type: 'round' })
  .build()


이렇게 하면 모든 두꺼비가 공통으로 공유하고 필요한 속성에만 집중할 수 있는 상용구를 구현하지 않아도 됩니다. 이는 두꺼비에만 더 많은 속성이 있을 때 더욱 유용합니다.


결론 


그리고 이것이 이 포스트의 끝을 마무리합니다! 나는 이것이 귀중한 것으로 나타 났으며 앞으로 더 많은 것을 기대하기를 바랍니다!



  • 트위터로 보내기
  • 페이스북으로 보내기
  • 구글플러스로 보내기
  • 카카오톡으로 보내기

페이지 정보

조회 20회 ]  작성일19-12-04 10:41

웹학교