การฉีดมรดกและการพึ่งพา


101

ฉันมีชุดของส่วนประกอบ angular2 ที่ทุกคนควรได้รับการฉีดบริการ ความคิดแรกของฉันคือการสร้างซูเปอร์คลาสและฉีดบริการที่นั่นจะดีที่สุด จากนั้นส่วนประกอบใด ๆ ของฉันจะขยายซุปเปอร์คลาสนั้น แต่วิธีนี้ไม่ได้ผล

ตัวอย่างง่าย:

export class AbstractComponent {
  constructor(private myservice: MyService) {
    // Inject the service I need for all components
  }
}

export MyComponent extends AbstractComponent {
  constructor(private anotherService: AnotherService) {
    super(); // This gives an error as super constructor needs an argument
  }
}

ฉันสามารถแก้ปัญหานี้ได้โดยการฉีดMyServiceเข้าไปในแต่ละองค์ประกอบและใช้อาร์กิวเมนต์นั้นสำหรับการsuper()โทร แต่นั่นก็เป็นเรื่องที่ไร้สาระอย่างแน่นอน

จะจัดระเบียบส่วนประกอบของฉันให้ถูกต้องได้อย่างไรเพื่อให้รับช่วงบริการจากซูเปอร์คลาส


นี่ไม่ใช่รายการที่ซ้ำกัน คำถามที่อ้างถึงคือเกี่ยวกับวิธีการสร้างคลาส DERIVED ที่สามารถเข้าถึงบริการที่ถูกแทรกโดยซูเปอร์คลาสที่กำหนดไว้แล้ว คำถามของฉันเกี่ยวกับวิธีสร้างคลาส SUPER ที่สืบทอดบริการไปยังคลาสที่ได้รับ มันเป็นวิธีอื่น ๆ
maxhb

คำตอบของคุณ (ในคำถามของคุณ) ไม่สมเหตุสมผลสำหรับฉัน ด้วยวิธีนี้คุณจะสร้างหัวฉีดที่ไม่ขึ้นกับหัวฉีด Angular ที่ใช้สำหรับแอปพลิเคชันของคุณ การใช้new MyService()แทนการฉีดจะให้ผลลัพธ์เหมือนกันทุกประการ (ยกเว้นมีประสิทธิภาพมากกว่า) หากคุณต้องการแชร์อินสแตนซ์บริการเดียวกันกับบริการและ / หรือส่วนประกอบต่างๆสิ่งนี้จะไม่ทำงาน ทุกชั้นเรียนจะได้รับMyServiceอินสแตนซ์อื่น
GünterZöchbauer

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

การฉีดหัวฉีดเป็นเพียงการปรับปรุงเมื่อมีบริการต่างๆที่ต้องฉีดในหลาย ๆ ที่ คุณยังสามารถฉีดบริการที่มีการอ้างอิงกับบริการอื่น ๆ และให้บริการโดยใช้ getter (หรือวิธีการ) วิธีนี้คุณจะต้องฉีดบริการเดียว แต่สามารถใช้ชุดบริการได้ โซลูชันของคุณและทางเลือกที่ฉันเสนอมีทั้งข้อเสียที่ทำให้ดูยากขึ้นว่าคลาสใดขึ้นอยู่กับบริการใด ฉันอยากจะสร้างเครื่องมือ (เช่นเทมเพลตสดใน WebStorm) ที่ช่วยให้สร้างโค้ดสำเร็จรูปได้ง่ายขึ้นและมีความชัดเจนเกี่ยวกับการอ้างอิง
GünterZöchbauer

คำตอบ:


73

ฉันสามารถแก้ปัญหานี้ได้โดยการฉีด MyService ภายในแต่ละองค์ประกอบและใช้อาร์กิวเมนต์นั้นสำหรับการเรียก super () แต่นั่นเป็นเรื่องที่ไร้สาระอย่างแน่นอน

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

คลาสที่ฉีดได้ทุกคลาสจะต้องประกาศการอ้างอิงเป็นพารามิเตอร์คอนสตรัคเตอร์และหากซูเปอร์คลาสมีการอ้างอิงด้วยสิ่งเหล่านี้จำเป็นต้องระบุไว้ในคอนสตรัคเตอร์ของคลาสย่อยด้วยและส่งต่อไปยังคลาสซูเปอร์คลาสด้วยการsuper(dep1, dep2)เรียก

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

ซ่อนการอ้างอิงซึ่งทำให้โค้ดอ่านยากขึ้น
มันละเมิดความคาดหวังของคนที่คุ้นเคยกับวิธีการทำงานของ Angular2 DI
มันทำลายการคอมไพล์ออฟไลน์ที่สร้างโค้ดแบบคงที่เพื่อแทนที่ DI ที่ประกาศและจำเป็นเพื่อปรับปรุงประสิทธิภาพและลดขนาดโค้ด


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

