Tại sao chuỗi setters không thông thường?


46

Có chuỗi thực hiện trên đậu rất tiện dụng: không cần quá tải các nhà xây dựng, nhà xây dựng lớn, nhà máy và giúp bạn tăng khả năng đọc. Tôi không thể nghĩ ra bất kỳ nhược điểm nào, trừ khi bạn muốn đối tượng của mình là bất biến , trong trường hợp đó nó sẽ không có bất kỳ setters nào. Vì vậy, có lý do tại sao đây không phải là một quy ước OOP?

public class DTO {

    private String foo;
    private String bar;

    public String getFoo() {
         return foo;
    }

    public String getBar() {
        return bar;
    }

    public DTO setFoo(String foo) {
        this.foo = foo;
        return this;
    }

    public DTO setBar(String bar) {
        this.bar = bar;
        return this;
    }

}

//...//

DTO dto = new DTO().setFoo("foo").setBar("bar");

32
Bởi vì Java có thể là ngôn ngữ duy nhất mà setters không phải là một kẻ gớm ghiếc đối với con người ...
Telastyn 2/2/2016

11
Đó là phong cách xấu bởi vì giá trị trả về không có ý nghĩa về mặt ngữ nghĩa. Đó là sự hiểu lầm. Ưu điểm duy nhất là tiết kiệm rất ít tổ hợp phím.
usr

58
Mô hình này không phải là hiếm ở tất cả. Thậm chí còn có một tên cho nó. Nó được gọi là giao diện trôi chảy .
Philipp

9
Bạn cũng có thể trừu tượng sáng tạo này trong một trình xây dựng để có kết quả dễ đọc hơn. myCustomDTO = DTOBuilder.defaultDTO().withFoo("foo").withBar("bar").Build();Tôi sẽ làm điều đó, để không xung đột với ý tưởng chung rằng setters là khoảng trống.

9
@Philipp, trong khi về mặt kỹ thuật bạn đúng, sẽ không nói rằng new Foo().setBar('bar').setBaz('baz')cảm thấy rất "trôi chảy". Ý tôi là, chắc chắn rằng nó có thể được thực hiện chính xác theo cùng một cách, nhưng tôi rất mong đợi được đọc một cái gì đó giống nhưFoo().barsThe('bar').withThe('baz').andQuuxes('the quux')
Wayne Werner

Câu trả lời:


50

Vì vậy, có lý do tại sao đây không phải là một quy ước OOP?

Dự đoán tốt nhất của tôi: bởi vì nó vi phạm CQS

Bạn đã có một lệnh (thay đổi trạng thái của đối tượng) và truy vấn (trả về một bản sao trạng thái - trong trường hợp này, chính đối tượng) trộn vào cùng một phương thức. Đó không hẳn là một vấn đề, nhưng nó vi phạm một số nguyên tắc cơ bản.

Chẳng hạn, trong C ++, std :: stack :: pop () là một lệnh trả về void và std :: stack :: top () là một truy vấn trả về một tham chiếu đến phần tử trên cùng trong ngăn xếp. Theo kinh điển, bạn muốn kết hợp cả hai, nhưng bạn không thể làm điều đó ngoại lệ an toàn. (Không phải là vấn đề trong Java, vì toán tử gán trong Java không ném).

Nếu DTO là một loại giá trị, bạn có thể đạt được kết thúc tương tự với

public DTO setFoo(String foo) {
    return new DTO(foo, this.bar);
}

public DTO setBar(String bar) {
    return new DTO(this.foo, bar);
}

Ngoài ra, các giá trị trả về chuỗi là một nỗi đau khổng lồ khi bạn đang đối phó với thừa kế. Xem "Mẫu định kỳ tò mò"

Cuối cùng, có một vấn đề là hàm tạo mặc định sẽ để lại cho bạn một đối tượng ở trạng thái hợp lệ. Nếu bạn phải chạy một loạt các lệnh để khôi phục đối tượng về trạng thái hợp lệ, một cái gì đó đã biến mất Rất sai.


4
Cuối cùng, một catain thực sự ở đây. Kế thừa là vấn đề thực sự. Tôi nghĩ rằng chuỗi có thể là một trình thiết lập thông thường cho các đối tượng biểu diễn dữ liệu được sử dụng trong thành phần.
Ben

6
"Trả lại một bản sao trạng thái từ một đối tượng" - nó không làm điều đó.
dùng253751

