Как стать автором
Обновить

Composable Contexts Architecture

Время на прочтение5 мин
Количество просмотров2.2K

Let’s talk about app architecture and the approach I apply as an iOS software engineer in a few companies. My team and I were trying to build something solid without slipping into a dense swamp where following the rules distracts you from actual business domain code. As a result, we got something that works for us and good enough to be told from my point of view.

Previously I wrote a short article on how iOS engineers can adopt Clean Architecture in SwiftUI world. After that, I realized that not many developers understood the ideas and abstractions I was talking about. So I decided to write this as a prequel to explain how I apply Clean Architecture in my everyday UIKit world.

The architecture of an entire application is the most important thing you should care about if you are building a solid, reliable, scalable product. Nowadays, you will be asked about MVC, MVVM in almost every job interview or VIPER, and RIBs if an interviewer added some creativity to a century-old checklist. However, here’s the problem — most of these cover the presentation layer and leave us alone when we step out from “presentation” boundaries. But can we build some abstractions that will work on every layer and across multiple applications no matter what?

My answer is yes. This recipe includes Clean Architecture, MVVM, RxSwift, and Dependency Injection. The glue how we put it together is the Composition. Composition is one of the essential things that help developers deal with difficulty and requirements mutation. As long as you make things composable, your abstractions will face all bumps and turns.

Let’s take a look at a simple example — the user screen. This screen should display the current user name and have a “Sign Out” button. On the domain level, we have an entity — User, which contains all user info.

As I mentioned before, we already have a bunch of well-known patterns for the presentation layer. I consider myself a big FRP (Functional Reactive Programming) fan, so my choice was quite obvious — MVVM with RxSwift.

It was the easiest part. That is why I don’t want to talk about a presentation based on MVVM-RxSwift. There are a lot of good articles about it, while I prefer to stay focused on the upcoming part. This is the part I’d like to really talk about and explain it as clearly as possible. Sorry for my drawings.

Thus we have ViewModel on the one side and some source of our User on the other. How will we connect them? The nasty dirty hack that comes to mind is putting all Stores, Managers, and Repositories we need into the initializer. Oo

But how it makes things composable? Where should I put a clean User’s domain code? How to provide the granularity of operations what this particular object can do? How to reuse it in some other place?

Instead, we aimed to decompose our application into multiple “pieces” and abstract them one from another with low coupling inspired by Clean Architecture idea. Clean Architecture, someone calls it Onion Architecture, is a decomposition of your application into layers and unidirectional data flow. In this article, I am talking about my real-life implementation and our example. So we put away digging deep into Clean Architecture itself.

We are going to abstract all the things our ViewModel needs into small pieces called UseCases. UseCase is a protocol with one particular function. It will provide us granularity for operations we are about to give our ViewModel and precise semantics because we can easily understand what this specific model can do at one glance.

protocol ObserveUserNameUseCase {
  var userName: Observable<String> { get }
}

protocol SignOutUseCase {
  func signOut()
}

struct UserScreenViewModel {
  typealias UseCases = ObserveUserNameUseCase & SignOutUseCase
  ...
  init(useCases: UseCases) {
    ...
  }
}

You can see that you can compose as many UseCases as you like and anywhere you need. The initializer, just waiting for the object confirming them. We will return to that object a little bit later. Now I’d like to focus on our business domain code. In UseCases, we deal with a tiny decomposed piece of our program and don’t care about the rest of the world. In other words, we can do some Protocol Oriented Programming and define the following statement: “Any object which has Repository A can do something.”

extension ObserveUserNameUseCase where Self: UserRepositoryHolderType {
  var userName: Observable<String> {
    return userRepository.user
      .compactMap { $0 }
      .map { "\($0.firstName) \($0.lastName)" }
      .distinctUntilChanged()
  }
}

protocol UserRepositoryType {
  var user: Observable<User?> { get }
}

protocol UserRepositoryHolderType {
  var userRepository: UserRepositoryType { get }
}

It’s cool. We don’t care where the data are coming from or what purpose they serve. We are just dealing with a very tiny problem that can easily fit in our minds. That’s a UseCase.

Let’s transform the diagram above into something closer to our app.

And here it comes, the Context. Context is the “bridge” between all the stuff that our ViewModels require and managers, services, repositories, etc. Usually, each screen has its Context, which constructs from a Dependency Injection container. The primary idea here is that Context doesn’t have any code. It holds the repository properties and gets all the code it needs via the composition of UseCases.

// MARK: Context
private final class UserScreenContext: UserRepositoryTypeHolderType {
  let userRepository: UserRepositoryType
  
  init(userRepository: UserRepositoryType) {
    self.userRepository = userRepository
  }
}

extension UserScreenContext: ObserveUserNameUseCase {}
extension UserScreenContext: SignOutUseCase {}

/// Here is an example of how we are constructing everything with DependencyContainer (Dip framework implementation)
enum UserScreenComposition {
  static func configure(_ container: DependencyContainer) {
    
    // UseCases
    container.register {
      UserScreenContext(userRepository: $0, ...)
    }
    .implements(UserScreenViewModel.UseCases.self)
    
    // View Models
    container.register {
      UserScreenViewModel(useCases: $0)
    }
    
    // View Controller
    container
      .register(storyboardType: UserController.self, tag: "UserScreen")
      .resolvingProperties { container, controller in
        controller.viewModel = try container.resolve()
      }
    
    DependencyContainer.uiContainers.append(container)
  }
}

extension UserController: StoryboardInstantiatable {}

That’s it. We achieved very low coupling and made things composable we are prepared for new requirements by adding new repositories mechanism and UseCase composition. We can write UnitTests with mocked UseCases or Repositories. We can even rearrange our ViewControllers, and as long as we have the required objects for its initialization in Dependency Container, they will be instantiated.

Furthermore, to put everything together, I have a straightforward repository which I am using from time to time on accidentally workshops for newcomers or some other occasions. The “master” branch is a starting point from where I explain everything I said above. After that, we connect the new repository, and the result is “workshops/ComposableContextsArchitecture” branch. I hope it helps.

by Nikolay Fiantsev

Теги:
Хабы:
Рейтинг0
Комментарии0

Публикации

Истории

Работа

Swift разработчик
32 вакансии
iOS разработчик
24 вакансии

Ближайшие события