หากตอนนี้คุณจำเป็นต้องเพิ่มประสิทธิภาพรหัส Java ที่คุณเขียนคุณจะทำอะไร? ในบทความนี้ผู้เขียนแนะนำวิธีการสี่วิธีที่สามารถปรับปรุงประสิทธิภาพของระบบและความสามารถในการอ่านรหัส หากคุณสนใจสิ่งนี้มาดูกันเถอะ
งานการเขียนโปรแกรมปกติของเราไม่มีอะไรมากไปกว่าการใช้ชุดเทคนิคเดียวกันกับโครงการต่าง ๆ ในกรณีส่วนใหญ่เทคโนโลยีเหล่านี้สามารถบรรลุเป้าหมายได้ อย่างไรก็ตามบางโครงการอาจต้องใช้เทคนิคพิเศษดังนั้นวิศวกรจึงต้องศึกษาในเชิงลึกเพื่อค้นหาวิธีที่ง่ายที่สุด แต่มีประสิทธิภาพมากที่สุด ในบทความก่อนหน้านี้เราได้พูดถึงเทคโนโลยีพิเศษสี่ประการที่สามารถใช้งานได้เมื่อจำเป็นเพื่อสร้างซอฟต์แวร์ Java ที่ดีขึ้น ในขณะที่อยู่ในบทความนี้เราจะแนะนำกลยุทธ์การออกแบบทั่วไปและเทคนิคการดำเนินการตามเป้าหมายที่ช่วยแก้ปัญหาทั่วไป ได้แก่ :
การเพิ่มประสิทธิภาพอย่างมีจุดประสงค์เท่านั้น
ใช้ enums ให้มากที่สุดเท่าที่จะเป็นไปได้สำหรับค่าคงที่
กำหนดวิธีการเท่ากับ () ในชั้นเรียนใหม่
ใช้ polymorphism ให้มากที่สุด
เป็นที่น่าสังเกตว่าเทคนิคที่อธิบายไว้ในบทความนี้ไม่สามารถใช้ได้กับทุกกรณี นอกจากนี้ควรใช้เทคโนโลยีเหล่านี้เมื่อใดและที่ไหนพวกเขาต้องการให้ผู้ใช้ต้องพิจารณาอย่างรอบคอบ
1. การเพิ่มประสิทธิภาพอย่างมีจุดมุ่งหมายเท่านั้น
ระบบซอฟต์แวร์ขนาดใหญ่จะต้องกังวลอย่างมากเกี่ยวกับปัญหาด้านประสิทธิภาพ แม้ว่าเราหวังว่าจะสามารถเขียนโค้ดที่มีประสิทธิภาพมากที่สุดได้หลายครั้งหากเราต้องการเพิ่มประสิทธิภาพรหัสเราก็ไม่รู้ว่าจะเริ่มต้นอย่างไร ตัวอย่างเช่นรหัสต่อไปนี้จะมีผลต่อประสิทธิภาพหรือไม่
โมฆะสาธารณะ ProcessInteGers (รายการ <จำนวนเต็ม> จำนวนเต็ม) {สำหรับ (ค่าจำนวนเต็ม: จำนวนเต็ม) {สำหรับ (int i = จำนวนเต็ม Size ()-1; i> = 0; i--) {value += integers.get (i); -ขึ้นอยู่กับสถานการณ์ ในรหัสข้างต้นเราจะเห็นว่าอัลกอริทึมการประมวลผลของมันคือ O (n³) (ใช้สัญลักษณ์ O ขนาดใหญ่) โดยที่ n คือขนาดของชุดรายการ หาก N เป็นเพียง 5 เท่านั้นจะไม่มีปัญหาจะมีการทำซ้ำเพียง 25 ครั้งเท่านั้น แต่ถ้า n คือ 100,000 มันอาจส่งผลกระทบต่อประสิทธิภาพ โปรดทราบว่าแม้เราจะไม่สามารถระบุได้ว่าจะมีปัญหา แม้ว่าวิธีนี้จะต้องมีการทำซ้ำตรรกะ 1 พันล้านครั้ง แต่จะมีผลกระทบต่อประสิทธิภาพหรือไม่
ตัวอย่างเช่นสมมติว่าไคลเอนต์ดำเนินการรหัสนี้ในเธรดของตัวเองและกำลังรอแบบอะซิงโครนัสเพื่อให้การคำนวณเสร็จสมบูรณ์จากนั้นเวลาดำเนินการอาจเป็นที่ยอมรับได้ ในทำนองเดียวกันหากระบบถูกปรับใช้ในสภาพแวดล้อมการผลิต แต่ไม่มีลูกค้าโทรหามันไม่จำเป็นต้องให้เราเพิ่มประสิทธิภาพรหัสนี้เนื่องจากจะไม่ใช้ประสิทธิภาพโดยรวมของระบบเลย ในความเป็นจริงระบบจะมีความซับซ้อนมากขึ้นหลังจากเพิ่มประสิทธิภาพประสิทธิภาพ แต่สิ่งที่น่าเศร้าคือประสิทธิภาพของระบบไม่ดีขึ้นเป็นผล
สิ่งที่สำคัญที่สุดคือไม่มีอาหารกลางวันฟรีในโลกดังนั้นเพื่อลดต้นทุนเรามักจะใช้เทคโนโลยีเช่นแคชการขยายตัวของลูปหรือค่าที่คำนวณล่วงหน้าเพื่อให้ได้การเพิ่มประสิทธิภาพซึ่งจะเพิ่มความซับซ้อนของระบบและลดความสามารถในการอ่านรหัส หากการเพิ่มประสิทธิภาพนี้สามารถปรับปรุงประสิทธิภาพของระบบมันก็คุ้มค่าแม้ว่ามันจะซับซ้อน แต่ก่อนที่จะตัดสินใจคุณต้องรู้ข้อมูลสองชิ้นนี้ก่อน:
ข้อกำหนดด้านประสิทธิภาพคืออะไร
คอขวดประสิทธิภาพอยู่ที่ไหน
ก่อนอื่นเราจำเป็นต้องรู้อย่างชัดเจนว่าข้อกำหนดด้านประสิทธิภาพคืออะไร หากในที่สุดมันก็อยู่ในข้อกำหนดและผู้ใช้ไม่ได้คัดค้านใด ๆ ก็ไม่จำเป็นต้องดำเนินการเพิ่มประสิทธิภาพประสิทธิภาพ อย่างไรก็ตามเมื่อมีการเพิ่มฟังก์ชั่นใหม่หรือปริมาณข้อมูลของระบบถึงระดับที่แน่นอนจะต้องได้รับการปรับให้เหมาะสมมิฉะนั้นปัญหาอาจเกิดขึ้น
ในกรณีนี้ไม่ควรขึ้นอยู่กับสัญชาตญาณหรือการตรวจสอบ เพราะแม้แต่นักพัฒนาที่มีประสบการณ์เช่นมาร์ตินฟาวเลอร์ก็มีแนวโน้มที่จะทำการเพิ่มประสิทธิภาพที่ผิดตามที่อธิบายไว้ในบทความ refactoring (หน้า 70):
หากคุณวิเคราะห์โปรแกรมที่เพียงพอคุณจะพบสิ่งที่น่าสนใจเกี่ยวกับประสิทธิภาพที่ส่วนใหญ่ของคุณสูญเปล่าในส่วนเล็ก ๆ ของรหัสในระบบ หากรหัสทั้งหมดได้รับการปรับให้เหมาะสมเหมือนกันผลลัพธ์สุดท้ายคือ 90% ของการเพิ่มประสิทธิภาพนั้นสูญเปล่าเนื่องจากรหัสหลังจากการปรับให้เหมาะสมไม่ทำงานมาก เวลาที่ใช้ในการปรับให้เหมาะสมโดยไม่มีเป้าหมายคือการเสียเวลา
ในฐานะนักพัฒนาที่แข็งแกร่งในการต่อสู้เราควรคำนึงถึงมุมมองนี้อย่างจริงจัง การคาดเดาครั้งแรกไม่เพียง แต่ไม่เพียง แต่ประสิทธิภาพของระบบยังไม่ได้รับการปรับปรุง แต่ 90% ของเวลาในการพัฒนานั้นสูญเปล่าอย่างสมบูรณ์ แต่เราควรดำเนินการกรณีการใช้งานทั่วไปในการผลิต (หรือก่อนการผลิต) และค้นหาว่าส่วนใดของระบบที่ใช้ทรัพยากรระบบในระหว่างการดำเนินการแล้วกำหนดค่าระบบ ตัวอย่างเช่นเพียง 10% ของรหัสที่ใช้ทรัพยากรส่วนใหญ่จากนั้นเพิ่มประสิทธิภาพที่เหลืออีก 90% ของรหัสเป็นเวลาที่เสียไป
ตามผลการวิเคราะห์หากเราต้องการใช้ความรู้นี้เราควรเริ่มต้นด้วยสถานการณ์ที่พบบ่อยที่สุด เพราะสิ่งนี้จะทำให้มั่นใจได้ว่าความพยายามที่แท้จริงจะปรับปรุงประสิทธิภาพของระบบในที่สุด หลังจากการเพิ่มประสิทธิภาพแต่ละขั้นตอนควรทำซ้ำขั้นตอนการวิเคราะห์ เนื่องจากสิ่งนี้ไม่เพียง แต่ช่วยให้มั่นใจได้ว่าประสิทธิภาพของระบบได้รับการปรับปรุงอย่างแท้จริง แต่ยังสามารถเห็นได้ว่าส่วนใดของคอขวดประสิทธิภาพคือการปรับระบบให้เหมาะสม (เพราะหลังจากการแก้ปัญหาคอขวดหนึ่งขวดอื่น ๆ อาจใช้ทรัพยากรโดยรวมของระบบมากขึ้น) ควรสังเกตว่าเปอร์เซ็นต์ของเวลาที่ใช้ในคอขวดที่มีอยู่มีแนวโน้มที่จะเพิ่มขึ้นเนื่องจากคอขวดที่เหลือจะไม่เปลี่ยนแปลงชั่วคราวและเวลาดำเนินการโดยรวมควรลดลงเนื่องจากคอขวดเป้าหมายถูกกำจัด
แม้ว่าจะต้องใช้ความสามารถมากมายในการตรวจสอบโปรไฟล์ในระบบ Java อย่างเต็มที่ แต่ก็มีเครื่องมือทั่วไปที่สามารถช่วยค้นพบฮอตสปอตประสิทธิภาพของระบบรวมถึง JMeter, AppDynamics และ YourKit นอกจากนี้คุณยังสามารถอ้างถึงคู่มือการตรวจสอบประสิทธิภาพของ Dzone สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการเพิ่มประสิทธิภาพประสิทธิภาพของโปรแกรม Java
แม้ว่าประสิทธิภาพจะเป็นองค์ประกอบที่สำคัญมากของระบบซอฟต์แวร์ขนาดใหญ่จำนวนมากและเป็นส่วนหนึ่งของชุดทดสอบอัตโนมัติในไปป์ไลน์การส่งมอบผลิตภัณฑ์ แต่ก็ไม่สามารถปรับให้เหมาะสมได้อย่างสุ่มสี่สุ่มห้าและไม่มีจุดประสงค์ แต่ควรปรับให้เหมาะสมเฉพาะกับคอขวดประสิทธิภาพที่ได้รับการควบคุม สิ่งนี้ไม่เพียง แต่ช่วยให้เราหลีกเลี่ยงการเพิ่มความซับซ้อนของระบบ แต่ยังช่วยให้เราสามารถหลีกเลี่ยงการออกนอกเส้นทางและหลีกเลี่ยงการเพิ่มประสิทธิภาพการเสียเวลา
2. พยายามใช้ enums สำหรับค่าคงที่
มีหลายสถานการณ์ที่ผู้ใช้จำเป็นต้องแสดงชุดของค่าที่กำหนดไว้ล่วงหน้าหรือคงที่เช่นรหัสการตอบกลับ HTTP ที่อาจพบในเว็บแอปพลิเคชัน หนึ่งในเทคนิคการใช้งานที่พบบ่อยที่สุดคือการสร้างคลาสใหม่ซึ่งมีค่าประเภทสุดท้ายคงที่มากมาย แต่ละค่าควรมีความคิดเห็นที่อธิบายถึงความหมายของค่า:
ชั้นเรียนสาธารณะ httpresponsecodes {สาธารณะคงที่สุดท้าย int ok = 200; สาธารณะคงที่สุดท้าย int not_found = 404; public Static Final int forbidden = 403;} if (gethttpresponse (). getStatusCode () == httpresponsecodes.ok) {// ทำอะไรบางอย่างหากรหัสตอบกลับตกลง}มันดีมากที่มีความคิดนี้ แต่ก็ยังมีข้อเสียอยู่บ้าง:
ไม่มีการตรวจสอบอย่างเข้มงวดของค่าจำนวนเต็มขาเข้า
เนื่องจากเป็นชนิดข้อมูลพื้นฐานวิธีการในรหัสสถานะจึงไม่สามารถเรียกได้
ในกรณีแรกค่าคงที่ที่เฉพาะเจาะจงจะถูกสร้างขึ้นเพื่อแสดงค่าจำนวนเต็มพิเศษ แต่ไม่มีการ จำกัด วิธีการหรือตัวแปรดังนั้นค่าที่ใช้อาจเกินขอบเขตของคำจำกัดความ ตัวอย่างเช่น:
ชั้นเรียนสาธารณะ httpresponseHandler {โมฆะสาธารณะคงที่ printMessage (Int StatusCode) {System.out.println ("สถานะที่ได้รับของ" + statusCode); }} httpresponseHandler.printMessage (15000);แม้ว่า 15000 ไม่ใช่รหัสตอบกลับ HTTP ที่ถูกต้อง แต่ก็ไม่มีข้อ จำกัด ทางฝั่งเซิร์ฟเวอร์ที่ไคลเอนต์จะต้องให้จำนวนเต็มที่ถูกต้อง ในกรณีที่สองเราไม่มีทางกำหนดวิธีการสำหรับรหัสสถานะ ตัวอย่างเช่นหากคุณต้องการตรวจสอบว่ารหัสสถานะที่กำหนดเป็นรหัสที่ประสบความสำเร็จคุณต้องกำหนดฟังก์ชั่นแยกต่างหาก:
ชั้นเรียนสาธารณะ httpresponsecodes {สาธารณะคงที่สุดท้าย int ok = 200; สาธารณะคงที่สุดท้าย int not_found = 404; public Static Final Int ต้องห้าม = 403; Public Static Boolean Issuccess (Int StatusCode) {return StatusCode> = 200 && StatusCode <300; }} if (httpresponsecodes.issuccess (gethttpresponse (). getStatusCode ())) {// ทำอะไรบางอย่างถ้ารหัสตอบกลับเป็นรหัสความสำเร็จ}ในการแก้ปัญหาเหล่านี้เราจำเป็นต้องเปลี่ยนประเภทคงที่จากประเภทข้อมูลพื้นฐานเป็นประเภทที่กำหนดเองและอนุญาตเฉพาะวัตถุเฉพาะของคลาสที่กำหนดเอง นี่คือสิ่งที่ Java enums มีไว้สำหรับ การใช้ enum เราสามารถแก้ปัญหาทั้งสองนี้ได้ในครั้งเดียว:
enum สาธารณะ httpresponsecodes {OK (200), ต้องห้าม (403), NOT_FOUND (404); รหัส INT สุดท้ายส่วนตัว; httpresponsecodes (int code) {this.code = code; } public int getCode () {รหัสส่งคืน; } บูลีนสาธารณะ issuccess () {รหัสส่งคืน> = 200 && รหัส <300; }} if (gethttpresponse (). getStatusCode (). isSuccess ()) {// ทำอะไรบางอย่างถ้ารหัสตอบกลับเป็นรหัสความสำเร็จ}ในทำนองเดียวกันตอนนี้เป็นไปได้ที่จะกำหนดให้รหัสสถานะที่ต้องใช้งานได้เมื่อเรียกวิธีการ:
คลาสสาธารณะ httpresponseHandler {โมฆะสาธารณะคงที่ printMessage (httpresponsecode statusCode) {System.out.println ("สถานะที่ได้รับของ" + statusCode.getCode ()); }} httpresponseHandler.printMessage (httpresponsecode.ok);เป็นที่น่าสังเกตว่าตัวอย่างนี้แสดงให้เห็นว่าหากเป็นค่าคงที่คุณควรพยายามใช้ enums แต่ไม่ได้หมายความว่าคุณควรใช้ enums ภายใต้ทุกสถานการณ์ ในบางกรณีอาจเป็นที่พึงปรารถนาที่จะใช้ค่าคงที่เพื่อแสดงค่าเฉพาะ แต่อนุญาตให้มีค่าอื่น ๆ เช่นกัน ตัวอย่างเช่นทุกคนอาจรู้เกี่ยวกับ PI และเราสามารถใช้ค่าคงที่ในการจับค่านี้ (และนำกลับมาใช้ใหม่):
NumericConstants ชั้นเรียนสาธารณะ {สาธารณะคงที่ double pi = 3.14; สาธารณะคงที่สุดท้าย double unit_circle_area = pi * pi;} พรมคลาสสาธารณะ {พื้นที่สองชั้นสุดท้าย การวิ่งระดับสาธารณะ (พื้นที่สองเท่า) {this.area = พื้นที่; } สาธารณะ double getCost () {พื้นที่ส่งคืน * 2; }} // สร้างพรมที่มีขนาดเส้นผ่าศูนย์กลาง 4 ฟุต (รัศมี 2 ฟุต) พรม Fourfootrug = พรมใหม่ (2 * numericConstants.unit_circle_area);ดังนั้นกฎสำหรับการใช้ enums สามารถสรุปได้เป็น:
เมื่อทราบถึงค่าที่ไม่ต่อเนื่องที่เป็นไปได้ทั้งหมดคุณสามารถใช้การแจงนับได้
ใช้รหัสตอบกลับ HTTP ที่กล่าวถึงข้างต้นเป็นตัวอย่าง เราอาจทราบค่าทั้งหมดของรหัสสถานะ HTTP (สามารถพบได้ใน RFC 7231 ซึ่งกำหนดโปรโตคอล HTTP 1.1) ดังนั้นจึงใช้การแจงนับ ในการคำนวณ PI เราไม่ทราบค่าที่เป็นไปได้ทั้งหมดเกี่ยวกับ PI (สองเท่าที่เป็นไปได้นั้นถูกต้อง) แต่ในเวลาเดียวกันเราต้องการสร้างค่าคงที่สำหรับพรมวงกลมเพื่อให้การคำนวณง่ายขึ้น (อ่านง่ายขึ้น); ดังนั้นชุดของค่าคงที่จึงถูกกำหนด
หากคุณไม่สามารถรู้ค่าที่เป็นไปได้ทั้งหมดล่วงหน้า แต่ต้องการรวมฟิลด์หรือวิธีการสำหรับแต่ละค่าวิธีที่ง่ายที่สุดคือการสร้างคลาสใหม่เพื่อแสดงข้อมูล แม้ว่าฉันจะไม่เคยบอกว่าไม่ควรมีการแจงนับในสถานการณ์ใด ๆ แต่กุญแจสำคัญที่จะรู้ว่าที่ไหนและเมื่อใดที่จะไม่ใช้การแจงนับจะต้องตระหนักถึงค่าทั้งหมดล่วงหน้าและห้ามการใช้ค่าอื่น ๆ
3. กำหนดวิธีการเท่ากับ () ในชั้นเรียนใหม่
การจดจำวัตถุอาจเป็นปัญหาที่ยากในการแก้ปัญหา: หากวัตถุสองชิ้นครอบครองตำแหน่งเดียวกันในหน่วยความจำพวกเขาจะเหมือนกันหรือไม่? หาก ID ของพวกเขาเหมือนกันพวกเขาจะเหมือนกันหรือไม่? หรือถ้าทุกฟิลด์เท่ากัน? แม้ว่าแต่ละชั้นเรียนจะมีตรรกะการระบุตัวเอง แต่ก็มีหลายประเทศตะวันตกในระบบที่จำเป็นต้องตัดสินว่าพวกเขาเท่าเทียมกันหรือไม่ ตัวอย่างเช่นมีคลาสด้านล่างที่ระบุการซื้อการสั่งซื้อ ...
การซื้อชั้นเรียนสาธารณะ {ID ส่วนตัวยาว; Public Long getId () {return id; } โมฆะสาธารณะ setId (Long id) {this.id = id; -... ตามที่เขียนไว้ด้านล่างจะต้องมีหลายสถานที่ในรหัสที่คล้ายกัน:
ซื้อ OriginalPurchase = การซื้อใหม่ (); ซื้อ updatedPurchase = ใหม่การซื้อ (); ถ้า (OriginalPurchase.getId () == UpdatedPurchase.getId ()) {// ดำเนินการตรรกะสำหรับการซื้อที่เท่าเทียมกัน}ยิ่งมีการโทรตรรกะเหล่านี้มากขึ้น (ในทางกลับกันก็เป็นการละเมิดหลักการแห้ง) การซื้อ
ข้อมูลตัวตนของชั้นเรียนจะกลายเป็นมากขึ้นเรื่อย ๆ หากมีเหตุผลบางอย่างการซื้อมีการเปลี่ยนแปลง
ตรรกะข้อมูลประจำตัวของคลาส (ตัวอย่างเช่นประเภทของตัวระบุได้รับการเปลี่ยนแปลง) ดังนั้นจะต้องมีหลายสถานที่ที่มีการปรับปรุงตรรกะประจำตัว
เราควรเริ่มต้นตรรกะนี้ภายในชั้นเรียนแทนที่จะกระจายตรรกะประจำตัวของคลาสซื้อมากเกินไปผ่านระบบ เมื่อมองแวบแรกเราสามารถสร้างวิธีการใหม่เช่น ISSAME ซึ่งพารามิเตอร์การรวมเป็นวัตถุซื้อและเปรียบเทียบ ID ของแต่ละวัตถุเพื่อดูว่าพวกเขาเหมือนกัน:
การซื้อชั้นเรียนสาธารณะ {ID ส่วนตัวยาว; Public Boolean issame (ซื้ออื่น ๆ ) {return getId () == other.gerid (); -แม้ว่านี่จะเป็นวิธีแก้ปัญหาที่มีประสิทธิภาพ แต่ฟังก์ชั่นในตัวของ Java จะถูกละเว้น: การใช้วิธี Equals แต่ละคลาสใน Java สืบทอดคลาสวัตถุแม้ว่าจะเป็นนัยดังนั้นมันจึงสืบทอดวิธีการเท่ากับ โดยค่าเริ่มต้นวิธีนี้จะตรวจสอบตัวตนของวัตถุ (วัตถุเดียวกันในหน่วยความจำ) ดังที่แสดงในตัวอย่างโค้ดต่อไปนี้ในนิยามคลาสวัตถุ (เวอร์ชัน 1.8.0_131) ใน JDK:
บูลีนสาธารณะเท่ากับ (Object obj) {return (this == obj);}วิธีนี้เท่ากับวิธีการทำหน้าที่เป็นธรรมชาติสำหรับการฉีดลอจิกประจำตัว (ดำเนินการโดยการเอาชนะค่าเริ่มต้นเท่ากับ):
การซื้อชั้นเรียนสาธารณะ {ID ส่วนตัวยาว; Public Long getId () {return id; } โมฆะสาธารณะ setId (Long id) {this.id = id; } @Override บูลีนสาธารณะเท่ากับ (วัตถุอื่น ๆ ) {ถ้า (นี่ == อื่น ๆ ) {return true; } อื่นถ้า (! (อินสแตนซ์อื่น ๆ ของการซื้อ)) {return false; } else {return ((ซื้อ) อื่น ๆ ) .getId () == getId (); -แม้ว่าวิธีการนี้จะดูซับซ้อนเนื่องจากวิธี Equals ยอมรับเฉพาะพารามิเตอร์ของประเภทวัตถุ แต่เราต้องพิจารณาสามกรณีเท่านั้น:
วัตถุอื่นคือวัตถุปัจจุบัน (เช่น OriginalPurchase.equals (OriginalPurchase)) โดยนิยามพวกมันเป็นวัตถุเดียวกันดังนั้นกลับมาจริง
วัตถุอื่นไม่ใช่วัตถุซื้อในกรณีนี้เราไม่สามารถเปรียบเทียบ ID ของการซื้อได้ดังนั้นวัตถุทั้งสองจึงไม่เท่ากัน
วัตถุอื่น ๆ ไม่ใช่วัตถุเดียวกัน แต่เป็นอินสแตนซ์ของการซื้อ ดังนั้นไม่ว่าจะเท่ากับขึ้นอยู่กับว่ารหัสการซื้อปัจจุบันและการซื้ออื่น ๆ นั้นเท่ากันหรือไม่ ตอนนี้เราสามารถปรับเงื่อนไขก่อนหน้าของเราได้ดังนี้:
ซื้อ OriginalPurchase = ใหม่การซื้อ (); ซื้อ updatedPurchase = การซื้อใหม่ (); ถ้า (OriginalPurchase.equals (UpdatedPurchase)) {// ดำเนินการตรรกะสำหรับการซื้อที่เท่าเทียมกัน}นอกเหนือจากการลดการจำลองแบบในระบบแล้วการปรับเปลี่ยนวิธีการเริ่มต้นเท่ากับมีข้อดีอื่น ๆ ตัวอย่างเช่นหากเราสร้างรายการวัตถุซื้อและตรวจสอบว่ารายการมีวัตถุซื้ออื่นที่มี ID เดียวกัน (วัตถุที่แตกต่างกันในหน่วยความจำ) เราจะได้รับค่าที่แท้จริงหรือไม่เนื่องจากค่าทั้งสองนั้นถือว่าเท่ากัน:
รายการ <purchase> การซื้อ = new ArrayList <> (); purbases.add (OriginalPurchase); purchases.contains (updatedPurchase); // จริง
โดยปกติไม่ว่าคุณจะอยู่ที่ไหนถ้าคุณต้องการตรวจสอบว่าทั้งสองคลาสนั้นเท่ากันคุณจะต้องใช้วิธีการเขียนใหม่ หากเราต้องการใช้วิธี Equals โดยปริยายเนื่องจากการสืบทอดวัตถุวัตถุเพื่อตัดสินความเท่าเทียมกันเรายังสามารถใช้ตัวดำเนินการ == ดังต่อไปนี้:
if (OriginalPurchase == UpdatedPurchase) {// วัตถุทั้งสองเป็นวัตถุเดียวกันในหน่วยความจำ}ควรสังเกตว่าหลังจากที่มีการเขียนวิธี Equals ใหม่แล้ววิธีการ HashCode ควรถูกเขียนใหม่ด้วย ข้อมูลเพิ่มเติมเกี่ยวกับความสัมพันธ์ระหว่างสองวิธีนี้และวิธีการกำหนด hashcode อย่างถูกต้อง
วิธีดูหัวข้อนี้
ดังที่เราได้เห็นการเขียนทับวิธี Equals ไม่เพียง แต่เริ่มต้นตรรกะของตัวตนภายในชั้นเรียน แต่ยังช่วยลดการแพร่กระจายของตรรกะนี้ตลอดทั้งระบบเท่านั้น แต่ยังช่วยให้ภาษา Java ทำการตัดสินใจเกี่ยวกับชั้นเรียนได้อย่างดี
4. ใช้ polymorphisms ให้มากที่สุด
สำหรับภาษาการเขียนโปรแกรมใด ๆ ประโยคที่มีเงื่อนไขเป็นโครงสร้างที่พบบ่อยมากและมีเหตุผลบางประการสำหรับการดำรงอยู่ของพวกเขา เนื่องจากชุดค่าผสมที่แตกต่างกันสามารถอนุญาตให้ผู้ใช้เปลี่ยนพฤติกรรมของระบบตามค่าที่กำหนดหรือสถานะทันทีของวัตถุ สมมติว่าผู้ใช้จำเป็นต้องคำนวณยอดคงเหลือของแต่ละบัญชีธนาคารสามารถพัฒนารหัสต่อไปนี้ได้:
Public Enum BankAccountType {การตรวจสอบ, ออม, ใบรับรอง _of_deposit;} ระดับสาธารณะ BankAccount {ส่วนตัว BankAccountType ประเภท; Public BankAccount (ประเภท BankAccountType) {this.type = type; } สาธารณะ double getInterestrate () {switch (type) {การตรวจสอบกรณี: return 0.03; // 3% การออมกรณี: ส่งคืน 0.04; // 4% CASE CERTIVEATE_OF_DEPOSIT: ส่งคืน 0.05; // 5% เริ่มต้น: โยน unsupportedoperationException ใหม่ (); }} บูลีนสาธารณะ supportsDeposits () {switch (type) {การตรวจสอบกรณี: ส่งคืนจริง; การออมกรณี: คืนค่าจริง; certificate_of_deposit: return false; ค่าเริ่มต้น: โยน unsupportedoperationException ใหม่ (); -แม้ว่ารหัสข้างต้นตรงตามข้อกำหนดพื้นฐาน แต่ก็มีข้อบกพร่องที่ชัดเจน: ผู้ใช้จะกำหนดพฤติกรรมของระบบตามประเภทของบัญชีที่กำหนดเท่านั้น สิ่งนี้ไม่เพียง แต่ต้องการให้ผู้ใช้ตรวจสอบประเภทบัญชีก่อนตัดสินใจ แต่ยังต้องทำซ้ำตรรกะนี้เมื่อทำการตัดสินใจ ตัวอย่างเช่นในการออกแบบข้างต้นผู้ใช้จะต้องตรวจสอบทั้งสองวิธี สิ่งนี้สามารถนำไปสู่การควบคุมได้โดยเฉพาะอย่างยิ่งเมื่อได้รับความจำเป็นในการเพิ่มประเภทบัญชีใหม่
เราสามารถใช้ polymorphism เพื่อทำการตัดสินใจโดยปริยายแทนที่จะใช้ประเภทบัญชีเพื่อแยกแยะพวกเขา ในการทำเช่นนี้เราแปลงคลาสคอนกรีตของ BankAccount เป็นอินเทอร์เฟซและส่งผ่านกระบวนการตัดสินใจเป็นชุดของคลาสคอนกรีตที่เป็นตัวแทนของบัญชีธนาคารแต่ละประเภท:
/*** กลุ่มการเรียนรู้และการสื่อสาร Java QQ Group: 589809992 มาเรียนรู้ Java ด้วยกัน! */อินเตอร์เฟสสาธารณะ BankAccount {Public Double Getinterestrate (); เงินฝากการสนับสนุนบูลีนสาธารณะ ();} การตรวจสอบระดับสาธารณะการดำเนินการใช้งาน BankAccount {@Override สาธารณะสองเท่า getIntestrate () {return 0.03; } @Override การสนับสนุนบูลีนสาธารณะ () {return true; }} คลาสสาธารณะ SavingsAccount ใช้ BankAccount {@Override สาธารณะ double getintestrate () {return 0.04; } @Override บูลีนสาธารณะ supportsdeposis () {return true; }} ใบรับรองคลาสสาธารณะ OfdEposItAccount ใช้ BankAccount {@Override สาธารณะ double getintestrate () {return 0.05; } @Override Public Boolean Supportdeposis () {return false; -สิ่งนี้ไม่เพียง แต่ห่อหุ้มข้อมูลเฉพาะสำหรับแต่ละบัญชีลงในชั้นเรียนของตัวเอง แต่ยังสนับสนุนผู้ใช้ให้เปลี่ยนการออกแบบของพวกเขาในสองวิธีที่สำคัญ ก่อนอื่นหากคุณต้องการเพิ่มประเภทบัญชีธนาคารใหม่คุณเพียงแค่ต้องสร้างคลาสเฉพาะใหม่ให้ใช้อินเตอร์เฟส BankAccount และให้การใช้งานเฉพาะของทั้งสองวิธี ในการออกแบบโครงสร้างแบบมีเงื่อนไขเราต้องเพิ่มค่าใหม่ให้กับ enum เพิ่มคำสั่งกรณีใหม่ในทั้งสองวิธีและแทรกตรรกะของบัญชีใหม่ภายใต้คำสั่งกรณีแต่ละกรณี
ประการที่สองหากเราต้องการเพิ่มวิธีการใหม่ในอินเทอร์เฟซ BankAccount เราเพียงแค่ต้องเพิ่มวิธีการใหม่ในแต่ละคลาสคอนกรีต ในการออกแบบตามเงื่อนไขเราต้องคัดลอกคำสั่งสวิตช์ที่มีอยู่และเพิ่มลงในวิธีใหม่ของเรา นอกจากนี้เราต้องเพิ่มตรรกะสำหรับแต่ละประเภทบัญชีในแต่ละคำสั่งกรณี
ในทางคณิตศาสตร์เมื่อเราสร้างวิธีการใหม่หรือเพิ่มประเภทใหม่เราต้องทำการเปลี่ยนแปลงเชิงตรรกะจำนวนเท่ากันในการออกแบบ polymorphic และเงื่อนไข ตัวอย่างเช่นหากเราเพิ่มวิธีการใหม่ในการออกแบบ polymorphic เราต้องเพิ่มวิธีการใหม่ในคลาสคอนกรีตของบัญชีธนาคาร N ทั้งหมดและในการออกแบบตามเงื่อนไขเราต้องเพิ่มคำสั่งกรณีใหม่ในวิธีใหม่ของเรา หากเราเพิ่มประเภทบัญชีใหม่ในการออกแบบ polymorphic เราต้องใช้หมายเลข M ทั้งหมดในอินเตอร์เฟส BankAccount และในการออกแบบตามเงื่อนไขเราต้องเพิ่มคำสั่งกรณีใหม่ในแต่ละวิธีที่มีอยู่ M
แม้ว่าจำนวนการเปลี่ยนแปลงที่เราต้องทำนั้นเท่าเทียมกัน แต่ธรรมชาติของการเปลี่ยนแปลงนั้นแตกต่างกันอย่างสิ้นเชิง ในการออกแบบ polymorphic ถ้าเราเพิ่มประเภทบัญชีใหม่และลืมที่จะรวมวิธีการคอมไพเลอร์จะเกิดข้อผิดพลาดเพราะเราไม่ได้ใช้วิธีทั้งหมดในอินเทอร์เฟซ BankAccount ของเรา ในการออกแบบแบบมีเงื่อนไขไม่มีการตรวจสอบดังกล่าวเพื่อให้แน่ใจว่าแต่ละประเภทมีคำสั่งเคส หากมีการเพิ่มประเภทใหม่เราสามารถลืมอัปเดตแต่ละคำสั่งสวิตช์แต่ละคำสั่ง ยิ่งปัญหานี้ร้ายแรงมากเท่าไหร่เราก็ยิ่งทำซ้ำคำสั่งสวิตช์ของเรา เราเป็นมนุษย์และเรามักจะทำผิดพลาด ดังนั้นเมื่อใดก็ตามที่เราสามารถพึ่งพาคอมไพเลอร์เพื่อเตือนเราถึงข้อผิดพลาดเราควรทำสิ่งนี้
หมายเหตุสำคัญที่สองเกี่ยวกับการออกแบบทั้งสองนี้คือพวกเขาเทียบเท่าภายนอก ตัวอย่างเช่นหากเราต้องการตรวจสอบอัตราดอกเบี้ยสำหรับบัญชีตรวจสอบการออกแบบแบบมีเงื่อนไขจะมีลักษณะเช่นนี้:
BankAccount CheckingAccount = ใหม่ BankAccount (BankAccountType.Checking); System.out.println (checkingaccount.getinterestrate ()); // เอาท์พุท: 0.03
การออกแบบ polymorphic จะคล้ายกับต่อไปนี้:
bankAccount checkingAccount = new CheckingActount (); System.out.println (checkingAccount.getInterestrate ()); // เอาท์พุท: 0.03
จากมุมมองภายนอกเราเพียงแค่เรียก getIntereunk () บนวัตถุ BankAccount สิ่งนี้จะชัดเจนยิ่งขึ้นถ้าเราเป็นนามธรรมกระบวนการสร้างในชั้นเรียนโรงงาน:
Public Class ConditionalAccountFactory {สาธารณะ BankAccount CreateCheckingAccount () {ส่งคืน BankAccount ใหม่ (BankAccountType.Checking); }} คลาสสาธารณะ PolymorphicAccountFactory {สาธารณะ bankAccount createCheckingAccount () {ส่งคืนใหม่ CheckingAcnount (); }} // ในทั้งสองกรณีเราสร้างบัญชีโดยใช้ FactoryBankAccount ConditionalCheckingAccount = ConditionalAccountFactory.CreateCheckkingAccount (); BankAccount PolymorphicCheckingAccount = PolymorphicaccountFactory samesystem.out.println (ConditionalCheckingAccount.getInterestrate ()); // เอาท์พุท: 0.03System.out.println (polymorphiccheckingaccount.getinterestrate ()); // เอาท์พุท: 0.03เป็นเรื่องธรรมดามากที่จะแทนที่ตรรกะแบบมีเงื่อนไขด้วยคลาส polymorphic ดังนั้นวิธีการได้รับการเผยแพร่เพื่อสร้างคำสั่งแบบมีเงื่อนไขในคลาส polymorphic นี่คือตัวอย่างง่ายๆ นอกจากนี้การ refactoring ของ Martin Fowler (หน้า 255) ยังอธิบายถึงกระบวนการโดยละเอียดของการดำเนินการฟื้นฟูนี้
เช่นเดียวกับเทคนิคอื่น ๆ ในบทความนี้ไม่มีกฎที่ยากและรวดเร็วเมื่อใดที่จะทำการเปลี่ยนจากตรรกะตามเงื่อนไขเป็นคลาส polymorphic ในความเป็นจริงเราไม่แนะนำให้ใช้มันในทุกสถานการณ์ ในการออกแบบที่ขับเคลื่อนด้วยการทดสอบ: ตัวอย่างเช่นเคนต์เบ็คออกแบบระบบสกุลเงินที่เรียบง่ายโดยมีเป้าหมายในการใช้คลาส polymorphic แต่พบว่าสิ่งนี้ทำให้การออกแบบซับซ้อนเกินไปและออกแบบการออกแบบของเขาให้เป็นสไตล์ที่ไม่ใช่โพลิมอร์ฟิค ประสบการณ์และการตัดสินที่สมเหตุสมผลจะกำหนดเวลาที่เหมาะสมในการแปลงรหัสเงื่อนไขเป็นรหัส polymorphic
บทสรุป
ในฐานะโปรแกรมเมอร์แม้ว่าเทคนิคทั่วไปที่ใช้ในเวลาปกติสามารถแก้ปัญหาได้มากที่สุด แต่บางครั้งเราควรทำลายกิจวัตรนี้และต้องการนวัตกรรมบางอย่าง ท้ายที่สุดในฐานะนักพัฒนาซอฟต์แวร์การขยายความกว้างและความลึกของความรู้ของเขาไม่เพียง แต่ช่วยให้เราสามารถตัดสินใจอย่างชาญฉลาด แต่ยังทำให้เราฉลาดขึ้น