จุดประสงค์ของอินเทอร์เฟซเครื่องหมายคืออะไร?
จุดประสงค์ของอินเทอร์เฟซเครื่องหมายคืออะไร?
คำตอบ:
นี่คือสัมผัสเล็กน้อยตามคำตอบของ "มิทช์วีท"
โดยทั่วไปเมื่อใดก็ตามที่ฉันเห็นผู้คนอ้างถึงแนวทางการออกแบบกรอบฉันมักจะพูดถึงว่า:
โดยทั่วไปคุณควรเพิกเฉยต่อแนวทางการออกแบบกรอบงานเกือบตลอดเวลา
นี่ไม่ได้เป็นเพราะปัญหาใด ๆ กับแนวทางการออกแบบกรอบ ฉันคิดว่า. NET framework เป็นไลบรารีคลาสที่ยอดเยี่ยม ความมหัศจรรย์จำนวนมากเกิดจากแนวทางการออกแบบกรอบ
อย่างไรก็ตามแนวทางการออกแบบใช้ไม่ได้กับโค้ดส่วนใหญ่ที่เขียนโดยโปรแกรมเมอร์ส่วนใหญ่ วัตถุประสงค์ของพวกเขาคือเพื่อให้สามารถสร้างเฟรมเวิร์กขนาดใหญ่ที่ใช้โดยนักพัฒนาหลายล้านคนไม่ใช่เพื่อให้การเขียนไลบรารีมีประสิทธิภาพมากขึ้น
คำแนะนำมากมายในนั้นสามารถแนะนำให้คุณทำสิ่งต่างๆที่:
. net framework มันใหญ่โตมาก มันใหญ่มากจนเป็นเรื่องที่ไม่สมควรอย่างยิ่งที่จะถือว่าใครก็ตามมีความรู้โดยละเอียดเกี่ยวกับทุกแง่มุมของมัน ในความเป็นจริงมันปลอดภัยกว่ามากที่จะสมมติว่าโปรแกรมเมอร์ส่วนใหญ่มักจะพบกับบางส่วนของกรอบงานที่พวกเขาไม่เคยใช้มาก่อน
ในกรณีนี้เป้าหมายหลักของนักออกแบบ API คือ:
แนวทางการออกแบบเฟรมเวิร์คผลักดันให้นักพัฒนาสร้างโค้ดที่บรรลุเป้าหมายเหล่านั้น
นั่นหมายถึงการทำสิ่งต่างๆเช่นการหลีกเลี่ยงการสืบทอดเลเยอร์แม้ว่าจะหมายถึงการทำซ้ำรหัสหรือการผลักดันข้อยกเว้นทั้งหมดที่โยนรหัสออกไปที่ "จุดเข้าใช้งาน" แทนที่จะใช้ตัวช่วยที่ใช้ร่วมกัน (เพื่อให้สแต็กเทรซมีความหมายมากขึ้นในดีบักเกอร์) ของสิ่งอื่นที่คล้ายคลึงกัน
เหตุผลหลักที่คำแนะนำเหล่านั้นแนะนำให้ใช้แอตทริบิวต์แทนอินเทอร์เฟซของเครื่องหมายเนื่องจากการลบอินเทอร์เฟซของเครื่องหมายทำให้โครงสร้างการสืบทอดของไลบรารีคลาสเข้าถึงได้ง่ายขึ้นมาก แผนภาพคลาสที่มี 30 ประเภทและ 6 ชั้นของลำดับชั้นการสืบทอดนั้นน่ากลัวมากเมื่อเทียบกับแบบที่มี 15 ประเภทและลำดับชั้น 2 ชั้น
หากมีนักพัฒนาหลายล้านคนใช้ API ของคุณหรือฐานรหัสของคุณใหญ่มาก (พูดมากกว่า 100K LOC) การปฏิบัติตามหลักเกณฑ์เหล่านั้นจะช่วยได้มาก
หากนักพัฒนา 5 ล้านคนใช้เวลา 15 นาทีในการเรียนรู้ API แทนที่จะใช้เวลา 60 นาทีในการเรียนรู้ผลที่ได้คือประหยัดเวลาได้ถึง 428 ปี นั่นเป็นเวลามาก
อย่างไรก็ตามโครงการส่วนใหญ่ไม่เกี่ยวข้องกับนักพัฒนาหลายล้านคนหรือ 100K + LOC ในโครงการทั่วไปที่มีผู้พัฒนา 4 รายและประมาณ 50K loc ชุดของสมมติฐานแตกต่างกันมาก นักพัฒนาในทีมจะมีความเข้าใจมากขึ้นว่าโค้ดทำงานอย่างไร นั่นหมายความว่าการปรับให้เหมาะสมสำหรับการผลิตโค้ดคุณภาพสูงอย่างรวดเร็วและเพื่อลดจำนวนข้อบกพร่องและความพยายามที่จำเป็นในการเปลี่ยนแปลง
การใช้เวลา 1 สัปดาห์ในการพัฒนาโค้ดที่สอดคล้องกับ. net framework เทียบกับ 8 ชั่วโมงในการเขียนโค้ดที่เปลี่ยนแปลงง่ายและมีบั๊กน้อยลงอาจส่งผลให้:
หากไม่มีนักพัฒนารายอื่น 4,999,999 รายเพื่อรับภาระค่าใช้จ่ายก็มักจะไม่คุ้มค่า
ตัวอย่างเช่นการทดสอบอินเทอร์เฟซของ marker จะมีนิพจน์ "is" เพียงรายการเดียวและส่งผลให้โค้ดที่ค้นหาแอตทริบิวต์น้อยลง
ดังนั้นคำแนะนำของฉันคือ:
virtual protected
เมธอด template DoSomethingCore
แทนDoSomething
ไม่ใช่งานเพิ่มเติมและคุณสื่อสารอย่างชัดเจนว่าเป็นวิธีเทมเพลต ... IMNSHO คนที่เขียนแอปพลิเคชันโดยไม่คำนึงถึง API ( But.. I'm not a framework developer, I don't care about my API!
) คือคนที่เขียนซ้ำกันมาก ( และยังไม่มีเอกสารและมักจะอ่านไม่ได้) รหัสไม่ใช่วิธีอื่น
Marker Interfaces ใช้เพื่อทำเครื่องหมายความสามารถของคลาสเป็นการใช้อินเทอร์เฟซเฉพาะในขณะรันไทม์
แนวทางการออกแบบอินเทอร์เฟซและ. NET ประเภท - การออกแบบอินเทอร์เฟซไม่สนับสนุนการใช้อินเทอร์เฟซของเครื่องหมายเพื่อสนับสนุนการใช้แอตทริบิวต์ใน C # แต่เนื่องจาก @Jay Bazuzi ชี้ให้เห็นว่าการตรวจสอบอินเทอร์เฟซของเครื่องหมายนั้นง่ายกว่าสำหรับแอตทริบิวต์:o is I
แทนที่จะเป็นสิ่งนี้:
public interface IFooAssignable {}
public class FooAssignableAttribute : IFooAssignable
{
...
}
แนวทาง. NET แนะนำให้คุณทำสิ่งนี้:
public class FooAssignableAttribute : Attribute
{
...
}
[FooAssignable]
public class Foo
{
...
}
เนื่องจากคำตอบอื่น ๆ ทั้งหมดระบุว่า "ควรหลีกเลี่ยง" จึงมีประโยชน์หากมีคำอธิบายว่าเหตุใด
ประการแรกเหตุใดจึงใช้อินเทอร์เฟซเครื่องหมาย: มีอยู่เพื่ออนุญาตให้โค้ดที่ใช้อ็อบเจ็กต์ที่ใช้งานเพื่อตรวจสอบว่าพวกเขาใช้อินเทอร์เฟซดังกล่าวหรือไม่และปฏิบัติต่ออ็อบเจ็กต์ต่างกันหรือไม่หากเป็นเช่นนั้น
ปัญหาของแนวทางนี้คือมันทำให้การห่อหุ้มแตก ขณะนี้วัตถุมีการควบคุมทางอ้อมว่าจะใช้ภายนอกอย่างไร นอกจากนี้ยังมีความรู้เกี่ยวกับระบบที่กำลังจะถูกนำมาใช้โดยการใช้อินเทอร์เฟซของเครื่องหมายคำจำกัดความของคลาสจะแนะนำให้ใช้ที่ใดที่หนึ่งเพื่อตรวจสอบการมีอยู่ของเครื่องหมาย มีความรู้โดยนัยเกี่ยวกับสภาพแวดล้อมที่ใช้และพยายามกำหนดว่าควรใช้อย่างไร สิ่งนี้ขัดต่อแนวคิดเรื่องการห่อหุ้มเนื่องจากมีความรู้เกี่ยวกับการนำส่วนหนึ่งของระบบที่มีอยู่นอกขอบเขตของตัวเองไปใช้โดยสิ้นเชิง
ในระดับที่ใช้งานได้จริงจะช่วยลดการพกพาและการนำกลับมาใช้ใหม่ หากคลาสถูกนำมาใช้ซ้ำในแอปพลิเคชันอื่นจะต้องคัดลอกอินเทอร์เฟซด้วยเช่นกันและอาจไม่มีความหมายใด ๆ ในสภาพแวดล้อมใหม่ทำให้ซ้ำซ้อนทั้งหมด
ด้วยเหตุนี้ "เครื่องหมาย" จึงเป็นข้อมูลเมตาเกี่ยวกับคลาส ข้อมูลเมตานี้ไม่ได้ใช้โดยคลาสเองและมีความหมายเฉพาะกับโค้ดไคลเอนต์ภายนอก (บางตัว!) เท่านั้นเพื่อให้สามารถปฏิบัติกับอ็อบเจ็กต์ในลักษณะที่แน่นอน เนื่องจากมีความหมายเฉพาะกับรหัสไคลเอ็นต์ข้อมูลเมตาจึงควรอยู่ในรหัสไคลเอ็นต์ไม่ใช่ API คลาส
ความแตกต่างระหว่าง "อินเทอร์เฟซเครื่องหมาย" และอินเทอร์เฟซปกติคืออินเทอร์เฟซที่มีเมธอดจะบอกโลกภายนอกว่าสามารถใช้งานได้อย่างไรในขณะที่อินเทอร์เฟซที่ว่างเปล่าหมายถึงมันบอกโลกภายนอกว่าควรใช้อย่างไร
IConstructableFromString<T>
ระบุว่าคลาสT
สามารถใช้งานIConstructableFromString<T>
ได้ก็ต่อเมื่อมีสมาชิกแบบคงที่ ...
public static T ProduceFromString(String params);
คลาสที่แสดงร่วมกับอินเทอร์เฟซสามารถเสนอวิธีการpublic static T ProduceFromString<T>(String params) where T:IConstructableFromString<T>
; ถ้ารหัสไคลเอนต์มีวิธีการเช่นT[] MakeManyThings<T>() where T:IConstructableFromString<T>
หนึ่งสามารถกำหนดประเภทใหม่ที่สามารถทำงานกับรหัสไคลเอนต์โดยไม่ต้องแก้ไขรหัสไคลเอนต์เพื่อจัดการกับพวกเขา หากข้อมูลเมตาอยู่ในรหัสไคลเอ็นต์จะไม่สามารถสร้างประเภทใหม่เพื่อใช้โดยไคลเอ็นต์ที่มีอยู่ได้
T
และคลาสที่ใช้เป็นIConstructableFromString<T>
ที่ที่คุณมีวิธีการในอินเทอร์เฟซที่อธิบายพฤติกรรมบางอย่างดังนั้นจึงไม่ใช่อินเทอร์เฟซของเครื่องหมาย
ProduceFromString
ในตัวอย่างด้านบนจะไม่เกี่ยวข้องกับ อินเทอร์เฟซไม่ว่าในทางใดก็ตามยกเว้นว่าอินเทอร์เฟซจะถูกใช้เป็นเครื่องหมายเพื่อระบุว่าคลาสใดที่ควรคาดหวังเพื่อใช้ฟังก์ชันที่จำเป็น
อินเทอร์เฟซเครื่องหมายบางครั้งอาจเป็นสิ่งชั่วร้ายที่จำเป็นเมื่อภาษาไม่รองรับประเภทสหภาพที่เลือกปฏิบัติ
สมมติว่าคุณต้องการกำหนดวิธีการที่คาดหวังอาร์กิวเมนต์ซึ่งประเภทต้องเป็นหนึ่งใน A, B หรือ C ในภาษาที่ใช้งานได้เป็นอันดับแรก (เช่นF # ) ประเภทดังกล่าวสามารถกำหนดได้อย่างสมบูรณ์เป็น:
type Arg =
| AArg of A
| BArg of B
| CArg of C
อย่างไรก็ตามในภาษา OO-first เช่น C # ไม่สามารถทำได้ วิธีเดียวที่จะบรรลุสิ่งที่คล้ายกันที่นี่คือการกำหนดอินเทอร์เฟซ IArg และ "ทำเครื่องหมาย" A, B และ C ด้วย
แน่นอนคุณสามารถหลีกเลี่ยงการใช้อินเทอร์เฟซของเครื่องหมายได้โดยเพียงแค่ยอมรับประเภท "วัตถุ" เป็นอาร์กิวเมนต์ แต่คุณจะสูญเสียการแสดงออกและความปลอดภัยในระดับหนึ่ง
ประเภทสหภาพแรงงานที่เลือกปฏิบัติมีประโยชน์อย่างยิ่งและมีอยู่ในภาษาที่ใช้งานได้เป็นเวลาอย่างน้อย 30 ปี น่าแปลกที่จนถึงทุกวันนี้ภาษา OO กระแสหลักทั้งหมดไม่สนใจคุณลักษณะนี้ - แม้ว่าจริงๆแล้วจะไม่เกี่ยวข้องกับการเขียนโปรแกรมเชิงฟังก์ชันต่อ se แต่เป็นของระบบ type
Foo<T>
จะมีชุดฟิลด์คงที่แยกกันสำหรับทุกประเภทT
จึงไม่ยากที่จะมีคลาสทั่วไปที่มีฟิลด์แบบคงที่ที่มีผู้รับมอบสิทธิ์ในการประมวลผล a T
และเติมฟิลด์เหล่านั้นไว้ล่วงหน้าพร้อมกับฟังก์ชันที่จะจัดการทุกประเภทที่คลาสนั้น ควรจะทำงานกับ. การใช้ข้อ จำกัด ของอินเทอร์เฟซทั่วไปตามประเภทT
จะตรวจสอบในเวลาคอมไพเลอร์ว่าประเภทที่ให้มาอย่างน้อยก็อ้างว่าถูกต้องแม้ว่าจะไม่สามารถมั่นใจได้ว่าเป็นจริงก็ตาม
อินเทอร์เฟซเครื่องหมายเป็นเพียงอินเทอร์เฟซที่ว่างเปล่า คลาสจะใช้อินเทอร์เฟซนี้เป็นข้อมูลเมตาเพื่อใช้ด้วยเหตุผลบางประการ ในภาษา C # คุณมักจะใช้แอตทริบิวต์เพื่อมาร์กอัปคลาสด้วยเหตุผลเดียวกับที่คุณใช้อินเทอร์เฟซเครื่องหมายในภาษาอื่น
อินเทอร์เฟซเครื่องหมายช่วยให้สามารถแท็กคลาสในลักษณะที่จะนำไปใช้กับคลาสที่สืบทอดทั้งหมด อินเทอร์เฟซเครื่องหมาย "บริสุทธิ์" จะไม่กำหนดหรือสืบทอดสิ่งใด ๆ อินเทอร์เฟซ marker ที่มีประโยชน์มากกว่าอาจเป็นอินเทอร์เฟซอื่นที่ "สืบทอด" แต่ไม่ได้กำหนดสมาชิกใหม่ ตัวอย่างเช่นหากมีอินเทอร์เฟซ "IReadableFoo" เราอาจกำหนดอินเทอร์เฟซ "IImmutableFoo" ซึ่งจะทำงานเหมือน "Foo" แต่จะสัญญากับทุกคนที่ใช้อินเทอร์เฟซนี้ว่าจะไม่มีอะไรเปลี่ยนแปลงค่า รูทีนที่ยอมรับ IImmutableFoo จะสามารถใช้งานได้เหมือนกับ IReadableFoo แต่รูทีนจะยอมรับเฉพาะคลาสที่ประกาศว่าใช้ IImmutableFoo เท่านั้น
ฉันไม่สามารถนึกถึงการใช้งานอินเทอร์เฟซเครื่องหมาย "บริสุทธิ์" ได้มากนัก สิ่งเดียวที่ฉันคิดได้คือถ้า EqualityComparer (ของ T) ค่าเริ่มต้นจะส่งคืน Object.Equals สำหรับประเภทใด ๆ ที่ใช้ IDoNotUseEqualityComparer แม้ว่าประเภทนั้นจะใช้ IEqualityComparer ด้วยก็ตาม สิ่งนี้จะช่วยให้มีชนิดที่ไม่สามารถปิดผนึกได้โดยไม่ละเมิดหลักการแทนที่ของ Liskov: ถ้าประเภทนั้นผนึกวิธีการทั้งหมดที่เกี่ยวข้องกับการทดสอบความเท่าเทียมกันประเภทที่ได้รับสามารถเพิ่มฟิลด์เพิ่มเติมและทำให้เปลี่ยนแปลงได้ แต่การกลายพันธุ์ของฟิลด์ดังกล่าวจะไม่ ไม่สามารถมองเห็นได้โดยใช้วิธีการประเภทฐานใด ๆ อาจไม่น่ากลัวที่จะมีคลาสที่ไม่สามารถปิดผนึกไม่ได้และหลีกเลี่ยงการใช้ EqualityComparer.Default หรือ trust ที่ได้รับคลาสที่ไม่ใช้ IEqualityComparer
วิธีการขยายทั้งสองนี้จะแก้ปัญหาส่วนใหญ่ที่สก็อตต์ยืนยันว่าอินเทอร์เฟซของเครื่องหมายแสดงความโปรดปรานเหนือคุณลักษณะ
public static bool HasAttribute<T>(this ICustomAttributeProvider self)
where T : Attribute
{
return self.GetCustomAttributes(true).Any(o => o is T);
}
public static bool HasAttribute<T>(this object self)
where T : Attribute
{
return self != null && self.GetType().HasAttribute<T>()
}
ตอนนี้คุณมี:
if (o.HasAttribute<FooAssignableAttribute>())
{
//...
}
เทียบกับ:
if (o is IFooAssignable)
{
//...
}
ฉันไม่เห็นว่าการสร้าง API จะใช้เวลานานกว่ารูปแบบแรกถึง 5 เท่าเมื่อเทียบกับรูปแบบที่สองตามที่ Scott กล่าวอ้าง
เครื่องหมายคืออินเทอร์เฟซที่ว่างเปล่า เครื่องหมายอยู่ที่นั่นหรือไม่มี
คลาส Foo: IConfidential
ที่นี่เราทำเครื่องหมาย Foo ว่าเป็นความลับ ไม่จำเป็นต้องมีคุณสมบัติหรือแอตทริบิวต์เพิ่มเติมที่แท้จริง
อินเทอร์เฟซ Marker เป็นอินเทอร์เฟซเปล่าทั้งหมดที่ไม่มีเนื้อความ / ข้อมูลสมาชิก / การนำไปใช้งาน
คลาสใช้อินเทอร์เฟซมาร์กเกอร์เมื่อจำเป็นก็แค่ " ทำเครื่องหมาย "; หมายความว่ามันบอก JVM ว่าคลาสเฉพาะนั้นมีไว้เพื่อจุดประสงค์ในการโคลนจึงอนุญาตให้โคลนได้ คลาสนี้มีไว้เพื่อทำให้เป็นอนุกรมของวัตถุดังนั้นโปรดอนุญาตให้ออบเจ็กต์ได้รับการทำให้เป็นอนุกรม
อินเทอร์เฟซเครื่องหมายเป็นเพียงการเขียนโปรแกรมขั้นตอนในภาษา OO อินเทอร์เฟซกำหนดสัญญาระหว่างผู้ใช้งานและผู้บริโภคยกเว้นอินเทอร์เฟซของเครื่องหมายเนื่องจากอินเทอร์เฟซของเครื่องหมายกำหนดสิ่งอื่นใดนอกจากตัวมันเอง ดังนั้นทันทีที่ประตูอินเทอร์เฟซเครื่องหมายจึงล้มเหลวตามวัตถุประสงค์พื้นฐานของการเป็นอินเทอร์เฟซ