มันสมเหตุสมผลหรือไม่ที่จะ "โปรแกรมไปยังส่วนต่อประสาน" ใน Java?


9

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

Map<X> map = new TreeMap<X>();

ในโปรแกรมของฉันฉันกำลังเรียกใช้ฟังก์ชั่นmap.pollFirstEntry()ซึ่งไม่ได้ประกาศในMapอินเทอร์เฟซ (และอีกสองสามคนที่อยู่ในMapอินเทอร์เฟซด้วย) ฉันจัดการเพื่อทำสิ่งนี้ได้โดยการส่งไปยังTreeMap<X>ทุกที่ที่ฉันเรียกวิธีนี้เช่น

someEntry = ((TreeMap<X>) map).pollFirstEntry();

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

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


1
ใช้ TreeMap เมื่อคุณต้องการคุณสมบัติ นั่นอาจเป็นตัวเลือกการออกแบบสำหรับเหตุผลเฉพาะดังนั้นจึงควรเป็นส่วนหนึ่งของการนำไปใช้

@ JordanReiter ฉันควรเพิ่มคำถามใหม่ที่มีเนื้อหาเดียวกันหรือมีกลไกการโพสต์ภายใน
jimijazz


2
"ฉันเข้าใจถึงข้อดีของแนวทางการกำหนดค่าเริ่มต้นตามที่อธิบายไว้ข้างต้นสำหรับโปรแกรมขนาดใหญ่" การใช้งานทั่วสถานที่ไม่ได้เปรียบไม่ว่าขนาดของโปรแกรม
Ben Aaronson

คำตอบ:


23

"การเขียนโปรแกรมไปยังอินเทอร์เฟซ" ไม่ได้หมายความว่า "ใช้เวอร์ชันที่เป็นนามธรรมที่สุดเท่าที่จะเป็นไปได้" Objectในกรณีที่ทุกคนก็จะใช้

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


2
TreeMap ไม่ใช่อินเทอร์เฟซ แต่เป็นคลาสการใช้งาน มันใช้อินเทอร์เฟซแผนที่, SortedMap และ NavigableMap วิธีการอธิบายเป็นส่วนหนึ่งของส่วนต่อประสานNavigableMap การใช้ TreeMap จะป้องกันไม่ให้มีการปรับใช้ไปเป็น ConcurrentSkipListMap (ตัวอย่าง) ซึ่งเป็นจุดรวมของการเข้ารหัสไปยังส่วนต่อประสานแทนที่จะใช้งาน

3
@MichaelT: ฉันไม่ได้ดูนามธรรมที่แน่นอนที่เขาต้องการในสถานการณ์เฉพาะนี้ดังนั้นฉันจึงใช้TreeMapเป็นตัวอย่าง "โปรแกรมไปยังอินเทอร์เฟซ" ไม่ควรใช้เป็นอินเทอร์เฟซหรือคลาสนามธรรม - การใช้งานสามารถพิจารณาอินเทอร์เฟซ
Jeroen Vannevel

1
แม้ว่าอินเทอร์เฟซสาธารณะ / วิธีการของคลาสการใช้งานเป็นเทคนิค 'อินเทอร์เฟซ' มันแบ่งแนวคิดที่อยู่เบื้องหลังLSPและป้องกันการทดแทน subclass ที่แตกต่างกันซึ่งเป็นสาเหตุที่คุณต้องการตั้งโปรแกรมให้เป็นpublic interface'วิธีสาธารณะของการใช้งาน' .

@JeroenVannevel ฉันยอมรับว่าการเขียนโปรแกรมไปยังส่วนต่อประสานสามารถทำได้เมื่ออินเทอร์เฟซถูกแสดงโดยคลาส อย่างไรก็ตามฉันไม่เห็นว่าการใช้ประโยชน์TreeMapจะมีมากกว่าSortedMapหรือNavigableMap
toniedzwiedz

16

หากคุณยังต้องการใช้ส่วนต่อประสานที่คุณสามารถใช้ได้

NavigableMap <X, Y> map = new TreeMap<X, Y>();

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


3

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

พิจารณาสถานการณ์ที่รหัสต้นฉบับของคุณใช้HashMapและเปิดเผยว่า

private HashMap foo = new HashMap();
public HashMap getFoo() { return foo; }  // This is bad, don't do this.

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

private Map foo = new HashMap();
public Map getFoo() { return foo; }

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

private NavigableMap foo = new TreeMap();
public Map getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

และนั่นก็ไม่ได้เป็นการทำลายรหัสที่เหลือทั้งหมด

คุณสามารถเสริมความแข็งแกร่งให้สัญญาที่ชั้นเรียนให้โดยไม่ทำลายอะไรก็ได้

private NavigableMap foo = new TreeMap();
public NavigableMap getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

