Explore in Dagger - Introduction (Factory)

توی سری مطالب Explore in Dagger میخوایم در مورد Dagger2 که محبوب‌ترین و پر استفاده‌ترین فریمورک Dependency Injection در اندروید هست صحبت کنیم.

https://twitter.com/riggaroo/status/1138099967820816384
https://twitter.com/riggaroo/status/1138099967820816384

رویکرد کلی این مطالب آموزش چگونگی استفاده و پیاده‌سازی Dagger نیست. بلکه بررسی چگونگی عملکرد این کتابخونه و ساختار کدهای geneate شده هست تا با یادگیری ساختاری Dagger با تسلط بیشتر و به صورت بهینه‌تری از این کتابخونه استفاده کنیم.

لازمه بگم که درک دیزاین پترن‌های Builder و Factory به درک عمیقتر این پست ها کمک میکنه. و همچنین آشنایی با Dagger هم از پیش‌نیازهای این مطالب هست.

همونطور که میدونید از دلایل محبوبیت Dagger قابلیت debuging در build time ،سرعت بالا و نداشتن سربار اضافه در runtime هست. که همه این موارد بخاطر generare کردن کدهای انجام DI در زمان build هست که میخوایم بخشی از این کد هارو بررسی کنیم.

تعریف Dependency

طبق تعریف عام، dependency injection یعنی وقتی کلاس A به کلاس B وابستگی داشته باشد، بتوانیم با تکنیکی خاص این وابستگی را تامین کنیم.

DI Concept

تا به اینجا، موضوع مورد توجه ما، ایجاد نمونه‌ای از کلاس B میباشد و کلاس A بدون اینکه بداند این وابستگی از کجا تامین شده، فقط از آن استفاده کند.

پس ما به یک کارخونه یا Factory ای برای ساخت نمونه‌ای از کلاس B نیاز داریم.

public interface Provider<T> {
    T get();
}

public interface Factory<T> extends Provider<T> {
}

پس به ازای هر type یا کلاسی که قرار است به عنوان وابستگی دیگر کلاس ها تامین شود، یکبار باید اینترفیس Factory پیاده‌سازی (implement) شود.

موقع کار با Dagger برای تامین وابستگی ها یا به عبارتی برای داشتن یک Factory برای هر type دو گزینه در اختیار داریم:

  • @Inject بالای متد constructor هر کلاس قرار میگیرد و به ازای آن یک کلاس Factory از جنس همان کلاس ساخته میشود.
  • @Provides توی ماژول‌های Dagger بر روی متدهایی که میخواهند وابستگی خاصی را تامین کنند، می‌آید و همانند بالا یک کلاس Factory از همان جنس میسازد.
    • @Binds این annotation نیز تنها داخل ماژولها کاربرد دارد و تقریبا شبیه به @Provides میباشد و در بعضی موارد حتی جایگزین آن میشود. لازم به ذکر است که این annotation کلاس Factory جدیدی نمیسازد. و تنها وظیفه اتصال یا bind کردن یک اینترفیس به implementation آن را دارد که در زمان ساخته شدن فایل های دیگر به جای interface ها نمونه پیاده‌سازی شده آن را جایگزین میکند.

پیاده سازی

در پروژه میخواهیم یک نمونه از CoffeeMaker داشته باشیم که به شکل زیر تعریف شده است:

Project Dependencies

تغییرات لازم مربوط به مدلهای سناریو در این کامیت ایجاد شده‌اند.

public class CoffeeMaker {
    private final Heater heater;
    private final Pump pump;

    public CoffeeMaker(Heater heater, Pump pump) {
        this.heater = heater;
        this.pump = pump;
    }

    public void brew() {
        heater.on();
        pump.pump();
        Log.i("Log", "[_]P coffee! [_]P ");
        heater.off();
    }
}
public class Pump {
    @Inject
    public Pump() {
    }

    public void pump() {
        Log.i("Log", "=> => pumping => =>");
    }
}
public interface Heater {
  void on();
  void off();
  boolean isHot();
}
public class ElectricHeater implements Heater {
  boolean heating;

  @Inject
  public ElectricHeater() {
  }

  @Override
  public void on() {
    Log.i("Log", "~ ~ ~ heating ~ ~ ~");
    this.heating = true;
  }

  @Override
  public void off() {
    this.heating = false;
  }

  @Override
  public boolean isHot() {
    return heating;
  }
}

