Làm cách nào để chọn giữa Tell không hỏi và tách truy vấn lệnh?


25

Nguyên tắc Tell Don't Ask nói:

bạn nên cố gắng nói với các đối tượng những gì bạn muốn họ làm; đừng hỏi họ những câu hỏi về tình trạng của họ, đưa ra quyết định và sau đó nói cho họ biết phải làm gì.

Vấn đề là, với tư cách là người gọi, bạn không nên đưa ra quyết định dựa trên trạng thái của đối tượng được gọi dẫn đến kết quả là bạn sẽ thay đổi trạng thái của đối tượng. Logic bạn đang thực hiện có lẽ là trách nhiệm của đối tượng được gọi, không phải của bạn. Để bạn đưa ra quyết định bên ngoài đối tượng vi phạm đóng gói của nó.

Một ví dụ đơn giản về "Nói, đừng hỏi"

Widget w = ...;
if (w.getParent() != null) {
  Panel parent = w.getParent();
  parent.remove(w);
}

và phiên bản cho biết là ...

Widget w = ...;
w.removeFromParent();

Nhưng nếu tôi cần biết kết quả từ phương thức removeFromParent thì sao? Phản ứng đầu tiên của tôi chỉ là thay đổi removeFromParent để trả về một biểu thị boolean nếu cha mẹ đã được gỡ bỏ hay không.

Nhưng sau đó tôi đã bắt gặp Mẫu tách biệt truy vấn lệnh mà KHÔNG làm điều này.

Nó tuyên bố rằng mọi phương thức nên là một lệnh thực hiện một hành động hoặc một truy vấn trả về dữ liệu cho người gọi, nhưng không phải cả hai. Nói cách khác, đặt câu hỏi không nên thay đổi câu trả lời. Chính thức hơn, các phương thức chỉ trả về một giá trị nếu chúng trong suốt được tham chiếu và do đó không có tác dụng phụ.

Có phải hai người này thực sự bất hòa với nhau và làm thế nào để tôi chọn giữa hai? Tôi có đi cùng với Lập trình viên thực dụng hay Bertrand Meyer về điều này không?


1
Bạn sẽ làm gì với boolean?
David

1
Có vẻ như bạn đang đi quá xa vào các mẫu mã hóa mà không cân bằng được khả năng sử dụng
Izkata

Re boolean ... đây là một ví dụ dễ dàng cõng nhưng tương tự như thao tác ghi bên dưới, mục tiêu của nó là trạng thái của hoạt động.
Bắc Dakotah

2
quan điểm của tôi là bạn không nên tập trung vào "trả lại một cái gì đó" mà nhiều hơn về những gì bạn muốn làm với nó. Như tôi đã nói trong một nhận xét khác, nếu bạn tập trung vào thất bại, sau đó sử dụng ngoại lệ, nếu bạn muốn làm gì đó khi thao tác hoàn tất, sau đó sử dụng gọi lại hoặc sự kiện, nếu bạn muốn đăng nhập những gì đã xảy ra thì đó là trách nhiệm của Widget ... Đó là tại sao tôi lại hỏi về nhu cầu của bạn Nếu bạn không thể tìm thấy một ví dụ cụ thể nơi bạn cần trả lại một cái gì đó, có thể điều đó có nghĩa là bạn sẽ không phải chọn giữa hai thứ đó.
David

1
Phân tách truy vấn lệnh là một nguyên tắc, không phải là một mẫu. Các mẫu là một cái gì đó bạn có thể sử dụng để giải quyết một vấn đề. Nguyên tắc là những gì bạn làm theo để không tạo ra vấn đề.
candied_orange

Câu trả lời:


25

Trên thực tế vấn đề ví dụ của bạn đã minh họa thiếu phân tách.

Hãy thay đổi một chút thôi:

Book b = ...;
if (b.getShelf() != null) 
    b.getShelf().remove(b);

Điều này không thực sự khác biệt, nhưng làm cho lỗ hổng rõ ràng hơn: Tại sao cuốn sách sẽ biết về kệ của nó? Nói một cách đơn giản, nó không nên. Nó giới thiệu một sự phụ thuộc của sách trên giá (không có ý nghĩa) và tạo ra các tài liệu tham khảo vòng tròn. Tất cả đều tệ.

