ตัวสร้างโดยทั่วไปไม่ควรเรียกวิธีการ


12

ฉันอธิบายให้เพื่อนร่วมงานฟังว่าทำไมคอนสตรัคเตอร์ที่เรียกใช้เมธอดสามารถเป็น antipattern ได้

ตัวอย่าง (ใน C ++ ที่เป็นสนิมของฉัน)

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
}

ฉันต้องการที่จะกระตุ้นให้ดีขึ้นความจริงนี้ผ่านส่วนเสริมของคุณ หากคุณมีตัวอย่างหนังสืออ้างอิงหน้าบล็อกหรือชื่อของหลักการพวกเขาจะยินดีมาก

แก้ไข: ฉันกำลังพูดคุยกันโดยทั่วไป แต่เรากำลังเขียนโค้ดด้วยไพ ธ อน


นี่เป็นกฎทั่วไปหรือเฉพาะเจาะจงกับภาษาใดภาษาหนึ่งหรือไม่?
ChrisF

ภาษาไหน? ใน C ++ มันเป็นมากกว่ารูปแบบการต่อต้าน: parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5
LennyProgrammers

@ Lenny222, การเจรจา OP เกี่ยวกับ "วิธีการเรียน" ซึ่ง - วิธีการ - เพื่อฉันอย่างน้อยวิธีการที่ไม่ใช่อินสแตนซ์ ซึ่งไม่สามารถเป็นเสมือนจริงได้
PéterTörök

3
@Alb ใน Java มันก็โอเคอย่างสมบูรณ์แบบ สิ่งที่คุณไม่ควรทำคือส่งผ่านthisไปยังวิธีการใด ๆ ที่คุณโทรจากนวกรรมิกอย่างชัดเจน
biziclop

3
@Stefano Borini: ถ้าคุณกำลังเขียนโปรแกรมใน Python ทำไมไม่แสดงตัวอย่างใน Python แทนที่จะเป็นสนิม C ++? โปรดอธิบายด้วยว่าทำไมสิ่งนี้จึงเป็นสิ่งที่ไม่ดี เราทำมันตลอดเวลา
S.Lott

คำตอบ:


26

คุณยังไม่ได้ระบุภาษา

ใน C ++ คอนสตรัคเตอร์ต้องระวังเมื่อเรียกใช้ฟังก์ชันเสมือนซึ่งฟังก์ชันจริงที่มันกำลังเรียกใช้คือการประยุกต์ใช้คลาส หากเป็นวิธีเสมือนจริงที่ไม่มีการดำเนินการนี่จะเป็นการละเมิดการเข้าถึง

ตัวสร้างอาจเรียกฟังก์ชันที่ไม่ใช่เสมือน

หากภาษาของคุณคือ Java ซึ่งโดยทั่วไปแล้วฟังก์ชั่นเสมือนโดยปกติคุณจะต้องระมัดระวังเป็นพิเศษ

C # ดูเหมือนว่าจะจัดการกับสถานการณ์ในแบบที่คุณคาดหวัง: คุณสามารถเรียกใช้วิธีการเสมือนในตัวสร้างและมันเรียกรุ่นสุดท้ายที่สุด ดังนั้นใน C # ไม่ใช่รูปแบบการต่อต้าน

เหตุผลทั่วไปสำหรับวิธีการโทรจากตัวสร้างคือคุณมีตัวสร้างหลายตัวที่ต้องการเรียกวิธีการ "init" ทั่วไป

โปรดทราบว่า destructors จะมีปัญหาเดียวกันกับวิธีเสมือนดังนั้นคุณไม่สามารถมีวิธี "การล้างข้อมูล" เสมือนที่อยู่นอกตัวทำลายและคาดว่าจะได้รับการเรียกโดย destructor ระดับฐาน

Java และ C # ไม่มี destructors พวกเขามี finalizers ฉันไม่รู้พฤติกรรมกับ Java

C # ปรากฏขึ้นเพื่อจัดการการล้างอย่างถูกต้องในเรื่องนี้

