Cách sử dụng Builder pattern trong Java

Chào mừng các bạn đã trở lại với thachleblog, chuyện là dạo này mình đang bắt đầu giai đoạn build cho một dự án mới khá là “challenge”, do đó có khá nhiều kĩ thuật và và kiến thức “background” mình cần củng cố. Trong quá trình tìm hiểu, mình đang tập thói quen là tìm hiểu thật cặn kẽ các vấn đề bằng nhiều nguồn khác nhau. Cảm thấy cái gì hay, thú vị mình sẽ chia sẻ cho các bạn cũng như là “document” lại để lâu lâu quên thì lấy ra đọc lại. Thế nên giai đoạn này có thể bài viết sẽ ra liên tục, hehe.

Ở bài trước, mình đã giới thiệu về Singleton, một design pattern dùng để tạo duy nhất một instance… Hôm nay mình sẽ tiếp tục giới thiệu một Java design pattern khác cũng rất là thường gặp đó là Builder. Cụ thể là gần đây mình tìm hiểu về Guava (maybe sẽ có bài về thằng này :D), cơ chế LoadingCache của có dùng Builder. Nói về Builder thì chúng ra rất thường gặp và sử dụng trong các trường hợp khác như StringBuilder,  DocumentBuilder, …(mà có thể không nhận ra). Hôm nay, mình sẽ giới thiệu thật rõ về lợi ích cũng như cách sử dụng của Builder. Nào, chúng ta cùng bắt đầu nhé.

Giới thiệu

Builder thuộc nhóm Creational pattern, tức là pattern dùng để tạo object. Builder tạo object trên nguyên lý static inner class (xem thêm bài static tại đây)

Builder giải quyết vấn đề gì?

Khi một object có nhiều parameter, thì việc tạo một object có nhiều parameter hoặc nhiều constructor sẽ làm cho code chúng ta trông khá là rườm rà và dễ gây lỗi (chúng ta sẽ vất vả nhưng hiệu quả không cao). Builder là một thiết kế giúp việc tạo một object phức tạp bằng nhiều object đơn giản.

builder pattern trong javaHình minh họa cơ chế của Builder pattern

Ví dụ lấy từ Item 2: Consider a builder when faced with many constructor parameters Effective Java 2nd.

Class NutritionFacts thể hiện thành phần dinh dưỡng của một sản phẩm. Thành phần dinh dưỡng trong thực phẩm sẽ bao gồm rất nhiều loại: calo, fats, carbohydrat, …., có những thứ là luôn luôn có (required) và những loại có thể có hoặc không (optional). Mình phiên bản sinh viên năm 3, 4 nhất định sẽ làm như sau:

  1. package thach.le.normal;
  2. public class NutritionFacts {
  3. private int servingSize; // (mL) required
  4. private int servings; // (per container) required
  5. private int calories; // optional
  6. private int fat; // (g) optional
  7. private int sodium; // (mg) optional
  8. private int carbohydrate; // (g) optional
  9. public void setServingSize(int servingSize) {
  10. this.servingSize = servingSize;
  11. }
  12. public void setServings(int servings) {
  13. this.servings = servings;
  14. }
  15. public void setCalories(int calories) {
  16. this.calories = calories;
  17. }
  18. public void setFat(int fat) {
  19. this.fat = fat;
  20. }
  21. public void setSodium(int sodium) {
  22. this.sodium = sodium;
  23. }
  24. public void setCarbohydrate(int carbohydrate) {
  25. this.carbohydrate = carbohydrate;
  26. }
  27. public int getServingSize() {
  28. return servingSize;
  29. }
  30. public int getServings() {
  31. return servings;
  32. }
  33. public int getCalories() {
  34. return calories;
  35. }
  36. public int getFat() {
  37. return fat;
  38. }
  39. public int getSodium() {
  40. return sodium;
  41. }
  42. public int getCarbohydrate() {
  43. return carbohydrate;
  44. }
  45. public static void main(String[] args) {
  46. NutritionFacts cocacola = new NutritionFacts();
  47. cocacola.setCalories(100);
  48. cocacola.setCarbohydrate(27);
  49. cocacola.setServings(8);
  50. cocacola.setServingSize(240);
  51. cocacola.setSodium(35);
  52. }
  53. }

Vấn đề xảy ra khi dùng getter, setter đó là chúng ta sẽ phải get đúng value mà chúng ta đã set, nếu không, kết quả khi ta get sẽ là null. Chúng ta sẽ gặp khó khăn khi debug.

