Giải thích về cách thức Tell Tell, Đừng hỏi Hãy xem OO tốt


49

Blogpost này đã được đăng trên Hacker News với một số upvote. Đến từ C ++, hầu hết các ví dụ này dường như đi ngược lại với những gì tôi đã được dạy.

Chẳng hạn như ví dụ # 2:

Xấu:

def check_for_overheating(system_monitor)
  if system_monitor.temperature > 100
    system_monitor.sound_alarms
  end
end

so với tốt

system_monitor.check_for_overheating

class SystemMonitor
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

Lời khuyên trong C ++ là bạn nên thích các hàm miễn phí thay vì các hàm thành viên vì chúng làm tăng sự đóng gói. Cả hai đều giống hệt nhau về mặt ngữ nghĩa, vậy tại sao lại thích lựa chọn có quyền truy cập vào trạng thái nhiều hơn?

Ví dụ 4:

Xấu:

def street_name(user)
  if user.address
    user.address.street_name
  else
    'No street name on file'
  end
end

so với tốt

def street_name(user)
  user.address.street_name
end

class User
  def address
    @address || NullAddress.new
  end
end

class NullAddress
  def street_name
    'No street name on file'
  end
end

Tại sao trách nhiệm Userđịnh dạng chuỗi lỗi không liên quan? Nếu tôi muốn làm gì đó ngoài việc in 'No street name on file'nếu nó không có đường thì sao? Nếu đường phố được đặt tên giống nhau thì sao?


Ai đó có thể khai sáng cho tôi về những lợi thế và lý do "Đừng hỏi" không? Tôi không tìm kiếm cái nào tốt hơn, mà thay vào đó là cố gắng hiểu quan điểm của tác giả.


Ví dụ mã có thể là Ruby và không phải Python, tôi không biết.
Pubby

2
Tôi luôn tự hỏi nếu một cái gì đó như ví dụ đầu tiên không phải là vi phạm SRP?
stijn

1
Bạn có thể đọc rằng: pragprog.com/articles/tell-dont-ask
Mik378

Ruby. @ là viết tắt chẳng hạn và Python kết thúc các khối của nó hoàn toàn bằng khoảng trắng.
Erik Reppen

3
"Lời khuyên trong C ++ là bạn nên thích các hàm miễn phí thay vì các hàm thành viên vì chúng làm tăng sự đóng gói." Tôi không biết ai đã nói với bạn điều đó, nhưng điều đó không đúng. Các hàm miễn phí có thể được sử dụng để tăng đóng gói, nhưng chúng không nhất thiết làm tăng đóng gói.
Rob K

Câu trả lời:


81

Hỏi đối tượng về trạng thái của nó, và sau đó gọi các phương thức trên đối tượng đó dựa trên các quyết định được đưa ra bên ngoài đối tượng, có nghĩa là đối tượng hiện đang bị trừu tượng hóa; một số hành vi của nó nằm bên ngoài đối tượng và trạng thái bên trong được phơi bày (có lẽ không cần thiết) với thế giới bên ngoà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ó.

Chắc chắn, bạn có thể nói, đó là hiển nhiên. Tôi sẽ không bao giờ viết mã như thế. Tuy nhiên, thật dễ dàng để được xem xét một số đối tượng được tham chiếu và sau đó gọi các phương thức khác nhau dựa trên kết quả. Nhưng đó có thể không phải là cách tốt nhất để thực hiện nó. Nói với đối tượng những gì bạn muốn. Hãy để nó tìm ra làm thế nào để làm điều đó. Hãy suy nghĩ khai báo thay vì thủ tục!

Sẽ dễ dàng hơn để thoát khỏi cái bẫy này nếu bạn bắt đầu bằng cách thiết kế các lớp dựa trên trách nhiệm của họ; sau đó bạn có thể tiến triển một cách tự nhiên để chỉ định các lệnh mà lớp có thể thực thi, trái ngược với các truy vấn thông báo cho bạn về trạng thái của đối tượng.

http://pragprog.com/articles/tell-dont-ask


4
Các văn bản ví dụ không cho phép nhiều điều rõ ràng là thực hành tốt.
DeadMG

13
@DeadMG nó chỉ làm những gì bạn nói với những người mù quáng theo dõi nó, những người mù quáng bỏ qua "thực dụng" trong tên trang web và suy nghĩ chính của các tác giả trang web đã được nêu rõ trong cuốn sách chính của họ: "không có gì là một giải pháp tốt nhất ... "
gnat

2
Không bao giờ đọc cuốn sách. Tôi cũng không muốn. Tôi chỉ đọc văn bản ví dụ, đó là hoàn toàn công bằng.
DeadMG