(โปรดทราบว่าแม้ว่า Java และ C # จะมีการรวบรวมขยะ แต่จัดการการจัดสรรหน่วยความจำเท่านั้นมีการล้างข้อมูลอื่น ๆ ที่ destructor ของคุณต้องทำโดยไม่ปล่อยหน่วยความจำ)


13
มีข้อผิดพลาดเล็กน้อยที่นี่ วิธีการใน C # ไม่ใช่เสมือนโดยค่าเริ่มต้น C # มีซีแมนทิกส์ที่แตกต่างจาก C ++ เมื่อเรียกเมธอดเสมือนในตัวสร้าง วิธีเสมือนในประเภทที่ได้รับมาส่วนใหญ่จะถูกเรียกไม่ใช่วิธีเสมือนในส่วนของประเภทที่กำลังสร้างอยู่ในปัจจุบัน C # จะเรียกวิธีการปิดท้ายของมันว่า "destructors" แต่คุณถูกต้องที่พวกเขามีความหมายของ finalizers วิธีการเสมือนที่เรียกใน C # destructors ทำงานเช่นเดียวกับที่ทำในตัวสร้าง วิธีการที่ได้รับมากที่สุดเรียกว่า
Eric Lippert

@ ปีเตอร์: ฉันตั้งใจวิธีการอินสแตนซ์ ขอโทษสำหรับความสับสน.
Stefano Borini

1
@Eric Lippert ขอบคุณสำหรับความเชี่ยวชาญของคุณใน C # ฉันได้แก้ไขคำตอบของฉันแล้ว ฉันไม่รู้ภาษานั้นฉันรู้ว่า C ++ ดีมากและ Java ไม่ค่อยดี
CashCow

5
ไม่เป็นไร โปรดทราบว่าการเรียกใช้วิธีเสมือนในตัวสร้างคลาสพื้นฐานใน C # ยังคงเป็นความคิดที่ไม่ดี
Eric Lippert

หากคุณเรียกใช้เมธอด (เสมือน) ใน Java จากนวกรรมิกมันจะเรียกใช้การแทนที่ที่ได้รับมากที่สุด อย่างไรก็ตามสิ่งที่คุณเรียกว่า "แบบที่คุณคาดหวัง" มันเป็นสิ่งที่ฉันเรียกว่าสับสน เนื่องจากในขณะที่ Java เรียกใช้การแทนที่ที่ได้รับมากที่สุดวิธีการนั้นจะเห็นเฉพาะ initializers ที่ถูกประมวลผล แต่ไม่ใช่ตัวสร้างของการรันคลาสของตัวเอง การเรียกใช้วิธีการในชั้นเรียนที่ไม่ได้มีการกำหนดค่าคงที่อาจเป็นอันตรายได้ ดังนั้นฉันคิดว่า C ++ เป็นตัวเลือกที่ดีกว่าที่นี่
5gon12eder

18

ตกลงตอนนี้ความสับสนเกี่ยวกับวิธีการเรียนเทียบกับวิธีการอินสแตนซ์ล้างฉันสามารถให้คำตอบ :-)

ปัญหาไม่ได้อยู่ที่วิธีการเรียกใช้อินสแตนซ์โดยทั่วไปจากตัวสร้าง มันเป็นกับวิธีการโทรเสมือน (โดยตรงหรือโดยอ้อม) และเหตุผลหลักก็คือว่าในขณะที่ภายในตัวสร้างวัตถุที่ยังสร้างไม่เสร็จสมบูรณ์ และโดยเฉพาะอย่างยิ่งส่วนย่อยของมันไม่ได้ถูกสร้างเลยในขณะที่ตัวสร้างคลาสพื้นฐานกำลังดำเนินการอยู่ ดังนั้นสถานะภายในของมันจึงไม่สอดคล้องกันในวิธีที่ขึ้นอยู่กับภาษาและสิ่งนี้อาจทำให้เกิดข้อบกพร่องที่ละเอียดอ่อนในภาษาต่างๆ

C ++ และ C # ถูกกล่าวถึงโดยบุคคลอื่นแล้ว ใน Java เมธอดเสมือนของชนิดที่ได้รับมาส่วนใหญ่จะถูกเรียกอย่างไรก็ตามชนิดนั้นยังไม่ได้เริ่มต้น ดังนั้นหากวิธีนั้นใช้เขตข้อมูลจากชนิดที่ได้รับเขตข้อมูลเหล่านั้นอาจยังไม่สามารถเริ่มต้นได้อย่างถูกต้อง ณ เวลานั้น ปัญหานี้จะกล่าวถึงในรายละเอียดในeffecive Java ฉบับที่ 2รายการที่ 17: การออกแบบและเอกสารประกอบการรับมรดกหรืออื่น ๆ ที่ห้ามมัน

ทราบว่าเป็นกรณีพิเศษของปัญหาทั่วไปของการนี้เผยแพร่อ้างอิงวัตถุก่อนเวลาอันควร วิธีการอินสแตนซ์มีthisพารามิเตอร์โดยนัยแต่การส่งผ่านthisไปยังวิธีการอย่างชัดเจนอาจทำให้เกิดปัญหาที่คล้ายกัน โดยเฉพาะอย่างยิ่งในโปรแกรมที่เกิดขึ้นพร้อมกันหากการอ้างอิงวัตถุถูกเผยแพร่ก่อนกำหนดไปยังเธรดอื่นเธรดนั้นสามารถเรียกเมธอดนั้นก่อนที่ตัวสร้างในเธรดแรกจะเสร็จสิ้น


