سرویس‌ها (Service) و تزریق وابستگی (Dependency Injection) در انگولار

16 آبان 1397
angular-services-and-di

با مطالعه‌ی ۵ فصل گذشته اطلاعات نسبتا جامعی نسبت به کامپوننت‌ها، دستورات و view بدست آوردید. در این فصل مبحثی تخصصی به نام تزریق وابستگی یا Dependency Injection‌ برای فریم‌ورک قدرتمند انگولار مطرح خواهیم کرد. هرچند در یکی از مقالات به صورت کامل این مبحث را از ۰ تا ۱۰۰ پوشش دادیم ولی اینبار لازم دانستیم که در انگولار این موضوع را تخصصی تر مورد بررسی قرار دهیم.

Dependency Injection یا تزریق وابستگی چیست؟

در زبان‌های برنامه‌نویسی تزریق وابستگی یا Dependency Injection یک پترن یا الگوی طراحی است که با استفاده از آن وابستگی‌های موجود بین دو کلاس با استفاده از یک واسط (Interface) حذف خواهند شد.

در صورتیکه با مفهوم تزریق وابستگی آشنایی ندارید لطفا مقاله زیر را حتما مطالعه بفرمایید:

Service (سرویس) چیست؟

سرویس (Service)‌ یک ساختار است که در انگولار مورد استفاده قرار گرفته و وظیفه‌ی آن جلوگیری از تکرار کدها درون کامپوننت‌ها و ایجاد یک مخزن یا Repository جهت دستیابی کامپوننت‌ها به آن اطلاعات است.

جهت تشریح بهتر این موضوع به تصویر زیر توجه کنید:

دیاگرام سرویس‌ در انگولار

در این دیاگرام فرض کنید قصد طراحی یک نرم‌افزار را داریم به گونه‌ای که قسمتی برای کاربران و قسمتی برای معرفی یا «درباره ما» درنظر گرفته‌ایم.

حال در کامپوننت کاربران همانطور که ملاحظه می‌‎کنید اطلاعات کاربر از طریق کامپوننت User دریافت و توسط کامپوننت User Detail نمایش داده می‌‎شود.

در این دیاگرام اگر از سرویس‌ها استفاده نکنیم طبیعتا باید دوبار دستور log.console را در دو کامپوننت جداگانه تکرار کرده و اطلاعات را درون کامپوننت UserComponent بازیابی کنیم. اما برای جلوگیری از این تکرار دو مخزن یا repository که به صورت Service‌ در انگولار مورد استفاده قرار می‌‎گیرد، ایجاد کرده و آنها را در اختیار کامپوننت‌هایی که نیاز به اطلاعات دارند، قرار می‌دهیم.

شروع با یک مثال

جهت درک بهتر این مفهوم یک مثال کاربردی ارائه خواهیم داد. فرض کنید می‌خواهیم یک فرم جهت ایجاد حساب کاربری بوجود بیاوریم که در آن هر کاربر یا در سه وضعیت فعال، غیرفعال و ناشناخته تنظیم کنیم.

در این مثال توضیحاتی بابت دستورهای قبلی ارائه نمی‌شود زیرا در ۵ فصل گذشته به تفصیل هر دستور را بررسی کرده‌ایم. بلکه تنها کدهای مربوط و مراحل ساخت کامپوننت‌ها را ارائه خواهیم کرد تا مرحله‌ی اضافه کردن سرویس (Service) به ساختار برنامه برسیم.

بنابراین در ابتدا دو کامپوننت به نام‌های account و new-account‌ در پوشه اصلی کامپوننت‌ها ایجاد می‌کنیم:

ng g c account
ng g c new-account

سپس فایل فایل new-account.component.html را باز کرده و کدهای زیر را درون آن قرار می‌دهیم:

