วิธีที่เหมาะสมในการจำลองกิจกรรมในโลกแห่งความเป็นจริงที่ดูเหมือนจะต้องการการอ้างอิงแบบวงกลมใน OOP


24

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

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

ชั้นเรียนคือ:

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

  • บอร์ด: การแสดงบอร์ดเกมอย่างง่ายซึ่งสามารถสั่งให้สะท้อนการเคลื่อนไหวได้

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

  • ย้าย: ย้ายเพียงครั้งเดียว มันรวมทุกอย่างเกี่ยวกับการเคลื่อนไหวเป็นรายการของอะตอม: หยิบชิ้นส่วนจากที่นี่วางไว้ที่นั่นเอาชิ้นส่วนที่ถูกจับออกจากที่นั่น

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

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

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

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

ขอบคุณสำหรับความช่วยเหลือใด ๆ ที่คุณสามารถให้!


7
เกิดอะไรขึ้นถ้าRuleBookเอาเช่นStateการโต้แย้งและกลับมาถูกต้องMoveListเช่น"นี่คือที่ที่เราอยู่ตอนนี้สิ่งที่สามารถทำได้ต่อไป?"
jonrsharpe

สิ่งที่ @ Jonrsharpe พูด เมื่อเล่นเกมกระดานจริงหนังสือกฎก็ไม่ทราบเกี่ยวกับเกมจริงที่เล่นอยู่ ฉันอาจจะแนะนำคลาสอื่นเพื่อคำนวณการเคลื่อนไหวจริง ๆ แต่นั่นอาจขึ้นอยู่กับว่าคลาส RuleBook นี้ใหญ่ขนาดไหน
Sebastiaan van den Broek

4
การหลีกเลี่ยงออบเจกต์พระเจ้า (บิ๊กคลาสนั่นที่สุดทุกอย่างในเกม) มีความสำคัญมากกว่าการหลีกเลี่ยงการอ้างอิงแบบวงกลม
281377

2
@ user281377 พวกเขาไม่จำเป็นต้องมีวัตถุประสงค์ร่วมกัน แต่อย่างใด!
jonrsharpe

1
คุณสามารถแสดงความพยายามสร้างแบบจำลองได้หรือไม่ แผนภาพตัวอย่างเช่น?
ผู้ใช้

คำตอบ:


47

ฉันต่อสู้กับปัญหาในโครงการ Java เกี่ยวกับการอ้างอิงแบบวงกลม

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

ฉันเขียนโค้ดนี้ขึ้น [... ] แต่ปัญหาคือมันเต็มไปด้วยการอ้างอิงแบบวงกลม ฉันนำมันกลับมาแล้วโดยการบรรจุคลาสพันทั้งหมดในไฟล์ต้นฉบับเดียว [... ]

ไม่จำเป็น. หากคุณเพียงรวบรวมไฟล์ต้นฉบับทั้งหมดในครั้งเดียว (เช่นjavac *.java) คอมไพเลอร์จะแก้ไขการอ้างอิงไปข้างหน้าทั้งหมดโดยไม่มีปัญหา

หรือฉันควรติดกับคลาสพึ่งพาซึ่งกันและกันและเกลี้ยกล่อมคอมไพเลอร์ในการรวบรวมพวกเขาอย่างใด [... ]

ใช่. คลาสแอปพลิเคชันคาดว่าจะมีการพึ่งพาซึ่งกันและกัน รวบรวมไฟล์ที่มา Java ทั้งหมดที่อยู่ในแพ็กเกจเดิมได้ในครั้งเดียวไม่ได้เป็นสับฉลาดก็แม่นยำทางจาวาควรไปทำงาน


24
"การอ้างอิงแบบวงกลมไม่ทำให้เกิดปัญหาใด ๆ ใน Java" ในแง่ของการรวบรวมนี่เป็นเรื่องจริง แม้ว่าการอ้างอิงแบบวงกลมนั้นถือว่าเป็นการออกแบบที่ไม่ดีก็ตาม
Chop

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

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

3
โปรดอย่ากระจาย FUD ที่ไม่มีการรับรองเกี่ยวกับภาษาการเขียนโปรแกรมที่ไม่เกี่ยวข้อง Python สนับสนุน GC ของรอบอ้างอิงตั้งแต่อายุ ( docsและ SO: ที่นี่และที่นี่ )
Christian Aichinger