3
@DeadMG không phải lo lắng. Bây giờ bạn đã biết điểm chính đặt ví dụ này (và bất kỳ ví dụ nào khác từ pragprog cho vấn đề đó) trong bối cảnh dự định ("không có gì là giải pháp tốt nhất ..."), không nên đọc cuốn sách
gnat

1
Tôi vẫn không chắc những gì Tell, Don't Ask được cho là đánh vần cho bạn mà không có ngữ cảnh nhưng đây thực sự là lời khuyên OOP tốt.
Erik Reppen

16

Nói chung, tác phẩm gợi ý rằng bạn không nên để lộ trạng thái thành viên cho người khác lý do, nếu bạn có thể tự mình suy luận về nó .

Tuy nhiên, điều không được nêu rõ là luật này rơi vào giới hạn rất rõ ràng khi lý luận vượt quá trách nhiệm của một lớp cụ thể. Ví dụ, mỗi lớp có công việc giữ một số giá trị hoặc cung cấp một số giá trị - đặc biệt là các giá trị chung hoặc trong đó lớp cung cấp hành vi phải được mở rộng.

Ví dụ: nếu hệ thống cung cấp temperaturedưới dạng truy vấn, thì ngày mai, máy khách có thể check_for_underheatingkhông phải thay đổi SystemMonitor. Đây không phải là trường hợp khi SystemMonitorthực hiện check_for_overheatingchính nó. Do đó, một SystemMonitorlớp có công việc là tăng báo động khi nhiệt độ quá cao sẽ tuân theo điều này - nhưng một SystemMonitorlớp có công việc là cho phép một đoạn mã khác đọc nhiệt độ để nó có thể kiểm soát, giả sử, TurboBoost hoặc đại loại như thế , không nên.

Cũng lưu ý rằng ví dụ thứ hai vô dụng sử dụng mẫu chống đối tượng Null.


19
Đối tượng của Null Null không phải là thứ mà tôi gọi là chống mẫu, vì vậy tôi tự hỏi lý do của bạn để làm như vậy là gì?
Konrad Rudolph

4
Khá chắc chắn rằng không ai có bất kỳ phương thức nào được chỉ định là "Không làm gì cả". Điều đó làm cho việc gọi họ là vô nghĩa. Điều đó có nghĩa là ít nhất bất kỳ đối tượng nào thực hiện Null Object đều phá vỡ LSP, và mô tả chính nó là việc thực hiện các hoạt động mà trên thực tế, nó không thực hiện. Người dùng mong đợi một giá trị trở lại. Sự đúng đắn của chương trình của họ phụ thuộc vào nó. Bạn chỉ đơn giản mang lại nhiều vấn đề hơn bằng cách giả vờ rằng đó là một giá trị khi nó không. Bạn đã bao giờ cố gắng để gỡ lỗi âm thầm phương pháp thất bại? Đó là điều không thể và không ai phải chịu đựng điều đó.
DeadMG

4
Tôi cho rằng điều này hoàn toàn phụ thuộc vào miền vấn đề.
Konrad Rudolph

5
@DeadMG Tôi đồng ý rằng ví dụ trên là một cách sử dụng xấu của các mô hình đối tượng Null, nhưng có một bằng khen cho sử dụng nó. Một vài lần tôi đã sử dụng triển khai 'no-op' của một số giao diện hoặc giao diện khác để tránh kiểm tra null hoặc có 'null' thực sự thấm vào hệ thống.
Tối đa

6
Không chắc chắn tôi thấy quan điểm của bạn với "khách hàng có thể check_for_underheatingmà không phải thay đổi SystemMonitor". Làm thế nào là khách hàng khác nhau từ SystemMonitorthời điểm đó? Bây giờ bạn có làm tiêu tan logic giám sát của bạn trên nhiều lớp không? Tôi cũng không thấy vấn đề với lớp màn hình cung cấp thông tin cảm biến cho các lớp khác trong khi bảo lưu các chức năng báo thức cho chính nó. Bộ điều khiển tăng cường nên được kiểm soát tăng tốc mà không phải lo lắng về việc tăng báo động nếu nhiệt độ quá cao.
TMN

9

Vấn đề thực sự với ví dụ quá nóng của bạn là các quy tắc cho những gì đủ điều kiện là quá nóng không dễ dàng thay đổi cho các hệ thống khác nhau. Giả sử Hệ thống A giống như bạn có (temp> 100 quá nóng) nhưng Hệ thống B tinh tế hơn (temp> 93 quá nóng). Bạn có thay đổi chức năng điều khiển để kiểm tra loại hệ thống, sau đó áp dụng giá trị đúng không?