2
Tôi sẽ lập luận rằng lý do lớn nhất để làm theo các hướng dẫn đó (với một số ngoại lệ đáng chú ý như các trình vòng lặp) là nó làm cho mã dễ dàng hơn rất nhiều để lý do.
jpmc26

1
@MatthieuM. không. Tôi chủ yếu nói tiếng Java và nó phổ biến ở đó trong các API "chất lỏng" nâng cao - tôi chắc chắn nó cũng tồn tại trong các ngôn ngữ khác. Về cơ bản, bạn làm cho loại chung của bạn trên một loại mở rộng chính nó. Sau đó, bạn thêm một abstract T getSelf()phương thức trả về kiểu chung. Bây giờ, thay vì quay trở lại thistừ setters của bạn, bạn return getSelf()và sau đó bất kỳ lớp ghi đè nào chỉ đơn giản là tạo kiểu chung trong chính nó và trả về thistừ đó getSelf. Bằng cách này, setters trả về kiểu thực tế chứ không phải kiểu khai báo.
nhện của Vladimir

1
@MatthieuM. có thật không? Âm thanh tương tự CRTP cho C ++ ...
Vô dụng

33
  1. Lưu một vài tổ hợp phím không hấp dẫn. Nó có thể tốt, nhưng các công ước OOP quan tâm nhiều hơn đến các khái niệm và cấu trúc, chứ không phải tổ hợp phím.

  2. Giá trị trả về là vô nghĩa.

  3. Thậm chí nhiều hơn là vô nghĩa, giá trị trả về là sai lệch, vì người dùng có thể mong đợi giá trị trả về có ý nghĩa. Họ có thể mong đợi rằng đó là một "setter bất biến"

    public FooHolder {
        public FooHolder withFoo(int foo) {
            /* return a modified COPY of this FooHolder instance */
        }
    }

    Trong thực tế, setter của bạn làm biến đổi đối tượng.

  4. Nó không hoạt động tốt với thừa kế.

    public FooHolder {
        public FooHolder setFoo(int foo) {
            ...
        }
    }
    public BarHolder extends FooHolder {
        public FooHolder setBar(int bar) {
            ...
        }
    } 

    tôi có thể viết

    new BarHolder().setBar(2).setFoo(1)

    nhưng không

    new BarHolder().setFoo(1).setBar(2)

Đối với tôi, # 1 đến # 3 là những người quan trọng. Mã được viết tốt không phải là về văn bản được sắp xếp một cách dễ chịu. Mã được viết tốt là về các khái niệm cơ bản, mối quan hệ và cấu trúc. Văn bản chỉ là sự phản ánh ra bên ngoài ý nghĩa thực sự của mã.


2
Mặc dù vậy, hầu hết các đối số này áp dụng cho bất kỳ giao diện lưu loát / chuỗi nào.
Casey

@Casey, vâng. Các nhà xây dựng (tức là với setters) là trường hợp tôi thấy nhiều nhất về chuỗi.
Paul Draper

10
Nếu bạn quen thuộc với ý tưởng về giao diện trôi chảy, # 3 không áp dụng, vì bạn có thể nghi ngờ giao diện trôi chảy từ loại trả về. Tôi cũng không đồng ý với "Mã được viết tốt không phải là về văn bản được sắp xếp một cách dễ chịu". Đó không phải là tất cả, nhưng văn bản được sắp xếp độc đáo khá quan trọng, vì nó cho phép người đọc chương trình con người hiểu nó với ít nỗ lực hơn.
Michael Shaw

1
Setter chained không phải là sắp xếp văn bản một cách dễ chịu hoặc về việc lưu tổ hợp phím. Đó là về việc giảm số lượng định danh. Với chuỗi setter, bạn có thể tạo và thiết lập đối tượng trong một biểu thức, điều đó có nghĩa là bạn có thể không phải lưu nó trong một biến - một biến bạn sẽ phải đặt tên và nó sẽ ở đó cho đến hết phạm vi.
Idan Arye

3
Về điểm số 4, đây là điều có thể được giải quyết trong Java như sau: public class Foo<T extends Foo> {...}với trình thiết lập trở lại Foo<T>. Ngoài ra, các phương thức 'setter' đó, tôi thích gọi chúng là các phương thức 'với'. Nếu quá tải hoạt động tốt, chỉ cần Foo<T> with(Bar b) {...}, nếu không Foo<T> withBar(Bar b).
YoYo

12