2
IMHO คำตอบนี้เป็นเพียงปานกลางเนื่องจากไม่มีหนึ่งคำเกี่ยวกับการอ้างอิงแบบวงกลมที่มีประโยชน์สำหรับตัวอย่างของ OP
Doc Brown

22

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

ที่จริงแล้วมีบางสถานการณ์ที่คอมไพเลอร์ java จะปฏิเสธการพึ่งพาแบบวงกลม (หมายเหตุ: อาจมีมากกว่านี้ฉันคิดได้เฉพาะตอนนี้)

  1. ในการสืบทอด: คุณไม่สามารถมีคลาส A ขยายคลาส B ซึ่งจะขยายคลาส A และมีเหตุผลอย่างสมบูรณ์แบบที่คุณไม่สามารถมีได้เนื่องจากทางเลือกจะไม่มีเหตุผลจากมุมมองเชิงตรรกะ

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

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

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

ในสถานการณ์เฉพาะที่คุณมีอยู่ฉันคิดว่าสิ่งที่คุณต้องการคือเอนทิตีใหม่ซึ่งอาจเรียกว่า "เกม" หรือ "GameLogic" ซึ่งรู้จักเอนทิตี้อื่น ๆ ทั้งหมด (ไม่มีหน่วยงานอื่นใดที่รู้ ) เพื่อให้เอนทิตีอื่น ๆ ไม่จำเป็นต้องรู้จักกัน

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

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


1
ขอบคุณไมค์ คุณพูดถูกเกี่ยวกับข้อเสียของเอนทิตีเกม; ด้วยรหัสแอปเพล็เก่าฉันสามารถสร้างเกมใหม่ที่มีมากกว่า subclass RuleBook ใหม่และการออกแบบกราฟิกที่เหมาะสมเล็กน้อย
Damian Walker

10

ทฤษฎีเกมถือว่าเกมเป็นรายการของการเคลื่อนไหวก่อนหน้านี้ (ประเภทค่ารวมถึงผู้ที่เล่นพวกเขา) และฟังก์ชั่น ValidMove (ก่อนหน้านี้ย้าย)

ฉันจะลองและทำตามรูปแบบนี้สำหรับส่วนที่ไม่ใช่ UI ของเกมและปฏิบัติกับสิ่งต่าง ๆ เช่นการตั้งค่าบอร์ดเป็นการเคลื่อนไหว

จากนั้น UI สามารถเป็นสิ่ง OO มาตรฐานโดยอ้างอิงจากตรรกะทางเดียว


อัปเดตเพื่อย่อความคิดเห็น

พิจารณาหมากรุก เกมหมากรุกมักถูกบันทึกเป็นรายการเคลื่อนไหว http://en.wikipedia.org/wiki/Portable_Game_Notation

รายการการเคลื่อนไหวจะกำหนดสถานะที่สมบูรณ์ของเกมได้ดีกว่าภาพของบอร์ด

พูดเช่นเราเริ่มสร้างวัตถุสำหรับบอร์ดชิ้นส่วนย้าย ฯลฯ และวิธีการเช่นชิ้นส่วน GetValidMoves ()

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

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

อย่างไรก็ตามหากเรากำหนด Move เป็นโครงสร้างที่ไม่เปลี่ยนรูปแบบและสถานะของเกมเป็นรายการของการเคลื่อนไหวก่อนหน้านี้เราพบว่าปัญหาเหล่านี้หายไป เพื่อดูว่าการขว้างปานั้นถูกต้องหรือไม่เราสามารถตรวจสอบรายการย้ายของการมีอยู่ของปราสาทและการเคลื่อนย้ายของกษัตริย์ เพื่อดูว่าจำนำสามารถใช้ en-passent เราสามารถตรวจสอบเพื่อดูว่าจำนำอื่น ๆ ที่ทำสองครั้งในการย้ายก่อนหน้านี้ ไม่ต้องการการอ้างอิงยกเว้นกฎ -> ย้าย

ตอนนี้หมากรุกมีกระดานแบบสแตติกและ peices มักจะตั้งค่าแบบเดียวกันเสมอ แต่สมมติว่าเรามีตัวแปรที่เราอนุญาตให้มีการตั้งค่าทางเลือก อาจละเว้นบางส่วนเป็นแต้มต่อ

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

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


สิ่งนี้มีแนวโน้มที่จะทำให้การใช้งานของ ValidMove ซับซ้อนซึ่งจะทำให้ตรรกะของคุณช้าลง
Taemyr

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

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