if (system is a System_A and system_monitor.temp >100)
  system_monitor.sound_alarms
else if (system is a System_B and system_monitor.temp > 93)
  system_monitor.sound_alarms
end

Hay bạn có từng loại hệ thống xác định khả năng sưởi ấm của nó?

BIÊN TẬP:

system.check_for_overheating

class SystemA : System
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

class SystemB : System
  def check_for_overheating
    if temperature > 93
      sound_alarms
    end
  end
end

Cách trước đây làm cho chức năng kiểm soát của bạn trở nên xấu xí khi bạn bắt đầu làm việc với nhiều hệ thống hơn. Cái sau cho phép chức năng điều khiển ổn định khi thời gian tiếp tục.


1
Tại sao không có mỗi hệ thống đăng ký với màn hình. Trong quá trình đăng ký, họ có thể chỉ ra khi quá nóng xảy ra.
Martin York

@LokiAstari - Bạn có thể, nhưng sau đó bạn có thể chạy vào một hệ thống mới cũng nhạy cảm với độ ẩm hoặc áp suất khí quyển. Nguyên tắc là trừu tượng hóa những gì thay đổi - trong trường hợp này là dễ bị quá nóng
Matthew Flynn

1
Đây chính xác là lý do tại sao bạn nên có một mô hình nói. Bạn nói với hệ thống các điều kiện hiện tại và nó thông báo cho bạn nếu nó nằm ngoài điều kiện làm việc bình thường. Do đó, bạn không bao giờ cần phải sửa đổi SystemMoniter. Đó là sự gói gọn cho bạn.
Martin York

@LokiAstari - Tôi nghĩ chúng ta đang nói về các mục đích chéo ở đây - Tôi thực sự đang tìm cách tạo ra các hệ thống khác nhau, thay vì các màn hình khác nhau. Vấn đề là, hệ thống nên biết khi nào nó ở trạng thái phát ra cảnh báo, trái ngược với một số chức năng điều khiển bên ngoài. SystemA nên có tiêu chí của nó, SystemB nên có tiêu chí riêng. Bộ điều khiển chỉ có thể hỏi (trong khoảng thời gian đều đặn) liệu hệ thống có ổn hay không.
Matthew Flynn

6

Trước hết, tôi cảm thấy tôi phải ngoại lệ với đặc tính của bạn về các ví dụ là "xấu" và "tốt". Bài viết sử dụng thuật ngữ "Không tốt" và "Tốt hơn", tôi nghĩ những thuật ngữ đó được chọn vì một lý do: đây là những hướng dẫn và tùy thuộc vào hoàn cảnh, cách tiếp cận "Không tốt" có thể phù hợp hoặc thực sự là giải pháp duy nhất.

Khi được lựa chọn, bạn nên ưu tiên bao gồm bất kỳ chức năng nào chỉ dựa vào lớp trong lớp thay vì bên ngoài nó - lý do là vì đóng gói và thực tế là nó giúp phát triển lớp theo thời gian dễ dàng hơn. Lớp học cũng thực hiện công việc quảng cáo tốt hơn các khả năng của nó hơn là một loạt các chức năng miễn phí.

Đôi khi bạn phải nói, bởi vì quyết định phụ thuộc vào điều gì đó bên ngoài lớp học hoặc bởi vì đó đơn giản là điều bạn không muốn hầu hết người dùng của lớp thực hiện. Đôi khi bạn muốn nói, bởi vì hành vi phản tác dụng trực quan cho lớp và bạn không muốn gây nhầm lẫn cho hầu hết người dùng của lớp.

Ví dụ: bạn phàn nàn về địa chỉ đường phố trả về thông báo lỗi, không phải vậy, những gì nó đang làm là cung cấp một giá trị mặc định. Nhưng đôi khi một giá trị mặc định không phù hợp. Nếu đây là Bang hoặc Thành phố, bạn có thể muốn mặc định khi gán bản ghi cho nhân viên bán hàng hoặc người thực hiện khảo sát, để tất cả những điều chưa biết đến một người cụ thể. Mặt khác, nếu bạn đang in phong bì, bạn có thể thích một ngoại lệ hoặc người bảo vệ giúp bạn không lãng phí giấy vào các chữ cái không thể gửi được.

Vì vậy, có thể có trường hợp "Không tốt" là cách để đi, nhưng nói chung, "Tốt hơn" là, tốt, tốt hơn.


3

Đối xứng dữ liệu / đối tượng