<div class="row">
    <div class="col-xs-12 col-md-8 col-md-offset-2">
        <div class="form-group">
            <label>نام کاربری</label>
            <input
                    type="text"
                    class="form-control"
                    #accountName>
        </div>
        <div class="form-group">
            <select class="form-control" #status>
                <option value="فعال">فعال</option>
                <option value="غیرفعال">غیرفعال</option>
                <option value="مخفی">مخفی</option>
            </select>
        </div>
        <button
                class="btn btn-primary"
                (click)="onCreateAccount(accountName.value, status.value)">
            اضافه کردن کاربر
        </button>
    </div>
</div>

سپس فایل new-account.component.ts را به صورت زیر بازنویسی می‌کنیم:

import {Component, OnInit, Output, EventEmitter} from '@angular/core';

@Component({
    selector: 'app-new-account',
    templateUrl: './new-account.component.html',
    styleUrls: ['./new-account.component.css']
})
export class NewAccountComponent implements OnInit {

    @Output() accountAdded = new EventEmitter<{ name: string, status: string }>();

    constructor() {
    }

    ngOnInit() {
    }

    onCreateAccount(accountName: string, accountStatus: string) {
        this.accountAdded.emit({
            name: accountName,
            status: accountStatus
        });
        console.log('وضعیت حساب کاربری تغییر کرد: ' + status);
    }

}

و همچنین برای فایل account.component.html‌ داریم:

<br>
<br>
<div class="row">
    <div class="col-xs-12 col-md-8 col-md-offset-2">
        <h5>{{ account.name }}</h5>
        <hr>
        <p>وضعیت این حساب کاربری: {{ account.status }}</p>
        <button class="btn btn-default" (click)="onSetTo('فعال')">تغییر وضعیت به «فعال»</button>
        <button class="btn btn-default" (click)="onSetTo('غیرفعال')">تغییر وضعیت به «غیرفعال»</button>
        <button class="btn btn-default" (click)="onSetTo('مخفی')">تغییر وضعیت به «مخفی»</button>
    </div>
</div>

سپس تغییرات فایل account.component.ts به صورت زیر خواهد بود:

import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';

@Component({
    selector: 'app-account',
    templateUrl: './account.component.html',
    styleUrls: ['./account.component.css']
})
export class AccountComponent implements OnInit {

    @Input() account: { name: string, status: string }
    @Input() id: number;

    @Output() statusChanged = new EventEmitter<{ id: number, newStatus: string }>();

    constructor() {
    }

    ngOnInit() {
    }

    onSetTo(status: string) {
        this.statusChanged.emit({id: this.id, newStatus:status});
        console.log('وضعیت حساب کاربری به مقدار جدیدی تغییر کرد:' + status);
    }

}

برای فایل app.component.html تغییرات زیر را خواهیم داشت:

<div class="container" dir="rtl" style="margin-top: 30px;">
    <div class="row">
        <div class="col-xs-12">
            <app-new-account (accountData)="onAddedAccount($event)"></app-new-account>
        </div>
    </div>
    <hr>
    <app-account
            *ngFor="let acc of accounts; let i= index"
            [id]="i"
            [account]="acc"
            (statusChanged)="onStatusChanged($event)"
    >
    </app-account>
</div>

در نهایت در آخرین فایل app.component.ts کدهای زیر را اضافه خواهیم کرد:

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

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {

    accounts = [
        {
            name: 'حساب کابری مدیر کل',
            status: 'فعال'
        },
        {
            name: 'اکانت تست',
            status: 'غیرفعال'
        },
        {
            name: 'حساب کاربری مخفی',
            status: 'مخفی'
        }
    ];

    onAddedAccount(newAccount:{name: string, status:string}){
        this.accounts.push(newAccount);
    }

    onStatusChanged(updateInfo: {id: number, newStatus:string}){
        this.accounts[updateInfo.id].status = updateInfo.newStatus;
    }
}

بسیار عالی! اگر تا به اینجای کار تمام کدهای فوق را به درستی ایجاد کرده باشید تصویری مشابه آنچه در زیر مشاهده می‌کنید، در مرورگر شما نمایش داده می‌شود:

