วิธีการเตือนโปรแกรมเมอร์อื่น ๆ ของการใช้งานในชั้นเรียน


96

ฉันกำลังเขียนชั้นเรียนที่ "ต้องใช้ในทางที่เฉพาะเจาะจง" (ฉันเดาว่าชั้นเรียนทั้งหมดต้อง ... )

ตัวอย่างเช่นผมสร้างfooManagerชั้นซึ่งต้องมีInitialize(string,string)การเรียกร้องให้พูดให้ และเพื่อผลักดันตัวอย่างต่อไปอีกเล็กน้อยชั้นเรียนจะไร้ประโยชน์หากเราไม่ฟังการThisHappenedกระทำของมัน

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

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

ฉันพบว่าตัวเองมีปัญหาด้วยวิธีการปัจจุบันที่ฉันมีที่นี่ซึ่งต่อไปนี้:

เพิ่มค่าบูลีนส่วนตัวในชั้นเรียนและตรวจสอบทุกที่ที่จำเป็นหากมีการเริ่มต้นชั้นเรียน ถ้าไม่ฉันจะโยนข้อยกเว้นว่า "ชั้นไม่ได้เริ่มต้นคุณแน่ใจหรือว่าคุณโทร.Initialize(string,string)?"

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

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

สิ่งที่ฉันกำลังมองหาคือ

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

พูดง่ายๆก็คือฉันกำลังพยายามหาวิธีที่จะไม่ลืมที่จะโทรออกเมื่อมีการนำคลาสนั้นมาใช้ในภายหลังหรือโดยคนอื่น

การจำแนก:

เพื่อชี้แจงคำถามมากมายที่นี่:

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

  • ฉันใช้ภาษา C #! ฉันไม่ได้พูดถึงสิ่งนั้นได้อย่างไร! ฉันอยู่ในสภาพแวดล้อม Xamarin และการสร้างแอพมือถือมักจะใช้เวลาประมาณ 6 ถึง 9 โครงการในการแก้ปัญหารวมถึงโครงการ PCL, iOS, Android และ Windows ฉันเป็นนักพัฒนามาประมาณหนึ่งปีครึ่ง (รวมโรงเรียนและที่ทำงาน) ดังนั้นบางครั้งข้อความ / คำถามที่ไร้สาระของฉัน สิ่งที่อาจไม่เกี่ยวข้องในที่นี้ แต่ข้อมูลที่มากเกินไปไม่ได้เป็นเรื่องเลวร้ายเสมอไป

  • ฉันไม่สามารถวางทุกอย่างที่จำเป็นใน Constructor ได้เสมอเนื่องจากข้อ จำกัด ของแพลตฟอร์มและการใช้ Dependency Injection การมีพารามิเตอร์อื่นนอกเหนือจากอินเตอร์เฟสจะปิดใช้งานตาราง หรือบางทีความรู้ของฉันไม่เพียงพอซึ่งเป็นไปได้สูง ส่วนใหญ่แล้วไม่ใช่ปัญหาการเริ่มต้น แต่มีมากกว่านั้น

ฉันจะแน่ใจได้อย่างไรว่าเขาลงทะเบียนกับกิจกรรมนั้น

ฉันจะแน่ใจได้อย่างไรว่าเขาไม่ลืมที่จะ "หยุดกระบวนการในบางจุด"

ที่นี่ฉันจำคลาสที่เรียกโฆษณาได้ ตราบใดที่มุมมองที่มองเห็นโฆษณาปรากฏให้เห็นชั้นจะดึงโฆษณาใหม่ทุกนาที คลาสนั้นต้องการมุมมองเมื่อสร้างที่ซึ่งสามารถแสดงโฆษณาซึ่งสามารถไปในพารามิเตอร์ได้อย่างชัดเจน แต่เมื่อมุมมองหายไปต้องเรียกใช้ StopFetching () มิฉะนั้นชั้นจะดึงโฆษณาสำหรับมุมมองที่ไม่ได้มีและยังไม่ดี

