วิธีทำให้คลาส stateful ที่ซับซ้อนและการทดสอบของพวกเขาง่ายขึ้น?


9

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

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

ตัวอย่างและเพื่อให้สิ่งต่าง ๆ ง่ายและชัดเจนสมมติว่าหุ่นยนต์และรถยนต์เป็นชั้นเรียนในโครงการของฉัน

ดังนั้นในคลาส Robot ฉันจะมีวิธีการมากมายในรูปแบบต่อไปนี้:

  • นอน(); isSleepAvaliable ();
  • ตื่น (); isAwakeAvaliable ();
  • เดิน (Direction); isWalkAvaliable ();
  • ยิง (Direction); isShootAvaliable ();
  • turnOnAlert (); isTurnOnAlertAvailable ();
  • turnOffAlert (); isTurnOffAlertAvailable ();
  • ชาร์จ (); isRechargeAvailable ();
  • ไฟดับ(); isPowerOffAvailable ();
  • stepInCar (รถยนต์); isStepInCarAvailable ();
  • stepOutCar (รถยนต์); isStepOutCarAvailable ();
  • selfDestruct (); isSelfDestructAvailable ();
  • ตาย(); isDieAvailable ();
  • isAlive (); isAwake (); isAlertOn (); getBatteryLevel (); getCurrentRidingCar (); getAmmo ();
  • ...

ในคลาสรถยนต์มันจะคล้ายกัน:

  • เปิด(); isTurnOnAvaliable ();
  • ปิด(); isTurnOffAvaliable ();
  • เดิน (Direction); isWalkAvaliable ();
  • เติมน้ำมัน (); isRefuelAvailable ();
  • selfDestruct (); isSelfDestructAvailable ();
  • ความผิดพลาด (); isCrashAvailable ();
  • isOperational (); เมดิสัน (); getFuelLevel (); getCurrentPassenger ();
  • ...

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

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

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

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

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

แก้ไข:
ฉันคิดว่าฉันไม่ชัดเจนเกี่ยวกับเครื่องจักรของรัฐ หุ่นยนต์เป็นเครื่องจักรกลของรัฐโดยที่สหรัฐฯ "หลับ", "ตื่น", "ชาร์จใหม่", "ตาย" ฯลฯ รถยนต์เป็นเครื่องจักรของรัฐอีกเครื่องหนึ่ง

แก้ไข 2: ในกรณีที่คุณอยากรู้ว่าระบบของฉันคืออะไรคลาสที่โต้ตอบคือสิ่งต่าง ๆ เช่น Server, IPAddress, Disk, Backup, User, SoftwareLicense เป็นต้นสถานการณ์ Robot และ Car เป็นเพียงกรณีที่ฉันพบ นั่นจะง่ายพอที่จะอธิบายปัญหาของฉัน


คุณลองถามที่Code Reviewหรือไม่ นอกเหนือจากนั้นสำหรับการออกแบบเหมือนของคุณฉันจะเริ่มคิดถึงการแยกประเภทการปรับโครงสร้างของคลาสอีกครั้ง
gnat

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

@gnat คุณสามารถให้ตัวอย่างของฉันจะใช้ Extract Class ในสถานการณ์หุ่นยนต์และรถยนต์ที่กำหนดได้อย่างไร
Victor Stafusa

ฉันจะแยกข้อมูลที่เกี่ยวกับรถยนต์จากโรบอทลงในคลาสอื่น ฉันจะดึงวิธีการทั้งหมดที่เกี่ยวข้องกับ sleep + ตื่นในชั้นเรียนเฉพาะ "ผู้สมัคร" คนอื่น ๆ ที่ดูเหมือนว่าสมควรได้รับการสกัดคือวิธีการเติมพลัง + เติมสิ่งที่เกี่ยวข้องกับการเคลื่อนไหว ฯลฯ หมายเหตุเนื่องจากนี่เป็นการปรับโครงสร้างใหม่ API ภายนอกสำหรับหุ่นยนต์น่าจะยังคงอยู่ ในขั้นตอนแรกฉันจะแก้ไข internals เท่านั้น BTDTGTTS
ริ้น

นี่ไม่ใช่คำถามทบทวนรหัส - สถาปัตยกรรมไม่ได้อยู่ที่นั่น
Michael K

คำตอบ:


8

รัฐรูปแบบการออกแบบอาจจะมีการใช้งานถ้าคุณไม่ได้ใช้มัน

แนวคิดหลักคือการที่คุณสร้างระดับชั้นแต่ละรัฐที่แตกต่างกัน - ดังนั้นเพื่อดำเนินการต่อตัวอย่างของคุณSleepingRobot, AwakeRobot, RechargingRobotและDeadRobotทั้งหมดจะเรียนดำเนินการติดต่อกัน