شروع با یک مثال برای سرویس ها در انگولار

خب تا به اینجای کار تمام دستورهای موجود در کدها برای شما آشنا بود و مشکلی نیست. اما در این مرحله قصد داریم مفهوم سرویس را به همراه اجرای عملی روی این مثال خدمت شما عزیزان ارائه دهیم. با ما همراه باشید.

همانطور که در مثال فوق مشاهده می‌کنید در دو کامپوننت account و new-account یک دستور به نام console.log‌ به صورت تکراری استفاده شده است

بنابراین برای حذف این تکرار باید یک سرویس (Service)‌ بسازیم. در نتیجه برای اینکار یک فایل به نام logging.service.ts‌ در پوشه‌ی اصلی (app) یا هر پوشه‌ی دیگری، ایجاد خواهیم کرد و سپس دستورهای زیر را به آن اضافه می‌کنیم:

export class LoggingService {
    logStatusChanged(status: string) {
        console.log('وضعیت حساب کاربری تغییر کرد:' + status)
    }
}

حال به فایل new-account.component.ts وارد شده و به جای دستور console.log مجموعه‌ی کد زیر را اضافه می‌کنیم:

import {Component, OnInit, Output, EventEmitter} from '@angular/core';
import {LoggingService} from '../logging.service'

@Component({
    selector: 'app-new-account',
    templateUrl: './new-account.component.html',
    styleUrls: ['./new-account.component.css']
})
export class NewAccountComponent implements OnInit {

    @Output() accountAdded = new EventEmitter<{ name: string, status: string }>();

    constructor() {
    }

    ngOnInit() {
    }

    onCreateAccount(accountName: string, accountStatus: string) {
        this.accountAdded.emit({
            name: accountName,
            status: accountStatus
        });
        const service = new LoggingService();
        service.logStatusChanged(accountStatus);
    }

}

همانطور که ملاحظه می‌کنید در ابتدای صفحه فایل service را import کرده و سپس درون متد onCreateAccount به جای دستور console.log از سرویس استفاده کردیم.

در این بخش ملاحظه کردید که چگونه به صورت دستی یک سرویس ایجاد کرده و سپس آن را به مجموعه‌ی فایل‌های خود اضافه کردیم. این نحوه‌ی دسترسی به سرویس را برای شما عزیزان شرح دادیم تا با ساختار آن آشنا شوید. اما راه‌های بهتری برای استفاده و بهره‌گیری از سرویس‌ها (Service) در انگولار وجود دارد که در ادامه به آن می‌پردازیم.

Injector انگولار

در انگولار مفهومی به نام Injector‌ به عنوان ابزاری جهت دسترسی به سرویس‌ها معرفی شد. در واقع با استفاده از Injector می‌توان یک وابستگی را به یک کامپوننت تزریق کرد.

یعنی یک نمونه از یک کلاس را به صورت خودکار در کامپوننت موردنظر ایجاد کرد. برای دسترسی به این نمونه باید با مفهومی به نام provider آشنا شوید.

provider‌ چیست؟ provider‌ به انگولار می‌گوید که چگونه یک کلاس را ایجاد کن! یعنی دقیقا فرمان تولید یک نمونه از کلاس را به صورت اتوماتیک برای سرویس فراهم می‌کند. یعنی یک service provider به عنوان بستری برای تولید خودکار نمونه از یک کلاس معرفی می‌شود.

بنابراین برای اینکار ابتدا یک آرگومان به سازنده پیش‌فرض از نوع LoggingService ارسال می‌کنیم. دلیل این کار، تولید یک نمونه از کلاس LoggingService‌ به هنگام به کارگیری کامپوننت می‌باشد. بنابراین در فایل new-account.component.ts‌ تغییرات زیر را اضافه می‌کنیم:

