Xây dựng các đối tượng lớn, bất biến mà không sử dụng các hàm tạo có danh sách tham số dài


96

Tôi có một số đối tượng lớn (hơn 3 trường) có thể và nên là bất biến. Mỗi khi tôi gặp phải trường hợp đó, tôi có xu hướng tạo ra các phương thức khởi tạo với danh sách tham số dài.

Nó cảm thấy không ổn, nó khó sử dụng và khả năng đọc bị ảnh hưởng.

Nó thậm chí còn tồi tệ hơn nếu các trường là một số loại tập hợp như danh sách. Một đơn giản addSibling(S s)sẽ dễ dàng tạo ra đối tượng rất nhiều nhưng làm cho đối tượng có thể thay đổi.

Các bạn dùng gì trong những trường hợp như vậy?

Tôi đang sử dụng Scala và Java, nhưng tôi nghĩ vấn đề là ngôn ngữ không khả tri miễn là ngôn ngữ đó hướng đối tượng.

Các giải pháp tôi có thể nghĩ đến:

  1. "Sự ghê tởm của hàm tạo với danh sách tham số dài"
  2. Mẫu Builder

5
Tôi nghĩ rằng mô hình xây dựng là giải pháp tốt nhất và tiêu chuẩn nhất cho việc này.
Zachary Wright

@Zachary: Những gì bạn đang ủng hộ chỉ hoạt động đối với một dạng đặc biệt của mẫu trình tạo, như được Joshua Bloch giải thích ở đây: drdobbs.com/java/208403883?pgno=2 Tôi thích sử dụng thuật ngữ "giao diện thông thạo" có liên quan để gọi "dạng đặc biệt của mô hình trình tạo" này (xem câu trả lời của tôi).
Cú phápT3rr0r

Câu trả lời:


76

Chà, bạn muốn cả một đối tượng dễ đọc hơn và bất biến sau khi được tạo?

Tôi nghĩ rằng một giao diện trôi chảy ĐƯỢC LÀM ĐÚNG CÁCH sẽ giúp ích cho bạn.

Nó sẽ trông như thế này (ví dụ hoàn toàn được tạo thành):

final Foo immutable = FooFactory.create()
    .whereRangeConstraintsAre(100,300)
    .withColor(Color.BLUE)
    .withArea(234)
    .withInterspacing(12)
    .build();

Tôi đã viết đậm "CORRECTLY DONE" bởi vì hầu hết các lập trình viên Java nhận được sai giao diện thông thạo và làm ô nhiễm đối tượng của họ với phương thức cần thiết để xây dựng đối tượng, điều này tất nhiên là hoàn toàn sai.

Bí quyết là chỉ có phương thức build () mới thực sự tạo ra một Foo (do đó Foo của bạn có thể là bất biến).

FooFactory.create () , trong đóXXX (..)vớiXXX (..) đều tạo ra "cái gì đó khác".

Đó là một thứ khác có thể là FooFactory, đây là một cách để làm điều đó ....

Bạn FooFactory sẽ trông như thế này:

// Notice the private FooFactory constructor
private FooFactory() {
}

public static FooFactory create() {
    return new FooFactory();
}

public FooFactory withColor( final Color col ) {
    this.color = color;
    return this;
}

public Foo build() {
    return new FooImpl( color, and, all, the, other, parameters, go, here );
}

11
@ all: vui lòng không phàn nàn về postfix "Impl" trong "FooImpl": lớp này ẩn bên trong một nhà máy và không ai có thể nhìn thấy nó ngoài người viết giao diện thông thạo. Tất cả những gì người dùng quan tâm là anh ta nhận được một "Foo". Tôi cũng có thể gọi "FooImpl" là "FooPointlessNitpick";)
SyntaxT3rr0r

5
Cảm thấy trước? ;) Trước đây bạn đã từng bị soi xét về điều này. :)
Greg D

3
Tôi tin rằng các lỗi phổ biến anh ấy đề cập đến là người ta thêm "withXXX" (vv) phương pháp để các Foođối tượng, thay vì phải một riêng biệt FooFactory.
Dean Harding

3
Bạn vẫn có hàm FooImpltạo với 8 tham số. Cải tiến là gì?
Một

4
Tôi sẽ không gọi mã này immutable, và tôi sẽ sợ mọi người sử dụng lại các đối tượng của nhà máy vì họ nghĩ nó đúng như vậy. Ý tôi là: FooFactory people = FooFactory.create().withType("person"); Foo women = people.withGender("female").build(); Foo talls = people.tallerThan("180m").build();nơi tallsbây giờ sẽ chỉ chứa phụ nữ. Điều này sẽ không xảy ra với một API bất biến.
Thomas Ahle,