9
คำตอบสำหรับคำถามของเขาคือแฮ็คที่น่าเกลียด คำถามนี้แสดงให้เห็นแล้วว่าควรทำอย่างไร ฉันอธิบายเพิ่มเติมอีกเล็กน้อย
GünterZöchbauer

7
คำตอบนี้ถูกต้อง OP ตอบคำถามของตัวเอง แต่มีข้อตกลงมากมายในการทำเช่นนั้น การที่คุณระบุข้อเสียที่แท้จริงนั้นมีประโยชน์เช่นกันและฉันจะรับรองมัน - ฉันคิดในสิ่งเดียวกัน
dudewad

6
ฉันต้องการ (และดำเนินการต่อไป) ใช้คำตอบนี้กับ "แฮ็ก" ของ OP แต่ฉันต้องบอกว่านี่ดูห่างไกลจาก DRY และเจ็บปวดมากเมื่อฉันต้องการเพิ่มการพึ่งพาในคลาสพื้นฐาน ฉันต้องเพิ่ม ctor injection (และการsuperโทรที่เกี่ยวข้อง) ไปยังคลาสมากกว่า 20 คลาสและจำนวนนั้นจะเพิ่มขึ้นในอนาคต มีสองอย่างคือ 1) ฉันไม่อยากเห็น "codebase ขนาดใหญ่" ทำแบบนี้ และ 2) ขอบคุณพระเจ้าสำหรับ vim qและ vscodectrl+.

6
เพียงเพราะไม่สะดวกไม่ได้หมายความว่าเป็นการปฏิบัติที่ไม่ดี คอนสตรัคเตอร์ไม่สะดวกเพราะการเริ่มต้นอ็อบเจ็กต์ทำได้ยาก ฉันจะโต้แย้งว่าการปฏิบัติที่แย่กว่านั้นคือการสร้างบริการที่ต้องการ "คลาสพื้นฐานที่ฉีด 15 บริการและสืบทอดโดย 6"
GünterZöchbauer

66

โซลูชันที่อัปเดตป้องกันไม่ให้ myService หลายอินสแตนซ์ถูกสร้างขึ้นโดยใช้ global injector

import {Injector} from '@angular/core';
import {MyServiceA} from './myServiceA';
import {MyServiceB} from './myServiceB';
import {MyServiceC} from './myServiceC';

export class AbstractComponent {
  protected myServiceA:MyServiceA;
  protected myServiceB:MyServiceB;
  protected myServiceC:MyServiceC;

  constructor(injector: Injector) {
    this.settingsServiceA = injector.get(MyServiceA);
    this.settingsServiceB = injector.get(MyServiceB);
    this.settingsServiceB = injector.get(MyServiceC);
  }
}

export MyComponent extends AbstractComponent {
  constructor(
    private anotherService: AnotherService,
    injector: Injector
  ) {
    super(injector);

    this.myServiceA.JustCallSomeMethod();
    this.myServiceB.JustCallAnotherMethod();
    this.myServiceC.JustOneMoreMethod();
  }
}

สิ่งนี้จะช่วยให้มั่นใจได้ว่า MyService สามารถใช้ภายในคลาสใดก็ได้ที่ขยาย AbstractComponent โดยไม่จำเป็นต้องฉีด MyService ในทุกคลาสที่ได้รับ

มีข้อเสียบางประการสำหรับการแก้ปัญหานี้ (ดู Ccomment จาก @ GünterZöchbauerด้านล่างคำถามเดิมของฉัน):

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

สำหรับคำอธิบายที่เป็นลายลักษณ์อักษรเกี่ยวกับการฉีดแบบพึ่งพาใน Angular2 โปรดดูโพสต์บล็อกนี้ซึ่งช่วยฉันอย่างมากในการแก้ปัญหา: http://blog.thoughtram.io/angular/2015/05/18/dependency-injection-in-angular- 2.html


7
ทำให้ยากที่จะเข้าใจว่าบริการใดบ้างที่ฉีดเข้าไป
Simon Dufour

1
ไม่ควรเป็นthis.myServiceA = injector.get(MyServiceA);ฯลฯ ?
jenson-button-event

10
คำตอบของ @Gunter Zochbauer คือคำตอบที่ถูกต้อง นี่ไม่ใช่วิธีที่ถูกต้องในการทำเช่นนี้และแบ่งข้อตกลงเชิงมุมจำนวนมาก อาจจะง่ายกว่าในการเข้ารหัสการเรียกการฉีดทั้งหมดนั้นเป็น "ความเจ็บปวด" แต่ถ้าคุณต้องการเสียสละที่จะต้องเขียนโค้ดคอนสตรัคเตอร์เพื่อให้สามารถรักษาโค้ดเบสขนาดใหญ่ได้แสดงว่าคุณกำลังถ่ายภาพตัวเองอยู่ โซลูชันนี้ไม่สามารถปรับขนาดได้ IMO และจะทำให้เกิดข้อบกพร่องที่สับสนมากมายตามท้องถนน
dudewad

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