นี่นำเสนอหลักการการทดแทน Liskov

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

เนื่องจาก NavigableMap เป็นประเภทย่อยของ Map การทดแทนนี้สามารถทำได้โดยไม่ต้องเปลี่ยนโปรแกรม

การเปิดเผยประเภทการนำไปปฏิบัติทำให้ยากต่อการเปลี่ยนแปลงวิธีการทำงานของโปรแกรมภายในเมื่อจำเป็นต้องทำการเปลี่ยนแปลง นี่เป็นกระบวนการที่เจ็บปวดและหลายครั้งสร้างวิธีแก้ปัญหาที่น่าเกลียดซึ่งทำหน้าที่สร้างความเจ็บปวดแก่ coder ในภายหลังเท่านั้น (ฉันกำลังดู coder ก่อนหน้านี้ที่เก็บข้อมูลสับระหว่าง LinkedHashMap และ TreeMap ด้วยเหตุผลบางอย่าง - เชื่อฉันทุกครั้งที่ฉัน เห็นชื่อของคุณใน svn ตำหนิฉันกังวล)

คุณยังต้องการหลีกเลี่ยงการรั่วไหลของประเภทการใช้งาน ตัวอย่างเช่นคุณอาจต้องการใช้ ConcurrentSkipListMap แทนเนื่องจากคุณสมบัติบางอย่างหรือคุณชอบjava.util.concurrent.ConcurrentSkipListMapมากกว่าjava.util.TreeMapในคำสั่งนำเข้าหรืออะไรก็ตาม


1

เห็นด้วยกับคำตอบอื่น ๆ ที่คุณควรใช้คลาสทั่วไป (หรืออินเทอร์เฟซ) ที่คุณต้องการบางสิ่งบางอย่างจริง ๆ ในกรณีนี้ TreeMap (หรือเหมือนใครบางคนแนะนำ NavigableMap) แต่ฉันต้องการเพิ่มว่านี่ดีกว่าการส่งไปทุกที่อย่างแน่นอนซึ่งจะเป็นกลิ่นที่ใหญ่กว่ามากในทุกกรณี ดู/programming/4167304/why-should-casting-be-avoidedด้วยเหตุผลบางประการ


1

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

private Map<String, String> processOrderedMap(LinkedHashMap<String, String> input) {
    // ...
}

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

private LinkedHashMap<String,String> processOrderedMap(LinkedHashMap<String,String> input) {
    // ...
}

แน่นอนว่าผู้โทรยังคงปฏิบัติต่อวัตถุส่งคืนMapเป็นเช่นนี้ แต่นั่นไม่ใช่วิธีการของคุณ:

private Map<String, String> output = processOrderedMap(input);

ก้าวถอยหลังไป

คำแนะนำทั่วไปของการเขียนโปรแกรมเพื่ออินเตอร์เฟซเป็น (โดยทั่วไป) บังคับเพราะมักจะเป็นอินเตอร์เฟซที่ให้การรับประกันว่าวัตถุที่ควรจะสามารถที่จะดำเนินการอาคาสัญญา ผู้เริ่มต้นจำนวนมากเริ่มต้นด้วยHashMap<K, V> map = new HashMap<>()และพวกเขาได้รับคำแนะนำให้ประกาศว่าMapเนื่องจาก a HashMapไม่ได้เสนออะไรมากไปกว่าสิ่งที่Mapควรทำ จากนี้พวกเขาจะสามารถเข้าใจได้ (หวังว่า) สาเหตุที่วิธีการของพวกเขาควรใช้MapแทนHashMapและสิ่งนี้ทำให้พวกเขาตระหนักถึงคุณลักษณะการสืบทอดใน OOP

หากต้องการอ้างอิงเพียงหนึ่งบรรทัดจากรายการ Wikipedia ของหลักการที่ทุกคนชื่นชอบเกี่ยวกับหัวข้อนี้:

มันเป็นความหมายมากกว่าความสัมพันธ์ทางวากยสัมพันธ์เพียงเพราะมันตั้งใจที่จะรับประกันการทำงานร่วมกันของความหมายของประเภทในลำดับชั้น ...

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

รหัสทำความสะอาด

ฉันพบว่าสิ่งนี้ทำให้ฉันสามารถเขียนโค้ดที่สะอาดขึ้นได้เช่นกันโดยเฉพาะอย่างยิ่งเมื่อมันมาถึงการทดสอบหน่วย การสร้างHashMapที่มีรายการทดสอบอ่านเพียงคนเดียวต้องใช้เวลามากกว่าสาย (ไม่รวมการใช้งานของการเริ่มต้นดับเบิลรั้ง) Collections.singletonMap()เมื่อฉันได้อย่างง่ายดายสามารถแทนที่ด้วย

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