60

Trong Scala 2.8, bạn có thể sử dụng các tham số được đặt tên và mặc định cũng như copyphương thức trên một lớp trường hợp. Đây là một số mã ví dụ:

case class Person(name: String, age: Int, children: List[Person] = List()) {
  def addChild(p: Person) = copy(children = p :: this.children)
}

val parent = Person(name = "Bob", age = 55)
  .addChild(Person("Lisa", 23))
  .addChild(Person("Peter", 16))

31
+1 để phát minh ra ngôn ngữ Scala. Vâng, đó là một sự lạm dụng hệ thống danh tiếng nhưng ... aww ... Tôi yêu Scala rất nhiều nên tôi đã phải làm điều đó. :)
Malax

1
Ôi trời ... Tôi vừa trả lời một điều gần như giống hệt nhau! Vâng, tôi đang ở trong một công ty tốt. :-) Tôi tự hỏi rằng tôi đã không thấy câu trả lời của bạn trước đây, mặc dù ... <shrug>
Daniel C. Sobral

20

Vâng, hãy xem xét điều này trên Scala 2.8:

case class Person(name: String, 
                  married: Boolean = false, 
                  espouse: Option[String] = None, 
                  children: Set[String] = Set.empty) {
  def marriedTo(whom: String) = this.copy(married = true, espouse = Some(whom))
  def addChild(whom: String) = this.copy(children = children + whom)
}

scala> Person("Joseph").marriedTo("Mary").addChild("Jesus")
res1: Person = Person(Joseph,true,Some(Mary),Set(Jesus))

Tất nhiên, điều này cũng có phần của các vấn đề. Ví dụ, hãy thử làm espouseOption[Person], và sau đó nhận được hai người kết hôn với nhau. Tôi không thể nghĩ ra cách nào để giải quyết vấn đề đó mà không cần dùng đến một private varvà / hoặc một nhà privatexây dựng cộng với một nhà máy.


11

Dưới đây là một số tùy chọn khác:

lựa chọn 1

Làm cho bản thân việc triển khai có thể thay đổi, nhưng tách biệt các giao diện mà nó hiển thị thành có thể thay đổi và bất biến. Điều này được lấy từ thiết kế thư viện Swing.

public interface Foo {
  X getX();
  Y getY();
}

public interface MutableFoo extends Foo {
  void setX(X x);
  void setY(Y y);
}

public class FooImpl implements MutableFoo {...}

public SomeClassThatUsesFoo {
  public Foo makeFoo(...) {
    MutableFoo ret = new MutableFoo...
    ret.setX(...);
    ret.setY(...);
    return ret; // As Foo, not MutableFoo
  }
}

Lựa chọn 2

Nếu ứng dụng của bạn chứa một tập hợp lớn nhưng được xác định trước các đối tượng bất biến (ví dụ: đối tượng cấu hình), bạn có thể cân nhắc sử dụng Spring framework.


3
Phương án 1 là khéo léo (nhưng không quá khéo léo), vì vậy tôi thích nó.
Ti-mô-thê

4
Tôi đã làm điều này sớm hơn, nhưng theo tôi nó không phải là một giải pháp tốt vì đối tượng vẫn có thể biến đổi, chỉ có các phương thức đột biến là "ẩn". Có lẽ tôi quá kén chọn chủ đề này ...
Malax

một biến thể trên tùy chọn 1 là có các lớp riêng biệt cho các biến thể Bất biến và Có thể thay đổi. Điều này mang lại sự an toàn tốt hơn so với cách tiếp cận giao diện. Có thể cho rằng bạn đang nói dối người tiêu dùng API bất cứ khi nào bạn cung cấp cho họ một đối tượng có thể thay đổi được có tên là Immutable chỉ cần được truyền đến giao diện có thể thay đổi của nó. Cách tiếp cận lớp riêng biệt yêu cầu các phương thức chuyển đổi qua lại. API JodaTime sử dụng mẫu này. Xem DateTime và MutableDateTime.
toolbear

6

Nó giúp nhớ rằng có nhiều loại bất biến khác nhau . Đối với trường hợp của bạn, tôi nghĩ rằng tính bất biến của "popsicle" sẽ hoạt động thực sự tốt:

Tính bất biến của Popsicle: là cái tôi thường gọi là sự suy yếu nhẹ của tính bất biến ghi một lần. Người ta có thể tưởng tượng một đối tượng hoặc một trường vẫn có thể thay đổi trong một thời gian ngắn trong quá trình khởi tạo, và sau đó bị "đóng băng" mãi mãi. Loại bất biến này đặc biệt hữu ích cho các đối tượng bất biến tham chiếu vòng quanh nhau hoặc các đối tượng bất biến đã được tuần tự hóa vào đĩa và khi giải không khí cần phải "linh hoạt" cho đến khi toàn bộ quá trình giải không khí được thực hiện, tại thời điểm đó tất cả các đối tượng có thể Đông cứng.

Vì vậy, bạn khởi tạo đối tượng của mình, sau đó đặt một cờ "đóng băng" của một số loại cho biết rằng nó không còn có thể ghi được nữa. Tốt hơn là bạn nên ẩn đột biến đằng sau một hàm để hàm vẫn thuần túy đối với các khách hàng sử dụng API của bạn.


1
Phản đối? Bất cứ ai muốn để lại nhận xét về lý do tại sao đây không phải là một giải pháp tốt?
Juliet

+1. Có thể ai đó đã phản đối điều này bởi vì bạn đang ám chỉ việc sử dụng clone()để lấy các phiên bản mới.
finnw

Hoặc có thể đó là vì nó có thể làm tổn hại an toàn thread trong Java: java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5
finnw

Một bất lợi là nó yêu cầu các nhà phát triển tương lai phải cẩn thận để xử lý cờ "đóng băng". Nếu một phương thức đột biến được thêm vào sau đó mà quên khẳng định rằng phương thức đó không được đóng băng, có thể có vấn đề. Tương tự, nếu một phương thức khởi tạo mới được viết mà nên, nhưng không gọi freeze()phương thức, thì mọi thứ có thể trở nên tồi tệ.
Stalepretzel

5

Bạn cũng có thể làm cho các đối tượng không thay đổi hiển thị các phương thức trông giống như các trình đột biến (như addSibling) nhưng hãy để chúng trả về một thể hiện mới. Đó là những gì các bộ sưu tập Scala bất biến làm.

Nhược điểm là bạn có thể tạo nhiều phiên bản hơn mức cần thiết. Nó cũng chỉ áp dụng khi tồn tại các cấu hình hợp lệ trung gian (như một số nút không có anh chị em, trong hầu hết các trường hợp là ok) trừ khi bạn không muốn xử lý các đối tượng được xây dựng một phần.

Ví dụ: một cạnh đồ thị chưa có điểm đến không phải là một cạnh đồ thị hợp lệ.


Nhược điểm bị cáo buộc - tạo ra nhiều trường hợp hơn mức cần thiết - không thực sự là vấn đề quá lớn. Việc phân bổ đối tượng rất rẻ, cũng như việc thu gom rác của các đối tượng tồn tại trong thời gian ngắn. Khi phân tích thoát được bật theo mặc định, loại "đối tượng trung gian" này có khả năng được phân bổ ngăn xếp và thực sự không tốn chi phí để tạo.
gustafc

2
@gustafc: Đúng. Cliff Click từng kể câu chuyện về cách họ đánh giá tiêu chuẩn mô phỏng Clojure Ant Colony của Rich Hickey trên một trong những hộp lớn của họ (864 lõi, 768 GB RAM): 700 luồng song song chạy trên 700 lõi, mỗi luồng ở mức 100%, tạo ra hơn 20 GB rác phù du mỗi giây . GC thậm chí không đổ một giọt mồ hôi.
Jörg W Mittag

5

Hãy xem xét bốn khả năng:

new Immutable(one, fish, two, fish, red, fish, blue, fish); /*1 */

params = new ImmutableParameters(); /*2 */
params.setType("fowl");
new Immutable(params);

factory = new ImmutableFactory(); /*3 */
factory.setType("fish");
factory.getInstance();

Immutable boringImmutable = new Immutable(); /* 4 */
Immutable lessBoring = boringImmutable.setType("vegetable");

Đối với tôi, mỗi thứ 2, 3 và 4 đều thích ứng với một tình huống khác nhau. Cái đầu tiên rất khó để yêu thích, vì những lý do được OP nêu ra, và nói chung là một dấu hiệu của một thiết kế đã bị một số lỗi và cần được tái cấu trúc lại.