Tôi không nghĩ rằng đây là một quy ước OOP, nó liên quan nhiều hơn đến thiết kế ngôn ngữ và các quy ước của nó.

Có vẻ như bạn thích sử dụng Java. Java có một đặc tả JavaBeans chỉ định kiểu trả về của setter là void, nghĩa là nó mâu thuẫn với chuỗi setters. Thông số kỹ thuật này được chấp nhận rộng rãi và thực hiện trong một loạt các công cụ.

Tất nhiên bạn có thể hỏi, tại sao không phải là một phần của đặc tả. Tôi không biết câu trả lời, có thể mẫu này chưa được biết đến / phổ biến tại thời điểm đó.


Vâng, JavaBeans chính xác là những gì tôi đã nghĩ.
Ben

Tôi không nghĩ rằng hầu hết các ứng dụng sử dụng JavaBeans đều quan tâm, nhưng chúng có thể (sử dụng sự phản chiếu để lấy các phương thức được đặt tên là 'set ...', có một tham số duy nhất và bị trả về void ). Nhiều chương trình phân tích tĩnh phàn nàn về việc không kiểm tra giá trị trả về từ một phương thức trả về một cái gì đó và điều này có thể rất khó chịu.

@MichaelT các cuộc gọi phản chiếu để nhận các phương thức trong Java không chỉ định kiểu trả về. Gọi một phương thức theo cách đó trả về Object, điều này có thể dẫn đến kiểu đơn Voidđược trả về nếu phương thức chỉ định voidlà kiểu trả về của nó. Trong một tin tức khác, rõ ràng "để lại một bình luận nhưng không bỏ phiếu" là căn cứ để không thực hiện kiểm toán "bài đăng đầu tiên" ngay cả khi đó là một bình luận tốt. Tôi đổ lỗi cho shog cho điều này.

7

Như những người khác đã nói, điều này thường được gọi là giao diện trôi chảy .

Thông thường setters là cuộc gọi chuyển qua các biến để đáp ứng với mã logic trong một ứng dụng; lớp DTO của bạn là một ví dụ về điều này. Mã thông thường khi setters không trả lại bất cứ điều gì là bình thường tốt nhất cho việc này. Các câu trả lời khác đã giải thích cách.

Tuy nhiên, có một vài trường hợp giao diện lưu loát có thể là một giải pháp tốt, chúng có điểm chung.

  • Các hằng số hầu hết được truyền cho các setters
  • Logic chương trình không thay đổi những gì được truyền cho setters.

Thiết lập cấu hình, ví dụ thông thạo-nhibernate

Id(x => x.Id);
Map(x => x.Name)
   .Length(16)
   .Not.Nullable();
HasMany(x => x.Staff)
   .Inverse()
   .Cascade.All();
HasManyToMany(x => x.Products)
   .Cascade.All()
   .Table("StoreProduct");

Thiết lập dữ liệu thử nghiệm trong các thử nghiệm đơn vị, sử dụng TestDataBulderClass đặc biệt (Bà mẹ đối tượng)

members = MemberBuilder.CreateList(4)
    .TheFirst(1).With(b => b.WithFirstName("Rob"))
    .TheNext(2).With(b => b.WithFirstName("Poya"))
    .TheNext(1).With(b => b.WithFirstName("Matt"))
    .BuildList(); // Note the "build" method sets everything else to
                  // senible default values so a test only need to define 
                  // what it care about, even if for example a member 
                  // MUST have MembershipId  set

Tuy nhiên, việc tạo giao diện lưu loát tốt là rất khó , vì vậy nó chỉ có giá trị khi bạn có nhiều cài đặt tĩnh tĩnh. Ngoài ra, giao diện lưu loát không nên được trộn lẫn với các lớp học bình thường của Cameron; do đó mẫu xây dựng thường được sử dụng.


5

Tôi nghĩ phần lớn lý do không phải là một quy ước để xâu chuỗi một setter này bởi vì trong những trường hợp đó, thông thường hơn là thấy một đối tượng tùy chọn hoặc tham số trong hàm tạo. C # có một cú pháp khởi tạo là tốt.

Thay vì:

DTO dto = new DTO().setFoo("foo").setBar("bar");

Người ta có thể viết:

(trong JS)

var dto = new DTO({foo: "foo", bar: "bar"});