3
(+1) "ขณะที่อยู่ภายในตัวสร้างวัตถุนั้นยังไม่ได้ถูกสร้างอย่างสมบูรณ์" เช่นเดียวกับ "class methods vs instance" ภาษาท้องถิ่นบางโปรแกรมคิดว่ามันสร้างขึ้นเมื่อเข้าสู่สิ่งก่อสร้างเหมือนกับว่าโปรแกรมเมอร์ที่กำหนดค่าให้กับตัวสร้าง
umlcat

7

ฉันไม่คิดว่าวิธีการเรียกที่นี่จะเป็นปฏิปักษ์ในตัวเองมากขึ้นกลิ่นรหัส ถ้าคลาสส่งresetเมธอดที่ส่งคืนออบเจ็กต์ให้เป็นสถานะดั้งเดิมการเรียกใช้reset()ใน constructor คือ DRY (ฉันไม่ได้ทำคำสั่งใด ๆ เกี่ยวกับวิธีการรีเซ็ต)

นี่คือบทความที่อาจช่วยตอบสนองการอุทธรณ์ของคุณสำหรับผู้มีอำนาจ: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

มันไม่ได้เกี่ยวกับวิธีการโทร แต่เกี่ยวกับตัวสร้างที่ทำมากเกินไป IMHO วิธีการเรียกใช้ในคอนสตรัคเตอร์เป็นกลิ่นที่อาจบ่งบอกว่าคอนสตรัคเตอร์หนักเกินไป

สิ่งนี้เกี่ยวข้องกับความง่ายในการทดสอบรหัสของคุณ เหตุผลประกอบด้วย:

  1. การทดสอบหน่วยเกี่ยวข้องกับการสร้างและการทำลายมากมายดังนั้นการก่อสร้างจึงควรรวดเร็ว

  2. ขึ้นอยู่กับวิธีการเหล่านั้นทำมันอาจทำให้ยากที่จะทดสอบหน่วยของรหัสโดยไม่ต้องอาศัยเงื่อนไข (อาจไม่สามารถทดสอบได้) ในการสร้าง (เช่นรับข้อมูลจากเครือข่าย)


3

ในทางปรัชญาวัตถุประสงค์ของคอนสตรัคเตอร์คือการเปลี่ยนหน่วยความจำดิบให้เป็นตัวอย่าง ในขณะที่ตัวสร้างกำลังดำเนินการวัตถุยังไม่มีอยู่ดังนั้นการเรียกวิธีการของมันจึงเป็นความคิดที่ไม่ดี คุณอาจไม่ทราบว่าพวกเขาทำอะไรภายในและพวกเขาอาจพิจารณาวัตถุอย่างถูกต้องอย่างน้อยก็มีอยู่ (duh!) เมื่อพวกเขาถูกเรียก

ในทางเทคนิคอาจไม่มีอะไรผิดปกติใน C ++ และโดยเฉพาะอย่างยิ่งใน Python คุณต้องระวัง

ในทางปฏิบัติคุณควร จำกัด การโทรเฉพาะวิธีการดังกล่าวที่เริ่มต้นสมาชิกคลาส


2

มันไม่ใช่ปัญหาทั่วไป เป็นปัญหาใน C ++ โดยเฉพาะเมื่อใช้การสืบทอดและวิธีเสมือนเนื่องจากการสร้างวัตถุเกิดขึ้นย้อนหลังและตัวชี้ vtable ได้รับการรีเซ็ตด้วยเลเยอร์คอนสตรัคเตอร์แต่ละเลเยอร์ในลำดับชั้นการสืบทอดดังนั้นหากคุณกำลังเรียกใช้วิธีเสมือน ท้ายสุดการรับอันที่สอดคล้องกับชั้นเรียนที่คุณพยายามสร้างซึ่งเอาชนะจุดประสงค์ทั้งหมดของการใช้วิธีการเสมือนจริง

ในภาษาที่รองรับ OOP แบบมีสติที่ตั้งค่าตัวชี้ vtable อย่างถูกต้องตั้งแต่เริ่มต้นปัญหานี้ไม่มีอยู่


2

มีปัญหาสองข้อในการเรียกใช้เมธอด:

  • เรียกวิธีเสมือนซึ่งสามารถทำสิ่งที่ไม่คาดคิด (C ++) หรือใช้บางส่วนของวัตถุที่ยังไม่ได้เริ่มต้น
  • เรียกวิธีการสาธารณะ (ซึ่งควรบังคับใช้ค่าคงที่ระดับ) เนื่องจากวัตถุยังไม่เสร็จสมบูรณ์ (และอาจไม่คงที่)

