ฉันควรส่งวัตถุไปยังตัวสร้างหรือสร้างอินสแตนซ์ในชั้นเรียนหรือไม่?


10

ลองพิจารณาสองตัวอย่างนี้:

ส่งผ่านวัตถุไปยังตัวสร้าง

class ExampleA
{
  private $config;
  public function __construct($config)
  {
     $this->config = $config;
  }
}
$config = new Config;
$exampleA = new ExampleA($config);

ยกระดับชั้นเรียน

class ExampleB
{
  private $config;
  public function __construct()
  {
     $this->config = new Config;
  }
}
$exampleA = new ExampleA();

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


อาจจะดีกว่านี้ในcodereview.stackexchange.com/questions ?
FrustratedWithFormsDesigner

9
@FrustratedWithFormsDesigner - นี่ไม่เหมาะสำหรับการตรวจสอบโค้ด
ChrisF

คำตอบ:


14

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

ในทางตรงกันข้ามบางทีคุณอาจExampleA ต้องการconfigวัตถุใหม่และสะอาดดังนั้นอาจมีบางกรณีที่ตัวอย่างที่สองเหมาะสมกว่าเช่นกรณีที่แต่ละอินสแตนซ์อาจมีการกำหนดค่าที่แตกต่างกัน


1
ดังนั้น A ดีสำหรับการทดสอบหน่วย B ดีสำหรับสถานการณ์อื่น ๆ ... ทำไมไม่มีทั้งสองอย่าง ฉันมักจะมีสอง constructors หนึ่งสำหรับ DI คู่มือหนึ่งสำหรับการใช้งานปกติ (ฉันใช้ Unity สำหรับ DI ซึ่งจะสร้างนวกรรมิกเพื่อตอบสนองความต้องการ DI)
Ed James

3
@EdWoodcock ไม่เกี่ยวกับการทดสอบหน่วยกับสถานการณ์อื่น ๆ วัตถุนั้นต้องการการพึ่งพาจากภายนอกหรือจัดการมันจากด้านใน ไม่เคยทั้งคู่ / อย่างใดอย่างหนึ่ง
MattDavey

9

อย่าลืมเกี่ยวกับการทดสอบ!

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

ดังนั้นฉันจะไปกับตัวเลือกแรกของ ExampleA


นั่นเป็นเหตุผลที่ผมกล่าวถึงการทดสอบ - ขอบคุณสำหรับการเพิ่มที่ :)
นักโทษ

5

หากวัตถุนั้นมีความรับผิดชอบในการจัดการอายุการใช้งานของการพึ่งพามันก็โอเคที่จะสร้างวัตถุในตัวสร้าง (และกำจัดทิ้งใน destructor) *

หากวัตถุนั้นไม่รับผิดชอบในการจัดการอายุการใช้งานของการพึ่งพาก็ควรส่งผ่านไปยังตัวสร้างและจัดการจากภายนอก (เช่นโดยคอนเทนเนอร์ IoC)

ในกรณีนี้ฉันไม่คิดว่า ClassA ของคุณควรรับผิดชอบในการสร้าง $ config เว้นแต่ว่าจะเป็นผู้รับผิดชอบในการกำจัดหรือหากการกำหนดค่านั้นไม่ซ้ำกันสำหรับแต่ละอินสแตนซ์ของ ClassA

* เพื่อช่วยในการทดสอบผู้สร้างสามารถอ้างอิงถึงระดับโรงงาน / วิธีการสร้างการพึ่งพาในตัวสร้างเพิ่มการทำงานร่วมกันและการทดสอบ

//Object which manages the lifetime of its dependency (C#):
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA()
    {
        this.Config = new Config(); // Tightly coupled to Config class...
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}

// Object which does not manage its dependency:
public class ClassA
{
    public Config Config { get; set; }

    public ClassA(Config config) // dependency may be injected...
    {
        this.Config = config;
    }
}

// Object which manages its dependency in a testable way:
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA(IConfigFactory configFactory) // dependency may be mocked...
    {
        this.Config = configFactory.BuildConfig();
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}

2

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

ในตัวอย่างของคุณจะเป็นอย่างไรถ้าคลาส Config ของคุณ:

a) มีการจัดสรรที่ไม่สำคัญเช่นมาจากกลุ่มหรือวิธีการจากโรงงาน

b) อาจไม่สามารถจัดสรรได้ ผ่านเข้าไปในตัวสร้างอย่างหลีกเลี่ยงปัญหานี้

c) จริง ๆ แล้วเป็น subclass ของ Config