Tương tự như vậy, không cần thiết cho một widget để biết cha mẹ của nó. Bạn sẽ nói: "Ok, nhưng widget cần bố mẹ của nó để bố trí chính xác, v.v." và dưới vỏ bọc, tiện ích biết cha mẹ của nó và yêu cầu nó cho các số liệu của nó để tính toán các số liệu của chính nó dựa trên chúng, v.v. Theo như nói, đừng hỏi điều này là sai. Phụ huynh nên nói với tất cả con cái của mình để kết xuất, chuyển tất cả thông tin cần thiết làm đối số. Bằng cách này, người ta có thể dễ dàng có cùng một tiện ích trong hai cha mẹ (điều này có thực sự có ý nghĩa hay không).

Để quay lại ví dụ về sách - hãy nhập thư viện:

Librarian l = ...;
Book b = ...;
l.findShelf(b).remove(b);

Xin hãy hiểu rằng chúng ta sẽ không hỏi nữa theo nghĩa , đừng hỏi .

Trong phiên bản đầu tiên, kệ là một tài sản của cuốn sách và bạn đã yêu cầu nó. Trong phiên bản thứ hai, chúng tôi không đưa ra giả định nào về cuốn sách. Tất cả những gì chúng tôi biết là người thủ thư có thể cho chúng tôi biết một kệ chứa sách. Có lẽ anh ta dựa vào một số loại bảng tra cứu để làm như vậy, nhưng chúng tôi không biết chắc chắn (anh ta cũng có thể chỉ cần đọc qua tất cả các sách trong mỗi kệ) và thực sự chúng tôi không muốn biết .
Chúng tôi không dựa vào việc phản ứng của người thủ thư được kết hợp với trạng thái của nó hay của các đối tượng khác mà nó có thể phụ thuộc vào. Chúng tôi nói với thủ thư để tìm kệ.
Trong kịch bản đầu tiên, chúng tôi đã mã hóa trực tiếp mối quan hệ giữa một cuốn sách và giá sách như một phần của trạng thái của cuốn sách và lấy trực tiếp trạng thái này. Điều này rất mong manh, bởi vì chúng tôi cũng đưa ra giả định rằng giá sách được trả lại bởi cuốn sách có chứa cuốn sách (đây là một ràng buộc mà chúng tôi phải đảm bảo, nếu không chúng tôi có thể không thể removeđặt cuốn sách từ giá sách mà nó thực sự ở trong đó).

Với sự giới thiệu của thủ thư, chúng tôi mô hình hóa mối quan hệ này một cách riêng biệt, do đó đạt được sự tách biệt mối quan tâm.


Tôi nghĩ rằng đây là một điểm rất hợp lệ, nhưng không trả lời câu hỏi nào cả. Trong phiên bản của bạn, câu hỏi đặt ra là phương thức remove (Book b) trong lớp Kệ có giá trị trả về không?
Scarfridge

1
@scarfridge: Ở điểm mấu chốt, hãy nói, đừng hỏi là một hệ quả của sự tách biệt quan tâm đúng đắn. Nếu bạn nghĩ về nó, tôi sẽ trả lời câu hỏi. Ít nhất tôi sẽ nghĩ như vậy;)
back2dos

1
@scarfridge Thay vì giá trị trả về, người ta có thể vượt qua một hàm / đại biểu được gọi là thất bại. Nếu thành công, bạn đã hoàn thành, phải không?
Bless Yahu

2
@BlessYahu Điều đó dường như quá sức đối với tôi. Cá nhân tôi cảm thấy rằng phân tách truy vấn lệnh là một lý tưởng trong thế giới thực (đa luồng). IMHO không sao khi một phương thức có tác dụng phụ trả về giá trị miễn là tên của phương thức chỉ rõ, nó sẽ thay đổi trạng thái của đối tượng. Hãy xem xét phương thức pop () của ngăn xếp. Nhưng một phương thức có tên truy vấn sẽ không có tác dụng phụ.
Scarfridge

13

