Ручное внедрение зависимостей, Ручное внедрение зависимостей

Рекомендуемая архитектура приложений Android поощряет разделение вашего кода на классы, чтобы получить выгоду от разделения задач — принципа, согласно которому каждый класс иерархии имеет одну определенную ответственность. Это приводит к появлению большего количества меньших классов, которые необходимо соединить вместе, чтобы удовлетворить зависимости друг друга.

Приложения Android обычно состоят из множества классов, и некоторые из них зависят друг от друга.
Рисунок 1. Модель графа приложения Android-приложения.

Зависимости между классами можно представить в виде графа, в котором каждый класс связан с классами, от которых он зависит. Представление всех ваших классов и их зависимостей составляет граф приложения . На рисунке 1 вы можете увидеть абстракцию графа приложения. Когда класс A ( ViewModel ) зависит от класса B ( Repository ), существует линия, указывающая от A к B, представляющая эту зависимость.

Внедрение зависимостей помогает установить эти связи и позволяет заменять реализации для тестирования. Например, при тестировании ViewModel , которая зависит от репозитория, вы можете передать различные реализации Repository либо с подделками, либо с макетами, чтобы протестировать разные случаи.

Основы ручного внедрения зависимостей

В этом разделе описано, как применить внедрение зависимостей вручную в реальном сценарии приложения Android. В нем рассматривается повторяющийся подход к тому, как вы можете начать использовать внедрение зависимостей в своем приложении. Подход совершенствуется до тех пор, пока не достигнет точки, очень похожей на то, что Dagger автоматически сгенерирует для вас. Для получения дополнительной информации о Dagger прочитайте Основы Dagger .

Считайте, что поток — это группа экранов вашего приложения, соответствующих определенной функции. Вход, регистрация и оформление заказа — все это примеры потоков.

При описании процесса входа в типичное приложение Android LoginActivity зависит от LoginViewModel , который, в свою очередь, зависит от UserRepository . Тогда UserRepository зависит от UserLocalDataSource и UserRemoteDataSource , которые, в свою очередь, зависят от службы Retrofit .

LoginActivity — это точка входа в поток входа в систему, и пользователь взаимодействует с действием. Таким образом, LoginActivity необходимо создать LoginViewModel со всеми ее зависимостями.

Классы Repository и DataSource потока выглядят следующим образом:

Котлин

class UserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

class UserLocalDataSource { ... }
class UserRemoteDataSource(
    private val loginService: LoginRetrofitService
) { ... }

Ява

class UserLocalDataSource {
    public UserLocalDataSource() { }
    ...
}

class UserRemoteDataSource {

    private final Retrofit retrofit;

    public UserRemoteDataSource(Retrofit retrofit) {
        this.retrofit = retrofit;
    }

    ...
}

class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }

    ...
}

Вот как выглядит LoginActivity :

Котлин

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // In order to satisfy the dependencies of LoginViewModel, you have to also
        // satisfy the dependencies of all of its dependencies recursively.
        // First, create retrofit which is the dependency of UserRemoteDataSource
        val retrofit = Retrofit.Builder()
            .baseUrl("https://example.com")
            .build()
            .create(LoginService::class.java)

        // Then, satisfy the dependencies of UserRepository
        val remoteDataSource = UserRemoteDataSource(retrofit)
        val localDataSource = UserLocalDataSource()

        // Now you can create an instance of UserRepository that LoginViewModel needs
        val userRepository = UserRepository(localDataSource, remoteDataSource)

        // Lastly, create an instance of LoginViewModel with userRepository
        loginViewModel = LoginViewModel(userRepository)
    }
}

Ява

public class MainActivity extends Activity {

    private LoginViewModel loginViewModel;

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

        // In order to satisfy the dependencies of LoginViewModel, you have to also
        // satisfy the dependencies of all of its dependencies recursively.
        // First, create retrofit which is the dependency of UserRemoteDataSource
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService.class);

        // Then, satisfy the dependencies of UserRepository
        UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit);
        UserLocalDataSource localDataSource = new UserLocalDataSource();

        // Now you can create an instance of UserRepository that LoginViewModel needs
        UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);

        // Lastly, create an instance of LoginViewModel with userRepository
        loginViewModel = new LoginViewModel(userRepository);
    }
}

Есть проблемы с этим подходом:

  1. Там много шаблонного кода. Если вы захотите создать еще один экземпляр LoginViewModel в другой части кода, у вас возникнет дублирование кода.

  2. Зависимости должны быть объявлены по порядку. Чтобы создать его, вам необходимо создать экземпляр UserRepository перед LoginViewModel .

  3. Повторно использовать объекты сложно. Если вы хотите повторно использовать UserRepository для нескольких функций, вам придется заставить его следовать шаблону Singleton . Шаблон Singleton усложняет тестирование, поскольку все тесты используют один и тот же экземпляр Singleton.

Управление зависимостями с помощью контейнера