(bằng C #)

DTO dto = new DTO{Foo = "foo", Bar = "bar"};

(bằng Java)

DTO dto = new DTO("foo", "bar");

setFoosetBarsau đó không còn cần thiết cho việc khởi tạo, và có thể được sử dụng cho đột biến sau này.

Mặc dù khả năng tạo chuỗi rất hữu ích trong một số trường hợp, điều quan trọng là không cố gắng nhồi nhét mọi thứ trên một dòng chỉ vì mục đích giảm các ký tự dòng mới.

Ví dụ

dto.setFoo("foo").setBar("fizz").setFizz("bar").setBuzz("buzz");

làm cho nó khó đọc và hiểu những gì đang xảy ra Định dạng lại thành:

dto.setFoo("foo")
    .setBar("fizz")
    .setFizz("bar")
    .setBuzz("buzz");

Dễ hiểu hơn nhiều và làm cho "lỗi" trong phiên bản đầu tiên trở nên rõ ràng hơn. Khi bạn đã tái cấu trúc mã theo định dạng đó, sẽ không có lợi thế thực sự nào:

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");

3
1. Tôi thực sự không thể đồng ý với vấn đề dễ đọc, việc thêm ví dụ trước mỗi cuộc gọi không làm cho nó rõ ràng hơn. 2. Các mẫu khởi tạo mà bạn đã hiển thị với js và C # không liên quan gì đến các nhà xây dựng: những gì bạn đã làm trong js được thông qua một đối số duy nhất và những gì bạn đã làm trong C # là một cú pháp cú pháp gọi hàm geters và setters đằng sau hậu trường và java không có shug-setter seter như C # nào.
Ben

1
@Benedictus, tôi đã chỉ ra nhiều cách khác nhau mà các ngôn ngữ OOP xử lý vấn đề này mà không cần xâu chuỗi. Vấn đề không phải là cung cấp mã giống hệt nhau, vấn đề là hiển thị các lựa chọn thay thế khiến cho việc xâu chuỗi không cần thiết.
zzzzBov

2
"Thêm phiên bản trước mỗi cuộc gọi không làm cho nó rõ ràng hơn" Tôi chưa bao giờ tuyên bố rằng việc thêm phiên bản trước mỗi cuộc gọi làm cho mọi thứ rõ ràng hơn, tôi chỉ nói rằng nó tương đối giống nhau.
zzzzBov

1
VB.NET cũng có một từ khóa "With" có thể sử dụng để tạo một tham chiếu tạm thời của trình biên dịch, do đó, ví dụ [sử dụng /để thể hiện ngắt dòng] With Foo(1234) / .x = 23 / .y = 47sẽ tương đương với Dim temp=Foo(1234) / temp.x = 23 / temp.y = 47. Cú pháp như vậy không tạo ra sự mơ hồ vì .xbản thân nó không có ý nghĩa gì ngoài việc liên kết với câu lệnh "With" ngay lập tức [nếu không có, hoặc đối tượng không có thành viên x, thì .xvô nghĩa]. Oracle đã không bao gồm bất cứ thứ gì như thế trong Java, nhưng cấu trúc như vậy sẽ phù hợp với ngôn ngữ.
supercat

5

Kỹ thuật đó thực sự được sử dụng trong mẫu Builder.

x = ObjectBuilder()
        .foo(5)
        .bar(6);

Tuy nhiên, nói chung là tránh vì nó mơ hồ. Không rõ liệu giá trị trả về là đối tượng (vì vậy bạn có thể gọi các setters khác) hoặc nếu đối tượng trả về là giá trị vừa được gán (cũng là một mẫu chung). Theo đó, Nguyên tắc bất ngờ tối thiểu cho thấy bạn không nên cố gắng giả sử người dùng muốn xem giải pháp này hay giải pháp khác, trừ khi nó là cơ bản cho thiết kế của đối tượng.


6
Sự khác biệt là, khi sử dụng mẫu trình xây dựng, bạn thường viết một đối tượng chỉ ghi (Trình tạo) mà cuối cùng sẽ xây dựng một đối tượng chỉ đọc (không thay đổi) (bất kỳ lớp nào bạn đang xây dựng). Về vấn đề đó, có một chuỗi các cuộc gọi phương thức dài là mong muốn bởi vì nó có thể được sử dụng như một biểu thức.
Darkhogg

"hoặc nếu đối tượng trả về là giá trị vừa được gán" Tôi ghét điều đó, nếu tôi muốn giữ giá trị tôi vừa truyền vào, tôi sẽ đặt nó vào một biến trước tiên. Nó có thể hữu ích để có được giá trị đã được gán trước đó , mặc dù (một số giao diện container làm điều này, tôi tin).
JAB

@JAB Tôi đồng ý, tôi không phải là một fan hâm mộ của ký hiệu đó, nhưng nó có chỗ đứng của nó. Điều Obj* x = doSomethingToObjAndReturnIt(new Obj(1, 2, 3)); tôi nghĩ đến là tôi nghĩ nó cũng trở nên phổ biến vì nó phản chiếu a = b = c = d, mặc dù tôi không tin rằng sự nổi tiếng là có cơ sở. Tôi đã thấy một trong những bạn đề cập, trả về giá trị trước đó, trong một số thư viện hoạt động nguyên tử. Đạo đức của câu chuyện? Nó thậm chí còn khó hiểu hơn tôi phát ra âm thanh =)
Cort Ammon

