รูปแบบการออกแบบ Dependency Injection & Singleton


92

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

ตัวอย่างเช่น Logger ฉันสามารถใช้Logger.GetInstance().Log(...) แต่แทนที่จะเป็นแบบนี้ทำไมฉันต้องฉีดทุกคลาสที่ฉันสร้างด้วยอินสแตนซ์ของคนตัดไม้?

คำตอบ:


67

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

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

ปัญหาเกี่ยวกับเสื้อกล้ามคือเป็นตัวแทนของสถานะทั่วโลกซึ่งยากที่จะคาดเดาโดยเฉพาะในการทดสอบ

โปรดจำไว้ว่าวัตถุสามารถเป็น de-facto singleton ได้ แต่ยังคงได้รับจากการฉีดแบบพึ่งพาแทนที่จะเป็นทาง Singleton.getInstance()แต่ยังคงได้รับผ่านการพึ่งพาการฉีดมากกว่าผ่าน

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


89

Singletons เป็นเหมือนลัทธิคอมมิวนิสต์: ทั้งคู่ฟังดูดีบนกระดาษ แต่ระเบิดด้วยปัญหาในทางปฏิบัติ

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

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

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


9
@BryanWatts ทั้งหมดที่กล่าวว่า Singletons ยังเร็วกว่ามากและมีข้อผิดพลาดน้อยกว่าในแอปพลิเคชันขนาดกลางปกติ ปกติฉันไม่มีการนำไปใช้งานที่เป็นไปได้มากกว่าหนึ่งรายการสำหรับตัวบันทึก (กำหนดเอง) ดังนั้นทำไมฉันจึงต้อง ** 1. สร้างอินเทอร์เฟซสำหรับสิ่งนั้นและเปลี่ยนแปลงทุกครั้งที่ฉันต้องการเพิ่ม / เปลี่ยนแปลงสมาชิกสาธารณะ 2. ดูแลรักษา การกำหนดค่า DI 3. ซ่อนความจริงที่ว่ามีออบเจ็กต์ประเภทนี้เพียงชิ้นเดียวในระบบทั้งหมด 4. ผูกมัดตัวเองกับการทำงานที่เข้มงวดซึ่งเกิดจากการแยกข้อกังวลก่อนเวลาอันควร
Uri Abramson

@UriAbramson: ดูเหมือนว่าคุณได้ตัดสินใจแล้วเกี่ยวกับการแลกเปลี่ยนที่คุณต้องการดังนั้นฉันจะไม่พยายามโน้มน้าวคุณ
Bryan Watts

@BryanWatts มันไม่ใช่อย่างนั้นบางทีความคิดเห็นของฉันก็รุนแรงไปหน่อย แต่จริงๆแล้วฉันสนใจมากที่จะได้ยินสิ่งที่คุณพูดเกี่ยวกับประเด็นที่ฉันพูดถึง ...
Uri Abramson

2
@UriAbramson: พอใช้ อย่างน้อยคุณจะเห็นด้วยว่าการสลับการใช้งานเพื่อแยกระหว่างการทดสอบมีความสำคัญหรือไม่?
Bryan Watts

6
การใช้เสื้อกล้ามและการฉีดแบบพึ่งพาไม่ได้ใช้ร่วมกัน ซิงเกิลตันสามารถใช้อินเทอร์เฟซได้ดังนั้นจึงสามารถใช้เพื่อตอบสนองการพึ่งพาคลาสอื่นได้ ความจริงที่ว่ามันเป็นซิงเกิลตันไม่ได้บังคับให้ผู้บริโภคทุกรายได้รับการอ้างอิงผ่านวิธีการ / คุณสมบัติ "GetInstance"
Oliver

19

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

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

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

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


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

