Trình xây dựng thường không nên gọi các phương thức


12

Tôi đã mô tả cho một đồng nghiệp tại sao một nhà xây dựng gọi một phương thức có thể là một phản mẫu.

ví dụ (trong C ++ rỉ sét của tôi)

class C {
public :
    C(int foo);
    void setFoo(int foo);
private:
    int foo;
}

C::C(int foo) {
    setFoo(foo);
}

void C::setFoo(int foo) {
    this->foo = foo
}

Tôi muốn thúc đẩy tốt hơn thực tế này thông qua đóng góp bổ sung của bạn. Nếu bạn có ví dụ, tài liệu tham khảo sách, trang blog hoặc tên của các nguyên tắc, chúng sẽ rất được hoan nghênh.

Chỉnh sửa: Tôi đang nói chung, nhưng chúng tôi đang mã hóa bằng python.


Đây có phải là một quy tắc chung hoặc cụ thể cho các ngôn ngữ cụ thể?
ChrisF

Ngôn ngữ nào? Trong C ++, nó không chỉ là một mô hình chống: parashift.com/c++-faq-lite/strange-inherribution.html#faq-23.5
LennyProgrammer 17/211

@ Lenny222, OP nói về "phương thức lớp", mà - với tôi ít nhất - có nghĩa là phương thức phi thể hiện . Do đó không thể là ảo.
Péter Török

3
@Alb Trong Java thì hoàn toàn ổn. Những gì bạn không nên làm mặc dù rõ ràng chuyển thisđến bất kỳ phương thức nào bạn gọi từ hàm tạo.
biziclop

3
@Stefano Borini: Nếu bạn đang mã hóa bằng Python, tại sao không hiển thị ví dụ bằng Python thay vì C ++ rỉ sét? Ngoài ra, hãy giải thích tại sao đây là một điều xấu. Chúng tôi làm cả ngày.
S.Lott

Câu trả lời:


26

Bạn chưa chỉ định một ngôn ngữ.

Trong C ++, hàm tạo phải cẩn thận khi gọi hàm ảo, trong đó hàm thực tế mà nó đang gọi là lớp thực hiện. Nếu nó là một phương thức ảo thuần túy mà không có triển khai, đây sẽ là một vi phạm truy cập.

Một constructor có thể gọi các hàm không ảo.

Nếu ngôn ngữ của bạn là Java nơi các hàm nói chung là ảo theo mặc định thì có nghĩa là bạn phải hết sức cẩn thận.

C # dường như xử lý tình huống theo cách bạn mong đợi: bạn có thể gọi các phương thức ảo trong các hàm tạo và nó gọi phiên bản cuối cùng nhất. Vì vậy, trong C # không phải là một mô hình chống.

Một lý do phổ biến cho việc gọi các phương thức từ các hàm tạo là bạn có nhiều hàm tạo muốn gọi một phương thức "init" chung.

Lưu ý rằng các hàm hủy sẽ có cùng một vấn đề với các phương thức ảo, do đó bạn không thể có một phương thức "dọn dẹp" ảo nằm bên ngoài hàm hủy của bạn và hy vọng nó sẽ được gọi bởi hàm hủy của lớp cơ sở.

Java và C # không có hàm hủy, chúng có bộ hoàn thiện. Tôi không biết hành vi với Java.

C # xuất hiện để xử lý dọn dẹp chính xác về vấn đề này.

(Lưu ý rằng mặc dù Java và C # có bộ sưu tập rác, nhưng chỉ quản lý cấp phát bộ nhớ. Có một cách dọn dẹp khác mà hàm hủy của bạn cần thực hiện đó là không giải phóng bộ nhớ).


13
Có một vài lỗi nhỏ ở đây. Các phương thức trong C # không phải là ảo theo mặc định. C # có ngữ nghĩa khác với C ++ khi gọi một phương thức ảo trong hàm tạo; phương thức ảo trên loại dẫn xuất nhất sẽ được gọi, không phải phương thức ảo trên phần của loại hiện đang được xây dựng. C # không gọi các phương thức hoàn thiện của nó là "kẻ hủy diệt" nhưng bạn đã đúng rằng chúng có ngữ nghĩa của các công cụ hoàn thiện. Các phương thức ảo được gọi trong hàm hủy C # hoạt động giống như cách chúng thực hiện trong các hàm tạo; phương pháp dẫn xuất nhất được gọi.
Eric Lippert

@ Péter: Tôi dự định phương pháp ví dụ. xin lỗi vì sự nhầm lẫn.
Stefano Borini

1
@Eric Lippert. Cảm ơn bạn đã có chuyên môn về C #, tôi đã chỉnh sửa câu trả lời của mình cho phù hợp. Tôi không biết gì về ngôn ngữ đó, tôi biết rất rõ về C ++ và Java kém hơn.
CashCow

5
Không có gì. Lưu ý rằng việc gọi một phương thức ảo trong hàm tạo của lớp cơ sở trong C # vẫn là một ý tưởng khá tệ.
Eric Lippert

Nếu bạn gọi một phương thức (ảo) trong Java từ một hàm tạo, nó sẽ luôn gọi ra ghi đè có nguồn gốc nhất. Tuy nhiên, những gì bạn gọi là theo cách bạn mong đợi, đó là những gì tôi gọi là khó hiểu. Bởi vì trong khi Java gọi ra ghi đè có nguồn gốc nhất, phương thức đó sẽ chỉ thấy các trình khởi tạo được xử lý chứ không phải là hàm tạo của lớp chạy riêng của nó. Gọi một phương thức trên một lớp không có bất biến được thiết lập nhưng có thể nguy hiểm. Vì vậy, tôi nghĩ rằng C ++ đã đưa ra lựa chọn tốt hơn ở đây.
5gon12eder

18

OK, bây giờ mà sự nhầm lẫn về phương pháp lớp vs phương pháp dụ được làm sáng tỏ, tôi có thể đưa ra một câu trả lời :-)

