ถ้า Singletons ไม่ดีเหตุใด Service Container จึงดี?


91

เราทุกคนรู้วิธีSingletons ไม่ดีเพราะพวกเขาซ่อนอ้างอิงและเหตุผลอื่น

แต่ในเฟรมเวิร์กอาจมีอ็อบเจ็กต์จำนวนมากที่ต้องสร้างอินสแตนซ์เพียงครั้งเดียวและถูกเรียกใช้จากทุกที่ (คนตัดไม้, ฐานข้อมูล ฯลฯ )

เพื่อแก้ปัญหานี้ฉันได้รับคำสั่งให้ใช้สิ่งที่เรียกว่า "Objects Manager" (หรือService Containerเช่น symfony) ที่เก็บข้อมูลอ้างอิงถึง Services (คนตัดไม้ ฯลฯ ) ไว้ภายใน

แต่ทำไมผู้ให้บริการถึงไม่เลวร้ายเท่ากับ Singleton ที่บริสุทธิ์?

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

ปล. ฉันรู้ว่าการไม่ซ่อนการอ้างอิงฉันควรใช้ DI (ตามที่ระบุไว้โดย Misko)

เพิ่ม

ฉันจะเพิ่ม: ทุกวันนี้เสื้อกล้ามไม่ได้ชั่วร้ายผู้สร้าง PHPUnit อธิบายไว้ที่นี่:

DI + Singleton แก้ปัญหา:

<?php
class Client {

    public function doSomething(Singleton $singleton = NULL){

        if ($singleton === NULL) {
            $singleton = Singleton::getInstance();
        }

        // ...
    }
}
?>

ค่อนข้างฉลาดแม้ว่าจะไม่สามารถแก้ปัญหาได้ทุกปัญหาก็ตาม

นอกเหนือจาก DI และ Service Container แล้วยังมีทางออกที่ดีที่ยอมรับได้ในการเข้าถึงวัตถุตัวช่วยนี้หรือไม่?


2
@ ใช่การแก้ไขของคุณกำลังตั้งสมมติฐานที่ผิดพลาด Sebastian ไม่แนะนำว่าข้อมูลโค้ดทำให้การใช้ Singleons มีปัญหาน้อยลง เป็นเพียงวิธีหนึ่งในการสร้างโค้ดที่มิฉะนั้นจะเป็นไปไม่ได้ที่จะทดสอบเพิ่มเติม แต่ก็ยังเป็นรหัสที่มีปัญหา ในความเป็นจริงเขาตั้งข้อสังเกตอย่างชัดเจนว่า: "Just Because You Can, Does not mean you Should" วิธีแก้ปัญหาที่ถูกต้องคือไม่ใช้ Singletons เลย
Gordon

3
@ ใช่เป็นไปตามหลักการ SOLID
Gordon

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

4
@paxdiablo แต่พวกเขามีที่ไม่ดี Singletons ละเมิด SRP, OCP และ DIP พวกเขาแนะนำสถานะทั่วโลกและการเชื่อมต่อที่แน่นหนาในแอปพลิเคชันของคุณและจะทำให้ API ของคุณโกหกเกี่ยวกับการพึ่งพา ทั้งหมดนี้จะส่งผลเสียต่อการบำรุงรักษาความสามารถในการอ่านและการทดสอบโค้ดของคุณ อาจมีบางกรณีที่หายากที่ข้อเสียเหล่านี้มีมากกว่าประโยชน์เล็กน้อย แต่ฉันขอยืนยันว่าใน 99% คุณไม่ต้องการ Singleton โดยเฉพาะอย่างยิ่งใน PHP ที่ Singletons มีลักษณะเฉพาะสำหรับการร้องขอเท่านั้นและการรวบรวมกราฟผู้ทำงานร่วมกันจาก Builder ทำได้ง่าย
Gordon

5
ไม่ฉันไม่เชื่ออย่างนั้น เครื่องมือเป็นเครื่องมือในการทำหน้าที่โดยปกติจะทำให้ง่ายขึ้นแม้ว่าบางคน (emac?) จะมีความแตกต่างที่หายากในการทำให้ยากขึ้น :-) ในสิ่งนี้ซิงเกิลตันไม่ต่างอะไรกับต้นไม้ที่สมดุลหรือคอมไพเลอร์ . หากคุณต้องการให้แน่ใจว่ามีเพียงสำเนาเดียวของวัตถุซิงเกิลตันจะทำสิ่งนี้ ไม่ว่ามันจะดีหรือไม่สามารถถกเถียงกันได้ แต่ฉันไม่เชื่อว่าคุณจะเถียงได้ว่ามันไม่ได้ทำเลย และอาจมีวิธีที่ดีกว่าเช่นเลื่อยยนต์เร็วกว่าเลื่อยมือหรือปืนตะปูกับค้อน นั่นไม่ได้ทำให้ Handaw / Hammer ใช้เครื่องมือน้อยลง
paxdiablo

