การปฏิบัติที่ดีหรือไม่ดีในการซ่อนคอลเลกชัน Java ด้วยชื่อคลาสที่มีความหมาย?


46

เมื่อเร็ว ๆ นี้ฉันเคยติดนิสัยคอลเลกชัน Java "กำบัง" ที่มีชื่อคลาสที่เป็นมิตรกับมนุษย์ ตัวอย่างง่ายๆ:

// Facade class that makes code more readable and understandable.
public class WidgetCache extends Map<String, Widget> {
}

หรือ:

// If you saw a ArrayList<ArrayList<?>> being passed around in the code, would you
// run away screaming, or would you actually understand what it is and what
// it represents?
public class Changelist extends ArrayList<ArrayList<SomePOJO>> {
}

เพื่อนร่วมงานคนหนึ่งชี้ให้ฉันเห็นว่านี่เป็นการฝึกฝนที่ไม่ดีและแนะนำความล่าช้า / ความล่าช้ารวมถึงการใช้รูปแบบการต่อต้าน OO ฉันสามารถเข้าใจได้ว่ามันจะนำเสนอค่าใช้จ่ายในระดับเล็กน้อยแต่ไม่สามารถจินตนาการได้เลยว่ามันจะมีความหมายอะไร ดังนั้นฉันถาม: นี่คือสิ่งที่ดีหรือไม่ดีที่จะทำและทำไม?


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

ChangeListรวบรวมที่จะทำลายextendsเพราะรายการimplementsคือการติดต่อที่ต้องการ @randomA สิ่งที่คุณกำลังจินตนาการถึงจุดพลาดเนื่องจากข้อผิดพลาดนี้
gnat

@gnat มันไม่ได้หายไปจากจุดที่ฉันสมมติว่าเขาขยายimplementationเช่นHashMapหรือTreeMapสิ่งที่เขามีพิมพ์ผิด
แจ้งเมื่อ

4
นี่คือการปฏิบัติที่ไม่ดี BAD BAD BAD อย่าทำอย่างนี้ ทุกคนรู้ว่า Map <String, Widget> คืออะไร แต่ WidgetCache? ตอนนี้ฉันต้องเปิด WidgetCache.java ฉันต้องจำไว้ว่า WidgetCache เป็นเพียงแผนที่ ฉันต้องตรวจสอบทุกครั้งที่เวอร์ชั่นใหม่ออกมาว่าคุณยังไม่ได้เพิ่มสิ่งใหม่ ๆ ใน WidgetCache พระเจ้าไม่เคยทำเช่นนี้
Miles Rout

"ถ้าคุณเห็น ArrayList <ArrayList <? >> กำลังถูกส่งผ่านไปในโค้ดคุณจะต้องตะโกนเรียกร้อง..?" ไม่ฉันค่อนข้างสบายใจกับคอลเลกชันทั่วไปที่ซ้อนกัน และคุณควรจะเป็นเช่นกัน
Kevin Krumwiede

คำตอบ:


75

Lag / แฝง? ฉันเรียก BS ว่า ควรมีค่าใช้จ่ายเป็นศูนย์อย่างแน่นอนจากการปฏิบัตินี้ ( แก้ไข:มีการชี้ให้เห็นในความคิดเห็นที่ว่าในความเป็นจริงสามารถยับยั้งการปรับให้เหมาะสมที่ดำเนินการโดย HotSpot VM ฉันไม่รู้เกี่ยวกับการนำไปใช้ของ VM เพื่อยืนยันหรือปฏิเสธสิ่งนี้ฉันอ้างอิงความคิดเห็นของฉันจาก C ++ การใช้งานฟังก์ชั่นเสมือน.)

มีโอเวอร์เฮดของรหัสบางส่วน คุณต้องสร้างตัวสร้างทั้งหมดจากคลาสฐานที่คุณต้องการส่งต่อพารามิเตอร์ของพวกเขา

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

นอกจากนี้ในกรณีของคอลเลกชันรูปแบบก็ไม่ได้ทำงานร่วมกับกฎทั่วไปของการใช้อินเตอร์เฟซการใช้งานไม่ได้ - นั่นคือในรหัสคอลเลกชันธรรมดาคุณจะต้องสร้างและจากนั้นกำหนดให้ตัวแปรประเภทHashMap<String, Widget> Map<String, Widget>คุณWidgetCacheไม่สามารถขยายได้Map<String, Widget>เนื่องจากเป็นส่วนต่อประสาน ไม่สามารถเป็นอินเทอร์เฟซที่ขยายอินเทอร์เฟซพื้นฐานได้เนื่องจากHashMap<String, Widget>ไม่ได้ใช้อินเทอร์เฟซนั้นและไม่มีการรวบรวมมาตรฐานอื่น ๆ และในขณะที่คุณสามารถทำให้มันเป็นคลาสที่ขยายออกไปHashMap<String, Widget>คุณจะต้องประกาศตัวแปรเป็นWidgetCacheหรือMap<String, Widget>และสิ่งแรกสูญเสียความยืดหยุ่นของคุณในการแทนที่คอลเล็กชั่นที่แตกต่างกัน ของการมีชั้นเรียน