Vấn đề không nằm ở việc gọi các phương thức cá thể nói chung từ một hàm tạo; đó là với việc gọi các phương thức ảo (trực tiếp hoặc gián tiếp). Và lý do chính là trong khi bên trong hàm tạo, đối tượng chưa được xây dựng đầy đủ . Và đặc biệt là các phần lớp con của nó hoàn toàn không được xây dựng trong khi hàm tạo của lớp cơ sở đang thực thi. Vì vậy, trạng thái bên trong của nó không nhất quán theo cách phụ thuộc ngôn ngữ và điều này có thể gây ra các lỗi tinh vi khác nhau trong các ngôn ngữ khác nhau.

C ++ và C # đã được thảo luận bởi những người khác. Trong Java, phương thức ảo của loại dẫn xuất nhất sẽ được gọi, tuy nhiên kiểu đó chưa được khởi tạo. Vì vậy, nếu phương thức đó đang sử dụng bất kỳ trường nào từ loại dẫn xuất, các trường đó có thể chưa được khởi tạo đúng vào thời điểm đó. Vấn đề này được thảo luận chi tiết trong Effecive Java 2nd Edition , Mục 17: Thiết kế và tài liệu để kế thừa hoặc nếu không thì cấm nó .

Lưu ý rằng đây là trường hợp đặc biệt của vấn đề chung về xuất bản tài liệu tham khảo đối tượng sớm . Các phương thức sơ thẩm có một thistham số ngầm định , nhưng chuyển thisrõ ràng đến một phương thức có thể gây ra các vấn đề tương tự. Đặc biệt là trong các chương trình đồng thời trong đó nếu tham chiếu đối tượng được xuất bản sớm sang một luồng khác, thì luồng đó có thể gọi các phương thức trên nó trước khi hàm tạo trong luồng đầu tiên kết thúc.


3
(+1) "trong khi bên trong hàm tạo, đối tượng chưa được xây dựng đầy đủ." Tương tự như "phương thức lớp vs thể hiện". Một số ngôn ngữ lập trình xem xét nó được xây dựng khi vào bộ điều khiển, như thể người lập trình nơi gán giá trị cho hàm tạo.
umlcat

7

Tôi sẽ không coi các cuộc gọi phương thức ở đây là một antipotype trong chính nó, hơn nữa là một mùi mã. Nếu một lớp cung cấp một resetphương thức, nó trả về một đối tượng về trạng thái ban đầu, thì việc gọi reset()hàm tạo là DRY. (Tôi không đưa ra bất kỳ tuyên bố nào về các phương thức thiết lập lại).

Đây là một bài viết có thể giúp đáp ứng sự hấp dẫn của bạn đối với chính quyền: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

Nó không thực sự là về các phương thức gọi, mà là về các hàm tạo quá nhiều. IMHO, các phương thức gọi trong hàm tạo là một mùi có thể chỉ ra rằng hàm tạo quá nặng.

Điều này có liên quan đến việc kiểm tra mã của bạn dễ dàng như thế nào. Lý do bao gồm:

  1. Thử nghiệm đơn vị liên quan đến rất nhiều sáng tạo và phá hủy - do đó việc xây dựng phải nhanh chóng.

  2. Tùy thuộc vào những gì các phương thức đó làm, có thể gây khó khăn cho việc kiểm tra các đơn vị mã rời rạc mà không dựa vào một số điều kiện tiên quyết (có thể không thể kiểm chứng) được thiết lập trong hàm tạo (ví dụ: lấy thông tin từ mạng).


3

Về mặt triết học, mục đích của nhà xây dựng là biến một đoạn bộ nhớ thô thành một thể hiện. Trong khi hàm tạo đang được thực thi, đối tượng chưa tồn tại, do đó gọi các phương thức của nó là một ý tưởng tồi. Rốt cuộc, bạn có thể không biết những gì họ làm trong nội bộ và họ có thể coi đối tượng đó ít nhất là tồn tại (duh!) Khi họ được gọi.

Về mặt kỹ thuật, có thể không có gì sai với điều đó, trong C ++ và đặc biệt là Python, bạn phải cẩn thận.