วิธีการในRobotชั้นเรียน (เช่นsleep()และisSleepAvaliable()) มีการใช้งานง่ายที่มอบหมายให้ชั้นในปัจจุบัน

การเปลี่ยนแปลงสถานะถูกนำไปใช้โดยการแลกเปลี่ยนคลาสภายในปัจจุบันกับอันอื่น

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


ฉันใช้จาวา
Victor Stafusa

คำแนะนำที่ดี วิธีนี้การใช้งานแต่ละครั้งจะมีจุดเน้นที่ชัดเจนซึ่งสามารถทดสอบเป็นรายบุคคลได้โดยไม่ต้องมีการทดสอบ Junit Class 2.000 ทุกรัฐพร้อมกัน
OliverS

3

ฉันไม่ทราบรหัสของคุณ แต่จากตัวอย่างของวิธี "sleep" ฉันจะถือว่ามันเป็นสิ่งที่คล้ายกับโค้ด "simplistic" ต่อไปนี้:

public void sleep() {
 if(!dead && awake) {
  sleeping = true;
  awake = false;
  this.updateState(SLEEPING);
 }
 throw new IllegalArgumentException("robot is either dead or not awake");
}

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

รับรหัสข้างต้นฉันจะล้อเลียนวัตถุ "machineState" และการทดสอบครั้งแรกของฉันจะเป็น:

testSleep_dead() {
 robot.dead = true;
 robot.awake = false;
 robot.setState(AWAKE);
 try {
  robot.sleep();
  fail("should have got an exception");
 } catch(Exception e) {
  assertTrue(e instanceof IllegalArgumentException);
  assertEquals("robot is either dead or not awake", e.getMessage());
 }
}

ความคิดเห็นส่วนตัวของฉันคือการเขียนการทดสอบหน่วยขนาดเล็กควรเป็นสิ่งแรกที่ต้องทำ คุณเขียนว่า:

การตั้งค่าการทดสอบมีความซับซ้อนมากเพราะพวกเขาจำเป็นต้องสร้างโลกที่ซับซ้อนเพื่อการออกกำลังกาย

การรันการทดสอบขนาดเล็กเหล่านี้ควรเร็วมากและคุณไม่ควรมีอะไรที่จะเริ่มต้นได้เหมือน "โลกที่ซับซ้อน" ของคุณ ตัวอย่างเช่นหากเป็นแอปพลิเคชันที่ใช้ IOC container (เช่น Spring) คุณไม่จำเป็นต้องเริ่มต้นบริบทระหว่างการทดสอบหน่วย

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

ในที่สุดสิ่งนี้สามารถทำได้ไม่ว่ารหัสของคุณจะซับซ้อน (ตามที่คุณบอกว่าตอนนี้) หรือหลังจากที่คุณ refactored


ฉันคิดว่าฉันไม่ชัดเจนเกี่ยวกับเครื่องจักรของรัฐ หุ่นยนต์เป็นเครื่องจักรกลของรัฐโดยที่สหรัฐฯ "หลับ", "ตื่น", "ชาร์จใหม่", "ตาย" ฯลฯ รถยนต์เป็นเครื่องจักรของรัฐอีกเครื่องหนึ่ง
Victor Stafusa

@Victor ตกลงคุณสามารถแก้ไขรหัสตัวอย่างของฉันได้ถ้าคุณต้องการ ถ้าคุณไม่บอกฉันเป็นอย่างอื่นฉันคิดว่าจุดของฉันในการทดสอบหน่วยยังคงใช้ได้ฉันหวังอย่างน้อยที่สุด
Jalayn

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

2

ฉันกำลังอ่านส่วน"แหล่งกำเนิด" ของบทความ Wikipedia เกี่ยวกับหลักการแยกอินเทอร์เฟซและฉันถูกเตือนให้นึกถึงคำถามนี้

ฉันจะอ้างอิงบทความ ปัญหา: "... หนึ่งคลาสงานหลัก .... คลาสที่อ้วนพร้อมวิธีการที่หลากหลายสำหรับลูกค้าที่แตกต่างหลากหลาย" วิธีแก้ปัญหา: "... เลเยอร์ของอินเตอร์เฟซระหว่างคลาสงานและลูกค้าทั้งหมด ... "

ดูเหมือนว่าปัญหาของคุณจะเปลี่ยนจาก Xerox ตัวหนึ่ง แทนที่จะเป็นคลาสอ้วนคุณมีสองคลาสและคลาสอ้วนทั้งสองนี้กำลังพูดคุยกันแทนที่จะเป็นไคลเอนต์จำนวนมาก

คุณสามารถจัดกลุ่มวิธีตามประเภทของการโต้ตอบแล้วสร้างคลาสอินเตอร์เฟสสำหรับแต่ละประเภทได้หรือไม่ ตัวอย่างเช่น: RobotPowerInterface, RobotNavigationInterface, RobotAlarmInterface classes?

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