import {Component, OnInit, Output, EventEmitter} from '@angular/core';
import {LoggingService} from '../logging.service'

@Component({
    selector: 'app-new-account',
    templateUrl: './new-account.component.html',
    styleUrls: ['./new-account.component.css'],
    providers: [LoggingService]
})
export class NewAccountComponent implements OnInit {

    @Output() accountAdded = new EventEmitter<{ name: string, status: string }>();

    constructor(private loggingService: LoggingService) {
    }

    ngOnInit() {
    }

    onCreateAccount(accountName: string, accountStatus: string) {
        this.accountAdded.emit({
            name: accountName,
            status: accountStatus
        });
        this.loggingService.logStatusChanged(accountStatus);
    }

}

همانطور که ملاحظه کردید به صورت اتوماتیک یک نمونه از کلاس LoggingService‌ توسط یک providers ایجاد شد.

همچنین همین تغییرات را نیز درون فایل account.component.ts اعمال می‌کنیم (فراموش نکنید که درون این فایل نیز یک دستور به نام console.log‌ وجود دارد و می‌خواهیم این تکرار را توسط سرویس‌ها حذف کنیم):

import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {LoggingService} from "../logging.service";

@Component({
    selector: 'app-account',
    templateUrl: './account.component.html',
    styleUrls: ['./account.component.css'],
    providers: [LoggingService]
})
export class AccountComponent implements OnInit {

    @Input() account: { name: string, status: string }
    @Input() id: number;

    @Output() statusChanged = new EventEmitter<{ id: number, newStatus: string }>();

    constructor(private loggingService: LoggingService) {
    }

    ngOnInit() {
    }

    onSetTo(status: string) {
        this.statusChanged.emit({id: this.id, newStatus:status});
        this.loggingService.logStatusChanged(status);
    }

}

حال به فایل اصلی app.component.ts مراجعه می‌کنیم. همانطور که ملاحظه می‌فرمایید درون این فایل اطلاعات مربوط به حساب‌های کاربری درون یک ویژگی آرایه‌ای به نام accounts ذخیره شده است. می‌خواهیم تمام عملیات‌های مربوط به اضافه شدن یک حساب کاربری یا تغییر وضعیت آن را درون یک سرویس جدا ارائه کنیم.

بنابراین یک فایل تحت عنوان accounts.service.ts درون پوشه اصلی app‌ ایجاد کرده و دستورهای زیر را به آن اضافه خواهیم کرد:

export class AccountsService{

    accounts = [
        {
            name: 'حساب کابری مدیر کل',
            status: 'فعال'
        },
        {
            name: 'اکانت تست',
            status: 'غیرفعال'
        },
        {
            name: 'حساب کاربری مخفی',
            status: 'مخفی'
        }
    ];

    addAccount(name: string, status:string){
        this.accounts.push({name: name, status: status});
    }

    updateStatus(id: number, status:string){
        this.accounts[id].status = status;
    }

}

سپس درون فایل app.component.ts‌ نیز تغییرات زیر را اعمال کرده بگونه‌ای که ابتدا یک سرویس را به این کامپوننت معرفی خواهیم کرد و سپس درون هوک ngOnInit مقادیر موجود در ویژگی accounts فایل accounts.service.ts را بارگذاری می‌کنیم:

import {Component, OnInit} from '@angular/core';
import {AccountsService} from "./accounts.service";

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css'],
    providers: [AccountsService]
})
export class AppComponent implements OnInit {
    accounts: { name: string, status: string }[] = []

    constructor(private accountsService: AccountsService) {

    }

    ngOnInit() {
        this.accounts = this.accountsService.accounts;
    }
}