Nếu bạn cần biết kết quả, thì bạn làm; đó là yêu cầu của bạn

Cụm từ "các phương thức chỉ trả về một giá trị nếu chúng trong suốt được tham chiếu và do đó không có tác dụng phụ" là một hướng dẫn tốt để tuân theo (đặc biệt là nếu bạn viết chương trình của mình theo kiểu chức năng , vì đồng thời hoặc các lý do khác), nhưng đó là không phải là tuyệt đối

Ví dụ, bạn có thể cần biết kết quả của thao tác ghi tệp (đúng hoặc sai). Đó là một ví dụ về phương thức trả về giá trị, nhưng luôn tạo ra tác dụng phụ; không có cách nào khác

Để thực hiện Tách lệnh / truy vấn, bạn sẽ phải thực hiện thao tác tệp bằng một phương thức và sau đó kiểm tra kết quả của nó bằng một phương thức khác, một kỹ thuật không mong muốn vì nó tách kết quả khỏi phương thức gây ra kết quả. Điều gì xảy ra nếu có gì đó xảy ra với trạng thái của đối tượng của bạn giữa lệnh gọi tệp và kiểm tra trạng thái?

Điểm mấu chốt là đây: nếu bạn đang sử dụng kết quả của một cuộc gọi phương thức để đưa ra quyết định ở nơi khác trong chương trình, thì bạn không vi phạm Tell Don't Ask. Mặt khác, nếu bạn đang đưa ra quyết định cho một đối tượng dựa trên một cuộc gọi phương thức đến đối tượng đó, thì bạn nên di chuyển các quyết định đó vào chính đối tượng đó để bảo toàn việc đóng gói.

Điều này hoàn toàn không mâu thuẫn với Tách truy vấn lệnh; trong thực tế, nó tiếp tục thi hành nó, bởi vì không còn cần phải đưa ra một phương thức bên ngoài cho các mục đích trạng thái.


1
Người ta có thể lập luận rằng trong ví dụ của bạn, nếu thao tác "ghi" thất bại, nên ném một ngoại lệ, do đó không cần phương thức "lấy trạng thái".
David

@David: Sau đó quay lại ví dụ của OP, điều này sẽ trở lại đúng nếu thay đổi thực sự xảy ra, sai nếu không. Tôi nghi ngờ bạn muốn ném một ngoại lệ ở đó.
Robert Harvey

Ngay cả ví dụ của OP cũng không phù hợp với tôi. Bạn không muốn được chú ý nếu không thể xóa, trong trường hợp đó bạn sẽ sử dụng ngoại lệ hoặc bạn muốn được chú ý khi một widget thực sự bị xóa trong trường hợp đó bạn có thể sử dụng cơ chế gọi lại hoặc sự kiện. Tôi chỉ không thấy một ví dụ thực tế mà bạn không muốn tôn trọng "Mẫu phân tách truy vấn lệnh"
David

@David: Tôi đã chỉnh sửa câu trả lời của mình để làm rõ.
Robert Harvey

Không nêu quan điểm quá rõ ràng, nhưng toàn bộ ý tưởng đằng sau phân tách truy vấn lệnh là bất cứ ai phát lệnh để viết tệp đều không quan tâm đến kết quả - ít nhất, không phải theo một nghĩa cụ thể (người dùng có thể sau này kéo lên một danh sách tất cả các hoạt động tập tin gần đây, nhưng điều đó không có mối tương quan với lệnh ban đầu). Nếu bạn cần thực hiện các hành động sau khi lệnh hoàn thành, bất kể bạn có quan tâm đến kết quả hay không, cách chính xác để thực hiện là với mô hình lập trình không đồng bộ bằng cách sử dụng các sự kiện (có thể) chứa chi tiết về lệnh gốc và / hoặc lệnh kết quả.
Aaronaught

2

Tôi nghĩ rằng bạn nên đi với bản năng ban đầu của bạn. Đôi khi thiết kế nên cố tình gây nguy hiểm, để gây ra sự phức tạp ngay lập tức khi bạn giao tiếp với đối tượng để buộc bạn xử lý nó ngay lập tức, thay vì cố gắng tự xử lý nó trên đối tượng và che giấu tất cả các chi tiết đẫm máu và làm cho nó khó sạch thay đổi các giả định cơ bản.