@maxhb คุณเคยสำรวจการขยายไปInjectorทั่วโลกเพื่อหลีกเลี่ยงการเชื่อมโยงพารามิเตอร์ใด ๆAbstractComponentหรือไม่? fwiw ฉันคิดว่าคุณสมบัติการแทรกการอ้างอิงลงในคลาสพื้นฐานที่ใช้กันอย่างแพร่หลายเพื่อหลีกเลี่ยงการเชื่อมโยงตัวสร้างที่ยุ่งเหยิงเป็นข้อยกเว้นที่ถูกต้องอย่างสมบูรณ์สำหรับกฎปกติ
quentin-starin

5

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

คลาสที่ได้รับ:

@Component({
    ...
    providers: [ProviderService]
})
export class DerivedComponent extends BaseComponent {
    constructor(protected providerService: ProviderService) {
        super(providerService);
    }
}

คลาสพื้นฐาน:

export class BaseComponent {
    constructor(protected providerService: ProviderService) {
        // do something with providerService
    }
}

ชั้นให้บริการ:

@Injectable()
export class ProviderService {
    constructor(private _apiService: ApiService, private _authService: AuthService) {
    }
}

3
ปัญหาคือคุณเสี่ยงที่จะสร้างบริการ "ลิ้นชักขยะ" ซึ่งโดยพื้นฐานแล้วเป็นเพียงพร็อกซีสำหรับบริการหัวฉีด
kpup

1

แทนที่จะฉีดบริการที่มีบริการอื่น ๆ ทั้งหมดเป็นการอ้างอิงเช่นนี้:

class ProviderService {
    constructor(private service1: Service1, private service2: Service2) {}
}

class BaseComponent {
    constructor(protected providerService: ProviderService) {}

    ngOnInit() {
        // Access to all application services with providerService
        this.providerService.service1
    }
}

class DerivedComponent extends BaseComponent {
    ngOnInit() {
        // Access to all application services with providerService
        this.providerService.service1
    }
}

ฉันจะข้ามขั้นตอนพิเศษนี้และเพิ่มการฉีดบริการทั้งหมดใน BaseComponent ดังนี้:

class BaseComponent {
    constructor(protected service1: Service1, protected service2: Service2) {}
}

class DerivedComponent extends BaseComponent {
    ngOnInit() {
        this.service1;
        this.service2;
    }
}

เทคนิคนี้ถือว่า 2 สิ่ง:

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

  2. ทุกส่วนประกอบขยายไฟล์ BaseComponent

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


2
จากนั้นคลาสพื้นฐานจะมีการพึ่งพาบริการทั้งหมดที่เด็ก ๆ ต้องการ ChildComponentA ต้องการ ServiceA? ตอนนี้ ChildComponentB ได้รับ ServiceA ด้วย
knallfrosch

1

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


0

หากได้รับคลาสหลักจากปลั๊กอินของบุคคลที่สาม (และคุณไม่สามารถเปลี่ยนแหล่งที่มาได้) คุณสามารถทำได้

import { Injector } from '@angular/core';

export MyComponent extends AbstractComponent {
  constructor(
    protected injector: Injector,
    private anotherService: AnotherService
  ) {
    super(injector.get(MyService));
  }
}

หรือวิธีที่ดีที่สุด (อยู่เพียงพารามิเตอร์เดียวในตัวสร้าง):

import { Injector } from '@angular/core';

export MyComponent extends AbstractComponent {
  private anotherService: AnotherService;

  constructor(
    protected injector: Injector
  ) {
    super(injector.get(MyService));
    this.anotherService = injector.get(AnotherService);
  }
}

0

แฮ็คน่าเกลียด

ไม่นานมานี้ลูกค้าของฉันบางคนต้องการเข้าร่วมโครงการเชิงมุมขนาดใหญ่สองโครงการเมื่อวานนี้ (เชิงมุม v4 เป็นเชิงมุม v8) Project v4 ใช้คลาส BaseView สำหรับแต่ละองค์ประกอบและมีtr(key)วิธีการแปล (ใน v8 ฉันใช้ ng-translate) ดังนั้นเพื่อหลีกเลี่ยงการสลับระบบการแปลและแก้ไขเทมเพลตหลายร้อยแบบ (ใน v4) หรือระบบการแปลการตั้งค่า 2 แบบขนานฉันใช้การแฮ็คที่น่าเกลียด (ฉันไม่ภูมิใจกับมัน) - ในAppModuleชั้นเรียนฉันเพิ่มตัวสร้างต่อไปนี้:

export class AppModule { 
    constructor(private injector: Injector) {
        window['UglyHackInjector'] = this.injector;
    }
}

และตอนนี้AbstractComponentคุณสามารถใช้

export class AbstractComponent {
  private myservice: MyService = null;

  constructor() {
    this.myservice = window['UglyHackInjector'].get(MyService);
  }
}

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