1
คุณยังหลีกเลี่ยงการเปลี่ยนแบบจำลองวัตถุของคุณเพื่อสะท้อนกฎ เช่นสำหรับหมากรุกหากคุณใช้ validMove -> Piece + Board คุณจะไม่สามารถใช้สกิลได้การเลื่อนครั้งแรกสำหรับการจำนำและการเลื่อนชิ้นและจะต้องเพิ่มข้อมูลพิเศษลงในวัตถุหรืออ้างอิงวัตถุที่สาม คุณยังสูญเสียความคิดว่าใครจะไปเป็นและแนวคิดเช่นการค้นพบตรวจสอบ
Ewan

1
@Gabe The boardLayoutเป็นฟังก์ชั่นของทุกคนpriorMoves(เช่นถ้าเรารักษามันไว้เป็นรัฐจะไม่มีส่วนอื่นนอกเหนือจากกันthisMove) ดังนั้นข้อเสนอแนะของอีแวนเป็นหลัก "ตัดชายกลาง" - validMoves( boardLayout( priorMoves ) )การเคลื่อนไหวที่ถูกต้องเป็นหน้าที่โดยตรงของทั้งหมดก่อนแทน
OJFord

8

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


6

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

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

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

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

ผู้ควบคุมจะต้องตระหนักถึงสถานะของเกมและกฎของหนังสือและอาจเป็นวัตถุอื่น ๆ ของโมเดลเกม แต่ไม่จำเป็นต้องยุ่งกับรายละเอียด


4
ความคิดของฉันอย่างชัดเจน OP กำลังผสมข้อมูลและขั้นตอนมากเกินไปในคลาสเดียวกัน มันเป็นการดีกว่าที่จะแยกสิ่งเหล่านี้ออกมากขึ้น นี่คือการพูดคุยที่ดีในเรื่อง Btw เมื่อฉันอ่าน "ดูสถานะเกม" ฉันคิดว่า "โต้แย้งฟังก์ชัน" +100 ถ้าทำได้
jpmc26

5

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

เริ่มจากอธิบายสิ่งที่แต่ละชั้นเรียนทำกัน

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

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

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

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

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

GameStateแรกคือ เนื่องจากคลาสนี้ขึ้นอยู่กับเกมโดยเฉพาะจึงไม่มีGamestateอินเตอร์เฟสหรือคลาสทั่วไป

Moveถัดไปคือ ดังที่ฉันได้กล่าวมาสิ่งนี้สามารถถูกแสดงด้วยอินเตอร์เฟสที่มีวิธีการเดียวที่ใช้สถานะ pre-move และสร้างสถานะ post-move นี่คือรหัสสำหรับส่วนต่อประสานนี้:

package boardgame;

/**
 *
 * @param <T> The type of GameState
 */
public interface Move<T> {

    T makeResultingState(T preMoveState) throws IllegalArgumentException;

}

ขอให้สังเกตว่ามีพารามิเตอร์ประเภท เพราะนี่คือตัวอย่างเช่น a ChessMoveจะต้องรู้เกี่ยวกับรายการของการย้ายChessGameStateล่วงหน้า ตัวอย่างเช่นการประกาศคลาสChessMoveจะเป็น

class ChessMove extends Move<ChessGameState>,

คุณจะกำหนดChessGameStateคลาสไว้ที่ไหน

ต่อไปฉันจะหารือเกี่ยวกับRuleBookชั้นเรียนทั่วไป นี่คือรหัส:

package boardgame;

import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public interface RuleBook<T> {

    T makeInitialState();

    List<Move<T>> makeMoveList(T gameState);

    StateEvaluation evaluateState(T gameState);

    boolean isMoveLegal(Move<T> move, T currentState);

}

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

package boardgame;

/**
 *
 */
public enum StateEvaluation {

    UNFINISHED,
    PLAYER_ONE_WINS,
    PLAYER_TWO_WINS,
    DRAW,
    ILLEGAL_STATE
}

สุดท้ายเรามาอธิบาย GameHistoryชั้นเรียน ชั้นนี้มีหน้าที่รับผิดชอบในการจดจำตำแหน่งทั้งหมดที่มาถึงในเกมเช่นเดียวกับการเคลื่อนไหวที่เล่น สิ่งสำคัญที่ควรทำคือบันทึกเสียงMoveที่เล่น คุณยังสามารถเพิ่มฟังก์ชันการทำงานสำหรับการเลิกMoveทำ ฉันมีการใช้งานด้านล่าง