นอกจากนี้คลาสนั้นยังมีกิจกรรมที่ต้องรับฟังเช่น "AdClicked" เป็นต้น ทุกอย่างทำงานได้ดีถ้าไม่ฟัง แต่เราสูญเสียการติดตามการวิเคราะห์ที่นั่นหากไม่ได้ลงทะเบียนก๊อก โฆษณายังคงทำงานอยู่ดังนั้นผู้ใช้และนักพัฒนาจะไม่เห็นความแตกต่างและการวิเคราะห์จะมีข้อมูลที่ไม่ถูกต้อง จำเป็นต้องหลีกเลี่ยง แต่ฉันไม่แน่ใจว่านักพัฒนาจะรู้ได้อย่างไรว่าพวกเขาจะต้องลงทะเบียนกับกิจกรรมเต่า นั่นเป็นตัวอย่างที่เรียบง่าย แต่มีแนวคิดอยู่ที่นั่น "ให้แน่ใจว่าเขาใช้การกระทำสาธารณะที่มีให้" และในเวลาที่ถูกต้องแน่นอน!


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


5
คำตอบสำหรับคำถามอื่นนั้นไม่เกี่ยวข้องคำถามนั้นไม่เหมือนกันดังนั้นจึงเป็นคำถามที่แตกต่างกัน หากใครบางคนมองหาการสนทนาเขาจะไม่พิมพ์ชื่อของคำถามอื่น
Gil Sand

6
การป้องกันไม่ให้รวมเนื้อหาของการเริ่มต้นใน ctor คืออะไร ต้องมีการเรียกinitializeสายหลังจากการสร้างวัตถุหรือไม่ ctor จะ "เสี่ยงเกินไป" ในแง่ที่มันสามารถโยนข้อยกเว้นและทำลายห่วงโซ่การสร้างได้หรือไม่?
เห็น

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

คำตอบ:


161

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

แต่ผู้โทรจำเป็นต้องสร้าง a FooManagerก่อนจึงจะสามารถกำหนดค่าเริ่มต้นได้เช่นเนื่องจากFooManagerมีการส่งต่อเป็นการพึ่งพา

อย่าสร้าง a FooManagerถ้าคุณไม่มี สิ่งที่คุณสามารถทำได้คือส่งผ่านวัตถุที่ช่วยให้คุณดึงข้อมูลที่สร้างขึ้นมาอย่างสมบูรณ์FooManagerแต่มีเพียงข้อมูลการเริ่มต้นเท่านั้น (ในการใช้งานฟังก์ชั่นการเขียนโปรแกรมฉันแนะนำให้คุณใช้แอพพลิเคชั่นบางส่วนสำหรับตัวสร้าง) เช่น:

ctorArgs = ...;
getFooManager = (initInfo) -> new FooManager(ctorArgs, initInfo);
...
getFooManager(myInitInfo).fooMethod();

ปัญหานี้ก็คือการที่คุณมีในการจัดหาข้อมูล init FooManagerทุกครั้งที่คุณเข้าถึง

หากจำเป็นในภาษาของคุณคุณสามารถปิดgetFooManager()การดำเนินการในชั้นเหมือนโรงงานหรือสร้างเหมือน

ฉันต้องการตรวจสอบรันไทม์จริง ๆ ว่าinitialize()วิธีการนั้นถูกเรียกแทนที่จะใช้โซลูชันระดับระบบ

เป็นไปได้ที่จะพบกับการประนีประนอม เราสร้างคลาส wrapper MaybeInitializedFooManagerที่มีget()เมธอดที่ส่งคืนค่าFooManagerแต่ส่งกลับถ้าFooManagerไม่ได้เริ่มต้นอย่างสมบูรณ์ ใช้งานได้ก็ต่อเมื่อการเตรียมใช้งานเสร็จสิ้นผ่าน wrapper หรือหากมี FooManager#isInitialized()วิธีการ

class MaybeInitializedFooManager {
  private final FooManager fooManager;

  public MaybeInitializedFooManager(CtorArgs ctorArgs) {
    fooManager = new FooManager(ctorArgs);
  }

  public FooManager initialize(InitArgs initArgs) {
    fooManager.initialize(initArgs);
    return fooManager;
  }

  public FooManager get() {
    if (fooManager.isInitialized()) return fooManager;
    throw ...;
  }
}

ฉันไม่ต้องการเปลี่ยน API ของชั้นเรียน

ในกรณีนี้คุณจะต้องหลีกเลี่ยงif (!initialized) throw;เงื่อนไขในแต่ละวิธี โชคดีที่มีรูปแบบง่าย ๆ ในการแก้ปัญหานี้

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

class FooManager {
  private CtorArgs ctorArgs;
  private Impl impl;

