Tại sao Clean Code đề xuất tránh các biến được bảo vệ?


168

Clean Code đề nghị tránh các biến được bảo vệ trong phần "Khoảng cách dọc" của chương "Định dạng":

Các khái niệm có liên quan chặt chẽ nên được giữ gần nhau theo chiều dọc. Rõ ràng quy tắc này không hoạt động đối với các khái niệm thuộc về các tệp riêng biệt. Nhưng sau đó không nên tách các khái niệm liên quan chặt chẽ thành các tệp khác nhau trừ khi bạn có lý do rất chính đáng. Thật vậy, đây là một trong những lý do mà các biến được bảo vệ nên tránh .

Lý do là gì?


7
hiển thị một ví dụ mà bạn nghĩ rằng bạn cần
gnat

63
Tôi sẽ không lấy nhiều trong cuốn sách đó làm phúc âm.
Giàn

40
@Rig bạn đang giáp với báng bổ trong những phần này với một nhận xét như thế.
Ryathal

40
Tôi ok với điều đó. Tôi đã đọc cuốn sách và có thể có ý kiến ​​của riêng tôi về nó.
Giàn khoan

10
Tôi đã đọc cuốn sách và mặc dù tôi đồng ý với hầu hết những gì trong đó, tôi cũng không nói tôi coi đó là tin lành. Tôi nghĩ rằng mọi tình huống đều khác nhau .. và việc có thể tuân thủ các hướng dẫn chính xác được trình bày trong cuốn sách là khá khó khăn cho rất nhiều dự án.
Simon Whitehead

Câu trả lời:


277

Các biến được bảo vệ nên tránh vì:

  1. Họ có xu hướng dẫn đến các vấn đề YAGNI . Trừ khi bạn có một lớp con cháu thực sự làm việc với thành viên được bảo vệ, hãy làm cho nó riêng tư.
  2. Họ có xu hướng dẫn đến các vấn đề LSP . Các biến được bảo vệ thường có một số bất biến nội tại liên quan đến chúng (hoặc nếu không chúng sẽ được công khai). Những người thừa kế sau đó cần phải duy trì những tài sản đó, mà mọi người có thể làm hỏng hoặc cố tình vi phạm.
  3. Họ có xu hướng vi phạm OCP . Nếu lớp cơ sở đưa ra quá nhiều giả định về thành viên được bảo vệ hoặc người thừa kế quá linh hoạt với hành vi của lớp, điều đó có thể dẫn đến hành vi của lớp cơ sở bị sửa đổi bởi phần mở rộng đó.
  4. Họ có xu hướng dẫn đến sự kế thừa để mở rộng hơn là thành phần. Điều này có xu hướng dẫn đến khớp nối chặt chẽ hơn, vi phạm SRP nhiều hơn, thử nghiệm khó khăn hơn và một loạt những thứ khác nằm trong phần 'ưu tiên thành phần thảo luận về thừa kế'.

Nhưng như bạn thấy, tất cả những thứ này là 'có xu hướng'. Đôi khi một thành viên được bảo vệ là giải pháp thanh lịch nhất. Và các chức năng được bảo vệ có xu hướng có ít hơn những vấn đề này. Nhưng có một số điều khiến chúng được chăm sóc. Với bất cứ điều gì đòi hỏi sự chăm sóc đó, mọi người sẽ phạm sai lầm và trong thế giới lập trình có nghĩa là lỗi và vấn đề thiết kế.


Cảm ơn vì nhiều lý do tốt. Tuy nhiên, cuốn sách đề cập đến nó một cách cụ thể trong bối cảnh định dạng và khoảng cách dọc, đó là phần tôi đang tự hỏi.
Matsemann

2
Có vẻ như chú Bob đang đề cập đến khoảng cách theo chiều dọc khi ai đó đang đề cập đến một thành viên được bảo vệ giữa các lớp và không cùng lớp.
Robert Harvey