การส่งผ่านวัตถุไปยังตัวสร้างให้ความยืดหยุ่นมากที่สุด


1

คำตอบด้านล่างไม่ถูกต้อง แต่ฉันจะเก็บไว้เพื่อให้ผู้อื่นได้เรียนรู้จากมัน (ดูด้านล่าง)

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

class ExampleA
{
  private $config;
  public function __construct()
  {
     $this->config = Config->getInstance();
  }
}
$exampleA = new ExampleA();

ในExampleBในมืออื่น ๆ คุณจะเคยได้รับอินสแตนซ์ที่แยกต่างหากจากตัวอย่างของแต่ละConfigExampleB

เวอร์ชันที่คุณควรใช้นั้นขึ้นอยู่กับว่าแอปพลิเคชันจะจัดการกับอินสแตนซ์ของConfig:

  • ถ้าอินสแตนซ์ของแต่ละExampleXควรมีอินสแตนซ์ที่แยกต่างหากจากConfigไปกับExampleB;
  • ถ้าอินสแตนซ์ของแต่ละExampleXจะแบ่งปันหนึ่ง (และมีเพียงหนึ่ง) ตัวอย่างของConfigการใช้ExampleA with Config Singleton;
  • ถ้ากรณีExampleXอาจจะใช้อินสแตนซ์ที่แตกต่างกันของการติดกับConfigExampleA

ทำไมการแปลงConfigเป็นซิงเกิลตันผิด:

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

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

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

  3. แม้ว่าอินสแตนซ์ที่หนึ่งเดียวเท่านั้นConfigอาจจะเป็นจริงสำหรับนิรันดร์ทั้งหมด แต่อาจเกิดขึ้นได้ที่คุณต้องการใช้คลาสย่อยบางส่วนของConfig(ในขณะที่ยังมีอินสแตนซ์เดียวเท่านั้น) แต่เนื่องจากรหัสได้รับอินสแตนซ์โดยตรงผ่านgetInstance()จากConfigซึ่งเป็นstaticวิธีการจึงไม่มีวิธีรับคลาสย่อย ต้องเขียนรหัสอีกครั้ง

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

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

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


4
ฉันจะไม่แนะนำให้ทำเช่นนี้ .. ด้วยวิธีนี้คุณจะได้เรียนคู่อย่างแน่นหนาExampleAและConfig- ซึ่งไม่ใช่สิ่งที่ดี
พอล

@ พอล: นั่นเป็นเรื่องจริง จับดีไม่ได้คิดเรื่องนั้น
gablin

3
ฉันมักจะแนะนำไม่ให้ใช้ Singletons เพื่อเหตุผลในการทดสอบ พวกมันเป็นตัวแปรระดับโลกและเป็นไปไม่ได้ที่จะล้อเลียนการพึ่งพา
MattDavey

4
ฉันจะแนะนำให้ใช้ Singletons อีกครั้งเพื่อเหตุผลที่คุณทำผิด
Raynos

1

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

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


1

ตัวอย่างแรกของคุณคือตัวอย่างของรูปแบบการฉีดที่ต้องพึ่งพา คลาสที่มีการพึ่งพาภายนอกจะถูกส่งมอบการพึ่งพาโดยนวกรรมิก setter ฯลฯ

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

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

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


0

ถ้าคลาสของคุณไม่ได้เปิดเผย$configคลาสภายนอกฉันจะสร้างคลาสนี้ภายใน constructor ด้วยวิธีนี้คุณจะรักษาสถานะภายในของคุณเป็นแบบส่วนตัว

หาก$configจำเป็นต้องมีการตั้งค่าสถานะภายในของตนเอง (เช่นจำเป็นต้องมีการเชื่อมต่อฐานข้อมูลหรือบางส่วนของเขตข้อมูลเริ่มต้น) ก่อนการใช้งานจากนั้นจึงเหมาะสมที่จะเลื่อนการเริ่มต้นเป็นรหัสภายนอกบางตัว (อาจเป็นคลาสโรงงาน) ตัวสร้าง หรืออย่างที่คนอื่น ๆ ชี้ให้เห็นถ้ามันต้องการที่จะแบ่งปันกับวัตถุอื่น


0

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

ExampleB ขอควบคู่ไปเรียนคอนกรีต Config ซึ่งเป็นที่ไม่ดี

การสร้างอินสแตนซ์ของวัตถุสร้างการเชื่อมต่อที่ดีระหว่างคลาส ควรทำในคลาสของโรงงาน

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