  public FooManager(CtorArgs ctorArgs) {
    this.ctorArgs = ctorArgs;
    this.impl = new UninitializedImpl();
  }

  public void initialize(InitArgs initArgs) {
    impl = new MainImpl(ctorArgs, initArgs);
  }

  public X foo() { return impl.foo(); }
  public Y bar() { return impl.bar(); }
}

interface Impl {
  X foo();
  Y bar();
}

class UninitializedImpl implements Impl {
  public X foo() { throw ...; }
  public Y bar() { throw ...; }
}

class MainImpl implements Impl {
  public MainImpl(CtorArgs c, InitArgs i);
  public X foo() { ... }
  public Y bar() { ... }
}

MainImplนี้แยกพฤติกรรมหลักของการเรียนลงใน


9
ผมชอบสองโซลูชั่นแรก (+1) แต่ผมไม่คิดว่าตัวอย่างข้อมูลที่ผ่านมาของคุณด้วยการทำซ้ำของวิธีการในFooManagerและเป็นที่ดีกว่าการทำซ้ำUninitialisedImpl if (!initialized) throw;
Bergi

3
@Bergi บางคนชอบเรียนมากเกินไป แม้ว่าจะFooManagerมีวิธีการมากมายแต่ก็อาจจะง่ายกว่าการลืมif (!initialized)เช็ค แต่ในกรณีนี้คุณควรเลิกเรียน
immibis

1
อาจจะเป็นอินเดียนแดงฟูไม่ได้ดูดีไปกว่าฉันที่เริ่มต้นฟูด้วย แต่ +1 สำหรับการให้ตัวเลือก / ไอเดีย
Matthew James Briggs

7
+1 โรงงานเป็นตัวเลือกที่ถูกต้อง คุณไม่สามารถปรับปรุงการออกแบบได้โดยไม่ต้องเปลี่ยนดังนั้น IMHO คำตอบสุดท้ายเกี่ยวกับการครึ่งว่ามันจะไม่ช่วย OP อย่างไร
นาธานคูเปอร์

6
@Bergi ฉันได้พบเทคนิคที่นำเสนอในส่วนสุดท้ายเพื่อเป็นประโยชน์อย่างมาก (ดู "การแทนที่เงื่อนไขด้วยความหลากหลาย" เทคนิคการสร้างโครงสร้างใหม่และรูปแบบของรัฐ) มันง่ายที่จะลืมการตรวจสอบในวิธีการหนึ่งถ้าเราใช้ if / else มันยากมากที่จะลืมการใช้วิธีการที่จำเป็นโดยอินเทอร์เฟซ สิ่งสำคัญที่สุดคือ MainImpl สอดคล้องกับวัตถุที่วิธีการเริ่มต้นถูกผสานกับตัวสร้าง ซึ่งหมายความว่าการใช้งาน MainImpl สามารถเพลิดเพลินไปกับการรับรองที่แข็งแกร่งกว่าซึ่งจะทำให้รหัสหลักง่ายขึ้น …
amon

79

วิธีที่มีประสิทธิภาพและเป็นประโยชน์มากที่สุดในการป้องกันไม่ให้ลูกค้า "ใช้ผิดวัตถุประสงค์" คือการทำให้เป็นไปไม่ได้

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

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

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

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

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

ตัวอย่างง่ายๆ: คุณมีFile-object ซึ่งอนุญาตให้คุณอ่านจากไฟล์ อย่างไรก็ตามลูกค้าจำเป็นต้องโทรOpenก่อนที่ReadLineวิธีการสามารถเรียกได้และต้องจำไว้เสมอที่จะโทรClose(แม้ว่าจะมีข้อยกเว้นเกิดขึ้น) หลังจากที่ReadLineวิธีการจะต้องไม่ถูกเรียกอีกต่อไป นี่คือการมีเพศสัมพันธ์ทางโลก มันสามารถหลีกเลี่ยงได้โดยมีวิธีการเดียวที่ใช้โทรกลับหรือผู้รับมอบสิทธิ์เป็นอาร์กิวเมนต์ วิธีการจัดการเปิดไฟล์โทรกลับและจากนั้นปิดไฟล์ มันสามารถผ่านส่วนต่อประสานที่แตกต่างด้วยReadวิธีการโทรกลับ ด้วยวิธีนี้มันเป็นไปไม่ได้สำหรับลูกค้าที่จะลืมวิธีการโทรในลำดับที่ถูกต้อง