همانطور که میبینید هر CoffeeMaker به یک Heater و یک Pump نیاز دارد.

کلاسهای Pump و ElectricHeater خود دارای @Inject در بالای constructor خود هستند و Factory مورد نیاز خود را خواهند ساخت. پس فقط نیاز است که ElectricHeater را به Heater بایند کنیم تا در نهایت بتوانیم CoffeeMaker را بسازیم. که برای این موارد باید از ماژول ها استفاده کنیم:

@Module
public abstract class AppModule {

    @Provides
    public static CoffeeMaker provideCoffeeMaker(Heater heater, Pump pump) {
        return new CoffeeMaker(heater, pump);
    }


//    @Provides
//    public static Heater provideHeater(ElectricHeater electricHeater) {
//        return electricHeater;
//    }
    @Binds
    abstract Heater bindsHeater(ElectricHeater electricHeater);
}

در کد بالا تفاوتی بین خروجی و نحوه کار متد provideHeater(...) و bindsHeater(...) وجود ندارد. چرا که هرکدام electricHeater را از Factory مربوطه گرفته و به عنوان یک نمونه از Heater تامین میکنند.

توجه داشته باشید تنها با هدف اینکه انواع مختلف Factory هارا داشته باشیم هرکدام از کلاس هارا به نوعی خاص annotation گذاری کرده‌ایم.

برای تزریق این وابستگی ها به کلاس‌های وابسته نیاز به component یی داریم:

@Component(modules = {AppModule.class})
public abstract class AppComponent {

    abstract public void inject(MainActivity mainActivity);

}

کلاس وابسته‌ی ما در واقع MainActivity ست که میخواهیم CoffeeMaker را در آن تزریق کنیم: کامیت

public class MainActivity extends AppCompatActivity {

    AppComponent mAppComponent;

    @Inject
    CoffeeMaker mCoffeeMaker;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mAppComponent = DaggerAppComponent.builder()
                .build();

        mAppComponent.inject(this);

        mCoffeeMaker.brew();

    }
}

Generated Codes

بعد از بیلد شدن پروژه فایل هایی در آدرس زیر ساخته خواهند شد: کامیت

// java annotationProcessor
<root>/app/build/generated/source/apt
// kotlin annotationProcessor
<root>/app/build/generated/source/kapt

همانطور که میبنیم هرکدام از کلاس هایی که @Inject روی constructor خود دارند Factory ای در همان پکیج با پسوند _Factory دارند که توابع مختلفی را علاوه بر تابع get() دارد. مانند newInstance()

public final class ElectricHeater_Factory implements Factory<ElectricHeater> {
  private static final ElectricHeater_Factory INSTANCE = new ElectricHeater_Factory();

  @Override
  public ElectricHeater get() {
    return new ElectricHeater();
  }

  public static ElectricHeater_Factory create() {
    return INSTANCE;
  }

  public static ElectricHeater newInstance() {
    return new ElectricHeater();
  }
}
public final class Pump_Factory implements Factory<Pump> {
  private static final Pump_Factory INSTANCE = new Pump_Factory();

  @Override
  public Pump get() {
    return new Pump();
  }

  public static Pump_Factory create() {
    return INSTANCE;
  }

  public static Pump newInstance() {
    return new Pump();
  }
}

به ازای متدهای دارای @Provides داخل ماژول نیز به همین روال Factory هایی ساخته شده که نام ماژول را به عنوان ماژول را به عنوان پیشوند، نام متد به عنوان نام اصلی و پسوند Factory نیز دارند.

public final class AppModule_ProvideCoffeeMakerFactory implements Factory<CoffeeMaker> {
  private final Provider<Heater> heaterProvider;

  private final Provider<Pump> pumpProvider;

  public AppModule_ProvideCoffeeMakerFactory(
      Provider<Heater> heaterProvider, Provider<Pump> pumpProvider) {
    this.heaterProvider = heaterProvider;
    this.pumpProvider = pumpProvider;
  }

  @Override
  public CoffeeMaker get() {
    return provideCoffeeMaker(heaterProvider.get(), pumpProvider.get());
  }

  public static AppModule_ProvideCoffeeMakerFactory create(
      Provider<Heater> heaterProvider, Provider<Pump> pumpProvider) {
    return new AppModule_ProvideCoffeeMakerFactory(heaterProvider, pumpProvider);
  }

