Khi bạn đang xây dựng ứng dụng bằng javascript, đôi khi bạn gặp khó khăn trong khi xây dựng các đối tượng phức tạp, khi đạt đến 1 độ lớn nhất định trong mã code của bạn, nó càng trở nên quan trọng vì nó sẽ trở nên phức tạp hơn khi ứng dụng của bạn trở nên lớn hơn.

Có nhiều dạng phức tạp. Một trong số đó là mã của bạn đang lặp đi lặp lại khi bạn cố gắng tạo ra các biến thể khác nhau của 1 đối tượng nhất định. Một điều nữa có thể là việc tạo ra các biến thể của đối tượng đó có thể trở nên khá dài bởi vì bạn phải xử lý logic trong 1 khối khổng lồ ở đâu đó, như trong hàm khởi tạo của 1 lớp chẳng hạn.

Bài viết sẽ đề cập đến vấn đề này, sẽ trình bày cách làm thế nào để xây dựng Design Pattern trong javascript để giải quyết vấn đề cho đỡ phức tạp hơn.

Vậy đâu là những vấn đề mà mẫu Builder có thể dễ dàng giải quyết?

Trước tiên hãy xem qua ví dụ không xây dựng mẫu và sau đó là ví dụ sử dụng mẫu:

Trong ví dụ bên dưới, chúng ta sẽ định nghĩa 1 lớp Frog , có đủ khả năng sống và mạo hiểm ngoài xã hội giống như ếch thật. Chúng sẽ có 2 mắt, 4 chân, 1 mũi, 1 lưỡi và 1 trái tim.

Xây dựng mà không dùng mẫu

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
    }
  }
}

Với cách xây dựng mẫu

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
  }
}

Bây giờ bạn có thể nhìn thấy đoạn mã khi xây dựng bằng mẫu lớn hơn khi bạn không xây dựng, nhưng khoan hãy suy nghĩ xa hơn 1 chút về tất cả trường hợp sẽ xảy ra khi bạn phát triển ứng dụng. Bằng cách xem xét 2 ví dụ này , ví dụ như mã được xây dựng Design pattern khi ứng dụng lớn hơn nó sẽ có tính đơn giản, mở rộng và khả năng bảo trì tốt hơn.

Dưới đây là 4 vấn đề mà Builder Design Pattern có thể giải quyết dễ dàng trong javascript:

1. Code lộn xộn và nhầm lẫn

không có gì lạ khi có có lỗi và tai nạn trong code xảy ra khi phát triển các khối chức năng có kích thước lớn, ngoài ra khi có nhiều thứ trong 1 khối thì chúng ta rất dễ bị nhầm lẫn.

Vì vậy, loại tình huống nào bạn sẽ gặp phải khi có nhiều thứ đang diễn ra trong khối chức năng như hàm khởi tạo(constructor)?

Quay trở lại mã code ví dụ đầu tiên không xây dựng mẫu, giả sử chúng ta phải thêm 1 số logic bổ xung để chấp nhận thông qua trong các đối số trước khi áp dụng chúng vào 1 instance

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: eyes[0], right: eyes[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,
)

Hàm khởi tạo của chúng ta hơi dài và trong một số trường hợp, nó sẽ chứa nhiều logic không cần thiết. Nó lộn xộn bởi logic xử lý các tham số khác nhau. Điều này có thể gây nhầm lẫn đặc biệt là nếu chúng ta đã không xem xét mã nguồn trong một thời gian dài.