การเชื่อมต่อกับการมีเพศสัมพันธ์ชั่วคราว:

class File {
     /// Must be called on a closed file.
     /// Remember to always call Close() when you are finished
     public void Open();

     /// must be called on on open file
     public string ReadLine();

     /// must be called on an open file
     public void Close();
}

โดยไม่ต้องมีเพศสัมพันธ์ชั่วคราว:

class File {
    /// Opens the file, executes the callback, and closes the file again.
    public void Consume(Action<IOpenFile> callback);
}

interface IOpenFile {
    string ReadLine();
}

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

abstract class File {
    /// Opens the file, executes ConsumeOpenFile(), and closes the file again.
    public void Consume();

    /// override this
    abstract protected ConsumeOpenFile();

    /// call this from your ConsumeOpenFile() implementation
    protected string ReadLine();
}

ข้อดีคือเหมือนกัน: ลูกค้าไม่จำเป็นต้องจำวิธีการโทรตามลำดับที่แน่นอน เป็นไปไม่ได้ที่จะเรียกวิธีการตามลำดับที่ผิด


2
นอกจากนี้ฉันยังจำกรณีที่มันเป็นไปไม่ได้ที่จะไปจากนวกรรมิก แต่ตอนนี้ฉันไม่มีตัวอย่างที่จะแสดง
Gil Sand

2
ตัวอย่างนี้อธิบายรูปแบบของโรงงาน - แต่ประโยคแรกอธิบายถึงกระบวนทัศน์ของRAII ดังนั้นคำตอบนี้ควรจะแนะนำข้อใด
Ext3h

11
@ Ext3h ฉันไม่คิดว่ารูปแบบของโรงงานและ RAII จะไม่เหมือนกัน
Pieter B

@PieterB ไม่พวกเขาไม่ใช่โรงงานอาจให้ความมั่นใจกับ RAII แต่เฉพาะกับความหมายเพิ่มเติมที่แม่แบบ / คอนสตรัคเตอร์ (เรียกว่าUnitializedFooManager) ต้องไม่ใช้สถานะร่วมกับอินสแตนซ์ ( InitializedFooManager) ตามที่Initialize(...)มีInstantiate(...)ความหมายจริง มิฉะนั้นตัวอย่างนี้นำไปสู่ปัญหาใหม่หากนักพัฒนาใช้แม่แบบเดียวกันสองครั้ง และนั่นเป็นไปไม่ได้ที่จะป้องกัน / ตรวจสอบแบบคงที่หากภาษาไม่สนับสนุนmoveความหมายอย่างชัดเจนเพื่อที่จะใช้แม่แบบได้อย่างน่าเชื่อถือ
Ext3h

2
@ Ext3h: ฉันขอแนะนำทางออกแรกเพราะมันง่ายที่สุด แต่ถ้าไคลเอนต์จำเป็นต้องสามารถเข้าถึงวัตถุก่อนที่จะเรียก Initialize คุณจะไม่สามารถใช้มันได้ แต่คุณอาจจะสามารถใช้วิธีที่สองได้
JacquesB

39

ปกติฉันจะตรวจสอบ intiialisation และโยน (พูด) IllegalStateExceptionถ้าคุณลองและใช้มันในขณะที่ไม่ได้เริ่มต้น

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

ComponentBuilder builder = new ComponentBuilder();
Component forClients = builder.initialise(); // the 'Component' is what you give your clients

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


1
คำตอบที่ดี - 2 ตัวเลือกที่ดีในคำตอบที่กระชับ คำตอบอื่น ๆ ข้างต้นยิมนาสติกประเภทระบบที่ใช้งานได้จริงในทางทฤษฎี แต่สำหรับกรณีส่วนใหญ่ตัวเลือกที่ง่ายกว่าสองตัวนี้เป็นตัวเลือกที่เหมาะสม
โทมัส W

1
หมายเหตุ: ใน .NET, InvalidOperationExceptionIllegalStateExceptionจะส่งเสริมโดยไมโครซอฟท์จะเป็นสิ่งที่คุณเรียกว่า
miroxlav

