Simplified VIPER architecture

updated 10 months ago; latest suggestion 10 months ago

Introduction

The proponents of VIPER architecture hail it as an architecture which enables you to write clean, maintainable and testable software. On the other hand, the opponents view this as an over-engineered approach with a lot of boilerplate code and unnecessary protocols. Having these conflicting ideas in mind, I started experimenting with VIPER architecture last year. I developed one of my apps following this architecture. It was a great learning experience. In this talk, I will explain how I leveraged the Swift language features like Enum, Generic, Protocol and Type Erasure to reduce the boilerplate code while maintaining the separation of concerns mandated by the architecture.

Design Philosophy

VIPER architecture is based on Clean Architecture proposed by Robert C. Martin aka Uncle Bob. Clean Architecture is based on the following two principles.

  1. Single Responsibility Principle
  2. Dependency Inversion Principle

Clean Architecture puts various components of a Software into different layers based on their responsibilities. Those layers are arranged in a concentric circle, where the innermost circles represent high-level policies like business logic and entities, whereas outer circles represent the low-level implementations like UI, database. Each circle follow the Dependency Inversion Principle which states:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend upon details. Details should depend upon abstractions.

These ideas are captured by VIPER architecture which has the following components, each having distinct responsibilities.

Components:

  1. View: Responsible for receiving user inputs and rendering data
  2. Interactor: Responsible for fetching data and handling business logic
  3. Presenter: Responsible for preparing data in a format suitable for the view
  4. Entity: Represents business logic
  5. Router: Handles navigation between the flows

Message Passing:

View receives input from the user and calls the appropriate methods of the Presenter. Then the Presenter calls the appropriate methods of the Interactor. Interactor executes the business logic and sends data to the Presenter and then the Presenter processes the data suitable for the View and asks it to render. So in a traditional approach, the dependency would be as follows. The view will have a reference of Presenter, Presenter will have the reference of View and Interactor. Interactor will have a reference of the Presenter.

View <-----> Presenter <----> Interactor

However, this violates the principle that "High-level modules should not depend on low-level modules. Both should depend on abstractions." Therefore, we need to invert the dependency and define the protocols.

  1. PresenterInputProtocol: This is implemented by the Presenter and the View keeps the reference of PresenterInputProtocol instead of the Presenter.
  2. InteractorInputProtocol: This is implemented by the Interactor and the Presenter keeps the reference of this instead of the Interactor.
  3. PresenterOutputProtocol: This is implemented by the View and the Presenter keeps the reference of this instead of the View.
  4. InteractorOutputProtocol: This is implemented by the Presenter and the Interactor keeps the reference of this instead of the Presenter.

Now for each module, instead of having just one ViewController class, we have 3 classes, 4 protocols and 4 mock classes.

The positive side of this approach is the clear separation of concerns and better test coverages. However, I felt a couple of things which can be improved:

  • Programming against the interfaces has huge benefits when each module is being developed by independent teams. However, in small applications, those protocols are useful only for mocking while writing tests.
  • Protocols for each module follow a similar pattern which can be abstracted.
  • Repetition logic in mock classes

Improved Approach

Therefore, I tried to implement an event-command based architecture. At the basic level, there are 4 protocols, ViewEvent, PresenterCommand, InteractorRequest, InteractorResponse.

  • View sends ViewEvent to Presenter.
  • Presenter responds to ViewEvent and sends InteractorRequest to Interactor.
  • Interactor responds to InteractorRequest and sends InteractorResponse to Presenter.
  • Presenter responds to InteractorResponse and sends PresenterCommand to View.

The following is an overview of the protocols defined.

protocol ViewEvent {}
protocol PresenterCommand {}
protocol InteractorRequest {}
protocol InteractorResponse {}

protocol Interactor: class, RequestListener {
      associatedtype Response: InteractorResponse
  var responseListener: AnyResponseListener<Response>? { get set }
  }

protocol Presenter: class, EventListener, ResponseListener {
  associatedtype Command: PresenterCommand
  associatedtype Request: InteractorRequest
  var requestListener: AnyRequestListener<Request>? { get set }
  var commandListener: AnyCommandListener<Command>? { get set }
  var router: Router? { get set }
  var scenePresenter: ScenePresenter? { get set }
 }

protocol View: class, CommandListener {
 associatedtype Event: ViewEvent
 var eventListener: AnyEventListener<Event>? { get set }
}

Each module will define event, command, request and response conforming to these protocols. There is no need to define separate protocols for Interactor, Presenter and View for each and every module. Each module will implement concrete class implementing these protocols.

The talk will focus on techniques to achieve this as well as demonstrate how writing mocks are easier.

Suggestions

  • The proposal author responds 10 months ago

    Hi, Thank you for your suggestion. I have updated the proposal with more details.

  • 7b66fb5480f4d6501b9f1be17661732bd39115c6?size=100x100 7b66fb5480f4d6501b9f1be17661732bd39115c6 suggests 10 months ago

    I think some information about the architecture that's going to be presented would be helpful before knowing whether to vote for this proposal.

    Right now it really just says "VIPER, but with generics, protocols and type erasure." If nothing else, that sounds at odds with the title's claim to "simplify" VIPER. What specifically are you going to remove/change to simplify things?