Khi chúng ta đang phát triển 1 ứng dụng frog  và chúng muốn khởi tạo instance của 1 Frog, nhược điểm là chúng ta sẽ phải đảm bảo rằng các tham số truyền vào gần như là 100%, hoặc tất cả nhưng thứ tồn tại trong hàm khởi tạo, nếu chúng ta cần kiểm tra kỹ bằng mắt, tìm ta sẽ dò qua 1 loạt mã để tìm mã mình tìm kiếm, bạn tìm dòng code mà bạn tìm nhưng sau đó lại nhận ra 1 đoạn mã khác nó lại tham chiếu và ảnh hưởng đến mã code của 50 dòng trên, bây giờ bạn phải quay lại và dò xem có điều gì xảy ra trong đoạn code trên. Nếu chúng ta có cái nhìn khác về hàm khởi tạo FrogBuilder từ ví dụ trước, chúng ta có thể đơn giản hóa hàm khởi tạo, để tự nhiên và loại bỏ sự nhầm lẫn hơn, chúng tôi vẫn thực hiện các xác nhận bổ sung, nó chỉ là tách ra các phương thức nhỏ của riêng nó, đó được coi như trái tim và linh hồn của hàm khởi tạo

2. Dễ đọc

Nếu chúng ta xem mã code gần đây nhất, nó sẽ hơi khó đọc vì chúng ta phải sử lý các biến khác nhau cùng 1 lúc, không có cách nào khác ngoài việc tìm hiểu mọi thư trước khi chúng ta tạo instance của Frog.

Ngoài ra, chúng ta phải cung cấp 1 số tài liệu nếu không chúng ta thấy lạ là tại sao tongueWidth lại được đổi tên thành width. Đây là điều nhảm nhí phải không!

Nếu chúng ta chuyển đổi ví dụ để sử dụng builder pattern, chúng ta có thể làm mọi thứ cho nó dễ đọc hơn:

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()

Chúng ta đã đạt được khả năng làm cho mã dễ đọc hơn theo 1 số cách:

1. updateTongueWidthFieldName dễ dàng xác định cho chúng tôi những gì nó làm và tại sao nó làm điều đó. Chúng ta được nó đang cập nhật tên trường và chúng tôi cũng biết tại sao nó làm điều đó. Chúng tôi biết rằng nó đang cập nhật tên trường

2. Các hàm khởi tạo ngắn và đơn giản hóa:

- Hoàn toàn ổn để thiết lập các thuộc tính sau.

3. Có thể hiểu rõ từng tham số khi bắt đầu 1 Frog mới, nó giống như đọc tiếng anh, bạn rõ ràng đang thiết lập mắt, chân, vv...và cuối cùng gọi các phương thức để tạo ra 1 Frog.

4. Mỗi logic giờ nằm trong khối riêng biệt để chúng ta có thể dễ dàng quản lý:

- Khi bạn thực hiện 1 số thay đổi, chỉ cần tập trung vào 1 thứ, đó là những gì chỉ giới hạn trong 1 khối chức năng

3. Ít kiểm soát

Điều quan trọng nhất trong danh sách này là được hưởng lợi từ việc kiểm soát nhiều hơn đối với việc thực hiện. Trước ví dụ về không sử dụng mẫu, viết nhiều mã hơn trong hàm khởi tạo, nhưng càng nhiều mã bạn cố gắng bám vào thì nó càng làm giảm khả năng đọc gây ra sự lộn xộn và nhầm lẫn.

Vì vậy chúng tôi có thể cô lập các chi tiết triển khai cho từng khối chức năng của riêng mình, giờ đây chúng ta có quyền kiểm soát tốt hơn theo nhiều cách.

Một cách là chúng ta có thể thêm các xác nhận mà không cần thêm nhiều vấn đề, điều này làm cho giai đoạn xây dựng hàm khởi tạo mạnh mẽ hơn:

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')
  }
}

Chúng tôi đã có 1 số lợi ích thực tế là mỗi phần của hàm tạo được cách ly bằng cách thêm vào xác nhận hợp lệ cũng như phương thức xác thực để đảm bảo rằng tất cả các trường bắt buộc đã được đặt trước khi cuối cùng xây dựng Frog.

Chúng ta cũng có thể tận dụng các cơ hội đã mở này để thêm các loại dữ liệu đầu vào tùy chỉnh khác để xây dựng giá trị trả về ban đầu của một tham số.

Ví dụ: chúng ta có thể thêm nhiều cách tùy chỉnh mà người gọi có thể truyền vào eyes, để cung cấp cho họ nhiều tiện lợi hơn so với những gì chúng ta đã cung cấp trước đây:

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
}