1
ฉันไม่ได้ลงคะแนนในคำตอบนี้ แต่ไม่ใช่คำตอบที่ดีจริงๆ เพียงแค่ขว้างIllegalStateExceptionหรือInvalidOperationExceptionออกจากสีน้ำเงินผู้ใช้จะไม่เข้าใจสิ่งที่เขาทำผิด ข้อยกเว้นเหล่านี้ไม่ควรหลีกเลี่ยงการแก้ไขข้อบกพร่องในการออกแบบ
displayName

คุณจะทราบว่าการโยนข้อยกเว้นเป็นหนึ่งในตัวเลือกที่เป็นไปได้และฉันจะอธิบายรายละเอียดเกี่ยวกับตัวเลือกที่ต้องการ - ทำให้การคอมไพล์เวลานี้ปลอดภัย ฉันได้แก้ไขคำตอบของฉันเพื่อเน้นสิ่งนี้
Brian Agnew

14

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

หากคุณมีFooManagerใน Haskell มันจะผิดกฎหมายที่จะอนุญาตให้ผู้ใช้ของคุณสร้างสิ่งที่ไม่สามารถจัดการFooได้เพราะระบบประเภททำให้มันง่ายมากและเป็นแบบแผนที่นักพัฒนา Haskell คาดไว้

ในทางตรงกันข้ามถ้าคุณเขียน C เพื่อนร่วมงานของคุณจะอยู่ภายใต้สิทธิ์ของพวกเขาที่จะพาคุณออกไปและยิงคุณเพื่อกำหนดแยกstruct FooManagerและstruct UninitializedFooManagerประเภทที่สนับสนุนการดำเนินงานที่แตกต่างกันเพราะมันจะนำไปสู่รหัสที่ซับซ้อนโดยไม่จำเป็น .

ถ้าคุณเขียน Python มันจะไม่มีกลไกอะไรในภาษาที่จะให้คุณทำสิ่งนี้ได้

คุณอาจไม่ได้เขียน Haskell, Python หรือ C แต่เป็นตัวอย่างที่แสดงให้เห็นว่าระบบประเภทนี้คาดว่าจะทำงานได้มากน้อยเพียงใดและสามารถทำอะไรได้บ้าง

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


ฉันสับสนเล็กน้อยว่า "ถ้าคุณเขียน Python มันไม่มีกลไกในภาษาที่จะให้คุณทำสิ่งนี้ได้" Python มีตัวสร้างและมันค่อนข้างเล็กน้อยที่จะสร้างวิธีการในโรงงาน ดังนั้นฉันอาจไม่เข้าใจความหมายของคำว่า "นี่"?
อัลฟลานาแกน

3
โดย "สิ่งนี้" ฉันหมายถึงข้อผิดพลาดในการคอมไพล์เวลาซึ่งต่างจากรันไทม์
Patrick Collins

เพียงแค่พูดถึง Python 3.5 ซึ่งเป็นเวอร์ชันปัจจุบันมีประเภทผ่านระบบประเภทที่เสียบได้ แน่นอนว่าเป็นไปได้ที่จะทำให้ Python เกิดข้อผิดพลาดก่อนที่จะรันโค้ดเพียงเพราะมันถูกอิมพอร์ต ถึง 3.5 มันถูกต้องทั้งหมดแม้ว่า
Benjamin Gruenbaum

โอวตกลง. +1 ล่าช้า สับสนแม้กระทั่งนี้เป็นที่ชาญฉลาดมาก
AL Flanagan

ฉันชอบสไตล์ของคุณแพทริค
Gil Sand

4

ในขณะที่คุณไม่ต้องการส่งรหัสตรวจสอบให้กับลูกค้า (แต่ดูเหมือนดีสำหรับโปรแกรมเมอร์) คุณสามารถใช้ฟังก์ชั่นassertถ้ามีอยู่ในภาษาโปรแกรมของคุณ

ด้วยวิธีการที่คุณมีการตรวจสอบในสภาพแวดล้อมการพัฒนา (และการทดสอบใด ๆ ที่ dev เพื่อนจะเรียกว่าจะล้มเหลวในการคาดเดา) แต่คุณจะไม่ส่งรหัสให้กับลูกค้าตามการยืนยัน (อย่างน้อยใน Java) จะถูกรวบรวมคัดเลือกเท่านั้น

ดังนั้นคลาส Java ที่ใช้สิ่งนี้จะมีลักษณะเช่นนี้:

/** For some reason, you have to call init and connect before the manager works. */
public class FooManager {
   private int state = 0;