Чтобы решить проблему повторного использования объектов, вы можете создать собственный класс -контейнер зависимостей , который вы будете использовать для получения зависимостей. Все экземпляры, предоставляемые этим контейнером, могут быть общедоступными. В этом примере, поскольку вам нужен только экземпляр UserRepository , вы можете сделать его зависимости частными с возможностью сделать их общедоступными в будущем, если их потребуется предоставить:

Котлин

// Container of objects shared across the whole app
class AppContainer {

    // Since you want to expose userRepository out of the container, you need to satisfy
    // its dependencies as you did before
    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    // userRepository is not private; it'll be exposed
    val userRepository = UserRepository(localDataSource, remoteDataSource)
}

Ява

// Container of objects shared across the whole app
public class AppContainer {

    // Since you want to expose userRepository out of the container, you need to satisfy
    // its dependencies as you did before
    private Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://example.com")
            .build()
            .create(LoginService.class);

    private UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit);
    private UserLocalDataSource localDataSource = new UserLocalDataSource();

    // userRepository is not private; it'll be exposed
    public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);
}

Поскольку эти зависимости используются во всем приложении, их необходимо поместить в общее место, которое могут использовать все действия: класс Application . Создайте собственный класс Application , содержащий экземпляр AppContainer .

Котлин

// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
class MyApplication : Application() {

    // Instance of AppContainer that will be used by all the Activities of the app
    val appContainer = AppContainer()
}

Ява

// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
public class MyApplication extends Application {

    // Instance of AppContainer that will be used by all the Activities of the app
    public AppContainer appContainer = new AppContainer();
}

Теперь вы можете получить экземпляр AppContainer из приложения и получить общий экземпляр UserRepository :

Котлин

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Gets userRepository from the instance of AppContainer in Application
        val appContainer = (application as MyApplication).appContainer
        loginViewModel = LoginViewModel(appContainer.userRepository)
    }
}

Ява

public class MainActivity extends Activity {

    private LoginViewModel loginViewModel;

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

        // Gets userRepository from the instance of AppContainer in Application
        AppContainer appContainer = ((MyApplication) getApplication()).appContainer;
        loginViewModel = new LoginViewModel(appContainer.userRepository);
    }
}

Таким образом, у вас нет одноэлементного UserRepository . Вместо этого у вас есть общий для всех действий AppContainer , который содержит объекты из графа и создает экземпляры этих объектов, которые могут использовать другие классы.

Если LoginViewModel требуется в большем количестве мест приложения, имеет смысл иметь централизованное место, где вы можете создавать экземпляры LoginViewModel . Вы можете переместить создание LoginViewModel в контейнер и предоставить новые объекты этого типа с помощью фабрики. Код LoginViewModelFactory выглядит следующим образом:

Котлин

// Definition of a Factory interface with a function to create objects of a type
interface Factory<T> {
    fun create(): T
}

// Factory for LoginViewModel.
// Since LoginViewModel depends on UserRepository, in order to create instances of
// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
class LoginViewModelFactory(private val userRepository: UserRepository) : Factory {
    override fun create(): LoginViewModel {
        return LoginViewModel(userRepository)
    }
}

Ява

// Definition of a Factory interface with a function to create objects of a type
public interface Factory<T> {
    T create();
}

// Factory for LoginViewModel.
// Since LoginViewModel depends on UserRepository, in order to create instances of
// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
class LoginViewModelFactory implements Factory {

    private final UserRepository userRepository;

    public LoginViewModelFactory(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public LoginViewModel create() {
        return new LoginViewModel(userRepository);
    }
}

Вы можете включить LoginViewModelFactory в AppContainer и заставить LoginActivity использовать его:

Котлин

// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory
class AppContainer {
    ...
    val userRepository = UserRepository(localDataSource, remoteDataSource)

    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Gets LoginViewModelFactory from the application instance of AppContainer
        // to create a new LoginViewModel instance
        val appContainer = (application as MyApplication).appContainer
        loginViewModel = appContainer.loginViewModelFactory.create()
    }
}

Ява

// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory
public class AppContainer {
    ...

    public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);

    public LoginViewModelFactory loginViewModelFactory = new LoginViewModelFactory(userRepository);
}

public class MainActivity extends Activity {

    private LoginViewModel loginViewModel;

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

        // Gets LoginViewModelFactory from the application instance of AppContainer
        // to create a new LoginViewModel instance
        AppContainer appContainer = ((MyApplication) getApplication()).appContainer;
        loginViewModel = appContainer.loginViewModelFactory.create();
    }
}

Этот подход лучше предыдущего, но есть еще некоторые проблемы, которые следует учитывать:

  1. Вам придется управлять AppContainer самостоятельно, вручную создавая экземпляры для всех зависимостей.

  2. Существует еще много шаблонного кода. Вам необходимо создавать фабрики или параметры вручную в зависимости от того, хотите ли вы повторно использовать объект или нет.

Управление зависимостями в потоках приложений