5
@Matsemann - chắc chắn rồi. Nếu tôi nhớ cuốn sách một cách chính xác, phần đó tập trung vào khả năng đọc và khả năng khám phá của mã. Nếu các biến và hàm làm việc trên một khái niệm thì chúng phải nằm trong mã. Các thành viên được bảo vệ sẽ được sử dụng bởi các loại dẫn xuất, mà nhất thiết không thể gần với thành viên được bảo vệ vì chúng nằm trong một tệp hoàn toàn khác.
Telastyn

2
@ steve314 Tôi không thể tưởng tượng ra một kịch bản trong đó lớp cơ sở và tất cả những người thừa kế của nó sẽ chỉ sống trong một tệp. Nếu không có một ví dụ, tôi có khuynh hướng nghĩ rằng đó là sự lạm dụng quyền thừa kế hoặc sự trừu tượng kém.
Telastyn

3
YAGNI = Bạn Aint Gonna Cần nó LSP = Nguyên tắc thay thế Liskov OCP = Nguyên tắc mở / đóng SRP = Nguyên tắc trách nhiệm duy nhất
Bryan Glazer

54

Đó thực sự là cùng một lý do bạn tránh toàn cầu, chỉ ở quy mô nhỏ hơn. Đó là bởi vì thật khó để tìm thấy ở khắp mọi nơi một biến đang được đọc, hoặc tệ hơn, được viết và khó để làm cho việc sử dụng nhất quán. Nếu việc sử dụng bị hạn chế, như được viết ở một nơi rõ ràng trong lớp dẫn xuất và đọc ở một vị trí rõ ràng trong lớp cơ sở, thì các biến được bảo vệ có thể làm cho mã rõ ràng và súc tích hơn. Nếu bạn muốn đọc và viết một biến số trong suốt nhiều tệp, tốt hơn hết là gói gọn nó.


26

Tôi chưa đọc cuốn sách này, nhưng tôi có thể hiểu ý của chú Bob.

Nếu bạn đặt protectedmột cái gì đó, điều đó có nghĩa là một lớp có thể kế thừa nó. Nhưng các biến thành viên được cho là thuộc về lớp mà chúng được chứa; đây là một phần của đóng gói cơ bản. Việc đưa protectedvào một biến thành viên sẽ phá vỡ đóng gói vì bây giờ một lớp dẫn xuất có quyền truy cập vào các chi tiết triển khai của lớp cơ sở. Đó là cùng một vấn đề xảy ra khi bạn thực hiện một biến publictrên một lớp bình thường.

Để khắc phục sự cố, bạn có thể đóng gói biến trong thuộc tính được bảo vệ, như vậy:

protected string Name
{
    get { return name; }
    private set { name = value; }
}

Điều này cho phép nameđược đặt an toàn từ một lớp dẫn xuất bằng cách sử dụng đối số hàm tạo, mà không để lộ chi tiết triển khai của lớp cơ sở.


Bạn đã tạo một tài sản công cộng với setter được bảo vệ. Trường được bảo vệ phải được giải quyết bằng thuộc tính được bảo vệ bằng setter riêng.
Andy

2
+1 ngay bây giờ. Setter có thể được bảo vệ quá, tôi cho rằng, nhưng id điểm cơ sở có thể thực thi nếu giá trị mới hợp lệ hay không.
Andy

Chỉ để đưa ra một quan điểm đối lập - tôi làm cho tất cả các biến của mình được bảo vệ trong một lớp cơ sở. Nếu chúng chỉ được truy cập thông qua một giao diện công cộng thì nó không phải là một lớp dẫn xuất, nó chỉ nên chứa một đối tượng cơ sở. Nhưng tôi cũng tránh các hệ thống phân cấp sâu hơn 2 lớp
Martin Beckett