   public FooManager () {
   }

   public synchronized void init() {
      assert state==0 : "you called init twice.";
      // do something
      state = 1;
   }

   public synchronized void connect() {
      assert state==1 : "you called connect before init, or connect twice.";
      // do something
      state = 2;
   }

   public void someWork() {
      assert state==2 : "You did not properly init FooManager. You need to call init and connect first.";
      // do the actual work.
   }
}

การยืนยันเป็นเครื่องมือที่ยอดเยี่ยมในการตรวจสอบสถานะรันไทม์ของโปรแกรมที่คุณต้องการ แต่อย่าคาดหวังให้ใครทำผิดในสภาพแวดล้อมจริง

พวกเขายังค่อนข้างผอมและไม่ใช้เวลามากถ้า () โยน ... ไวยากรณ์พวกเขาไม่จำเป็นต้องถูกจับ ฯลฯ


3

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

class SomeClass
{
   private int valueRequiredByMethodB;

   public IReadyForB a (int v) { valueRequiredByMethodB = v; return new ReadyForB(this); }

   public interface IReadyForB { public void b (); }

   private class ReadyForB : IReadyForB
   {
      SomeClass owner;
      private ReadyForB (SomeClass owner) { this.owner = owner; }
      public void b () { Console.WriteLine (owner.valueRequiredByMethodB); }
   }
}

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


2

เมื่อฉันใช้คลาสฐานที่ต้องการข้อมูลการเริ่มต้นพิเศษหรือลิงก์ไปยังวัตถุอื่นก่อนที่จะมีประโยชน์ฉันมักจะทำให้นามธรรมคลาสฐานนั้นจากนั้นกำหนดวิธีนามธรรมหลายอย่างในคลาสนั้นที่ใช้ในการไหลของชั้นฐาน (เช่นabstract Collection<Information> get_extra_information(args);และabstract Collection<OtherObjects> get_other_objects(args);ตามสัญญาการสืบทอดจะต้องดำเนินการโดยชั้นคอนกรีตบังคับให้ผู้ใช้ของชั้นฐานที่จะจัดหาทุกสิ่งที่จำเป็นโดยชั้นฐาน

ดังนั้นเมื่อฉันใช้คลาสฐานฉันทันทีและชัดเจนว่าฉันต้องเขียนอะไรเพื่อให้คลาสฐานทำงานได้อย่างถูกต้องเนื่องจากฉันเพียงแค่ต้องใช้วิธีนามธรรมและนั่นคือ

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


2

คำตอบคือ: ใช่ไม่ใช่และบางครั้ง :-)

ปัญหาที่คุณอธิบายบางอย่างสามารถแก้ไขได้ง่ายอย่างน้อยก็ในหลายกรณี

การตรวจสอบแบบคอมไพล์เวลานั้นดีกว่าการตรวจสอบแบบรันไทม์ซึ่งเป็นวิธีที่ดีกว่าที่จะไม่ตรวจสอบเลย

RE เวลาทำงาน:

อย่างน้อยก็เป็นแนวคิดอย่างง่าย ๆ ที่จะบังคับให้เรียกฟังก์ชั่นในลำดับที่แน่นอนและอื่น ๆ ด้วยการตรวจสอบเวลาทำงาน เพียงแค่ใส่ธงที่บอกว่าฟังก์ชั่นใดที่ได้รับการทำงานและให้แต่ละฟังก์ชั่นเริ่มต้นด้วยบางสิ่งบางอย่างเช่น "ถ้าไม่ใช่ pre-condition-1 หรือไม่ใช่ pre-condition-2 แล้วโยนข้อยกเว้น" หากการตรวจสอบมีความซับซ้อนคุณสามารถผลักพวกเขาไปสู่ฟังก์ชั่นส่วนตัว

