Trình xây dựng với hàng tấn tham số so với mẫu xây dựng


21

Chúng ta cũng biết rằng nếu lớp của bạn có một hàm tạo có nhiều tham số, giả sử nhiều hơn 4, thì đó rất có thể là mùi mã . Bạn cần xem xét lại nếu lớp thỏa mãn SRP .

Nhưng điều gì sẽ xảy ra nếu chúng ta xây dựng và đối tượng phụ thuộc vào 10 tham số trở lên và cuối cùng kết thúc bằng việc thiết lập tất cả các tham số đó thông qua mẫu Builder? Hãy tưởng tượng, bạn xây dựng một Personđối tượng loại với thông tin cá nhân, thông tin công việc, thông tin bạn bè, thông tin sở thích, thông tin giáo dục, v.v. Điều này đã tốt rồi, nhưng bằng cách nào đó bạn đã đặt cùng hơn 4 tham số, phải không? Tại sao hai trường hợp này không được coi là giống nhau?

Câu trả lời:


24

Mô hình Builder không giải quyết được vấn đề về mối quan hệ của nhiều người. Nhưng tại sao nhiều tranh luận có vấn đề?

  • Chúng cho thấy lớp học của bạn có thể làm quá nhiều . Tuy nhiên, có nhiều loại có chứa nhiều thành viên hợp pháp không thể được nhóm lại một cách hợp lý.
  • Kiểm tra và hiểu một chức năng với nhiều đầu vào trở nên phức tạp hơn theo cấp số nhân - theo nghĩa đen!
  • Khi ngôn ngữ không cung cấp các tham số được đặt tên, một lệnh gọi hàm không tự ghi lại . Đọc một lệnh gọi hàm với nhiều đối số là khá khó khăn vì bạn không biết tham số thứ 7 được cho là phải làm gì. Bạn thậm chí sẽ không nhận ra nếu đối số thứ 5 và thứ 6 bị hoán đổi một cách tình cờ, đặc biệt nếu bạn đang sử dụng ngôn ngữ được gõ động hoặc mọi thứ xảy ra là một chuỗi hoặc khi tham số cuối cùng là truevì một lý do nào đó.

Làm giả tên

Các mẫu Builder địa chỉ chỉ là một trong những vấn đề này, cụ thể là những mối quan tâm bảo trì của các cuộc gọi chức năng với nhiều đối số * . Vì vậy, một chức năng gọi như

MyClass o = new MyClass(a, b, c, d, e, f, g);

có thể trở thành

MyClass o = MyClass.builder()
  .a(a).b(b).c(c).d(d).e(e).f(f).g(g)
  .build();

Pattern Mẫu Builder ban đầu được dự định là một cách tiếp cận đại diện - bất khả tri để lắp ráp các đối tượng hỗn hợp, đó là một khát vọng lớn hơn nhiều so với các đối số chỉ được đặt tên cho các tham số. Đặc biệt, mẫu xây dựng không yêu cầu giao diện lưu loát.

Điều này mang lại một chút an toàn vì nó sẽ nổ tung nếu bạn gọi một phương thức xây dựng không tồn tại, nhưng nó không mang lại cho bạn bất cứ điều gì mà một lời nhận xét trong lời gọi của nhà xây dựng sẽ không có. Ngoài ra, việc tạo một trình xây dựng theo cách thủ công yêu cầu mã và nhiều mã hơn luôn có thể chứa nhiều lỗi hơn.

Trong các ngôn ngữ dễ xác định loại giá trị mới, tôi thấy rằng sử dụng microtyping / loại nhỏ để mô phỏng các đối số được đặt tên là cách tốt hơn . Nó được đặt tên như vậy bởi vì các loại thực sự nhỏ, nhưng cuối cùng bạn lại gõ nhiều hơn ;-)

MyClass o = new MyClass(
  new MyClass.A(a), new MyClass.B(b), new MyClass.C(c),
  new MyClass.D(d), new MyClass.E(e), new MyClass.F(f),
  new MyClass.G(g));

Rõ ràng, các tên kiểu A, B, C, ... nên có tên tự tài liệu minh họa ý nghĩa của các tham số, thường là tên giống như bạn muốn cung cấp cho các biến tham số. So với thành ngữ của trình xây dựng cho tên được đặt tên, việc triển khai được yêu cầu đơn giản hơn rất nhiều và do đó ít có khả năng chứa lỗi. Ví dụ: (với cú pháp Java-ish):