Những gì tôi liệt kê là (2) là tốt khi không có trạng thái đằng sau 'nhà máy', trong khi (3) là thiết kế được lựa chọn khi có trạng thái. Tôi thấy mình đang sử dụng (2) thay vì (3) khi tôi không muốn lo lắng về luồng và đồng bộ hóa, và tôi không cần phải lo lắng về việc khấu hao một số thiết lập đắt tiền so với việc sản xuất nhiều đối tượng. (3), mặt khác, được gọi ra khi công việc thực đi vào quá trình xây dựng nhà máy (thiết lập từ SPI, đọc các tệp cấu hình, v.v.).

Cuối cùng, câu trả lời của người khác đã đề cập đến tùy chọn (4), nơi bạn có rất nhiều đối tượng nhỏ bất biến và mô hình thích hợp là lấy tin tức từ những đối tượng cũ.

Lưu ý rằng tôi không phải là thành viên của 'câu lạc bộ người hâm mộ khuôn mẫu' - chắc chắn, một số thứ đáng để mô phỏng, nhưng với tôi, dường như họ tự nhận một cuộc sống vô ích khi mọi người đặt cho họ những cái tên và những chiếc mũ ngộ nghĩnh.


6
Đây là Mô hình Người xây dựng (tùy chọn 2)
Simon Nickerson

Đó sẽ không phải là một đối tượng factory ('người xây dựng') phát ra các đối tượng bất biến của anh ta?
bmargulies

sự phân biệt đó có vẻ khá về mặt ngữ nghĩa. Làm thế nào để đậu được làm? Điều đó khác với người xây dựng như thế nào?
Carl

Bạn không muốn gói các quy ước Java Bean cho một trình tạo (hoặc nhiều thứ khác).
Tom Hawtin - tackline

4

Một tùy chọn tiềm năng khác là tái cấu trúc để có ít trường có thể định cấu hình hơn. Nếu các nhóm trường chỉ hoạt động (chủ yếu) với nhau, hãy tập hợp chúng lại thành một đối tượng nhỏ bất biến của riêng chúng. Các hàm tạo / xây dựng của đối tượng "nhỏ" đó sẽ dễ quản lý hơn, cũng như hàm tạo / trình xây dựng cho đối tượng "lớn" này.


1
lưu ý: số dặm cho câu trả lời này có thể thay đổi tùy theo vấn đề, cơ sở mã và kỹ năng của nhà phát triển.
Carl

2

Tôi sử dụng C #, và đây là những cách tiếp cận của tôi. Xem xét:

class Foo
{
    // private fields only to be written inside a constructor
    private readonly int i;
    private readonly string s;
    private readonly Bar b;

    // public getter properties
    public int I { get { return i; } }
    // etc.
}

Tùy chọn 1. Khối mã lệnh với các tham số tùy chọn

public Foo(int i = 0, string s = "bla", Bar b = null)
{
    this.i = i;
    this.s = s;
    this.b = b;
}

Được sử dụng như vd new Foo(5, b: new Bar(whatever)). Không dành cho các phiên bản Java hoặc C # trước 4.0. nhưng vẫn có giá trị hiển thị, vì nó là một ví dụ cho thấy không phải tất cả các giải pháp đều là ngôn ngữ bất khả tri.

Tùy chọn 2. Hàm tạo lấy một đối tượng tham số duy nhất

public Foo(FooParameters parameters)
{
    this.i = parameters.I;
    // etc.
}

class FooParameters
{
    // public properties with automatically generated private backing fields
    public int I { get; set; }
    public string S { get; set; }
    public Bar B { get; set; }

    // All properties are public, so we don't need a full constructor.
    // For convenience, you could include some commonly used initialization
    // patterns as additional constructors.
    public FooParameters() { }
}

Ví dụ sử dụng:

FooParameters fp = new FooParameters();
fp.I = 5;
fp.S = "bla";
fp.B = new Bar();
Foo f = new Foo(fp);`

C # từ 3.0 trở đi làm cho điều này trở nên thanh lịch hơn với cú pháp khởi tạo đối tượng (tương đương về mặt ngữ nghĩa với ví dụ trước):

FooParameters fp = new FooParameters { I = 5, S = "bla", B = new Bar() };
Foo f = new Foo(fp);

Tùy chọn 3:
Thiết kế lại lớp của bạn không cần một số lượng lớn các tham số như vậy. Bạn có thể chia các chức năng của nó thành nhiều lớp. Hoặc truyền các tham số không phải cho hàm tạo mà chỉ cho các phương thức cụ thể, theo yêu cầu. Không phải lúc nào cũng khả thi, nhưng khi có, điều đó rất đáng làm.

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.