package boardgame;

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public class GameHistory<T> {

    private List<T> states;
    private List<Move<T>> moves;

    public GameHistory(T initialState) {
        states = new ArrayList<>();
        states.add(initialState);
        moves = new ArrayList<>();
    }

    void recordMove(Move<T> move) throws IllegalArgumentException {
        moves.add(move);
        states.add(move.makeResultingState(getMostRecentState()));
    }

    void resetToNthState(int n) {
        states = states.subList(0, n + 1);
        moves = moves.subList(0, n);
    }

    void undoLastMove() {
        resetToNthState(getNumberOfMoves() - 1);
    }

    T getMostRecentState() {
        return states.get(getNumberOfMoves());
    }

    T getStateAfterNthMove(int n) {
        return states.get(n + 1);
    }

    Move<T> getNthMove(int n) {
        return moves.get(n);
    }

    int getNumberOfMoves() {
        return moves.size();
    }

}

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

package boardgame;

import java.util.List;

/**
 *
 * @author brian
 * @param <T> The type of GameState
 */
public class Game<T> {

    GameHistory<T> gameHistory;
    RuleBook<T> ruleBook;

    public Game(RuleBook<T> ruleBook) {
        this.ruleBook = ruleBook;
        final T initialState = ruleBook.makeInitialState();
        gameHistory = new GameHistory<>(initialState);
    }

    T getCurrentState() {
        return gameHistory.getMostRecentState();
    }

    List<Move<T>> getLegalMoves() {
        return ruleBook.makeMoveList(getCurrentState());
    }

    void doMove(Move<T> move) throws IllegalArgumentException {
        if (!ruleBook.isMoveLegal(move, getCurrentState())) {
            throw new IllegalArgumentException("Move is not legal in this position");
        }
        gameHistory.recordMove(move);
    }

    void undoMove() {
        gameHistory.undoLastMove();
    }

    StateEvaluation evaluateState() {
        return ruleBook.evaluateState(getCurrentState());
    }

}

ขอให้สังเกตในชั้นนี้ว่าRuleBookไม่รับผิดชอบต่อการรู้ว่าปัจจุบันGameStateคืออะไร นั่นคือGameHistoryงานของ ดังนั้นการGameถามGameHistoryสถานะปัจจุบันคืออะไรและให้ข้อมูลนี้แก่RuleBookเมื่อGameจำเป็นต้องพูดว่าการเคลื่อนไหวทางกฎหมายคืออะไรหรือใครก็ตามชนะ

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


3

จากประสบการณ์ของฉันการอ้างอิงแบบวงกลมโดยทั่วไปบ่งบอกว่าการออกแบบของคุณไม่ได้คิดมาก

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


+1 คุณซื้อเกมกระดานทางกายภาพคุณจะได้รับหนังสือกฎที่สามารถอธิบายกฎโดยไม่ต้องแจ้งให้ทราบ
unperson325680

1

การพึ่งพาแบบวงกลมไม่จำเป็นต้องเป็นปัญหาทางเทคนิค แต่ควรได้รับการพิจารณาว่าเป็นกลิ่นรหัสซึ่งโดยปกติจะเป็นการละเมิด Single รับผิดชอบหลักการ

การพึ่งพาแบบวงกลมของคุณมาจากความจริงที่ว่าคุณพยายามทำมากเกินไป Stateวัตถุ

วัตถุใด ๆ ที่ stateful ควรให้วิธีการที่เกี่ยวข้องโดยตรงกับการจัดการสถานะท้องถิ่นนั้น หากต้องการอะไรมากกว่าตรรกะพื้นฐานที่สุดก็ควรจะแบ่งออกเป็นรูปแบบที่มีขนาดใหญ่กว่า บางคนมีความคิดเห็นที่แตกต่างกันเกี่ยวกับเรื่องนี้ แต่ตามกฎทั่วไปแล้วถ้าคุณทำอะไรมากกว่า getters และ setters กับข้อมูลคุณกำลังทำอะไรมากเกินไป

ในกรณีนี้คุณควรที่จะมี a StateFactoryซึ่งสามารถรู้เกี่ยวกับ a Rulebookได้ดีกว่า คุณอาจจะมีคลาสตัวควบคุมอื่นซึ่งใช้ของคุณStateFactoryเพื่อสร้างเกมใหม่ แน่นอนไม่ควรรู้เกี่ยวกับState อาจทราบเกี่ยวกับการปฏิบัติตามกฎของคุณRulebookRulebookState


0

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

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

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


-2

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


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