2
Có một sự khác biệt rất lớn giữa protectedpubliccác thành viên. Nếu BaseTypeđể lộ một thành viên công cộng, điều đó ngụ ý rằng tất cả các loại dẫn xuất phải có cùng một thành viên đó hoạt động theo cùng một cách, vì một DerivedTypethể hiện có thể được chuyển đến mã nhận được BaseTypetham chiếu và dự kiến ​​sẽ sử dụng thành viên đó. Ngược lại, nếu BaseTypeđể lộ một protectedthành viên, thì DerivedTypecó thể mong đợi truy cập vào thành viên đó base, nhưng basekhông thể là gì khác ngoài a BaseType.
supercat

18

Đối số của chú Bob chủ yếu là một khoảng cách: nếu bạn có một khái niệm quan trọng đối với một lớp, hãy kết hợp khái niệm đó với lớp trong tệp đó. Không tách rời hai tệp trên đĩa.

Các biến thành viên được bảo vệ nằm rải rác ở hai nơi, và, trông giống như ma thuật. Bạn tham khảo biến này, tuy nhiên nó không được định nghĩa ở đây ... nơi được nó được xác định? Và thế là cuộc săn bắt đầu. Tốt hơn để tránh protectedhoàn toàn, tranh luận của mình.

Bây giờ tôi không tin rằng quy tắc cần phải được tuân theo thư mọi lúc (như: bạn sẽ không sử dụng protected). Nhìn vào tinh thần của những gì anh ta đang nhận được ... bó những thứ liên quan lại với nhau thành một tệp - sử dụng các kỹ thuật và tính năng lập trình để làm điều đó. Tôi khuyên bạn không nên phân tích quá mức và bị cuốn vào các chi tiết về điều này.


9

Một số câu trả lời rất tốt đã ở đây. Nhưng một điều cần thêm:

Trong hầu hết các ngôn ngữ OO hiện đại, một thói quen tốt (trong Java AFAIK là cần thiết) để đặt mỗi lớp vào tệp riêng của nó (vui lòng không nhận nitty về C ++ khi bạn thường có hai tệp).

Vì vậy, hầu như không thể giữ các hàm trong một lớp dẫn xuất có được một biến được bảo vệ của một lớp cơ sở "đóng theo chiều dọc" trong mã nguồn theo định nghĩa biến. Nhưng lý tưởng nhất là chúng nên được giữ ở đó, vì các biến được bảo vệ được dành cho sử dụng nội bộ, điều này làm cho các chức năng nhận được chúng thường là "một khái niệm liên quan chặt chẽ".


1
Không có trong Swift. Thật thú vị, Swift không có được bảo vệ và nhưng có bộ lọc Fildrivate, nó thay thế cho cả hai người bạn được bảo vệ và C ++.
gnasher729

@ gnasher729 - tất nhiên, Swift có rất nhiều Smalltalk trong di sản của mình. Smalltalk không có gì khác ngoài các biến riêng tư và các phương thức công khai. Và private trong Smalltalk có nghĩa là riêng tư: hai đối tượng thậm chí của cùng một lớp không thể truy cập các biến của nhau.
Jules

4

Ý tưởng cơ bản là một "trường" (biến cấp độ thể hiện) được khai báo là được bảo vệ có thể dễ nhìn thấy hơn so với nó và ít được "bảo vệ" hơn bạn muốn. Không có công cụ sửa đổi truy cập trong C / C ++ / Java / C # tương đương với "chỉ có thể truy cập bởi các lớp con trong cùng một cụm", do đó cung cấp cho bạn khả năng xác định con của bạn có thể truy cập vào trường trong cụm của bạn, nhưng không cho phép trẻ em được tạo ra trong các hội đồng khác có cùng quyền truy cập; C # có các sửa đổi nội bộ và được bảo vệ, nhưng việc kết hợp chúng làm cho quyền truy cập "nội bộ hoặc được bảo vệ", không phải "nội bộ và được bảo vệ". Vì vậy, một lĩnh vực được bảo vệ có thể truy cập bởi bất kỳ đứa trẻ nào, cho dù bạn đã viết đứa trẻ đó hoặc người khác đã làm. Do đó, được bảo vệ là một cánh cửa mở cho tin tặc.