Tôi tin rằng các học giả có một chút phạm vi: Không có gì sai với thành ngữ này. Nó rất hữu ích trong việc thêm sự rõ ràng trong một số trường hợp nhất định. Lấy lớp định dạng văn bản sau đây làm ví dụ thụt lề từng dòng của chuỗi và vẽ một hộp nghệ thuật xung quanh nó new BetterText(string).indent(4).box().print();. Trong trường hợp đó, tôi đã bỏ qua một nhóm yêu tinh và lấy một chuỗi, thụt lề, đóng hộp và xuất nó. Bạn thậm chí có thể muốn một phương thức sao chép trong tầng (chẳng hạn như, giả sử .copy()) để cho phép tất cả các hành động sau không còn sửa đổi bản gốc.
tgm1024

2

Đây là một nhận xét nhiều hơn là một câu trả lời, nhưng tôi không thể bình luận, vì vậy ...

chỉ muốn đề cập rằng câu hỏi này làm tôi ngạc nhiên vì tôi không thấy đây là điều hiếm gặp . Trên thực tế, trong môi trường làm việc của tôi (nhà phát triển web) là rất rất phổ biến.

Chẳng hạn, đây là cách học thuyết của Symfony: tạo: lệnh thực thể tự động tạo tất cả setters , theo mặc định .

jQuery kinda chuỗi hầu hết các phương thức của nó theo một cách rất giống nhau.


Js là một sự gớm ghiếc
Ben

@Benedictus Tôi muốn nói rằng PHP là sự ghê tởm lớn hơn. JavaScript là một ngôn ngữ hoàn toàn tốt và đã trở nên khá hay với việc bao gồm các tính năng ES6 (mặc dù tôi chắc chắn rằng một số người vẫn thích CoffeeScript hoặc các biến thể; cá nhân tôi không phải là người hâm mộ về cách CoffeeScript xử lý phạm vi biến khi kiểm tra phạm vi bên ngoài đầu tiên thay vì xử lý các biến được gán trong phạm vi cục bộ là cục bộ trừ khi được chỉ định rõ ràng là không cục bộ / toàn cầu theo cách Python thực hiện).
JAB 4/2/2016

@Benedictus Tôi đồng tình với JAB. Trong những năm đại học, tôi thường coi JS như một ngôn ngữ viết kịch bản gớm ghiếc, nhưng sau khi làm việc vài năm với nó, và học cách sử dụng QUYỀN , tôi thực sự thích nó . Và tôi nghĩ rằng với ES6 nó sẽ trở thành một ngôn ngữ thực sự trưởng thành . Nhưng, điều khiến tôi quay đầu lại là ... câu trả lời của tôi phải làm gì với JS ngay từ đầu? ^^ U
xDaizu 8/2/2016

Tôi thực sự đã làm việc một vài năm với js và có thể nói rằng tôi đã học được cách đó. Tuy nhiên, tôi thấy ngôn ngữ này là không có gì ngoài một sai lầm. Nhưng tôi đồng ý, Php còn tệ hơn nhiều.
Ben

@Benedictus Tôi hiểu vị trí của bạn và tôi tôn trọng nó. Tôi vẫn thích nó mặc dù. Chắc chắn, nó có những đặc điểm và đặc quyền (ngôn ngữ nào không?) Nhưng nó đang dần đi đúng hướng. Nhưng ... tôi thực sự không thích nó đến mức tôi cần phải bảo vệ nó. Tôi không nhận được bất cứ điều gì từ những người yêu thương hoặc ghét nó ... hahaha Ngoài ra, đây không phải là nơi dành cho điều đó, câu trả lời của tôi thậm chí không phải về JS.
xDaizu
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.