Bạn sẽ có cảm giác chìm nếu một đối tượng cung cấp cho bạn các hành vi openclosehành vi tương đương và bạn ngay lập tức bắt đầu hiểu những gì bạn xử lý khi bạn thấy giá trị trả về boolean cho thứ gì đó bạn nghĩ sẽ là một nhiệm vụ nguyên tử đơn giản.

Làm thế nào bạn đối phó với sau này là điều của bạn. Bạn có thể tạo một sự trừu tượng bên trên nó, một loại widget với removeFromParent(), nhưng bạn phải luôn có một dự phòng ở mức độ thấp, nếu không bạn có thể đưa ra các giả định sớm.

Đừng cố làm mọi thứ đơn giản. Không có gì đáng thất vọng khi bạn dựa vào thứ gì đó có vẻ thanh lịch và ngây thơ, chỉ để nhận ra đó là nỗi kinh hoàng thực sự sau này trong những khoảnh khắc tồi tệ nhất.


2

Những gì bạn mô tả là một "ngoại lệ" đã biết đối với nguyên tắc Phân tách Truy vấn Lệnh.

Trong bài viết này Martin Fowler giải thích cách anh ta sẽ đe dọa một ngoại lệ như vậy.

Meyer thích sử dụng phân tách truy vấn lệnh hoàn toàn, nhưng có những trường hợp ngoại lệ. Popping một ngăn xếp là một ví dụ tốt về một truy vấn sửa đổi trạng thái. Meyer nói chính xác rằng bạn có thể tránh có phương pháp này, nhưng nó là một thành ngữ hữu ích. Vì vậy, tôi thích làm theo nguyên tắc này khi tôi có thể, nhưng tôi đã sẵn sàng phá vỡ nó để có được nhạc pop của mình.

Trong ví dụ của bạn, tôi sẽ xem xét ngoại lệ tương tự.


0

Phân tách truy vấn lệnh rất dễ hiểu nhầm.

Nếu tôi nói với bạn trong hệ thống của tôi, có một lệnh cũng trả về giá trị từ truy vấn và bạn nói "Ha! Bạn đang vi phạm!" bạn đang nhảy súng

Không, đó không phải là điều bị cấm.

Điều bị cấm là khi lệnh đó là cách DUY NHẤT để thực hiện truy vấn đó. Không. Tôi không cần phải thay đổi trạng thái để đặt câu hỏi. Điều đó không có nghĩa là tôi phải nhắm mắt mỗi khi tôi thay đổi trạng thái.

Không thành vấn đề nếu tôi làm vì tôi sẽ mở chúng lần nữa. Lợi ích duy nhất cho phản ứng thái quá này là đảm bảo các nhà thiết kế không bị lười biếng và quên bao gồm truy vấn không thay đổi trạng thái.

Ý nghĩa thực sự rất tinh vi đến nỗi những người lười biếng nói rằng setters sẽ trở lại khoảng trống dễ dàng hơn. Điều này giúp dễ dàng hơn để chắc chắn rằng bạn không vi phạm quy tắc thực sự nhưng đó là một phản ứng quá mức có thể trở thành một nỗi đau thực sự.

Giao diện lưu loát và iDSL "vi phạm" phản ứng thái quá này mọi lúc. Có rất nhiều sức mạnh bị bỏ qua nếu bạn phản ứng quá mức.

Bạn có thể lập luận rằng một lệnh chỉ nên làm một điều và một truy vấn chỉ nên làm một điều. Và trong nhiều trường hợp đó là một điểm tốt. Ai sẽ nói chỉ có một truy vấn nên tuân theo một lệnh? Tốt nhưng đó không phải là phân tách truy vấn lệnh. Đó là nguyên tắc trách nhiệm duy nhất.

Nhìn theo cách này và một ngăn xếp pop không phải là một ngoại lệ kỳ lạ. Đó là một trách nhiệm duy nhất. Pop chỉ vi phạm phân tách truy vấn lệnh nếu bạn quên cung cấp peek.

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.