ความแตกต่างเหล่านี้บางส่วนยังนำไปใช้กับชั้นเรียนพิเศษที่ฉันเสนอ

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


1
โปรดทราบว่าแม้ว่าจะเป็นไปได้ที่จะมีผลกระทบต่อประสิทธิภาพการทำงานบางอย่าง แต่ก็ไม่ได้เป็นเรื่องที่น่ากลัว (ขาดการเพิ่มประสิทธิภาพและการเพิ่ม vcall ในกรณีที่แย่ที่สุด - ไม่ดีในวงในสุดของคุณ
Voo

5
คำตอบที่ดีจริงๆนี้บอกทุกคนว่า "อย่าตัดสินการตัดสินใจด้วยค่าใช้จ่ายด้านประสิทธิภาพ" - และการสนทนาเพียงอย่างเดียวที่นี่ด้านล่างคือ "HotSpot จัดการกับเรื่องนี้อย่างไรมีบางอย่าง (ใน 99.9% ของทุกกรณี) คุณรำคาญที่จะอ่านมากกว่าประโยคแรกหรือไม่?
Doc Brown

16
+1 สำหรับ"แทนที่จะสร้างคลาสที่ได้รับคลาสพื้นฐานเพียงเพื่อเปลี่ยนชื่อคุณสร้างคลาสที่มีคอลเลกชันและเสนออินเทอร์เฟซเฉพาะกรณีและปรับปรุงใหม่ได้อย่างไร"
user11153

6
ตัวอย่างการปฏิบัติที่แสดงจุดหลักในคำตอบนี้ที่: เอกสารสำหรับpublic class Properties extends Hashtable<Object,Object>ในjava.utilกล่าวว่า "เพราะสืบทอดคุณสมบัติจาก Hashtable ใส่และวิธีการ putAll สามารถนำไปใช้เป็นวัตถุคุณสมบัติการใช้งานของพวกเขาเป็นกำลังใจอย่างยิ่งที่พวกเขาให้โทรไปแทรกรายการที่มีคีย์. หรือค่าไม่ใช่สตริง ". องค์ประกอบน่าจะสะอาดกว่านี้มาก
Patricia Shanahan

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

25

ตาม IBMนี้จริง ๆ แล้วเป็นการต่อต้านแบบ 'typedef' คล้ายคลาสเหล่านี้เรียกว่าประเภท psuedo

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

  • รหัสใด ๆ ที่คาดว่าWidgetCacheจะไม่สามารถจัดการได้Map<String, Widget>
  • Pseudotypes เหล่านี้คือ 'viral' เมื่อใช้หลายแพ็คเกจพวกเขานำไปสู่ความไม่ลงรอยกันในขณะที่ประเภทพื้นฐาน (เพียงแผนที่โง่ <... >) จะได้ทำงานในทุกกรณีในทุกแพ็คเกจ
  • ประเภทหลอกมักจะเป็นรูปธรรมพวกเขาไม่ได้ใช้อินเทอร์เฟซเฉพาะเนื่องจากคลาสพื้นฐานของพวกเขาใช้รุ่นทั่วไปเท่านั้น

ในบทความพวกเขาเสนอเคล็ดลับต่อไปนี้เพื่อทำให้ชีวิตง่ายขึ้นโดยไม่ต้องใช้ประเภทหลอก:

public static <K,V> Map<K,V> newHashMap() {
    return new HashMap<K,V>(); 
}

Map<Socket, Future<String>> socketOwner = Util.newHashMap();

ซึ่งทำงานได้เนื่องจากการอนุมานประเภทอัตโนมัติ

(ฉันมาถึงคำตอบนี้ผ่านคำถามล้นที่เกี่ยวข้องนี้ )


13
newHashMapตอนนี้ผู้ดำเนินการเพชรไม่ต้องการการแก้ไขหรือไม่?
svick

จริงอย่างแน่นอนลืมเรื่องนั้นไป ปกติแล้วฉันจะไม่ทำงานใน Java
Roy T.

ฉันหวังว่าภาษาจะอนุญาตให้จักรวาลของตัวแปรประเภทซึ่งมีการอ้างอิงถึงอินสแตนซ์ของประเภทอื่น ๆ แต่ก็ยังสามารถรองรับการตรวจสอบเวลาคอมไพล์เช่นว่าค่าประเภทWidgetCacheสามารถกำหนดให้กับตัวแปรประเภทWidgetCacheหรือMap<String,Widget>ไม่มีนักแสดง แต่มี เป็นวิธีการที่วิธีการคงที่ในWidgetCacheสามารถทำการอ้างอิงMap<String,Widget>และส่งกลับเป็นประเภทWidgetCacheหลังจากทำการตรวจสอบที่ต้องการใด ๆ คุณลักษณะดังกล่าวอาจเป็นประโยชน์อย่างยิ่งกับยาชื่อสามัญซึ่งไม่จำเป็นต้องลบประเภท (ตั้งแต่ ...
supercat

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

5
นี่เป็นเหตุผลที่ดีว่าทำไมจาวาจึงต้องการนามแฝงประเภท
GlenPeterson

13

ผลการปฏิบัติงานจะถูก จำกัด ในการค้นหา vtable มากที่สุดซึ่งคุณน่าจะเกิดขึ้นแล้ว นั่นไม่ใช่เหตุผลที่ถูกต้องที่จะต่อต้านมัน

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

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


นอกจากนี้ยังมีค่าใช้จ่ายเล็ก ๆ น้อย ๆ ในการก่อสร้าง
สตีเฟ่นซี

11

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

ตัวอย่างคลาสจาก codebase ของฉันพร้อมวิธีแก้ไขปัญหาที่คล้ายกันที่คุณ desribed:

public class Translations {

    private Map<Locale, Properties> translations = new HashMap<>();

    public void appendMessage(Locale locale, String code, String message) {
        /* code */
    }

    public void addMessages(Locale locale, Properties messages) {
        /* code */
    }

    public String getMessage(Locale locale, String code) {
        /* code */
    }

    public boolean localeExists(Locale locale) {
        /* code */
    }
}

คุณจะเห็นว่าภายในมันเป็นเพียงแค่แผนที่ แต่อินเทอร์เฟซสาธารณะไม่แสดงสิ่งนี้ และมีวิธีการที่ "เป็นมิตรกับโปรแกรมเมอร์" เช่นวิธีที่appendMessage(Locale locale, String code, String message)ง่ายและมีความหมายมากขึ้นในการแทรกข้อความใหม่ และผู้ใช้ระดับไม่สามารถทำเช่นtranslations.clear()นี้เพราะไม่ได้ขยายTranslationsMap

คุณสามารถมอบหมายวิธีการที่จำเป็นบางอย่างให้กับแผนที่ที่ใช้ภายใน


6

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

  1. มันซ่อนรายละเอียดการใช้งานซึ่งไม่เกี่ยวข้องกับรหัสที่ใช้ไป

  2. มันซับซ้อนเท่าที่จำเป็นเท่านั้น

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

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


4

+1 กับคำตอบอื่น ๆ ที่นี่ ฉันจะเพิ่มว่าจริง ๆ แล้วถือว่าเป็นการปฏิบัติที่ดีมากโดยชุมชน Domain Driven Design (DDD) พวกเขาสนับสนุนว่าโดเมนของคุณและการโต้ตอบกับมันควรมีความหมายโดเมนความหมายซึ่งตรงข้ามกับโครงสร้างข้อมูลพื้นฐาน A Map<String, Widget>อาจเป็นแคชได้ แต่อาจเป็นอย่างอื่นสิ่งที่คุณทำอย่างถูกต้องใน My Not So Humble Opinion (IMNSHO) คือการสร้างแบบจำลองสิ่งที่คอลเลกชันเป็นตัวแทนในกรณีนี้คือแคช

ฉันจะเพิ่มการแก้ไขที่สำคัญในการที่คลาสโดเมน wrapper รอบโครงสร้างข้อมูลพื้นฐานน่าจะมีตัวแปรสมาชิกหรือฟังก์ชั่นอื่น ๆ ที่ทำให้ชั้นโดเมนที่มีการโต้ตอบอย่างแท้จริงเมื่อเทียบกับเพียงโครงสร้างข้อมูล (ถ้า Java มีประเภทค่าเท่านั้น เราจะรับพวกเขาใน Java 10 - สัญญา!)

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

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