ไม่มีอะไรผิดปกติในการเรียกใช้ฟังก์ชันผู้ช่วยตราบใดที่มันไม่ได้อยู่ในสองกรณีก่อนหน้านี้


1

ฉันไม่ซื้อสิ่งนี้ ในระบบเชิงวัตถุการเรียกใช้เมธอดนั้นเป็นสิ่งเดียวที่คุณทำได้ ในความเป็นจริงนั่นเป็นคำจำกัดความของ "เชิงวัตถุ" มากขึ้นหรือน้อยลง ดังนั้นถ้านวกรรมิกไม่สามารถเรียกวิธีการใด ๆ แล้วสิ่งที่สามารถได้ทำอย่างไร


เริ่มต้นวัตถุ
Stefano Borini

@Stefano Borini: อย่างไร ในระบบเชิงวัตถุสิ่งเดียวที่คุณทำได้คือวิธีการโทร หรือมองจากมุมตรงข้าม: ทำทุกอย่างด้วยวิธีการโทร และ "อะไรก็ได้" อย่างชัดเจนรวมถึงการเริ่มต้นวัตถุ ดังนั้นหากในการเริ่มต้นวัตถุคุณจำเป็นต้องเรียกใช้วิธีการ แต่ตัวสร้างไม่สามารถเรียกวิธีการได้ดังนั้นตัวสร้างจะเริ่มต้นวัตถุได้อย่างไร
Jörg W Mittag

ไม่จริงอย่างแน่นอนสิ่งเดียวที่คุณทำได้คือโทรหาวิธี คุณสามารถเริ่มต้นสถานะโดยไม่มีการเรียกใด ๆ โดยตรงไปยัง internals ของวัตถุของคุณ ... จุดของตัวสร้างคือการทำให้วัตถุอยู่ในสถานะที่สอดคล้องกัน ถ้าคุณเรียกใช้วิธีการอื่น ๆ เหล่านี้อาจมีปัญหาการจัดการวัตถุในรัฐบางส่วนจนกว่าพวกเขาจะใช้วิธีการเฉพาะทำที่จะเรียกจากคอนสตรัค (มักจะเป็นวิธีการช่วยเหลือ)
สเตฟาโนโอบอรินี่

@Stefano Borini: "คุณสามารถเริ่มต้นสถานะโดยไม่ต้องโทรใด ๆ โดยตรงไปยัง internals ของวัตถุของคุณ" น่าเศร้าเมื่อมันเกี่ยวข้องกับวิธีการคุณจะทำอย่างไร? คัดลอกและวางรหัสหรือไม่
S.Lott

1
@ S.Lott: ไม่ฉันเรียกมันว่า แต่ฉันพยายามที่จะให้มันเป็นฟังก์ชั่นโมดูลแทนวิธีการของวัตถุและให้มันคืนข้อมูลที่ฉันสามารถใส่ลงในสถานะวัตถุในตัวสร้าง ถ้าฉันต้องมีวิธีการของวัตถุจริงๆฉันจะทำให้เป็นส่วนตัวและชี้แจงว่าเป็นวิธีการเริ่มต้นเช่นให้ชื่อที่เหมาะสม อย่างไรก็ตามฉันจะไม่เรียกวิธีการสาธารณะเพื่อตั้งสถานะวัตถุจากตัวสร้าง
Stefano Borini

0

ในทางทฤษฎี OOP ก็ควรได้เรื่อง แต่ในทางปฏิบัติแต่ละ OOP การเขียนโปรแกรมภาษาที่แตกต่างกันจับก่อสร้าง ฉันไม่ได้ใช้วิธีการคงที่บ่อยมาก

ใน C ++ & Delphi ถ้าฉันต้องกำหนดค่าเริ่มต้นให้กับคุณสมบัติบางอย่าง ("สมาชิกของฟิลด์") และโค้ดนั้นถูกขยายออกไปมากฉันจะเพิ่มวิธีการที่สองเป็นส่วนเสริมของตัวสร้าง

และอย่าเรียกวิธีการอื่นที่ทำสิ่งที่ซับซ้อนกว่านี้

สำหรับวิธีการ "getters" & "setters" คุณสมบัติฉันมักจะใช้ตัวแปรส่วนตัว / ที่มีการป้องกันเพื่อเก็บสถานะของพวกเขารวมทั้งวิธีการ "getters" & "setters"

ในตัวสร้างฉันกำหนดค่า "เริ่มต้น" ให้กับเขตข้อมูลคุณสมบัติของรัฐโดยไม่เรียก "accessors"

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.