class MyClass {
  ...
  public static class A {
    public final int value;
    public A(int a) { value = a; }
  }
  ...
}

Trình biên dịch giúp bạn đảm bảo rằng tất cả các đối số đã được cung cấp; với Trình tạo, bạn phải kiểm tra thủ công các đối số bị thiếu hoặc mã hóa máy trạng thái vào hệ thống loại ngôn ngữ máy chủ - cả hai đều có thể chứa lỗi.

Có một cách tiếp cận phổ biến khác để mô phỏng các đối số được đặt tên: một đối tượng tham số trừu tượng duy nhất sử dụng cú pháp lớp nội tuyến để khởi tạo tất cả các trường. Trong Java:

MyClass o = new MyClass(new MyClass.Arguments(){{ argA = a; argB = b; argC = c; ... }});

class MyClass {
  ...
  public static abstract class Arguments {
    public int argA;
    public String ArgB;
    ...
  }
}

Tuy nhiên, có thể quên các trường và đây là một giải pháp khá cụ thể về ngôn ngữ (tôi đã thấy sử dụng trong JavaScript, C # và C).

May mắn thay, hàm tạo vẫn có thể xác thực tất cả các đối số, đó không phải là trường hợp khi các đối tượng của bạn được tạo ở trạng thái được xây dựng một phần và yêu cầu người dùng cung cấp thêm các đối số thông qua setters hoặc một init()phương thức - những yêu cầu ít nỗ lực mã hóa nhất, nhưng thực hiện khó khăn hơn để viết chương trình chính xác .

Vì vậy, trong khi có nhiều cách tiếp cận để giải quyết vấn đề, nhiều tham số chưa được đặt tên khiến mã khó duy trì vấn đề, thì các vấn đề khác vẫn còn.

Tiếp cận vấn đề gốc

Ví dụ như bài toán kiểm tra. Khi tôi viết thử nghiệm đơn vị, tôi cần khả năng tiêm dữ liệu thử nghiệm và cung cấp các triển khai thử nghiệm để loại bỏ các phụ thuộc và hoạt động có tác dụng phụ bên ngoài. Tôi không thể làm điều đó khi bạn khởi tạo bất kỳ lớp nào trong hàm tạo của bạn. Trừ khi trách nhiệm của lớp bạn là tạo ra các đối tượng khác, nó không nên khởi tạo bất kỳ lớp không tầm thường nào. Điều này đi đôi với vấn đề trách nhiệm duy nhất. Trách nhiệm của một lớp càng tập trung, thì càng dễ kiểm tra (và thường dễ sử dụng hơn).

Cách tiếp cận dễ nhất và thường xuyên nhất là để nhà xây dựng lấy các phụ thuộc được xây dựng đầy đủ làm tham số , mặc dù điều này làm giảm trách nhiệm quản lý các phụ thuộc cho người gọi - không lý tưởng, trừ khi các phụ thuộc là các thực thể độc lập trong mô hình miền của bạn.

Đôi khi, các nhà máy (trừu tượng) hoặc khung tiêm phụ thuộc đầy đủ được sử dụng thay thế, mặc dù những điều này có thể là quá mức cần thiết trong phần lớn các trường hợp sử dụng. Cụ thể, những điều này chỉ làm giảm số lượng đối số nếu nhiều trong số các đối số này là các đối tượng toàn cầu hoặc các giá trị cấu hình không thay đổi giữa khởi tạo đối tượng. Ví dụ: nếu tham số adlà toàn cầu, chúng tôi sẽ nhận được

Dependencies deps = new Dependencies(a, d);
...
MyClass o = deps.newMyClass(b, c, e, f, g);

class MyClass {
  MyClass(Dependencies deps, B b, C c, E e, F f, G g) {
    this.depA = deps.newDepA(b, c);
    this.depB = deps.newDepB(e, f);
    this.g = g;
  }
  ...
}

class Dependencies {
  private A a;
  private D d;
  public Dependencies(A a, D d) { this.a = a; this.d = d; }
  public DepA newDepA(B b, C c) { return new DepA(a, b, c); }
  public DepB newDepB(E e, F f) { return new DepB(d, e, f); }
  public MyClass newMyClass(B b, C c, E e, F f, G g) {
    return new MyClass(deps, b, c, e, f, g);
  }
}

Tùy thuộc vào ứng dụng, đây có thể là một công cụ thay đổi trò chơi trong đó các phương thức xuất xưởng gần như không có đối số vì tất cả có thể được cung cấp bởi người quản lý phụ thuộc hoặc có thể là một số lượng lớn mã làm phức tạp hóa việc tạo tức thời mà không có lợi ích rõ ràng. Các nhà máy như vậy rất hữu ích cho việc ánh xạ các giao diện tới các loại cụ thể hơn là để quản lý các tham số. Tuy nhiên, phương pháp này cố gắng giải quyết vấn đề gốc của quá nhiều tham số thay vì chỉ ẩn nó với giao diện khá trôi chảy.


Tôi thực sự sẽ tranh luận chống lại phần tự viết tài liệu. Nếu bạn có một IDE tốt + bình luận hợp lý, hầu hết thời gian của hàm tạo và định nghĩa tham số là một lần nhấn phím.
JavierIEH

Trong Android Studio 3.0, tên tham số trong hàm tạo được hiển thị liền kề với giá trị được truyền trong lệnh gọi hàm tạo. ví dụ: A mới (toán hạng1: 34, toán hạng2: 56); operand1 và operand2 là tên tham số trong hàm tạo. Chúng được IDE hiển thị để làm cho mã dễ đọc hơn. Vì vậy, không cần phải đi đến định nghĩa để tìm ra tham số là gì.
garnet

9

Mẫu xây dựng không giải quyết bất cứ điều gì cho bạn và không khắc phục các lỗi thiết kế.

Nếu bạn có một lớp cần 10 tham số sẽ được xây dựng, làm cho trình xây dựng để xây dựng nó sẽ không đột nhiên làm cho thiết kế của bạn tốt hơn. Bạn nên chọn tái cấu trúc lớp trong câu hỏi.

Mặt khác, nếu bạn có một lớp, có lẽ là một DTO đơn giản, trong đó các thuộc tính nhất định của lớp là tùy chọn, mẫu xây dựng có thể dễ dàng xây dựng đối tượng nói trên.


1

Mỗi phần, ví dụ thông tin cá nhân, thông tin công việc (từng "việc làm"), mỗi người bạn, v.v., nên được chuyển đổi thành các đối tượng của riêng họ.

Xem xét cách bạn sẽ thực hiện một chức năng cập nhật. Người dùng muốn thêm một lịch sử công việc mới. Người dùng không muốn thay đổi bất kỳ phần thông tin nào khác, cũng như lịch sử công việc trước đó - chỉ cần giữ chúng như cũ.

Khi có nhiều thông tin, việc xây dựng thông tin từng phần một là không thể tránh khỏi. Bạn có thể nhóm các phần đó một cách hợp lý - dựa trên trực giác của con người (giúp người lập trình dễ sử dụng hơn) hoặc dựa trên mô hình sử dụng (phần thông tin nào thường được cập nhật cùng lúc).

Mẫu xây dựng chỉ là một cách để nắm bắt một danh sách (được sắp xếp hoặc không có thứ tự) các đối số được nhập, sau đó có thể được chuyển vào hàm tạo thực tế (hoặc hàm tạo có thể đọc các đối số đã bắt của nhà xây dựng).

Hướng dẫn của 4 chỉ là một quy tắc. Có những tình huống đòi hỏi nhiều hơn 4 đối số và không có cách nào hợp lý hoặc hợp lý để nhóm chúng. Trong những trường hợp đó, có thể có ý nghĩa để tạo ra một structcái có thể được điền trực tiếp hoặc với setters tài sản. Lưu ý rằng structvà người xây dựng trong trường hợp này phục vụ một mục đích rất giống nhau.

Tuy nhiên, trong ví dụ của bạn, bạn đã mô tả những gì sẽ là một cách hợp lý để nhóm chúng. Nếu bạn đang phải đối mặt với một tình huống không đúng, có lẽ bạn có thể minh họa điều đó bằng một ví dụ khác.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.