ปรับสมดุลการพึ่งพาการฉีดด้วยการออกแบบ API สาธารณะ


13

ฉันได้ไตร่ตรองถึงวิธีการสร้างความสมดุลให้กับการออกแบบที่ทดสอบได้โดยใช้การฉีดแบบพึ่งพาและการให้ API สาธารณะคงที่แบบง่าย ขึ้นเขียงของฉันคือ: ผู้คนต้องการทำสิ่งที่ชอบvar server = new Server(){ ... }และไม่ต้องกังวลเกี่ยวกับการสร้างการอ้างอิงจำนวนมากและกราฟของการอ้างอิงที่ a Server(,,,,,,)อาจมี ในขณะที่กำลังพัฒนาฉันไม่ต้องกังวลมากเกินไปเพราะฉันใช้กรอบ IoC / DI เพื่อจัดการทุกอย่าง (ฉันไม่ได้ใช้แง่มุมการจัดการวงจรชีวิตของคอนเทนเนอร์ใด ๆ ซึ่งจะทำให้สิ่งต่าง ๆ ยุ่งยากมากขึ้น)

ตอนนี้การพึ่งพาไม่น่าจะนำมาใช้ใหม่ การแยกชิ้นส่วนในกรณีนี้เกือบหมดจดสำหรับการทดสอบ (และการออกแบบที่เหมาะสม!) แทนที่จะสร้างตะเข็บเพื่อขยาย ฯลฯ คนจะ 99.999% ของเวลาที่ต้องการใช้การกำหนดค่าเริ่มต้น ดังนั้น. ฉันสามารถ hardcode การอ้างอิง ไม่ต้องการทำอย่างนั้นเราแพ้การทดสอบของเรา! ฉันสามารถสร้างคอนสตรัคเตอร์เริ่มต้นพร้อมการอ้างอิงแบบฮาร์ดโค้ดและตัวอ้างอิงที่ขึ้นต่อกัน นั่นคือ ... ยุ่งและมีแนวโน้มที่จะสับสน แต่ทำงานได้ ฉันสามารถสร้างการพึ่งพาที่ได้รับภายในตัวสร้างและทำให้หน่วยของฉันทดสอบการชุมนุมเพื่อน (สมมติว่า C #) ซึ่งเป็นที่สาธารณะ API สาธารณะ แต่ทิ้งกับดักซ่อนน่ารังเกียจที่ซุ่มซ่อนสำหรับการบำรุงรักษา การมีคอนสตรัคเตอร์สองตัวที่เชื่อมต่อโดยนัยมากกว่าการออกแบบที่ไม่ดีโดยทั่วไปในหนังสือของฉัน

ในขณะนั้นเป็นสิ่งที่ชั่วร้ายที่สุดที่ฉันสามารถนึกได้ ความเห็น? ภูมิปัญญา?

คำตอบ:


11

การพึ่งพาการฉีดเป็นรูปแบบที่ทรงพลังเมื่อใช้งานได้ดี แต่บ่อยครั้งที่ผู้ปฏิบัติงานของมันต้องพึ่งพากรอบภายนอกบางอย่าง API ที่ได้นั้นค่อนข้างเจ็บปวดสำหรับพวกเราที่ไม่ต้องการผูกแอพของเราอย่างแน่นหนาด้วยเทปพันสาย XML อย่าลืมวัตถุเก่าธรรมดา (POO)

ก่อนอื่นให้พิจารณาว่าคุณคาดหวังว่าจะมีคนใช้ API ของคุณอย่างไร - โดยไม่ต้องมีกรอบการทำงาน

  • คุณสร้างอินสแตนซ์ของเซิร์ฟเวอร์ได้อย่างไร
  • คุณขยายเซิร์ฟเวอร์อย่างไร
  • คุณต้องการเปิดเผยอะไรใน API ภายนอก
  • คุณต้องการซ่อนอะไรใน API ภายนอก

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

public Server() 
    : this(new HttpListener(80), new HttpResponder())
{}

public Server(Listener listener, Responder responder)
{
    // ...
}

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

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

หากคุณต้องการให้friendการเข้าถึงการทดสอบของคุณเพื่อตั้งค่าวัตถุเซิร์ฟเวอร์เพื่อแยกการทดสอบให้ไปหามัน มันจะไม่เป็นส่วนหนึ่งของ API สาธารณะซึ่งเป็นสิ่งที่คุณต้องการระวัง

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


ฉันเห็นด้วยและแน่นอนว่าฉันไม่มีแฟนของซุปเปอร์มาร์เก็ต IoC - การกำหนดค่า XML ไม่ใช่สิ่งที่ฉันใช้เลยที่เกี่ยวข้องกับ IoC แน่นอนว่าการใช้ IoC นั้นเป็นสิ่งที่ฉันหลีกเลี่ยง - มันเป็นข้อสันนิษฐานที่ผู้ออกแบบ API สาธารณะไม่สามารถทำได้ ขอบคุณสำหรับความคิดเห็นมันเป็นไปตามบรรทัดที่ฉันคิดและมันก็มั่นใจที่จะมีสติตรวจสอบในขณะนี้และอีกครั้ง!
kolektiv

-1 คำตอบนี้โดยทั่วไปบอกว่า "อย่าใช้การฉีดพึ่งพา" และมีสมมติฐานที่ไม่ถูกต้อง ในตัวอย่างของคุณถ้าคอนHttpListenerสตรัคเตอร์เปลี่ยนแปลงServerคลาสจะต้องเปลี่ยนด้วย พวกเขากำลังเชื่อมโยงกัน ทางออกที่ดีกว่าคือสิ่งที่ @Winston Ewert กล่าวไว้ด้านล่างซึ่งจะให้คลาสเริ่มต้นพร้อมการอ้างอิงที่ระบุไว้ล่วงหน้านอก API ของไลบรารี จากนั้นโปรแกรมเมอร์ไม่ได้ถูกบังคับให้ตั้งค่าการพึ่งพาทั้งหมดตั้งแต่คุณตัดสินใจสำหรับเขา แต่เขายังคงมีความยืดหยุ่นในการเปลี่ยนพวกเขาในภายหลัง นี่เป็นเรื่องใหญ่เมื่อคุณมีการอ้างอิงของบุคคลที่สาม
M. Dudley

FWIW ตัวอย่างที่ให้มาเรียกว่า "การฉีดไอ้เลว" stackoverflow.com/q/2045904/111327 stackoverflow.com/q/6733667/111327
M. Dudley

1
@MichaelDudley คุณอ่านคำถามของ OP แล้วหรือยัง "สมมติฐาน" ทั้งหมดขึ้นอยู่กับข้อมูลที่ให้ไว้ของ OP ครั้งหนึ่ง "รูปแบบการฉีดไอ้เลว" ตามที่คุณเรียกว่าเป็นรูปแบบการฉีดที่ได้รับความนิยมโดยเฉพาะอย่างยิ่งสำหรับระบบที่องค์ประกอบไม่เปลี่ยนแปลงระหว่างรันไทม์ Setters และ getters ก็เป็นอีกรูปแบบหนึ่งของการฉีดแบบพึ่งพา ฉันใช้ตัวอย่างตามสิ่งที่ OP จัดไว้ให้
Berin Loritsch

4

ใน Java ในกรณีเช่นนี้เป็นปกติที่จะมีการกำหนดค่า "เริ่มต้น" ที่สร้างโดยโรงงานเก็บเป็นอิสระจากเซิร์ฟเวอร์ที่มีพฤติกรรม "ถูกต้อง" ตามจริง ดังนั้นผู้ใช้ของคุณเขียน:

var server = DefaultServer.create();

ในขณะที่คอนServerสตรัคของยังคงยอมรับการอ้างอิงทั้งหมดและสามารถใช้สำหรับการปรับแต่งลึก


+1, SRP ความรับผิดชอบของสำนักงานทันตแพทย์ไม่ได้เป็นการสร้างสำนักงานทันตแพทย์ ความรับผิดชอบของเซิร์ฟเวอร์ไม่ใช่การสร้างเซิร์ฟเวอร์
R. Schmitz

2

วิธีการเกี่ยวกับการจัดคลาสย่อยซึ่งมี "public api"

class StandardServer : Server
{
    public StandardServer():
        this( depends1, depends2, depends3)
    {
    }
}

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

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


1

ฉันสามารถสร้างการพึ่งพาที่ได้รับภายในตัวสร้างและทำให้หน่วยของฉันทดสอบการชุมนุมเพื่อน (สมมติว่า C #) ซึ่งเป็นที่สาธารณะ API สาธารณะ แต่ทิ้งกับดักซ่อนน่ารังเกียจที่ซุ่มซ่อนสำหรับการบำรุงรักษา

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


0

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

ฉันขอแนะนำให้ใช้InjectableFactory / InjectableInstanceรูปแบบ InjectableInstanceเป็นอาคารเรียนง่ายนำมาใช้ใหม่ซึ่งเป็นผู้ถือคุ้มค่าแน่นอน อินเทอร์เฟซคอมโพเนนต์ประกอบด้วยการอ้างอิงไปยังตัวยึดค่านี้ซึ่งเริ่มต้นด้วยการใช้งานเริ่มต้น อินเทอร์เฟซจะให้วิธีการแบบซิงเกิลget ()ซึ่งมอบหมายให้ผู้ถือค่า ไคลเอ็นต์ส่วนประกอบจะเรียกใช้วิธีการget ()แทนวิธีใหม่เพื่อรับอินสแตนซ์ของการนำไปใช้เพื่อหลีกเลี่ยงการพึ่งพาโดยตรง ในช่วงเวลาทดสอบเราสามารถเลือกแทนที่การใช้งานเริ่มต้นด้วยการจำลอง ฉันจะใช้TimeProviderหนึ่งตัวอย่างคลาสที่เป็นนามธรรมของDate ()เพื่อให้สามารถทดสอบหน่วยในการฉีดและจำลองเพื่อจำลองช่วงเวลาที่แตกต่างกัน ขอโทษฉันจะใช้จาวาที่นี่เพราะฉันคุ้นเคยกับจาวามากขึ้น C # ควรคล้ายกัน

public interface TimeProvider {
  // A mutable value holder with default implementation
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  
  static TimeProvider get() { return instance.get(); }  // Singleton method.

  class Impl implements TimeProvider {        // Default implementation                                    
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  class Mock implements TimeProvider {   // Mock implemention
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// The client of TimeProvider
Order order = new Order().setCreationDate(TimeProvider.get().getDate()));

// In the unit testing
TimeProvider.Mock timeMock = new TimeProvider.Mock();
TimeProvider.instance.setInstance(timeMock);  // Inject mock implementation

InjectableInstance นั้นใช้งานง่ายมากฉันมีการใช้งานอ้างอิงใน java สำหรับรายละเอียดโปรดอ้างอิงโพสต์บล็อกของฉันอ้างอิงทางอ้อมกับโรงงานฉีด


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

ในโครงการ Java maven อินสแตนซ์ที่ใช้ร่วมกันไม่ใช่ปัญหาเนื่องจากเอ็นจิ้น junit จะรันการทดสอบแต่ละครั้งในคลาสโหลดเดอร์ที่ต่างกันอย่างไรก็ตามฉันพบปัญหานี้ใน IDE บางตัวเนื่องจากอาจใช้ตัวโหลดคลาสเดียวกันซ้ำได้ เพื่อแก้ปัญหานี้คลาสจะจัดเตรียมวิธีการรีเซ็ตที่จะถูกเรียกให้รีเซ็ตเป็นสถานะดั้งเดิมหลังจากการทดสอบแต่ละครั้ง ในทางปฏิบัติมันเป็นสถานการณ์ที่หายากที่การทดสอบที่แตกต่างต้องการใช้การจำลองที่แตกต่างกัน เราใช้รูปแบบนี้สำหรับโครงการขนาดใหญ่เป็นเวลาหลายปีทุกอย่างทำงานได้อย่างราบรื่นเราสนุกกับการอ่านโค้ดที่สะอาดโดยไม่เสียสละการพกพาและการทดสอบ
Jianwu Chen
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.