حال اگر نرم‌افزار خود را در آدرس http://localhost:4200 در مرورگر خود مشاهده کنید، به صورت کلی همه چیز درست است اما وقتی روی دکمه‌های تغییر وضعیت کلیک کنید درون console‌ خطاهایی برای شما به نمایش می‌گذارد زیرا متدهایی که در پاسخ به رویداد در فایل app.component.ts وجود داشتند حذف و به سرویس انتقال پیدا کردند. حال برای اینکه این مشکل را حل کنیم ابتدا فایل new-account.component.ts را باز کرده و تغییرات زیر را درون آن لحاظ می‌کنیم:

import {Component, OnInit} from '@angular/core';
import {LoggingService} from '../logging.service'
import {AccountsService} from "../accounts.service";

@Component({
    selector: 'app-new-account',
    templateUrl: './new-account.component.html',
    styleUrls: ['./new-account.component.css'],
    providers: [LoggingService, AccountsService]
})
export class NewAccountComponent implements OnInit {

    constructor(private loggingService: LoggingService, private accountsService: AccountsService) {
    }

    ngOnInit() {
    }

    onCreateAccount(accountName: string, accountStatus: string) {
        this.accountsService.addAccount(accountName, accountStatus);
        this.loggingService.logStatusChanged(accountStatus);
    }

}

همانطور که ملاحظه کردید به زیبایی هر چه تمام رویدادهای خروجی Output‌ را حذف کرده و سپس از یک سرویس مشترک به نام AccountsService استفاده کردیم. حال این تغییرات را برای فایل account.component.ts‌ اعمال می‌کنیم:

import {Component, Input, OnInit} from '@angular/core';
import {LoggingService} from "../logging.service";
import {AccountsService} from "../accounts.service";

@Component({
    selector: 'app-account',
    templateUrl: './account.component.html',
    styleUrls: ['./account.component.css'],
    providers: [LoggingService, AccountsService]
})
export class AccountComponent implements OnInit {

    @Input() account: { name: string, status: string }
    @Input() id: number;


    constructor(private loggingService: LoggingService, private  accountsService: AccountsService) {
    }

    ngOnInit() {
    }

    onSetTo(status: string) {
        this.accountsService.updateStatus(this.id, status);
        this.loggingService.logStatusChanged(status);
    }

}

چقدر عالی و جذاب این کار انجام می‌شود و وابستگی یک کامپوننت به داده‌ها را به واسطه‌ی یک سرویس مشترک حذف می‌کنیم. حال اگر صفحه خود را در آدرس http://localhost:4200 مشاهده کنید بدون هیچ خطایی کنسول شما کار می‌کند و اطلاعات را برای شما بازیابی خواد کرد.

برای تکمیل کردن این بخش یک توضیح کلی در ارتباط با ساختار درختی سرویس‌ها و نرم‌افزار انگولار ۴ خدمت شما عزیزان مطرح می‌کنیم.

ساختار درختی Injector در سرویس‌ ها

سرویس‌ها نیز همانند کامپوننت‌ها می‌توانند دارای فرزند باشند و روابط والد و فرزندی بین ‌آنها نیز برقرار است اما قبل از بررسی این موضوع یک ساختار درختی برای ارتباط بین کامپوننت‌ها و سرویس‌ها در نظر می‌گیریم.

در بالاترین رده‌ی ممکن AppModule وجود دارد که در آن تمام سرویس‌ها، تمام دستورهاو تمام کامپوننت‌ها در دسترس است. یعنی بالاترین سطح ممکن در یک نرم‌افزار انگولاری مربوط به این کلاس است.

در مرحله‌ی بعدی AppComponent‌ها به عنوان بالاترین سطح معرفی می‌شوند که در آن‌ها تمام سرویس‌ها در دسترس هستند اما در این سطح سرویس‌ها برای یکدیگر در دسترس نخواهند بود.

مرحله‌ی آخر به Any Other Component ختم می‌شود که یک نمونه از کلاس سرویس درون یک کامپوننت و تمام کامپوننت‌های فرزند آن در دسترس است.

با اطلاعاتی که در فوق در دسترس شما قرار دادیم می‌خواهیم مثال قبلی را بهینه‌تر کنیم.