คุณสามารถทำให้บางสิ่งเกิดขึ้นโดยอัตโนมัติ ในการยกตัวอย่างง่ายๆโปรแกรมเมอร์มักพูดถึง "ประชากรที่ขี้เกียจ" ของวัตถุ เมื่ออินสแตนซ์ถูกสร้างขึ้นคุณตั้งค่าสถานะ is-populated เป็น false หรือการอ้างอิงวัตถุสำคัญเป็น null จากนั้นเมื่อเรียกใช้ฟังก์ชันที่ต้องการข้อมูลที่เกี่ยวข้องฟังก์ชันจะตรวจสอบการตั้งค่าสถานะ หากเป็นเท็จระบบจะเติมข้อมูลและตั้งค่าสถานะเป็นจริง ถ้ามันเป็นความจริงมันก็จะดำเนินต่อไปโดยสมมติว่ามีข้อมูลอยู่ คุณสามารถทำสิ่งเดียวกันกับเงื่อนไขอื่น ๆ ตั้งค่าสถานะในตัวสร้างหรือเป็นค่าเริ่มต้น จากนั้นเมื่อคุณไปถึงจุดที่บางฟังก์ชั่น pre-condition ควรได้รับการเรียกถ้ายังไม่ได้รับการเรียกเรียกมัน แน่นอนว่าจะใช้งานได้ก็ต่อเมื่อคุณมีข้อมูลที่จะเรียกใช้ ณ จุดนั้น แต่ในหลายกรณีคุณสามารถรวมข้อมูลที่จำเป็นในพารามิเตอร์ฟังก์ชันของ "

รวบรวมเวลา:

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

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

หากทั้งสองฟังก์ชั่นควรจะทำงานร่วมกันเสมอทางออกที่ง่ายคือการทำให้ฟังก์ชั่นเดียว เช่นถ้าทุกครั้งที่คุณเพิ่มโฆษณาในหน้าคุณควรเพิ่มตัวจัดการตัวชี้วัดสำหรับมันแล้วใส่รหัสเพื่อเพิ่มตัวจัดการตัวชี้วัดภายในฟังก์ชั่น "เพิ่มโฆษณา"

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

RE เป็นไปไม่ได้ที่จะบังคับใช้

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

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

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

แต่ก็มีบางจุดที่คุณต้องพูดว่าโปรแกรมเมอร์ควรอ่านเอกสารเกี่ยวกับฟังก์ชั่น


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


2

ใช่สัญชาตญาณของคุณตรงจุด หลายปีมาแล้วที่ผมเขียนบทความในนิตยสาร (ชนิดเช่นสถานที่แห่งนี้ แต่ตายต้นไม้และนำออกมาอีกครั้งต่อเดือน) ถกแก้จุดบกพร่อง , AbuggingและAntibugging

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

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

การออกแบบองค์ประกอบเพื่อหลีกเลี่ยงการที่แม้จะเป็นปัญหาในสถานที่แรกที่ลึกซึ้งยิ่งขึ้นและฉันชอบBuffer เป็นเหมือนตัวอย่างองค์ประกอบ ตอนที่ 4 (เผยแพร่เมื่อยี่สิบปีที่แล้ว! ว้าว!) มีตัวอย่างของการสนทนาที่ค่อนข้างคล้ายกับกรณีของคุณ: คลาสนี้จะต้องใช้อย่างระมัดระวังเนื่องจากบัฟเฟอร์ () อาจไม่ถูกเรียกหลังจากคุณเริ่มใช้ read () แต่จะต้องใช้เพียงครั้งเดียวทันทีหลังจากการก่อสร้าง การให้คลาสจัดการบัฟเฟอร์โดยอัตโนมัติและผู้ที่เรียกไม่ได้จะหลีกเลี่ยงปัญหาทางความคิด

คุณถามว่าถ้าการทดสอบสามารถทำได้ในเวลาทำงานเพื่อให้แน่ใจว่าการใช้งานที่เหมาะสมคุณควรทำเช่นนั้น?

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

บางทีการตรวจสอบอาจทำได้เฉพาะในการตรวจแก้จุดบกพร่อง

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

คุณอาจตัดสินใจว่าค่าใช้จ่ายเล็กน้อยหลังจากทั้งหมด หากคลาสทำไฟล์ I / O และไบต์พิเศษของรัฐและการทดสอบดังกล่าวจะไม่มีอะไรเกิดขึ้น คลาส iostream ทั่วไปตรวจสอบกระแสที่อยู่ในสถานะไม่ดี

สัญชาตญาณของคุณดี ตามทัน!


0

หลักการของการซ่อนสารสนเทศ (Encapsulation) คือเอนทิตีภายนอกชั้นเรียนไม่ควรรู้มากกว่าสิ่งที่จำเป็นต้องใช้ในชั้นเรียนนี้อย่างถูกต้อง