AppContainer усложняется, когда вы хотите включить в проект больше функций. Когда ваше приложение становится больше и вы начинаете вводить различные потоки функций, возникает еще больше проблем:

  1. Если у вас разные потоки, вы можете захотеть, чтобы объекты просто находились в пределах этого потока. Например, при создании LoginUserData (который может состоять из имени пользователя и пароля, используемых только в процессе входа в систему) вы не хотите сохранять данные из старого потока входа в систему от другого пользователя. Вам нужен новый экземпляр для каждого нового потока. Этого можно добиться, создав объекты FlowContainer внутри AppContainer , как показано в следующем примере кода.

  2. Оптимизация графа приложения и контейнеров потоков также может оказаться сложной задачей. Вам нужно не забыть удалить ненужные экземпляры, в зависимости от потока, в котором вы находитесь.

Представьте, что у вас есть поток входа в систему, состоящий из одного действия ( LoginActivity ) и нескольких фрагментов ( LoginUsernameFragment и LoginPasswordFragment ). Эти взгляды хотят:

  1. Получите доступ к тому же экземпляру LoginUserData , который необходимо использовать совместно до завершения процесса входа в систему.

  2. Создайте новый экземпляр LoginUserData , когда поток запустится снова.

Этого можно добиться с помощью контейнера потока входа в систему. Этот контейнер необходимо создать при запуске потока входа в систему и удалить из памяти после его завершения.

Давайте добавим LoginContainer в пример кода. Вы хотите иметь возможность создавать в приложении несколько экземпляров LoginContainer , поэтому вместо того, чтобы делать его одноэлементным, сделайте его классом с зависимостями, необходимыми потоку входа в систему от AppContainer .

Котлин

class LoginContainer(val userRepository: UserRepository) {

    val loginData = LoginUserData()

    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

// AppContainer contains LoginContainer now
class AppContainer {
    ...
    val userRepository = UserRepository(localDataSource, remoteDataSource)

    // LoginContainer will be null when the user is NOT in the login flow
    var loginContainer: LoginContainer? = null
}

Ява

// Container with Login-specific dependencies
class LoginContainer {

    private final UserRepository userRepository;

    public LoginContainer(UserRepository userRepository) {
        this.userRepository = userRepository;
        loginViewModelFactory = new LoginViewModelFactory(userRepository);
    }

    public LoginUserData loginData = new LoginUserData();

    public LoginViewModelFactory loginViewModelFactory;
}

// AppContainer contains LoginContainer now
public class AppContainer {
    ...
    public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);

    // LoginContainer will be null when the user is NOT in the login flow
    public LoginContainer loginContainer;
}

Если у вас есть контейнер, специфичный для потока, вам нужно решить, когда создавать и удалять экземпляр контейнера. Поскольку ваш поток входа в систему является автономным в действии ( LoginActivity ), именно действие управляет жизненным циклом этого контейнера. LoginActivity может создать экземпляр в onCreate() и удалить его в onDestroy() .

Котлин

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel
    private lateinit var loginData: LoginUserData
    private lateinit var appContainer: AppContainer


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        appContainer = (application as MyApplication).appContainer

        // Login flow has started. Populate loginContainer in AppContainer
        appContainer.loginContainer = LoginContainer(appContainer.userRepository)

        loginViewModel = appContainer.loginContainer.loginViewModelFactory.create()
        loginData = appContainer.loginContainer.loginData
    }

    override fun onDestroy() {
        // Login flow is finishing
        // Removing the instance of loginContainer in the AppContainer
        appContainer.loginContainer = null
        super.onDestroy()
    }
}

Ява

public class LoginActivity extends Activity {

    private LoginViewModel loginViewModel;
    private LoginData loginData;
    private AppContainer appContainer;

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

        appContainer = ((MyApplication) getApplication()).appContainer;

        // Login flow has started. Populate loginContainer in AppContainer
        appContainer.loginContainer = new LoginContainer(appContainer.userRepository);

        loginViewModel = appContainer.loginContainer.loginViewModelFactory.create();
        loginData = appContainer.loginContainer.loginData;
    }

    @Override
    protected void onDestroy() {
        // Login flow is finishing
        // Removing the instance of loginContainer in the AppContainer
        appContainer.loginContainer = null;

        super.onDestroy();
    }
}

Как и LoginActivity , фрагменты входа могут получать доступ LoginContainer из AppContainer и использовать общий экземпляр LoginUserData .

Поскольку в этом случае вы имеете дело с логикой жизненного цикла представления, использование наблюдения за жизненным циклом имеет смысл.

Заключение

Внедрение зависимостей — хороший метод создания масштабируемых и тестируемых приложений для Android. Используйте контейнеры как способ совместного использования экземпляров классов в разных частях вашего приложения и как централизованное место для создания экземпляров классов с помощью фабрик.

Когда ваше приложение станет больше, вы начнете видеть, что пишете много шаблонного кода (например, фабрик), который может быть подвержен ошибкам. Вам также придется самостоятельно управлять объемом и жизненным циклом контейнеров, оптимизируя и удаляя контейнеры, которые больше не нужны, чтобы освободить память. Неправильное выполнение этого действия может привести к тонким ошибкам и утечкам памяти в вашем приложении.

В разделе Dagger вы узнаете, как можно использовать Dagger для автоматизации этого процесса и создания того же кода, который в противном случае вы бы написали вручную.