Ngoài ra, các trường theo định nghĩa của chúng có khá nhiều không có xác nhận vốn có trong việc thay đổi chúng. trong C #, bạn có thể tạo một chỉ đọc, điều này làm cho các loại giá trị không đổi một cách hiệu quả và các loại tham chiếu không thể được khởi tạo lại (nhưng vẫn rất dễ thay đổi), nhưng đó là về nó. Như vậy, thậm chí được bảo vệ, con cái của bạn (mà bạn không thể tin tưởng) có quyền truy cập vào trường này và có thể đặt nó thành một cái gì đó không hợp lệ, làm cho trạng thái của đối tượng không nhất quán (cần tránh).

Cách được chấp nhận để làm việc với các trường là đặt chúng ở chế độ riêng tư và truy cập chúng bằng một thuộc tính và / hoặc phương thức getter và setter. Nếu tất cả người tiêu dùng của lớp cần giá trị, hãy đặt công khai (ít nhất). Nếu chỉ có trẻ em cần nó, làm cho getter được bảo vệ.

Một cách tiếp cận khác trả lời câu hỏi là tự hỏi bản thân; Tại sao mã trong phương thức con cần khả năng sửa đổi trực tiếp dữ liệu trạng thái của tôi? Điều đó nói gì về mã đó? Đó là đối số "khoảng cách dọc" trên khuôn mặt của nó. Nếu có mã ở một đứa trẻ phải trực tiếp thay đổi trạng thái cha mẹ, thì có lẽ mã đó phải thuộc về cha mẹ ở vị trí đầu tiên?


4

Đó là một cuộc thảo luận thú vị.