  public static CoffeeMaker provideCoffeeMaker(Heater heater, Pump pump) {
    return Preconditions.checkNotNull(
        AppModule.provideCoffeeMaker(heater, pump),
        "Cannot return null from a non-@Nullable @Provides method");
  }
}

حال که تمامی dependency ها جدا جدا آماده شده‌اند، باید محلی داشته باشیم تا بر اساس نیازمندی، آنهارا کنار هم قرار دهیم.

منظور MemberInjector میباشد که تنها یک متد injectMembers(T instance) دارد:

public interface MembersInjector<T> {
  void injectMembers(T instance);
}

کلاسهایی که MemberInjector را implement کرده‌اند، این کار را با توجه به متغیر/متدهای علامتگذاری شده با @Inject در کلاس وابسته (MainActivity) ساخته میشوند. نیازمندی‌های خود را دریافت کرده و به متغیرهای کلاس وابسته نسبت میدهند.

public final class MainActivity_MembersInjector implements MembersInjector<MainActivity> {
  private final Provider<CoffeeMaker> mCoffeeMakerProvider;

  public MainActivity_MembersInjector(Provider<CoffeeMaker> mCoffeeMakerProvider) {
    this.mCoffeeMakerProvider = mCoffeeMakerProvider;
  }

  public static MembersInjector<MainActivity> create(Provider<CoffeeMaker> mCoffeeMakerProvider) {
    return new MainActivity_MembersInjector(mCoffeeMakerProvider);
  }

  @Override
  public void injectMembers(MainActivity instance) {
    injectMCoffeeMaker(instance, mCoffeeMakerProvider.get());
  }

  public static void injectMCoffeeMaker(MainActivity instance, CoffeeMaker mCoffeeMaker) {
    instance.mCoffeeMaker = mCoffeeMaker;
  }
}

و در نهایت کامپوننت باید موارد مورد نیاز MemberInjector را از طریق Factory ها فراهم کرده و متدهای injectXXX را صدا کند.

public final class DaggerAppComponent extends AppComponent {
  private DaggerAppComponent() {}

  public static Builder builder() {
    return new Builder();
  }

  public static AppComponent create() {
    return new Builder().build();
  }

  private CoffeeMaker getCoffeeMaker() {
    return AppModule_ProvideCoffeeMakerFactory.provideCoffeeMaker(new ElectricHeater(), new Pump());
  }

  @Override
  public void inject(MainActivity mainActivity) {
    injectMainActivity(mainActivity);
  }

  private MainActivity injectMainActivity(MainActivity instance) {
    MainActivity_MembersInjector.injectMCoffeeMaker(instance, getCoffeeMaker());
    return instance;
  }

  public static final class Builder {
    private Builder() {}

    public AppComponent build() {
      return new DaggerAppComponent();
    }
  }
}

این کلاس که نیازمند بررسی عمیق‌تری میباشد، abstract class یا interface کامپوننت را implement کرده است. همانطور که میبینیم dagger برای رعایت اصل Single Responsibility متدهای اضافه بسیاری میسازد، مثلا بدلیل اینکه CoffeeMaker دارای آرگومانهای دیگری ست که هرکدام از مسیر متفاوتی بدست آمده‌اند، آنهارا داخل متد injectMainActivity() نساخته، بلکه از طریق یک متد getter ای به نام getCoffeeMaker() آنهارا فراهم آورده. لینک

جمع بندی

  • هر @Inject روی constructor و Provides روی متدهای ماژول یک Factory مجزا خواهند ساخت.
  • ماژول‌هایی که ها abstract class یا interface تعریف میشوند را میتوان، یک کلاس مجازی محسوب کرد و چرا که عمل خاصی انجام نمیدهندو تنها شمایی هستند برای generate شدن Factory ها و درک type های قابل bind شدن.
  • هر کلاسی که اصطلاحا field/method Injection انجام داده باشد دارای یک MemberInjector خواهد بود.
  • کامپوننت ها عامل اصلی injection هستند.
  • تنها در مواردی که کلاسی فاقد Scope باشد، بجای ساخت نمونه‌ای از Factory و سپس صدا زدن متد newInstance() آن، مستقیما آن کلاس instantiate شده است. (در این موارد Factory عملا اضافه هست :) ) مثال

تمامی فایل های این پست روی برنچ p1-factory در دسترس است.

خیلی خیلی ادامه دارد…

rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora telegram heart