در مثال بالا همانطور که ملاحظه می‌کنید درون providers های موجود در account.component.ts و new-account.component.ts سرویس AccountsService را معرفی کردیم. اما این معرفی تکراری بیش نیست! زیرا همانطور که در مرحله‌ی آخر ساختار درختی Injector ها در سرویس‌ها بررسی کردیم. یک سرویس می‌تواند درون یک کامپوننت و تمام کامپوننت‌های فرزند آن در دسترس باشد.

بنابراین کامپوننت app یک کامپوننت والد و کامپوننت‌های account و new-account به عنوان کامپوننت‌های فرزند شناخته می‌شوند. حال اگر از providers هر یک از این دو کامپوننت فرزند دستور AccountsService را حذف کنیم برنامه مجددا بدون هیچ مشکلی اجرا خواهد شد.

در نهایت فایل account.component.ts به صورت زیر خواهد بود:

import {Component, Input, OnInit} from '@angular/core';
import {LoggingService} from "../logging.service";
import {AccountsService} from "../accounts.service";

@Component({
    selector: 'app-account',
    templateUrl: './account.component.html',
    styleUrls: ['./account.component.css'],
    providers: [LoggingService]
})
export class AccountComponent implements OnInit {

    @Input() account: { name: string, status: string }
    @Input() id: number;


    constructor(private loggingService: LoggingService, private  accountsService: AccountsService) {
    }

    ngOnInit() {
    }

    onSetTo(status: string) {
        this.accountsService.updateStatus(this.id, status);
        this.loggingService.logStatusChanged(status);
    }

}

همچنین فایل new-account.component.ts به صورت زیر تغییر می‌کند:

import {Component, OnInit} from '@angular/core';
import {LoggingService} from '../logging.service'
import {AccountsService} from "../accounts.service";

@Component({
    selector: 'app-new-account',
    templateUrl: './new-account.component.html',
    styleUrls: ['./new-account.component.css'],
    providers: [LoggingService]
})
export class NewAccountComponent implements OnInit {

    constructor(private loggingService: LoggingService, private accountsService: AccountsService) {
    }

    ngOnInit() {
    }

    onCreateAccount(accountName: string, accountStatus: string) {
        this.accountsService.addAccount(accountName, accountStatus);
        this.loggingService.logStatusChanged(accountStatus);
    }

}

بسیار عالی به شما عزیزان تبریک می‌گوییم در این بخش توانستید به مهارت‌های خود اضافه کرده و نحوه‌ی کار با سرویس‌ها و فواید استفاده از dependency injection در نرم‌افزارهای انگولاری را به وضوح مشاهده کنید. اما این فصل با تمام شیرینی‌ای که دارد هنوز به اتمام نرسیده و در بخش بعدی توضیحات تکمیلی را خدمت شما عزیزان ارائه خواهیم کرد. با ما همراه باشید.

توجه: دوستان عزیز آموزش ویدیویی انگولار 6 از مقدماتی تا پیشرفته به زبان فارسی را می‌توانید با کلیک روی اینجا یاد بگیرید.

دوره آموزش انگولار به زبان فارسی + پروژه ساخت فروشگاه اینترنتی

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش رایگان انگولار توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما (1 دیدگاه)

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

آرش
20 فروردین 1398
سلام شما بعد از این پاراگراف ، کد های مربوط به کامپوننت های فرزند app رو گذاشتید که همچنان دارای providers هستن ، چرا ؟ بنابراین کامپوننت app یک کامپوننت والد و کامپوننت‌های account و new-account به عنوان کامپوننت‌های فرزند شناخته می‌شوند. حال اگر از providers هر یک از این دو کامپوننت فرزند دستور AccountsService را حذف کنیم برنامه مجددا بدون هیچ مشکلی اجرا خواهد شد. در نهایت فایل account.component.ts به صورت زیر خواهد بود:

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.