เนื้อหา
- การแนะนำ
- อินเทอร์เฟซ
- เครื่องหมายอินเทอร์เฟซ
- ส่วนต่อประสานการทำงาน วิธีการแบบสแตติก และวิธีการเริ่มต้น
- ชั้นเรียนที่เป็นนามธรรม
- คลาสที่ไม่เปลี่ยนรูป (ถาวร)
- ชั้นเรียนที่ไม่ระบุชื่อ
- ทัศนวิสัย
- มรดก
- มรดกหลายรายการ
- มรดกและองค์ประกอบ
- การห่อหุ้ม
- คลาสสุดท้ายและวิธีการ
- อะไรต่อไป
- ดาวน์โหลดซอร์สโค้ด
1. บทนำ
ไม่ว่าคุณจะใช้ภาษาโปรแกรมอะไร (และ Java ก็ไม่มีข้อยกเว้น) การทำตามหลักการออกแบบที่ดีคือกุญแจสำคัญในการเขียนโค้ดที่สะอาด เข้าใจได้ และตรวจสอบได้ และยังสร้างให้มีอายุยืนยาวและรองรับการแก้ปัญหาได้ง่ายอีกด้วย ในส่วนนี้ของบทช่วยสอน เราจะหารือเกี่ยวกับ Building Block พื้นฐานที่ภาษา Java มีให้ และแนะนำหลักการออกแบบสองสามประการเพื่อช่วยให้คุณตัดสินใจออกแบบได้ดีขึ้น โดยเฉพาะอย่างยิ่ง เราจะหารือเกี่ยวกับอินเทอร์เฟซและอินเทอร์เฟซโดยใช้วิธีการเริ่มต้น (คุณลักษณะใหม่ใน Java 8) คลาสนามธรรมและคลาสสุดท้าย คลาสที่ไม่เปลี่ยนรูป การสืบทอด องค์ประกอบ และทบทวนกฎการมองเห็น (หรือการเข้าถึง) ที่เรากล่าวถึงสั้น ๆ ใน ส่วนที่ 1 บทเรียน
“วิธีสร้างและทำลายวัตถุ” .
2. อินเทอร์เฟซ
ในการเขียนโปรแกรม เชิงวัตถุแนวคิดของอินเทอร์เฟซเป็นพื้นฐานสำหรับการพัฒนาสัญญา โดยสรุป อินเทอร์เฟซกำหนดชุดของวิธีการ (สัญญา) และแต่ละคลาสที่ต้องการการสนับสนุนสำหรับอินเทอร์เฟซเฉพาะนั้นจะต้องจัดให้มีการนำวิธีการเหล่านั้นไปใช้: เป็นแนวคิดที่ค่อนข้างเรียบง่ายแต่ทรงพลัง ภาษาการเขียนโปรแกรมจำนวนมากมีอินเทอร์เฟซในรูปแบบเดียวหรืออีกรูปแบบหนึ่ง แต่ Java โดยเฉพาะให้การสนับสนุนภาษาสำหรับสิ่งนี้ มาดูคำจำกัดความอินเทอร์เฟซอย่างง่ายใน Java กัน
package com.javacodegeeks.advanced.design;
public interface SimpleInterface {
void performAction();
}
ในตัวอย่างข้างต้น อินเทอร์เฟซที่เราเรียกว่า
SimpleInterface
ประกาศเพียงวิธีเดียวที่เรียก
performAction
ว่า ข้อแตกต่างที่สำคัญระหว่างอินเทอร์เฟซและคลาสก็คือ อินเทอร์เฟซจะสรุปว่าผู้ติดต่อควรเป็นอย่างไร (ประกาศวิธีการ) แต่ไม่ได้จัดให้มีการใช้งาน อย่างไรก็ตาม อินเทอร์เฟซใน Java อาจมีความซับซ้อนมากขึ้น โดยอาจรวมถึงอินเทอร์เฟซแบบซ้อน คลาส จำนวน จำนวน คำอธิบายประกอบ และค่าคงที่ ตัวอย่างเช่น:
package com.javacodegeeks.advanced.design;
public interface InterfaceWithDefinitions {
String CONSTANT = "CONSTANT";
enum InnerEnum {
E1, E2;
}
class InnerClass {
}
interface InnerInterface {
void performInnerAction();
}
void performAction();
}
ในตัวอย่างที่ซับซ้อนกว่านี้ มีข้อจำกัดหลายประการที่อินเทอร์เฟซกำหนดโดยไม่มีเงื่อนไขกับโครงสร้างการซ้อนและการประกาศเมธอด และสิ่งเหล่านี้บังคับใช้โดยคอมไพเลอร์ Java ประการแรก แม้ว่าจะไม่ได้ประกาศอย่างชัดเจน แต่การประกาศวิธีการทุกรายการในอินเทอร์เฟซจะเป็น
แบบสาธารณะ (และสามารถเป็นแบบสาธารณะได้เท่านั้น) ดังนั้นการประกาศวิธีการต่อไปนี้จึงเทียบเท่ากัน:
public void performAction();
void performAction();
เป็นที่น่าสังเกตว่าทุก ๆ วิธีการในอินเทอร์เฟซได้รับการประกาศโดยปริยาย
abstractและแม้แต่การประกาศวิธีการเหล่านี้ก็ยังเทียบเท่ากัน:
public abstract void performAction();
public void performAction();
void performAction();
สำหรับฟิลด์คงที่ที่ประกาศไว้ นอกเหนือจากการเป็น
สาธารณะแล้ว ฟิลด์เหล่านั้นยังเป็น
แบบคงที่ โดยปริยาย และทำเครื่องหมายเป็น
ขั้นสุดท้าย ด้วย ดังนั้นการประกาศต่อไปนี้จึงเทียบเท่ากันด้วย:
String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";
สุดท้ายนี้ คลาสที่ซ้อนกัน อิน เท อร์เฟซ หรือการนับ นอกเหนือจากการเป็น
สาธารณะยังได้รับการประกาศโดยปริยาย
static ด้วย ตัวอย่างเช่น การประกาศเหล่านี้เทียบเท่ากับ:
class InnerClass {
}
static class InnerClass {
}
สไตล์ที่คุณเลือกนั้นเป็นความชอบส่วนตัว แต่การรู้คุณสมบัติง่ายๆ ของอินเทอร์เฟซเหล่านี้สามารถช่วยคุณประหยัดจากการพิมพ์ที่ไม่จำเป็นได้
3. เครื่องหมายอินเทอร์เฟซ
อินเทอร์เฟซของมาร์กเกอร์เป็นอินเทอร์เฟซชนิดพิเศษที่ไม่มีเมธอดหรือโครงสร้างที่ซ้อนกันอื่นๆ วิธีที่ไลบรารี Java กำหนด:
public interface Cloneable {
}
เครื่องหมายอินเทอร์เฟซไม่ใช่สัญญาต่อตัว แต่เป็นเทคนิคที่ค่อนข้างมีประโยชน์สำหรับการ "แนบ" หรือ "เชื่อมโยง" ลักษณะเฉพาะบางอย่างกับคลาส ตัวอย่างเช่น ในส่วนที่เกี่ยวกับ
Cloneableคลาสจะถูกทำเครื่องหมายว่า cloneable แต่วิธีที่สามารถทำได้หรือควรจะนำไปใช้นั้นไม่ได้เป็นส่วนหนึ่งของอินเทอร์เฟซ อีกตัวอย่างที่มีชื่อเสียงและใช้กันอย่างแพร่หลายของเครื่องหมายอินเทอร์เฟซคือ
Serializable
:
public interface Serializable {
}
อินเทอร์เฟซนี้ทำเครื่องหมายคลาสว่าเหมาะสำหรับการซีเรียลไลซ์และการดีซีเรียลไลซ์ และอีกครั้ง ไม่ได้ระบุว่าสิ่งนี้สามารถหรือควรนำไปใช้อย่างไร เครื่องหมายอินเทอร์เฟซมีตำแหน่งในการเขียนโปรแกรมเชิงวัตถุ แม้ว่าจะไม่เป็นไปตามวัตถุประสงค์หลักของอินเทอร์เฟซที่เป็นสัญญาก็ตาม
4. อินเตอร์เฟซการทำงาน วิธีการเริ่มต้น และวิธีการแบบคงที่
นับตั้งแต่เปิดตัว Java 8 อินเทอร์เฟซได้รับคุณสมบัติใหม่ที่น่าสนใจมากมาย: วิธีการคงที่ วิธีการเริ่มต้น และการแปลงอัตโนมัติจาก lambdas (อินเทอร์เฟซการทำงาน) ในส่วนของอินเทอร์เฟซ เราเน้นย้ำถึงข้อเท็จจริงที่ว่าอินเทอร์เฟซใน Java สามารถประกาศวิธีการได้เท่านั้น แต่ไม่ได้จัดให้มีการใช้งาน ด้วยวิธีการเริ่มต้น สิ่งต่างๆ จะแตกต่างออกไป: อินเทอร์เฟซสามารถทำเครื่องหมายวิธีการด้วย คำหลัก
เริ่มต้นและจัดเตรียมการนำไปปฏิบัติ ตัวอย่างเช่น:
package com.javacodegeeks.advanced.design;
public interface InterfaceWithDefaultMethods {
void performAction();
default void performDefaulAction() {
}
}
เมื่ออยู่ที่ระดับอินสแตนซ์ วิธีการเริ่มต้นอาจถูกแทนที่โดยการใช้อินเทอร์เฟซแต่ละครั้ง แต่ขณะนี้อินเทอร์เฟซยังสามารถรวม วิธีการ
คงที่ได้เช่น: package com.javacodegeeks.advanced.design;
public interface InterfaceWithDefaultMethods {
static void createAction() {
}
}
อาจกล่าวได้ว่าการติดตั้งใช้งานในอินเทอร์เฟซขัดขวางวัตถุประสงค์ทั้งหมดของการเขียนโปรแกรมตามสัญญา แต่มีสาเหตุหลายประการว่าทำไมคุณลักษณะเหล่านี้จึงถูกนำมาใช้ในภาษา Java และไม่ว่าคุณลักษณะเหล่านี้จะมีประโยชน์หรือน่าสับสนเพียงใด คุณลักษณะเหล่านี้ก็พร้อมสำหรับคุณและการใช้งานของคุณ อินเทอร์เฟซการใช้งานเป็นเรื่องราวที่แตกต่างไปจากเดิมอย่างสิ้นเชิงและได้รับการพิสูจน์แล้วว่าเป็นส่วนเสริมที่มีประโยชน์มากสำหรับภาษา โดยพื้นฐานแล้ว ส่วนต่อประสานการทำงานคือส่วนต่อประสานที่มีวิธีนามธรรมเพียงวิธีเดียวที่ประกาศไว้
Runnable
อินเทอร์เฟซไลบรารีมาตรฐานเป็นตัวอย่างที่ดีของแนวคิดนี้
@FunctionalInterface
public interface Runnable {
void run();
}
คอมไพเลอร์ Java ปฏิบัติต่ออินเทอร์เฟซการทำงานที่แตกต่างกัน และสามารถเปลี่ยนฟังก์ชันแลมบ์ดาให้เป็นการใช้งานอินเทอร์เฟซการทำงานที่เหมาะสม ลองพิจารณาคำอธิบายฟังก์ชันต่อไปนี้:
public void runMe( final Runnable r ) {
r.run();
}
หากต้องการเรียกใช้ฟังก์ชันนี้ใน Java 7 และต่ำกว่า จะต้องจัดให้มีการใช้งานอินเทอร์เฟซ
Runnable
(เช่น การใช้คลาสที่ไม่ระบุชื่อ) แต่ใน Java 8 ก็เพียงพอแล้วที่จะจัดให้มีการใช้งานเมธอด run() โดยใช้ไวยากรณ์แลมบ์ดา:
runMe( () -> System.out.println( "Run!" ) );
นอกจากนี้ คำอธิบายประกอบ
@FunctionalInterface (คำอธิบายประกอบจะกล่าวถึงรายละเอียดในส่วนที่ 5 ของบทช่วยสอน) บอกเป็นนัยว่าคอมไพเลอร์สามารถตรวจสอบได้ว่าอินเทอร์เฟซมีเมธอดนามธรรมเพียงวิธีเดียวหรือไม่ ดังนั้นการเปลี่ยนแปลงใด ๆ ที่เกิดขึ้นกับอินเทอร์เฟซในอนาคตจะไม่ละเมิดสมมติฐานนี้ .
5. ชั้นเรียนบทคัดย่อ
แนวคิดที่น่าสนใจอีกประการหนึ่งที่สนับสนุนโดยภาษา Java คือแนวคิดของคลาสนามธรรม คลาสนามธรรมค่อนข้างคล้ายกับอินเทอร์เฟซใน Java 7 และอยู่ใกล้กับอินเทอร์เฟซวิธีการเริ่มต้นใน Java 8 มาก คลาสนามธรรมไม่สามารถสร้างอินสแตนซ์ได้ซึ่งแตกต่างจากคลาสทั่วไป แต่สามารถจัดคลาสย่อยได้ (โปรดดูที่ส่วนการสืบทอดสำหรับรายละเอียดเพิ่มเติม) ที่สำคัญกว่านั้น คลาสนามธรรมสามารถมีวิธีการแบบนามธรรมได้: วิธีการชนิดพิเศษที่ไม่มีการใช้งาน เช่นเดียวกับอินเทอร์เฟซ ตัวอย่างเช่น:
package com.javacodegeeks.advanced.design;
public abstract class SimpleAbstractClass {
public void performAction() {
}
public abstract void performAnotherAction();
}
ในตัวอย่างนี้ คลาส
SimpleAbstractClass
ถูกประกาศให้เป็น
นามธรรมและมีวิธีการประกาศแบบนามธรรมหนึ่งวิธี คลาสนามธรรมมีประโยชน์มาก รายละเอียดการใช้งานส่วนใหญ่หรือบางส่วนสามารถแชร์ระหว่างคลาสย่อยจำนวนมากได้ แต่อย่างไรก็ตาม พวกเขายังคงแง้มประตูเอาไว้ และอนุญาตให้คุณปรับแต่งพฤติกรรมที่มีอยู่ในแต่ละคลาสย่อยโดยใช้วิธีนามธรรม เป็นที่น่าสังเกตว่าไม่เหมือนกับอินเทอร์เฟซซึ่งสามารถมีเพียง การประกาศ
สาธารณะ เท่านั้น คลาสนามธรรมสามารถใช้กฎการเข้าถึงแบบเต็มกำลังเพื่อควบคุมการมองเห็นของวิธีนามธรรม
6. ชั้นเรียนทันที
ความไม่เปลี่ยนรูปมีความสำคัญมากขึ้นเรื่อยๆ ในการพัฒนาซอฟต์แวร์ในปัจจุบัน การเพิ่มขึ้นของระบบมัลติคอร์ทำให้เกิดปัญหามากมายที่เกี่ยวข้องกับการแบ่งปันข้อมูลและความเท่าเทียม แต่ปัญหาหนึ่งเกิดขึ้นอย่างแน่นอน: การมีสถานะที่ไม่แน่นอนเพียงเล็กน้อย (หรือไม่มีเลย) นำไปสู่การขยายที่ดีขึ้น (ความสามารถในการปรับขนาด) และการให้เหตุผลที่ง่ายขึ้นเกี่ยวกับระบบ น่าเสียดายที่ภาษา Java ไม่ได้ให้การสนับสนุนที่เหมาะสมสำหรับความไม่เปลี่ยนรูปของคลาส อย่างไรก็ตาม ด้วยการผสมผสานเทคนิคต่างๆ ทำให้สามารถออกแบบคลาสที่ไม่เปลี่ยนรูปได้ ก่อนอื่น ทุกสาขาของชั้นเรียนจะต้องถือเป็นที่สิ้นสุด (ทำเครื่องหมายว่าเป็น
ที่สิ้นสุด ) นี่เป็นการเริ่มต้นที่ดี แต่ก็ไม่มีการรับประกัน
package com.javacodegeeks.advanced.design;
import java.util.Collection;
public class ImmutableClass {
private final long id;
private final String[] arrayOfStrings;
private final Collection<String> collectionOfString;
}
ประการที่สอง ตรวจสอบให้แน่ใจว่ามีการเริ่มต้นที่เหมาะสม: หากฟิลด์มีการอ้างอิงถึงคอลเลกชันหรืออาร์เรย์ อย่ากำหนดฟิลด์เหล่านั้นโดยตรงจากอาร์กิวเมนต์ตัวสร้าง ให้ทำสำเนาแทน สิ่งนี้จะช่วยให้มั่นใจได้ว่าสถานะของคอลเลกชันหรืออาเรย์จะไม่ถูกแก้ไขภายนอก
public ImmutableClass( final long id, final String[] arrayOfStrings,
final Collection<String> collectionOfString) {
this.id = id;
this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
this.collectionOfString = new ArrayList<>( collectionOfString );
}
และสุดท้าย รับรองการเข้าถึงที่เหมาะสม (getters) สำหรับคอลเลกชัน ความไม่เปลี่ยนรูปจะต้องระบุเป็น wrapper
Collections.unmodifiableXxx
: สำหรับอาร์เรย์ วิธีเดียวที่จะระบุความไม่เปลี่ยนรูปได้อย่างแท้จริงคือการจัดทำสำเนาแทนที่จะส่งคืนการอ้างอิงไปยังอาร์เรย์ สิ่งนี้อาจไม่เป็นที่ยอมรับจากมุมมองเชิงปฏิบัติ เนื่องจากมันขึ้นอยู่กับขนาดของอาเรย์เป็นอย่างมาก และอาจสร้างแรงกดดันมหาศาลให้กับผู้รวบรวมขยะ
public String[] getArrayOfStrings() {
return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}
แม้แต่ตัวอย่างเล็กๆ น้อยๆ นี้ก็ยังให้ความคิดที่ดีว่าการเปลี่ยนแปลงไม่ได้ยังไม่ใช่พลเมืองชั้นหนึ่งในชวา สิ่งต่างๆ อาจมีความซับซ้อนได้หากคลาสที่ไม่เปลี่ยนรูปมีฟิลด์ที่อ้างอิงถึงอ็อบเจ็กต์ของคลาสอื่น คลาสเหล่านั้นควรไม่เปลี่ยนรูปเช่นกัน แต่ไม่มีวิธีใดที่จะรับประกันสิ่งนี้ มีโปรแกรมวิเคราะห์ซอร์สโค้ด Java ที่เหมาะสมหลายตัว เช่น FindBugs และ PMD ที่สามารถช่วยได้อย่างมากโดยการตรวจสอบโค้ดของคุณและชี้ให้เห็นข้อบกพร่องทั่วไปในการเขียนโปรแกรม Java เครื่องมือเหล่านี้เป็นเพื่อนที่ดีของนักพัฒนา Java ทุกคน
7. ชั้นเรียนที่ไม่ระบุชื่อ
ในยุคก่อน Java 8 คลาสที่ไม่เปิดเผยตัวตนเป็นวิธีเดียวที่จะให้แน่ใจว่าคลาสถูกกำหนดได้ทันทีและเริ่มต้นทันที วัตถุประสงค์ของคลาสที่ไม่ระบุชื่อคือเพื่อลดรูปแบบสำเร็จรูปและจัดเตรียมวิธีที่สั้นและง่ายในการแสดงคลาสเป็นบันทึก มาดูวิธีดั้งเดิมในการสร้างเธรดใหม่ใน Java:
package com.javacodegeeks.advanced.design;
public class AnonymousClass {
public static void main( String[] args ) {
new Thread(
new Runnable() {
@Override
public void run() {
}
}
).start();
}
}
ในตัวอย่างนี้ การใช้งาน
Runnable
อินเทอร์เฟซจะถูกจัดเตรียมทันทีเป็นคลาสที่ไม่ระบุชื่อ แม้ว่าจะมีข้อจำกัดบางประการที่เกี่ยวข้องกับคลาสที่ไม่ระบุชื่อ แต่ข้อเสียเปรียบหลักของการใช้คลาสเหล่านี้คือไวยากรณ์การสร้างที่ละเอียดมากซึ่ง Java เป็นภาษาจำเป็นต้องใช้ แม้แต่คลาสที่ไม่เปิดเผยตัวตนที่ไม่ได้ทำอะไรเลยก็จำเป็นต้องมีโค้ดอย่างน้อย 5 บรรทัดทุกครั้งที่เขียน
new Runnable() {
@Override
public void run() {
}
}
โชคดีที่มี Java 8, lambda และอินเทอร์เฟซการทำงาน แบบเหมารวมเหล่านี้จะหายไปในไม่ช้า ในที่สุดการเขียนโค้ด Java จะดูกระชับอย่างแท้จริง
package com.javacodegeeks.advanced.design;
public class AnonymousClass {
public static void main( String[] args ) {
new Thread( () -> { } ).start();
}
}
8. การมองเห็น
เราได้พูดคุยกันเล็กน้อยเกี่ยวกับกฎการมองเห็นและการเข้าถึงใน Java ในส่วนที่ 1 ของบทช่วยสอน ในส่วนนี้ เราจะกลับมาทบทวนหัวข้อนี้อีกครั้ง แต่ในบริบทของคลาสย่อย
การมองเห็นในระดับที่แตกต่างกันอนุญาตหรือป้องกันไม่ให้คลาสมองเห็นคลาสหรืออินเทอร์เฟซอื่น ๆ (เช่น หากอยู่ในแพ็คเกจที่แตกต่างกันหรือซ้อนกันภายในกันและกัน) หรือคลาสย่อยไม่ให้เห็นและเข้าถึงเมธอด ตัวสร้าง และฟิลด์ของพาเรนต์ ในส่วนถัดไป เรื่องการสืบทอด เราจะเห็นการดำเนินการนี้
9. มรดก
การสืบทอดเป็นหนึ่งในแนวคิดหลักของการเขียนโปรแกรมเชิงวัตถุ ซึ่งทำหน้าที่เป็นพื้นฐานสำหรับการสร้างคลาสของความสัมพันธ์ เมื่อรวมกับกฎการมองเห็นและการเข้าถึง การสืบทอดจะช่วยให้คลาสได้รับการออกแบบเป็นลำดับชั้นที่สามารถขยายและดูแลรักษาได้ ในระดับแนวคิด การสืบทอดใน Java จะถูกนำไปใช้โดยใช้คลาสย่อยและ คีย์เวิร์ด
ขยายร่วมกับคลาสพาเรนต์ คลาสย่อยสืบทอดองค์ประกอบสาธารณะและองค์ประกอบที่ได้รับการป้องกันทั้งหมดของคลาสพาเรนต์ นอกจากนี้ คลาสย่อยจะสืบทอดองค์ประกอบแพ็กเกจส่วนตัวของคลาสพาเรนต์ หากทั้งสองคลาส (คลาสย่อยและคลาส) อยู่ในแพ็กเกจเดียวกัน ดังที่กล่าวไปแล้ว เป็นสิ่งสำคัญมาก ไม่ว่าคุณจะพยายามออกแบบอะไรก็ตาม จะต้องยึดติดกับชุดวิธีการขั้นต่ำที่คลาสเปิดเผยต่อสาธารณะหรือคลาสย่อย ตัวอย่างเช่น ลองดูที่คลาส
Parent
และคลาสย่อย
Child
เพื่อแสดงให้เห็นความแตกต่างในระดับการมองเห็นและเอฟเฟกต์
package com.javacodegeeks.advanced.design;
public class Parent {
public static final String CONSTANT = "Constant";
private String privateField;
protected String protectedField;
private class PrivateClass {
}
protected interface ProtectedInterface {
}
public void publicAction() {
}
protected void protectedAction() {
}
private void privateAction() {
}
void packageAction() {
}
}
package com.javacodegeeks.advanced.design;
public class Child extends Parent implements Parent.ProtectedInterface {
@Override
protected void protectedAction() {
super.protectedAction();
}
@Override
void packageAction() {
}
public void childAction() {
this.protectedField = "value";
}
}
Inheritance เป็นหัวข้อที่ใหญ่มากในตัวเอง โดยมีรายละเอียดเล็กๆ น้อยๆ มากมายเกี่ยวกับ Java โดยเฉพาะ อย่างไรก็ตาม มีกฎบางประการที่ง่ายต่อการปฏิบัติตามและสามารถช่วยรักษาลำดับชั้นของชั้นเรียนให้สั้นกระชับได้ ใน Java แต่ละคลาสย่อยสามารถแทนที่เมธอดที่สืบทอดมาจากพาเรนต์ของมันได้ เว้นแต่จะได้รับการประกาศขั้นสุดท้าย อย่างไรก็ตาม ไม่มีไวยากรณ์หรือคีย์เวิร์ดพิเศษในการทำเครื่องหมายวิธีการว่าถูกแทนที่ ซึ่งอาจนำไปสู่ความสับสนได้
นี่คือสาเหตุว่าทำไมจึงมีการนำคำอธิบาย ประกอบ @Overrideมาใช้: เมื่อใดก็ตามที่เป้าหมายของคุณคือการแทนที่วิธีที่สืบทอดมา โปรดใช้ คำอธิบายประกอบ
@Overrideเพื่อระบุอย่างชัดเจน ภาวะที่กลืนไม่เข้าคายไม่ออกอีกประการหนึ่งที่นักพัฒนา Java เผชิญอยู่ตลอดเวลาในการออกแบบคือการสร้างลำดับชั้นของคลาส (ด้วยคลาสที่เป็นรูปธรรมหรือนามธรรม) เทียบกับการใช้งานอินเทอร์เฟซ เราขอแนะนำอย่างยิ่งให้เลือกใช้อินเทอร์เฟซมากกว่าคลาสหรือคลาสนามธรรมทุกครั้งที่เป็นไปได้ อินเทอร์เฟซมีน้ำหนักเบา ทดสอบและบำรุงรักษาง่ายกว่า และยังลดผลข้างเคียงจากการเปลี่ยนแปลงการใช้งานอีกด้วย เทคนิคการเขียนโปรแกรมขั้นสูงหลายอย่าง เช่น การสร้างคลาสพร็อกซีในไลบรารีมาตรฐาน Java อาศัยอินเทอร์เฟซอย่างมาก
10. มรดกหลายรายการ
ไม่เหมือนกับ C++ และภาษาอื่นๆ บางภาษา Java ไม่รองรับการสืบทอดหลายรายการ โดยใน Java แต่ละคลาสสามารถมีพาเรนต์โดยตรงได้เพียงพาเรนต์เดียวเท่านั้น (โดยมีคลาส
Object
อยู่ด้านบนสุดของลำดับชั้น) อย่างไรก็ตาม คลาสสามารถใช้หลายอินเทอร์เฟซได้ ดังนั้นการซ้อนอินเทอร์เฟซจึงเป็นวิธีเดียวที่จะบรรลุ (หรือจำลอง) การสืบทอดหลายรายการใน Java
package com.javacodegeeks.advanced.design;
public class MultipleInterfaces implements Runnable, AutoCloseable {
@Override
public void run() {
}
@Override
public void close() throws Exception {
}
}
การใช้งานหลายอินเทอร์เฟซนั้นค่อนข้างทรงพลัง แต่บ่อยครั้งที่ความจำเป็นในการใช้งานซ้ำแล้วซ้ำเล่านำไปสู่ลำดับชั้นของคลาสระดับลึกซึ่งเป็นวิธีการเอาชนะการขาดการสนับสนุนของ Java สำหรับการสืบทอดหลายรายการ
public class A implements Runnable {
@Override
public void run() {
}
}
public class B extends A implements AutoCloseable {
@Override
public void close() throws Exception {
}
}
public class C extends B implements Readable {
@Override
public int read(java.nio.CharBuffer cb) throws IOException {
}
}
และอื่นๆ... Java 8 รุ่นล่าสุดค่อนข้างจะแก้ไขปัญหาเกี่ยวกับการฉีดเมธอดเริ่มต้น เนื่องจากวิธีการเริ่มต้น อินเทอร์เฟซไม่เพียงแต่ให้สัญญาเท่านั้น แต่ยังรวมถึงการใช้งานด้วย ดังนั้นคลาสที่ใช้อินเทอร์เฟซเหล่านี้จะสืบทอดวิธีการนำไปใช้เหล่านี้โดยอัตโนมัติ ตัวอย่างเช่น:
package com.javacodegeeks.advanced.design;
public interface DefaultMethods extends Runnable, AutoCloseable {
@Override
default void run() {
}
@Override
default void close() throws Exception {
}
}
public class C implements DefaultMethods, Readable {
@Override
public int read(java.nio.CharBuffer cb) throws IOException {
}
}
โปรดทราบว่าการสืบทอดหลายรายการเป็นเครื่องมือที่ทรงพลังมาก แต่ในขณะเดียวกันก็เป็นอันตราย ปัญหา Diamond of Death ที่รู้จักกันดีมักถูกอ้างถึงว่าเป็นข้อบกพร่องสำคัญในการใช้งานการสืบทอดหลายรายการ ทำให้นักพัฒนาต้องออกแบบลำดับชั้นของคลาสอย่างระมัดระวัง น่าเสียดายที่อินเทอร์เฟซ Java 8 พร้อมวิธีการเริ่มต้นก็ตกเป็นเหยื่อของข้อบกพร่องเหล่านี้เช่นกัน
interface A {
default void performAction() {
}
}
interface B extends A {
@Override
default void performAction() {
}
}
interface C extends A {
@Override
default void performAction() {
}
}
ตัวอย่างเช่น ข้อมูลโค้ดต่อไปนี้จะไม่สามารถคอมไพล์ได้:
interface E extends B, C {
}
ณ จุดนี้ มันยุติธรรมที่จะกล่าวว่า Java ในฐานะภาษาหนึ่งพยายามหลีกเลี่ยงกรณีมุมของการเขียนโปรแกรมเชิงวัตถุมาโดยตลอด แต่เมื่อภาษาพัฒนาขึ้น บางกรณีเหล่านั้นก็เริ่มปรากฏขึ้นทันที
11. มรดกและองค์ประกอบ
โชคดีที่การสืบทอดไม่ใช่วิธีเดียวในการออกแบบชั้นเรียนของคุณ อีกทางเลือกหนึ่งที่นักพัฒนาหลายคนเชื่อว่าดีกว่าการสืบทอดมากก็คือการจัดองค์ประกอบ แนวคิดนี้ง่ายมาก: แทนที่จะสร้างลำดับชั้นของคลาส คลาสเหล่านั้นจำเป็นต้องประกอบด้วยคลาสอื่น ลองดูตัวอย่างนี้:
interface E extends B, C {
}
ชั้นเรียน
Vehicle
ประกอบด้วยเครื่องยนต์และล้อ (รวมถึงชิ้นส่วนอื่นๆ อีกมากมายที่ถูกละไว้เพื่อความเรียบง่าย) อย่างไรก็ตาม อาจกล่าวได้ว่าคลาส
Vehicle
ก็เป็นกลไกเช่นกัน ดังนั้นจึงสามารถออกแบบโดยใช้การสืบทอดได้
public class Vehicle extends Engine {
private Wheels[] wheels;
}
โซลูชันการออกแบบใดที่จะถูกต้อง หลักเกณฑ์หลักทั่วไปเรียกว่าหลักการ IS-A (เป็น) และ HAS-A (ประกอบด้วย) IS-A เป็นความสัมพันธ์แบบสืบทอด: คลาสย่อยยังเป็นไปตามข้อกำหนดคลาสของคลาสพาเรนต์และการเปลี่ยนแปลงของคลาสพาเรนต์ คลาสย่อย) จะขยายพาเรนต์ หากคุณต้องการทราบว่าเอนทิตีหนึ่งขยายเอนทิตีอื่นหรือไม่ ให้ทำการทดสอบการจับคู่ - IS -A (is).") ดังนั้น HAS-A จึงเป็นความสัมพันธ์ในการเรียบเรียง: คลาสเป็นเจ้าของ (หรือมี) อ็อบเจ็กต์ที่ ในกรณีส่วนใหญ่ หลักการ HAS-A ทำงานได้ดีกว่า IS-A ด้วยเหตุผลหลายประการ:
- การออกแบบมีความยืดหยุ่นมากขึ้น
- โมเดลมีเสถียรภาพมากขึ้นเนื่องจากการเปลี่ยนแปลงไม่ได้เผยแพร่ผ่านลำดับชั้นของคลาส
- คลาสและองค์ประกอบของคลาสนั้นเชื่อมโยงกันอย่างหลวมๆ เมื่อเปรียบเทียบกับการจัดองค์ประกอบ ซึ่งเชื่อมโยงคลาสพาเรนต์และคลาสย่อยเข้าด้วยกันอย่างแน่นหนา
- การฝึกคิดเชิงตรรกะในชั้นเรียนนั้นง่ายกว่า เนื่องจากการพึ่งพาทั้งหมดรวมอยู่ในนั้นในที่เดียว
โดยไม่คำนึงว่ามรดกจะเข้ามาแทนที่และแก้ไขปัญหาการออกแบบจำนวนหนึ่งที่มีอยู่ในหลายวิธี ดังนั้นจึงไม่ควรละเลย โปรดคำนึงถึงทางเลือกทั้งสองนี้เมื่อออกแบบโมเดลเชิงวัตถุของคุณ
12. การห่อหุ้ม
แนวคิดของการห่อหุ้มในการเขียนโปรแกรมเชิงวัตถุคือการซ่อนรายละเอียดการใช้งานทั้งหมด (เช่น โหมดการทำงาน วิธีการภายใน ฯลฯ) จากโลกภายนอก ประโยชน์ของการห่อหุ้มคือการบำรุงรักษาและความง่ายในการเปลี่ยนแปลง การใช้งานภายในของคลาสถูกซ่อนไว้ การทำงานกับข้อมูลคลาสจะเกิดขึ้นผ่านวิธีการสาธารณะของคลาสเท่านั้น (ปัญหาที่แท้จริงหากคุณกำลังพัฒนาไลบรารีหรือเฟรมเวิร์กที่คนจำนวนมากใช้) การห่อหุ้มใน Java ทำได้ผ่านกฎการมองเห็นและการเข้าถึง ใน Java ถือเป็นแนวปฏิบัติที่ดีที่สุดที่จะไม่เปิดเผยฟิลด์โดยตรง เฉพาะผ่าน getters และ setters เท่านั้น (เว้นแต่ว่าฟิลด์นั้นจะถูกทำเครื่องหมายเป็นขั้นสุดท้าย) ตัวอย่างเช่น:
package com.javacodegeeks.advanced.design;
public class Encapsulation {
private final String email;
private String address;
public Encapsulation( final String email ) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getEmail() {
return email;
}
}
ตัวอย่างนี้ชวนให้นึกถึงสิ่งที่เรียกว่า
JavaBeansในภาษา Java: คลาส Java มาตรฐานถูกเขียนตามชุดของแบบแผน ซึ่งหนึ่งในนั้นอนุญาตให้เข้าถึงฟิลด์ได้โดยใช้เมธอด getter และ setter เท่านั้น ตามที่เราได้เน้นไปแล้วในส่วนการรับมรดก โปรดปฏิบัติตามสัญญาประชาสัมพันธ์ขั้นต่ำในชั้นเรียนเสมอ โดยใช้หลักการห่อหุ้ม ทุกสิ่งที่ไม่ควรเปิดเผยต่อสาธารณะควรกลายเป็นส่วนตัว (หรือป้องกัน/แพ็คเกจส่วนตัว ขึ้นอยู่กับปัญหาที่คุณกำลังแก้ไข) สิ่งนี้จะคุ้มค่าในระยะยาวด้วยการให้อิสระแก่คุณในการออกแบบโดยไม่ต้อง (หรืออย่างน้อยก็ลดทอน) การเปลี่ยนแปลงที่เสียหาย
13. ชั้นเรียนสุดท้ายและวิธีการ
ใน Java มีวิธีการป้องกันไม่ให้คลาสกลายเป็นคลาสย่อยของคลาสอื่น: คลาสอื่นจะต้องถูกประกาศให้เป็นที่สิ้นสุด
package com.javacodegeeks.advanced.design;
public final class FinalClass {
}
คำสำคัญ
สุดท้าย เดียวกัน ในการประกาศวิธีการป้องกันไม่ให้คลาสย่อยแทนที่วิธีการ
package com.javacodegeeks.advanced.design;
public class FinalMethod {
public final void performAction() {
}
}
ไม่มีกฎทั่วไปในการตัดสินใจว่าคลาสหรือเมธอดควรถือเป็นที่สิ้นสุดหรือไม่ คลาสและวิธีการขั้นสุดท้ายจำกัดความสามารถในการขยาย และเป็นเรื่องยากมากที่จะคิดล่วงหน้าว่าคลาสควรหรือไม่ควรสืบทอด หรือควรแทนที่วิธีการในอนาคตหรือไม่ นี่เป็นสิ่งสำคัญอย่างยิ่งสำหรับนักพัฒนาห้องสมุด เนื่องจากการตัดสินใจในการออกแบบเช่นนี้อาจจำกัดความสามารถในการใช้งานของห้องสมุดได้อย่างมาก Java Standard Library มีตัวอย่างคลาสสุดท้ายหลายตัวอย่าง โดยคลาสที่มีชื่อเสียงที่สุดคือคลาส String ในช่วงแรก การตัดสินใจนี้มีขึ้นเพื่อป้องกันไม่ให้นักพัฒนาพยายามหาวิธีแก้ปัญหาที่ "ดีกว่า" ของตนเองในการนำสตริงไปใช้
14. อะไรต่อไป
ในส่วนนี้ของบทเรียน เราได้กล่าวถึงแนวคิดของการเขียนโปรแกรมเชิงวัตถุใน Java นอกจากนี้เรายังดูอย่างรวดเร็วเกี่ยวกับการเขียนโปรแกรมตามสัญญา สัมผัสแนวคิดการทำงานบางอย่าง และดูว่าภาษามีการพัฒนาอย่างไรเมื่อเวลาผ่านไป ในส่วนถัดไปของบทเรียน เราจะมาพบกับยาชื่อสามัญและวิธีที่พวกมันเปลี่ยนวิธีที่เราจัดการกับความปลอดภัยของประเภทในการเขียนโปรแกรม
15. ดาวน์โหลดซอร์สโค้ด
คุณสามารถดาวน์โหลดซอร์สได้ที่นี่ -
Advanced-java-part-3 ที่มา:
วิธีการออกแบบคลาสและ
GO TO FULL VERSION