Thẳng thắn mà nói, các công cụ tốt có thể giảm thiểu nhiều vấn đề "dọc" này. Trên thực tế, theo tôi, khái niệm "tập tin" thực sự là thứ cản trở sự phát triển phần mềm - thứ mà dự án Light Table đang thực hiện (http://www.chris-granger.com/2012/04/12/light- bảng --- a-new-ide-khái niệm /).

Đưa các tập tin ra khỏi hình ảnh, và phạm vi được bảo vệ sẽ trở nên hấp dẫn hơn rất nhiều. Khái niệm được bảo vệ trở nên rõ ràng nhất trong các ngôn ngữ như SmallTalk - nơi bất kỳ sự kế thừa nào chỉ đơn giản là mở rộng khái niệm ban đầu của một lớp. Nó nên làm mọi thứ, và có mọi thứ, mà lớp cha mẹ đã làm.

Theo tôi, hệ thống phân cấp lớp nên càng nông càng tốt, với hầu hết các phần mở rộng đến từ thành phần. Trong các mô hình như vậy, tôi không thấy bất kỳ lý do nào để các biến riêng tư không được bảo vệ thay thế. Nếu lớp mở rộng phải đại diện cho một phần mở rộng bao gồm tất cả hành vi của lớp cha (mà tôi tin rằng nó nên, và do đó tin rằng các phương thức riêng tư vốn đã xấu), tại sao lớp mở rộng cũng không thể hiện việc lưu trữ dữ liệu đó?

Nói cách khác - trong bất kỳ trường hợp nào tôi cảm thấy một biến riêng sẽ phù hợp hơn biến được bảo vệ, sự kế thừa không phải là giải pháp cho vấn đề ngay từ đầu.


0

Như đã nói, đây chỉ là một trong những lý do, đó là mã được liên kết logic nên được đặt bên trong các thực thể được liên kết vật lý (tệp, gói và các thứ khác). Ở quy mô nhỏ hơn, đây giống như lý do bạn không đặt một lớp tạo các truy vấn DB bên trong gói với các lớp hiển thị UI.

Tuy nhiên, lý do chính mà các biến được bảo vệ không được khuyến nghị là vì chúng có xu hướng phá vỡ đóng gói. Các biến và phương thức nên có tầm nhìn hạn chế nhất có thể; để tham khảo thêm, hãy xem Java hiệu quả của Joshua Bloch , Mục 13 - "Tối thiểu hóa khả năng truy cập của các lớp và thành viên".

Tuy nhiên, bạn không nên lấy tất cả các quảng cáo này, chỉ như một dòng bang hội; nếu các biến được bảo vệ là rất xấu, chúng sẽ không được đặt bên trong ngôn ngữ ở vị trí đầu tiên. Một vị trí hợp lý cho các trường được bảo vệ imho nằm trong lớp cơ sở từ khung kiểm tra (các lớp kiểm thử tích hợp mở rộng nó).


1
Đừng cho rằng vì ngôn ngữ cho phép, nên bạn có thể làm được. Tôi không chắc chắn thực sự có một lý do thần để cho phép các lĩnh vực được bảo vệ; chúng tôi thực sự không cho phép họ tại chủ nhân của tôi.
Andy

2
@Andy Bạn chưa đọc những gì tôi nói; Tôi đã nói rằng các lĩnh vực được bảo vệ không được khuyến khích và lý do cho việc này. Rõ ràng có các kịch bản trong đó các biến được bảo vệ là cần thiết; bạn có thể muốn một giá trị cuối cùng không đổi chỉ trong các lớp con.
m3th0dman

Bạn có thể muốn điều chỉnh lại một số bài đăng của mình sau đó, vì dường như bạn sử dụng biến và trường thay thế cho nhau và nói rõ rằng bạn coi những thứ như bảo vệ là một ngoại lệ.
Andy

3
@Andy Một điều khá rõ ràng là vì tôi đã đề cập đến các biến được bảo vệ nên tôi không nói về các biến cục bộ hoặc biến tham số mà là các trường. Tôi cũng đã đề cập đến một kịch bản trong đó các biến được bảo vệ không liên tục là hữu ích; Imho thật sai lầm khi một công ty thực thi cách các lập trình viên viết mã.
m3th0dman

1
Nếu rõ ràng tôi sẽ không nhầm lẫn và đánh giá thấp bạn. Quy tắc được đưa ra bởi các nhà phát triển cao cấp của chúng tôi, cũng như quyết định của chúng tôi để chạy StyleCop, FxCop và yêu cầu kiểm tra đơn vị và đánh giá mã.
Andy

0

Tôi thấy việc sử dụng các biến được bảo vệ cho các bài kiểm tra JUnit có thể hữu ích. Nếu bạn đặt chúng ở chế độ riêng tư thì bạn không thể truy cập chúng trong khi thử nghiệm mà không có phản ánh. Điều này có thể hữu ích nếu bạn đang thử nghiệm một quy trình phức tạp thông qua nhiều thay đổi trạng thái.

Bạn có thể đặt chúng ở chế độ riêng tư, với các phương thức getter được bảo vệ, nhưng nếu các biến chỉ được sử dụng bên ngoài trong quá trình kiểm tra JUnit, tôi không chắc đó là giải pháp tốt hơn.


-1

Vâng, tôi đồng ý rằng điều này hơi kỳ lạ khi (như tôi nhớ lại) cuốn sách cũng thảo luận về tất cả các biến không riêng tư là điều cần tránh.

Tôi nghĩ vấn đề với công cụ sửa đổi được bảo vệ là không chỉ thành viên tiếp xúc với các lớp con mà nó còn được hiển thị cho toàn bộ gói. Tôi nghĩ rằng đây là khía cạnh kép mà chú Bob đang ngoại lệ ở đây. Tất nhiên, người ta không thể luôn luôn có được các phương thức được bảo vệ, và do đó trình độ biến được bảo vệ.

Tôi nghĩ rằng một cách tiếp cận thú vị hơn là làm cho mọi thứ trở nên công khai, điều đó sẽ buộc bạn phải có các lớp nhỏ, từ đó làm cho toàn bộ số liệu khoảng cách theo chiều dọc có phần hơi lộn xộn.

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.