Chúng ta còn có một kỹ thuật khác để giải quyết vấn đề này gọi là telescoping constructor:

  1. package thach.le.telescoping;
  2. public class NutritionFacts {
  3. private final int servingSize; // (mL) required
  4. private final int servings; // (per container) required
  5. private final int calories; // optional
  6. private final int fat; // (g) optional
  7. private final int sodium; // (mg) optional
  8. private final int carbohydrate; // (g) optional
  9. public int getServingSize() {
  10. return servingSize;
  11. }
  12. public int getServings() {
  13. return servings;
  14. }
  15. public int getCalories() {
  16. return calories;
  17. }
  18. public int getFat() {
  19. return fat;
  20. }
  21. public int getSodium() {
  22. return sodium;
  23. }
  24. public int getCarbohydrate() {
  25. return carbohydrate;
  26. }
  27. public NutritionFacts(int servingSize, int servings) {
  28. this(servingSize, servings, 0);
  29. }
  30. public NutritionFacts(int servingSize, int servings, int calories) {
  31. this(servingSize, servings, calories, 0);
  32. }
  33. public NutritionFacts(int servingSize, int servings, int calories, int fat) {
  34. this(servingSize, servings, calories, fat, 0);
  35. }
  36. public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
  37. this(servingSize, servings, calories, fat, sodium, 0);
  38. }
  39. public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
  40. this.servingSize = servingSize;
  41. this.servings = servings;
  42. this.calories = calories;
  43. this.fat = fat;
  44. this.sodium = sodium;
  45. this.carbohydrate = carbohydrate;
  46. }
  47. }

Tạo mới một object dùng telescoping constructor

  1. NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

Như phân tích ở trên, chúng ta sẽ có constructor nhiều parameter và nhiều constructor để thể hiện thành phần dinh dưỡng cho nhiều loại thực phẩm khác nhau (nếu không muốn sử dụng constructor nhiều parameter cho thực phẩm có nhiều dinh dưỡng).

Với thiết kế này thì có vẻ ổn hơn cách dùng getter, setter nhưng chúng ta sẽ gặp rắc rối trong việc tạo object vì sẽ phải nhớ các required parameter việc có nhiều constructor sẽ làm ta dễ nhầm lẫn và code thì trông khá là “chuối”. Lúc này thì Builder xuất hiện và giải quyết vấn đề, chúng ta cùng xem Builder làm việc như thế nào nhé:

  1. package thach.le.builder;
  2. public class NutritionFacts {
  3. private final int servingSize;
  4. private final int servings;
  5. private final int calories;
  6. private final int fat;
  7. private final int sodium;
  8. private final int carbohydrate;
  9. public int getServingSize() {
  10. return servingSize;
  11. }
  12. public int getServings() {
  13. return servings;
  14. }
  15. public int getCalories() {
  16. return calories;
  17. }
  18. public int getFat() {
  19. return fat;
  20. }
  21. public int getSodium() {
  22. return sodium;
  23. }
  24. public int getCarbohydrate() {
  25. return carbohydrate;
  26. }
  27. public static class Builder {
  28. // Required parameters
  29. private final int servingSize;
  30. private final int servings;
  31. // Optional parameters - initialized to default values
  32. private int calories = 0;
  33. private int fat = 0;
  34. private int carbohydrate = 0;
  35. private int sodium = 0;
  36. public Builder(int servingSize, int servings) {
  37. this.servingSize = servingSize;
  38. this.servings = servings;
  39. }
  40. public Builder calories(int val) {
  41. calories = val;
  42. return this;
  43. }
  44. public Builder fat(int val) {
  45. fat = val;
  46. return this;
  47. }
  48. public Builder carbohydrate(int val) {
  49. carbohydrate = val;
  50. return this;
  51. }
  52. public Builder sodium(int val) {
  53. sodium = val;
  54. return this;
  55. }
  56. public NutritionFacts build() {
  57. return new NutritionFacts(this);
  58. }
  59. }
  60. private NutritionFacts(Builder builder) {
  61. servingSize = builder.servingSize;
  62. servings = builder.servings;
  63. calories = builder.calories;
  64. fat = builder.fat;
  65. sodium = builder.sodium;
  66. carbohydrate = builder.carbohydrate;
  67. }
  68. }

Tạo mới một object dùng Builder

  1. NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
  2. calories(100).sodium(35).carbohydrate(27).build();

Chúng ta có thể thấy việc tạo mới object khá là “flexible”, chúng ta không cần quan tâm đến thứ tự của các loại dinh dưỡng và quan trọng hơn, code chúng ta rõ ràng hơn, có thể thấy ngay là thành phần dinh dưỡng của cocacola gồm những gì, và code dễ maintain hơn (tuy có phần dài hơn ^.^)

Vừa rồi là phần trình bày của mình về Builder design pattern. Mong bài viết của mình sẽ giúp các bạn hiểu về Builder và nếu có cơ hội, có thể tự tin “apply” cho dự án của mình. Cảm ơn các bạn đã đọc đến đây. Hẹn gặp lại các bạn ở các bài viết sau.

Tham khảo thêm:
Effective Java 2nd (Item 2)
https://www.tutorialspoint.com/design_pattern/builder_pattern.htm
https://app.pluralsight.com/player?course=design-patterns-java-creational