Như những người khác đã chỉ ra, Tell-Dont-Ask dành riêng cho các trường hợp bạn thay đổi trạng thái đối tượng sau khi bạn hỏi (xem ví dụ: văn bản của Pragprog được đăng ở nơi khác trên trang này). Điều này không phải lúc nào cũng đúng, ví dụ: đối tượng 'user' không bị thay đổi sau khi được yêu cầu user.address. Do đó, có thể gỡ lỗi nếu đây là trường hợp thích hợp để áp dụng Tell-Dont-Ask.

Tell-Dont-Ask có liên quan đến trách nhiệm, với việc không rút logic ra khỏi một lớp nên được coi là hợp lý trong đó. Nhưng không phải tất cả logic liên quan đến các đối tượng nhất thiết phải là logic của các đối tượng đó. Đây là gợi ý ở cấp độ sâu hơn, thậm chí vượt xa cả Tell-Dont-Ask và tôi muốn thêm một nhận xét ngắn về điều đó.

Là một vấn đề của thiết kế kiến ​​trúc, bạn có thể muốn có các đối tượng thực sự chỉ là các thùng chứa cho các thuộc tính, thậm chí có thể bất biến và sau đó chạy các hàm khác nhau trên các bộ sưu tập của các đối tượng đó, đánh giá, lọc hoặc chuyển đổi chúng thay vì gửi các lệnh (đó là thêm tên miền của Tell-Dont-Ask).

Quyết định phù hợp hơn cho vấn đề của bạn phụ thuộc vào việc bạn có mong muốn có dữ liệu ổn định (các đối tượng khai báo) hay không với việc thay đổi / thêm vào bên chức năng. Hoặc nếu bạn mong muốn có một tập hợp các hàm như vậy ổn định và hạn chế nhưng mong đợi thông lượng nhiều hơn ở cấp đối tượng, ví dụ: bằng cách thêm các loại mới. Trong tình huống đầu tiên, bạn sẽ thích các hàm miễn phí, trong các phương thức đối tượng thứ hai.

Bob Martin, trong cuốn sách "Clean Code", gọi đây là "Đối xứng dữ liệu / đối tượng" (p.95ff), các cộng đồng khác có thể gọi nó là " vấn đề biểu hiện ".


3

Mô hình này đôi khi được gọi là 'Nói, đừng hỏi' , nghĩa là nói cho đối tượng biết phải làm gì, đừng hỏi về trạng thái của nó; và đôi khi như 'Hỏi, đừng nói' , nghĩa là yêu cầu đối tượng làm điều gì đó cho bạn, đừng nói với nó trạng thái của nó là gì. Cả hai cách thực hành tốt nhất đều giống nhau - cách một đối tượng nên thực hiện một hành động là mối quan tâm của đối tượng đó, chứ không phải đối tượng gọi. Các giao diện nên tránh phơi bày trạng thái của chúng (ví dụ: thông qua các phụ kiện hoặc thuộc tính công cộng) và thay vào đó phơi bày các phương thức 'làm' mà việc triển khai không rõ ràng. Những người khác đã bao gồm điều này với các liên kết đến lập trình viên thực dụng.

Quy tắc này liên quan đến quy tắc tránh mã "dấu chấm kép" hoặc "mũi tên kép", thường được gọi là 'Chỉ nói chuyện với bạn bè ngay lập tức', trạng thái foo->getBar()->doSomething()là xấu, thay vào đó sử dụng foo->doSomething();lệnh gọi bao quanh chức năng của thanh và thực hiện đơn giản return bar->doSomething();- nếu foochịu trách nhiệm quản lý bar, thì hãy để nó làm như vậy!


1

Ngoài các câu trả lời hay khác về "nói, đừng hỏi", một số bình luận về các ví dụ cụ thể của bạn có thể giúp ích:

Lời khuyên trong C ++ là bạn nên thích các hàm miễn phí thay vì các hàm thành viên vì chúng làm tăng sự đóng gói. Cả hai đều giống hệt nhau về mặt ngữ nghĩa, vậy tại sao lại thích lựa chọn có quyền truy cập vào trạng thái nhiều hơn?

Sự lựa chọn đó không có quyền truy cập vào trạng thái nhiều hơn. Cả hai đều sử dụng cùng một lượng trạng thái để thực hiện công việc của mình, nhưng ví dụ 'xấu' đòi hỏi trạng thái lớp phải được công khai để thực hiện công việc của mình. Hơn nữa, hành vi của lớp đó trong ví dụ 'xấu' được lan truyền sang hàm miễn phí, khiến cho việc tìm kiếm và khó tái cấu trúc trở nên khó khăn hơn.