Thực tế, bạn nên giới hạn các cuộc gọi chỉ các phương thức như vậy để khởi tạo các thành viên lớp.


2

Đây không phải là một vấn đề mục đích chung. Đó là một vấn đề trong C ++, cụ thể là khi sử dụng kế thừa và phương thức ảo, vì việc xây dựng đối tượng xảy ra ngược và (các) con trỏ vtable được đặt lại với mỗi lớp của hàm tạo trong hệ thống phân cấp thừa kế, vì vậy nếu bạn gọi một phương thức ảo, bạn có thể không cuối cùng nhận được một cái thực sự tương ứng với lớp bạn đang cố gắng tạo ra, nó đánh bại toàn bộ mục đích sử dụng các phương thức ảo.

Trong các ngôn ngữ có hỗ trợ OOP lành mạnh, đặt con trỏ vtable chính xác ngay từ đầu, vấn đề này không tồn tại.


2

Có hai vấn đề với việc gọi một phương thức:

  • gọi một phương thức ảo, có thể làm điều gì đó bất ngờ (C ++) hoặc sử dụng các phần của các đối tượng chưa được khởi tạo
  • gọi một phương thức công khai (nên thực thi các bất biến lớp), vì đối tượng chưa nhất thiết phải hoàn thành (và do đó, bất biến của nó có thể không giữ được)

Không có gì sai khi gọi một hàm trợ giúp, miễn là nó không thuộc hai trường hợp trước.


1

Tôi không mua cái này. Trong một hệ thống hướng đối tượng, gọi một phương thức là điều duy nhất bạn có thể làm. Trên thực tế, đó ít nhiều là định nghĩa của "hướng đối tượng". Vì vậy, nếu một nhà xây dựng không thể gọi bất kỳ phương thức nào, thì nó có thể làm gì?


Khởi tạo đối tượng.
Stefano Borini

@Stefano Borini: Thế nào? Trong một hệ thống hướng đối tượng, điều duy nhất bạn có thể làm là gọi các phương thức. Hoặc để nhìn nó từ góc độ ngược lại: mọi thứ đều được thực hiện bằng cách gọi các phương thức. Và "bất cứ điều gì" rõ ràng bao gồm khởi tạo đối tượng. Vì vậy, nếu, để khởi tạo đối tượng, bạn cần gọi các phương thức, nhưng các hàm tạo không thể gọi các phương thức, thì làm thế nào một hàm tạo có thể khởi tạo đối tượng?
Jörg W Mittag

Hoàn toàn không đúng khi điều duy nhất bạn có thể làm là gọi các phương thức. Bạn chỉ có thể khởi tạo trạng thái mà không có bất kỳ cuộc gọi nào, trực tiếp đến bên trong đối tượng của bạn ... Điểm của nhà xây dựng là làm cho một đối tượng ở trạng thái nhất quán. Nếu bạn gọi các phương thức khác, chúng có thể gặp khó khăn khi xử lý một đối tượng ở trạng thái một phần, trừ khi chúng là các phương thức được tạo riêng để gọi từ hàm tạo (thường là phương thức của trình trợ giúp)
Stefano Borini 17/211

@Stefano Borini: "Bạn chỉ có thể khởi tạo trạng thái mà không cần bất kỳ cuộc gọi nào, trực tiếp đến phần bên trong của đối tượng của bạn." Đáng buồn thay, khi điều đó liên quan đến một phương pháp, bạn sẽ làm gì? Sao chép và qua mã?
S.Lott

1
@ S.Lott: không, tôi gọi nó, nhưng tôi cố gắng giữ cho nó một chức năng mô-đun thay vì một phương thức đối tượng và để nó cung cấp dữ liệu trả về mà tôi có thể đưa vào trạng thái đối tượng trong hàm tạo. Nếu tôi thực sự phải có một phương thức đối tượng, tôi sẽ đặt nó ở chế độ riêng tư và làm rõ rằng đó là để khởi tạo, chẳng hạn như đặt cho nó một tên thích hợp. Tuy nhiên, tôi sẽ không bao giờ gọi một phương thức công khai để đặt trạng thái đối tượng từ hàm tạo.
Stefano Borini

0

Trong lý thuyết OOP, điều đó không quan trọng, nhưng trong thực tế, mỗi ngôn ngữ lập trình OOP xử lý các hàm tạo khác nhau . Tôi không sử dụng phương pháp tĩnh rất thường xuyên.

Trong C ++ & Delphi, nếu tôi phải đưa ra các giá trị ban đầu cho một số thuộc tính ("thành viên trường") và mã được mở rộng, tôi thêm một số phương thức phụ làm phần mở rộng của các hàm tạo.

Và đừng gọi các phương pháp khác làm những việc phức tạp hơn.

Đối với các phương thức "getters" & "setters", tôi thường sử dụng các biến riêng tư / được bảo vệ để lưu trữ trạng thái của chúng, cộng với các phương thức "getters" & "setters".

Trong hàm tạo tôi gán các giá trị "mặc định" cho các trường trạng thái thuộc tính, KHÔNG gọi "bộ truy cập".

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.