คำตอบ:


76

ตัวระบุตำแหน่งบริการเป็นเพียงความชั่วร้ายน้อยกว่าสองประการที่ต้องพูด "น้อยกว่า" เดือดถึงความแตกต่างทั้งสี่นี้ ( อย่างน้อยตอนนี้ฉันก็นึกไม่ออก ):

หลักการความรับผิดชอบเดียว

Service Container ไม่ละเมิดหลักการความรับผิดชอบเดียวเหมือนที่ Singleton ทำ Singletons ผสมผสานการสร้างอ็อบเจ็กต์และตรรกะทางธุรกิจในขณะที่ Service Container มีหน้าที่รับผิดชอบอย่างเคร่งครัดในการจัดการวงจรชีวิตอ็อบเจ็กต์ของแอปพลิเคชันของคุณ ในเรื่องนี้ Service Container ดีกว่า

ข้อต่อ

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

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

การพึ่งพาที่ซ่อนอยู่

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

การดึงการอ้างอิงจากภายในอินสแตนซ์ยังเป็นการละเมิดLaw of Demeterซึ่งระบุว่าคุณไม่ควรเจาะลึกถึงผู้ทำงานร่วมกัน อินสแตนซ์ควรพูดคุยกับผู้ทำงานร่วมกันในทันทีเท่านั้น นี่เป็นปัญหากับทั้ง Singleton และ ServiceLocator

รัฐสากล

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

นอกจากนี้โปรดดู Fowler เกี่ยวกับService Locator vs Dependency Injectionเพื่อการสนทนาในเชิงลึกมากขึ้น


หมายเหตุเกี่ยวกับการอัปเดตของคุณและบทความที่เชื่อมโยงโดยSebastian Bergmann เกี่ยวกับโค้ดการทดสอบที่ใช้ Singletons : Sebastian ไม่ได้แนะนำว่าวิธีแก้ปัญหาที่เสนอทำให้การใช้ Singleons น้อยลงไม่มีปัญหา เป็นเพียงวิธีหนึ่งในการสร้างโค้ดที่มิฉะนั้นจะเป็นไปไม่ได้ที่จะทดสอบเพิ่มเติม แต่ก็ยังเป็นรหัสที่มีปัญหา ในความเป็นจริงเขาตั้งข้อสังเกตอย่างชัดเจนว่า: "Just Because You Can, Does not mean you should"


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

44

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

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

หลีกเลี่ยงหลีกเลี่ยงหลีกเลี่ยง


6
ดูเหมือนว่าคุณไม่รู้อะไรเกี่ยวกับอินเทอร์เฟซเลย คลาสเพียงอธิบายอินเทอร์เฟซที่จำเป็นในลายเซ็นตัวสร้าง - และทุกอย่างที่เขาจำเป็นต้องรู้ Passed Service Locator ควรใช้อินเทอร์เฟซนั่นคือทั้งหมด และหาก IDE ตรวจสอบการใช้งานอินเทอร์เฟซการควบคุมการเปลี่ยนแปลงจะค่อนข้างง่าย
OZ_

4
@ yes123: คนที่บอกว่าผิดและพวกเขาผิดเพราะ SL เป็นรูปแบบการต่อต้าน คำถามของคุณคือ "ทำไม SL ถึงดี" คำตอบของฉันคือพวกเขาไม่ใช่
สัน

5
ฉันจะไม่เถียงว่า SL เป็นรูปแบบ anit หรือไม่ แต่สิ่งที่ฉันจะพูดก็คือความชั่วร้ายน้อยกว่ามากเมื่อเทียบกับซิงเกิลตันและลูกโลก คุณไม่สามารถทดสอบคลาสที่ขึ้นอยู่กับซิงเกิลตันได้ แต่คุณสามารถทดสอบคลาสที่ขึ้นอยู่กับ SL ได้อย่างแน่นอน (แม้ว่าคุณจะสามารถออกแบบ SL จนถึงจุดที่ใช้งานไม่ได้) ... นั่นก็คุ้มค่า สังเกต ...
ircmaxell

3
@ Jason คุณต้องส่งผ่านวัตถุที่ใช้อินเทอร์เฟซ - และเป็นเพียงสิ่งที่คุณต้องรู้ คุณกำลัง จำกัด ตัวเองด้วยคำจำกัดความของตัวสร้างคลาสเท่านั้นและต้องการเขียนในคอนสตรัคเตอร์ทุกคลาส (ไม่ใช่อินเทอร์เฟซ) - มันเป็นความคิดที่โง่ สิ่งที่คุณต้องมีคืออินเทอร์เฟซ คุณสามารถทดสอบคลาสนี้ด้วยการล้อเลียนได้สำเร็จคุณสามารถเปลี่ยนพฤติกรรมได้อย่างง่ายดายโดยไม่ต้องเปลี่ยนรหัสไม่มีการพึ่งพาและการมีเพศสัมพันธ์เพิ่มเติม - นั่นคือทั้งหมด (โดยทั่วไป) สิ่งที่เราต้องการมีใน Dependency Injection
OZ_