Bằng cách này, nó giúp người gọi dễ dàng hơn trong việc chọn bất kỳ biến thể nào của loại đầu vào mà họ muốn:

// 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. Giải quyết bằng: Templating

Một mối quan tâm chúng ta có thể gặp trong tương lai là chúng ta kết thúc với 1 số mã lặp đi lặp lại.

Ví dụ, hãy quay lại với class Frog,  bạn có nghĩ rằng khi chúng ta muốn tạo ra một số loại ếch nhất định, một số trong số chúng có thể có những đặc điểm chính xác giống nhau không?

Trong một kịch bản thế giới thực, có những biến thể khác nhau của ếch. Một con cóc chẳng hạn là một loại ếch, nhưng không phải tất cả ếch đều là cóc. Vì vậy, điều đó cho chúng ta biết rằng có một số tính chất đặc biệt của một con cóc không nên thuộc về ếch bình thường.

Một điểm khác biệt giữa cóc và ếch là cóc dành phần lớn thời gian trên đất liền so với những con ếch bình thường dành phần lớn thời gian ở dưới nước. Ngoài ra, cóc cũng có làn da khô cằn trong khi da ếch bình thường hơi nhếch nhác.

Điều đó có nghĩa là chúng ta sẽ phải đảm bảo một số cách mà mỗi khi ếch được tạo ra, chỉ một số giá trị được chấp nhận.

Chúng ta hãy quay lại với hàm khởi tạo Frog của chúng ta và thêm vào 2 tham số mới habitatskin

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
    }
  }
}

Thực hiện 2 thay đổi đơn giản cho hàm khởi tạo này đã hơi khó hiểu! Đây là lý do tại sao mô hình xây dựng được khuyến khích. Nếu chúng ta đặt các tham số habitat skin ở cuối, nó có thể gây ra lỗi vì weight skin có thể không xác định được vì cả hai đều là tùy chọn! Và vì chúng là tùy chọn, nếu người gọi không vượt qua được chúng, thì môi trường sống và da sẽ bị sử dụng nhầm cho chúng. Rất tiếc!

Hãy chỉnh sửa FrogBuilder để hỗ trợ habitat và skin:

setHabitat(habitat) {
  this.habitat = habitat
}

setSkin(skin) {
  this.skin = skin
}

Bây giờ giả sử  chúng ta cần tạo 2 con riêng biệt,  1 con cóc và 1 con ếch:

// 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()

vậy đâu là mã lặp đi lặp lại trong này?Nếu chúng ta quan sát kỹ, chúng ta đã lặp lại habitat skin. 

 Điều gì xảy ra nếu có thêm 5 setters chỉ dành riêng cho cóc?

 Chúng tôi sẽ phải áp dụng thủ công mẫu này cho cóc mỗi khi chúng tôi tạo ra chúng - điều tương tự cũng xảy ra với những con ếch bình thường.

Những gì chúng ta có thể làm là tạo ra một templater, thông thường theo quy ước được gọi là Director.

Director chịu trách nhiệm thực hiện các bước để tạo các đối tượng - thường là nơi có một số cấu trúc phổ biến có thể được xác định trước khi xây dựng đối tượng cuối cùng, như trong trường hợp này là con cóc của chúng tôi.

Vì vậy, thay vì phải đặt thủ công các thuộc tính khác biệt giữa các con cóc, chúng ta có thể yêu Director tạo mẫu đó cho chúng ta:

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()

Bằng cách đó, bạn tránh thực hiện lặp lai điều mà các toads chia sẽ chung và chỉ tập trung vào các thuộc tính bạn cần. Điều này hữu ích hơn khi có nhiều thuộc tính chỉ dành riêng cho toads


Phần Kết luận

Và điều đó kết thúc phần cuối của bài viết này! Tôi hy vọng bạn thấy điều này có giá trị và tìm kiếm nhiều hơn trong tương lai!

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