กรณีของคุณดูเหมือนว่าคุณกำลังพยายามทำให้วัตถุของคลาสทำงานอย่างถูกต้องโดยไม่บอกผู้ใช้ภายนอกเกี่ยวกับคลาสให้เพียงพอ คุณซ่อนข้อมูลมากกว่าที่ควรจะเป็น

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

คำพูดของภูมิปัญญา:

* วิธีที่คุณต้องออกแบบคลาสและ / หรือคอนสตรัคเตอร์และ / หรือเมธอด

* โดยการออกแบบอย่างไม่ถูกต้องและการอดอาหารในชั้นเรียนจากข้อมูลที่ถูกต้องคุณกำลังเสี่ยงที่ชั้นเรียนของคุณจะแตกที่ไม่ใช่ที่เดียว แต่อาจมีหลายแห่ง

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

* ในที่สุดหากบุคคลภายนอกควรจะรู้ว่ามันต้องเริ่มต้น a, b และ c แล้วเรียก d (), e (), f (int) จากนั้นคุณมีการรั่วไหลในสิ่งที่เป็นนามธรรมของคุณ


0

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

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

public abstract class Foo
{
   [TemporallyCoupled(1)]
   public abstract void Init();

   [TemporallyCoupled(2)]
   public abstract void DoBar();

   [TemporallyCoupled(3)]
   public abstract void Close();
}

1: ฉันไม่เคยเขียนตัววิเคราะห์โค้ดของโรสลินดังนั้นนี่อาจไม่ใช่วิธีที่ดีที่สุด แนวคิดที่จะใช้ตัววิเคราะห์รหัส Roslyn เพื่อตรวจสอบว่า API ของคุณถูกใช้อย่างถูกต้องหรือไม่นั้นเป็นเสียง 100%


-1

วิธีที่ฉันแก้ไขปัญหานี้มาก่อนคือตัวสร้างส่วนตัวและMakeFooManager()วิธีการคงที่ภายในชั้นเรียน ตัวอย่าง:

public class FooManager
{
     public string Foo { get; private set; }
     public string Bar { get; private set; }

     private FooManager(string foo, string bar)
     {
         Foo = foo;
         Bar = bar;
     }

     public static FooManager MakeFooManager(string foo, string bar)
     {
         // Can do other checks here too.
         if(foo == null || bar == null)
         {
             return null;
         }
         else
         {
             return new FooManager(foo, bar);
         }
     }
}

เนื่องจากคอนสตรัคจะดำเนินการมีวิธีการที่ทุกคนสร้างอินสแตนซ์ของไม่มีโดยไม่ต้องผ่านFooManagerMakeFooManager()


1
ดูเหมือนว่าจะทำซ้ำจุดและอธิบายในคำตอบก่อนหน้าซึ่งโพสต์หลายชั่วโมงก่อน
ริ้น

1
สิ่งนี้มีประโยชน์ใด ๆ เลยเพียงแค่ตรวจสอบ null ใน ctor หรือไม่ ดูเหมือนว่าคุณเพิ่งทำวิธีการที่ไม่ทำอะไรเลยนอกจากห่อวิธีอื่น ทำไม?
ร่า

นอกจากนี้ทำไมตัวแปร foo และ bar bar จึงเป็นเช่นนั้น
Patrick M

-1

มีหลายวิธี:

  1. ทำให้ชั้นเรียนใช้งานง่ายถูกต้องและใช้งานยาก คำตอบอื่น ๆ บางข้อเน้นเฉพาะจุดนี้ดังนั้นฉันจะพูดถึงRAIIและรูปแบบการสร้างเท่านั้น

  2. ระวังวิธีการใช้ความคิดเห็น หากชั้นเรียนอธิบายตนเองอย่าเขียนความคิดเห็น * ด้วยวิธีนี้ผู้คนมักจะอ่านความคิดเห็นของคุณเมื่อชั้นเรียนต้องการพวกเขาจริงๆ และถ้าคุณมีหนึ่งในกรณีที่หายากเหล่านี้ซึ่งชั้นเรียนนั้นยากต่อการใช้อย่างถูกต้องและต้องการความคิดเห็นให้ใส่ตัวอย่าง

  3. ใช้assertsในบิลด์ debug ของคุณ

  4. ส่งข้อยกเว้นถ้าการใช้คลาสไม่ถูกต้อง

* นี่เป็นความคิดเห็นที่คุณไม่ควรเขียน:

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