4
@kyoryu ฉันกำลังพูดถึงกรณี "ปกติ" ซึ่ง IMHO บอกเป็นนัยว่าใช้กรอบการบันทึกมาตรฐาน (โดยพฤตินัย) (ซึ่งโดยทั่วไปสามารถกำหนดค่าได้ผ่านไฟล์ property / XML btw) แน่นอนว่ามีข้อยกเว้นเช่นเคย หากฉันรู้ว่าแอปของฉันยอดเยี่ยมในแง่นี้ฉันจะไม่ใช้ Singleton แน่นอน แต่การพูดมากเกินไปว่า "สิ่งนี้อาจมีประโยชน์ในบางครั้ง" นั้นมักจะเป็นการสูญเปล่า
PéterTörök

หากคุณใช้ DI อยู่แล้วมันก็ไม่ได้เป็นวิศวกรรมอะไรเพิ่มเติม (BTW ฉันไม่เห็นด้วยกับคุณฉันเป็นคนโหวตคะแนน) คนตัดไม้จำนวนมากต้องการข้อมูล "หมวดหมู่" หรือข้อมูลบางอย่างและการเพิ่มพารามิเตอร์เพิ่มเติมอาจทำให้เกิดความเจ็บปวดได้ การซ่อนไว้ด้านหลังอินเทอร์เฟซสามารถช่วยให้โค้ดที่ใช้นั้นสะอาดและสามารถเปลี่ยนไปใช้กรอบการบันทึกอื่นได้ง่ายขึ้น
kyoryu

1
@kyoryu ขอโทษที่ตั้งสมมติฐานผิด ฉันเห็นการโหวตลงคะแนนและความคิดเห็นดังนั้นฉันจึงเชื่อมต่อจุดต่างๆ - วิธีที่ไม่ถูกต้องตามที่ปรากฎ :-( ฉันไม่เคยประสบความจำเป็นในการเปลี่ยนไปใช้กรอบการบันทึกแบบอื่น แต่อีกครั้งฉันเข้าใจว่ามันอาจเป็นสิ่งที่ถูกต้อง ความกังวลในบางโครงการ
PéterTörök

5
แก้ไขฉันถ้าฉันผิด แต่ซิงเกิลตันจะเป็นของ LogFactory ไม่ใช่คนตัดไม้ นอกจากนี้ LogFactory ยังมีแนวโน้มที่จะเป็น apache commons หรือ Slf4j logging facade ดังนั้นการสลับการใช้งานการบันทึกจึงไม่ยุ่งยาก ไม่ใช่ความเจ็บปวดที่แท้จริงกับการใช้ DI ในการฉีด LogFactory ที่คุณต้องไปที่ applicationContext เพื่อสร้างทุกอินสแตนซ์ในแอปของคุณ?
HDave

9

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

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

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


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

2

ในช่วงเวลาเดียวที่คุณควรใช้ Singleton แทน Dependency Injection คือถ้า Singleton แสดงถึงค่าที่ไม่เปลี่ยนรูปเช่น List.Empty หรือ the like (สมมติว่ารายการไม่เปลี่ยนรูป)

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


1

เพิ่งตรวจสอบบทความ Monostate - เป็นทางเลือกที่ดีสำหรับ Singleton แต่มีคุณสมบัติที่ดีกว่า:

class Mono{
    public static $db;
    public function setDb($db){
       self::$db = $db;
    }

}

class Mapper extends Mono{
    //mapping procedure
    return $Entity;

    public function save($Entity);//requires database connection to be set
}

class Entity{
public function save(){
    $Mapper = new Mapper();
    $Mapper->save($this);//has same static reference to database class     
}

$Mapper = new Mapper();
$Mapper->setDb($db);

$User = $Mapper->find(1);
$User->save();

ไม่น่ากลัวแบบนี้ - เพราะ Mapper ขึ้นอยู่กับการเชื่อมต่อฐานข้อมูลเพื่อทำการ save () - แต่ถ้ามีการสร้าง mapper อื่นก่อนหน้านี้ก็สามารถข้ามขั้นตอนนี้ในการรับการอ้างอิงได้ แม้ว่าจะเรียบร้อย แต่ก็ดูยุ่งเหยิงด้วยใช่ไหม


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