Tại sao Người dùng có trách nhiệm định dạng chuỗi lỗi không liên quan? Điều gì sẽ xảy ra nếu tôi muốn làm gì đó ngoài việc in 'Không có tên đường phố trong tệp' nếu nó không có đường phố? Nếu đường phố được đặt tên giống nhau thì sao?

Tại sao trách nhiệm của 'street_name' phải thực hiện cả 'lấy tên đường' và 'cung cấp thông báo lỗi'? Ít nhất là trong phiên bản 'tốt', mỗi tác phẩm có một trách nhiệm. Tuy nhiên, nó không phải là một ví dụ tuyệt vời.


2
Đo không phải sự thật. Bạn cho rằng việc kiểm tra quá nhiệt là điều duy nhất có thể làm với nhiệt độ. Điều gì sẽ xảy ra nếu lớp được dự định là một trong một số máy theo dõi nhiệt độ và một hệ thống phải thực hiện các hành động khác nhau tùy thuộc vào nhiều kết quả của chúng, chẳng hạn? Khi hành vi này có thể được giới hạn ở hành vi được xác định trước của một trường hợp duy nhất, thì chắc chắn. Khác, nó rõ ràng không thể áp dụng.
DeadMG

Chắc chắn, hoặc nếu bộ điều chỉnh nhiệt và báo động tồn tại trong các lớp khác nhau (như chúng có thể nên).
Telastyn

1
@DeadMG: Lời khuyên chung là làm cho mọi thứ riêng tư / được bảo vệ cho đến khi bạn cần truy cập vào chúng. Trong khi ví dụ cụ thể này là meh, điều đó không tranh chấp thực tiễn tiêu chuẩn.
Guvante

Một ví dụ trong một bài viết về việc thực hành 'meh' là một tranh chấp. Nếu thực hành này là tiêu chuẩn vì nó mang lại lợi ích lớn, vậy thì tại sao lại gặp khó khăn khi tìm một ví dụ phù hợp?
Stijn de Witt

1

Những câu trả lời này rất hay, nhưng đây là một ví dụ khác chỉ cần nhấn mạnh: lưu ý rằng đó thường là một cách để tránh trùng lặp. Ví dụ: giả sử bạn có các địa điểm SEVERAL với mã như:

Product product = productMgr.get(productUuid)
if (product.userUuid != currentUser.uuid) {
    throw BlahException("This product doesn't belong to this user")
}

Điều đó có nghĩa là bạn tốt hơn nên có một cái gì đó như thế này:

Product product = productMgr.get(productUuid, currentUser)

Bởi vì sự trùng lặp đó có nghĩa là hầu hết các máy khách của giao diện của bạn sẽ sử dụng phương thức mới, thay vì lặp lại cùng một logic ở đây và ở đó. Bạn giao cho đại biểu của bạn công việc bạn muốn thực hiện, thay vì yêu cầu thông tin bạn cần để tự làm.


0

Tôi tin rằng điều này đúng hơn khi viết đối tượng cấp cao, nhưng ít đúng hơn khi đi xuống cấp độ sâu hơn, ví dụ như thư viện lớp vì không thể viết mọi phương thức duy nhất để làm hài lòng tất cả người tiêu dùng.

Ví dụ # 2, tôi nghĩ nó quá đơn giản. Nếu chúng ta thực sự sẽ thực hiện điều này, SystemMonitor cuối cùng sẽ có mã cho truy cập phần cứng và logic cấp thấp cho sự trừu tượng hóa mức cao được nhúng trong cùng một lớp. Thật không may, nếu chúng tôi đang cố tách điều đó thành hai lớp, chúng tôi sẽ vi phạm chính "Nói, đừng hỏi".

Ví dụ # 4 ít nhiều giống nhau - đó là nhúng logic UI vào lớp dữ liệu. Bây giờ nếu chúng ta sẽ sửa những gì người dùng muốn thấy trong trường hợp không có địa chỉ, chúng ta phải sửa đối tượng trong tầng dữ liệu, và nếu hai dự án sử dụng cùng một đối tượng này nhưng cần sử dụng văn bản khác cho địa chỉ null thì sao?

Tôi đồng ý rằng nếu chúng ta có thể thực hiện "Nói, đừng hỏi" cho tất cả mọi thứ, nó sẽ rất hữu ích - Bản thân tôi sẽ rất vui nếu tôi chỉ có thể nói thay vì hỏi (và tự làm) trong cuộc sống thực! Tuy nhiên, giống như trong cuộc sống thực, tính khả thi của giải pháp rất hạn chế đối với các lớp cấp cao.

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.