2
แน่นอนว่าฉันจะรวมฐานข้อมูลคนบันทึกดิสก์เทมเพลตแคชและผู้ใช้ลงในวัตถุ "อินพุต" เดียวแน่นอนว่าจะง่ายกว่าที่จะบอกว่าวัตถุของฉันพึ่งพาการอ้างอิงใดมากกว่าการใช้คอนเทนเนอร์
Mahn

4

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

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

นี่เป็นคำถามที่ดีที่ SO อธิบายถึงความแตกต่างของทั้งสอง: อะไรคือความแตกต่างระหว่างรูปแบบ Dependency Injection และ Service Locator?


"the DIC แทรกการอ้างอิงที่เหมาะสมสำหรับคุณ" สิ่งนี้ไม่เกิดขึ้นกับ Singleton ด้วยหรือ?
ไดนามิก

5
@ yes123 - ถ้าคุณใช้ Singleton คุณจะไม่ฉีดมันส่วนใหญ่แล้วคุณจะเข้าถึงมันได้ทั่วโลก (นั่นคือประเด็นของ Singleton) ฉันคิดว่าถ้าคุณบอกว่าถ้าคุณฉีด Singleton มันจะไม่ซ่อนการอ้างอิง แต่มันเป็นการเอาชนะจุดประสงค์ดั้งเดิมของรูปแบบ Singleton - คุณจะถามตัวเองว่าถ้าฉันไม่ต้องการให้คลาสนี้เข้าถึงได้ทั่วโลกทำไม ฉันต้องทำให้เป็น Singleton หรือไม่?
rickchristie

2

เนื่องจากคุณสามารถแทนที่อ็อบเจ็กต์ใน Service Container ได้อย่างง่ายดายโดย
1) การสืบทอด (คลาส Object Manager สามารถสืบทอดและเมธอดสามารถลบล้างได้)
2) การเปลี่ยนคอนฟิกูเรชัน (ในกรณีที่มี Symfony)

และ Singletons ไม่เพียง แต่ไม่ดีเนื่องจากการมีเพศสัมพันธ์ที่สูง แต่เนื่องจากเป็น _ Single _tons เป็นสถาปัตยกรรมที่ไม่ถูกต้องสำหรับวัตถุเกือบทุกชนิด

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


0

สำหรับฉันฉันพยายามที่จะหลีกเลี่ยงค่าคงที่ทั่วโลก singletons ด้วยเหตุผลง่ายๆมีหลายกรณีที่ฉันอาจต้องใช้ API

ตัวอย่างเช่นฉันมีส่วนหน้าและผู้ดูแลระบบ ภายในผู้ดูแลระบบฉันต้องการให้พวกเขาสามารถเข้าสู่ระบบในฐานะผู้ใช้ พิจารณารหัสภายในผู้ดูแลระบบ

$frontend = new Frontend();
$frontend->auth->login($_GET['user']);
$frontend->redirect('/');

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

ความคิดของฉันเกี่ยวกับซิงเกิลตันคือ - คุณไม่สามารถเพิ่มวัตถุเดียวกันภายในพาเรนต์สองครั้งได้ ตัวอย่างเช่น

$logger1=$api->add('Logger');
$logger2=$api->add('Logger');

จะทำให้คุณมีอินสแตนซ์เดียวและตัวแปรทั้งสองชี้ไปที่มัน

สุดท้ายถ้าคุณต้องการใช้การพัฒนาเชิงวัตถุให้ทำงานกับวัตถุไม่ใช่กับคลาส


1
ดังนั้นวิธีการของคุณคือการส่ง$api var รอบกรอบงานของคุณ? ฉันไม่เข้าใจว่าคุณหมายถึงอะไร นอกจากนี้หากการโทรadd('Logger')ส่งคืนอินสแตนซ์เดียวกันโดยทั่วไปคุณมี cotainer บริการ
ไดนามิก

ใช่ถูกต้อง. ฉันเรียกพวกเขาว่า "System Controller" และมีไว้เพื่อปรับปรุงการทำงานของ API ในทำนองเดียวกันการเพิ่มตัวควบคุม "ตรวจสอบได้" ลงในโมเดลสองครั้งจะทำงานในลักษณะเดียวกัน - สร้างเพียงอินสแตนซ์เดียวและฟิลด์การตรวจสอบเพียงชุดเดียว
romaninsh
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.