Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Facing the VIPER

Esteban Torres
September 15, 2017

Facing the VIPER

What I learned while implementing VIPER on a side project; all the pitfalls you might want to avoid and why you might to also give it a try.

Esteban Torres

September 15, 2017
Tweet

More Decks by Esteban Torres

Other Decks in Technology

Transcript

  1. POP
    MARIN TODOROV
    1 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  2. POP FACING
    THE VIPER
    MARIN TODOROV
    2 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  3. POP FACING
    THE VIPER
    MARIN TODOROV ESTEBAN TORRES
    • IOS @ SOUNDCLOUD
    3 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  4. MARIN IS !
    HE TRIED UNTIL THE VERY LAST
    MINUTE BUT GOT WORST AND
    COULDN'T MAKE IT IN THE END
    4 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  5. FACING THE
    VIPER
    ESTEBAN TORRES • IOS @
    SOUNDCLOUD
    5 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  6. So we have around 30 minutes where I'll bore you with how I tried
    and failed and tried again and midly succeeded in learning VIPER
    AGENDA
    6 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  7. I'm going to quickly sumarise what my talk will consist of.
    I know that every other week there seems to be yet another talk or article
    about which architecture you should or shouldn't use; so bear with me
    while I try to explain if this is yet another talk just like those or how it differs
    Big disclaimer here; I'm not an expert in VIPER and this is more a talk
    regarding what I LEARNED while implementing VIPER in a small project
    Even though this is not entirely a 100% VIPER talk per se; we can't go
    through it without actually getting our hands a little bit dirty with at least
    the basic concepts of VIPER
    This talk and its accompanying blog post started because I wanted to learn
    VIPERin more detail; understand is concept and incorporate Clean Code
    into my day to day life.
    In order to do so I decided to create a small simple project/app and build it
    using VIPER to try and teach me its principles.
    I'll try to carefully explain my ! of thought when first encountering this
    challenge and how I approach each step of it.
    Once we have gone through the «how?» it might do us some good to
    understand the «why?». And there's no better way to understadn the
    «why» of something than by enumerating all its benefits (or the benefits
    that you perceived) while implementing or using it.
    As with antyhing this is not a silver bullet; so I'll try to summarize what
    issues I found while implemementing my app following VIPER's principles.
    And finally I'll give you a wrap up of how I felt about this architecture; how it
    1. Not another VIPER talk…
    2. What is VIPER!?
    3. Learning through experience
    4. What benefits did I get?
    5. What problems did I encounter?
    6. So… VIPER then?
    7 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  8. So… let clear things up
    NOT ANOTHER
    VIPER TALK…
    8 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  9. So yeah; I think it was pretty obvious if you think about
    the name of the talk, and it containing VIPER in it
    THIS IS
    ANOTHER
    VIPER TALK
    9 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  10. What…
    As I mentioned initially; I'm not an expert in VIPER;
    nor am I claiming to be one
    And this talk is more related to how ended up
    implementing VIPER, what I learned; what mistakes
    I made and perhaps prevent you from doing the
    same mistakes
    NO, IT IS NOT ANOTHER
    VIPER TALK
    10 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  11. OK, so we are slowly starting to ease into the
    main topic of the talk.
    Let's start slowly, and at the beginning
    because… where else would you begin
    WHAT IS
    VIPER?
    11 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  12. …our application of Clean
    Architecture to iOS apps.
    …is a backronym for View,
    Interactor, Presenter,
    Entity and Routing.
    — Jeff Gilbert & Conrad Stoll1
    1 https://mutualmobile.com/posts/meet-viper-fast-agile-non-lethal-ios-architecture-framework
    12 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  13. The blog post that incited this talk started as a result
    of my past failed attempts at VIPER
    Some time ago I first started learning about VIPER;
    probably around the time when all the talks where
    related to this architecture
    And as many other developers my first couple of
    tries were fruitless…
    !
    13 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  14. My first impression of VIPER some time ago when I first
    tried to implement it was that there were too many layers;
    where things should go, why so many layers…
    it was pretty discouraging and quite overwhelming… didn't
    have enough time back then to spend a decent amount of
    time to learn this concept and thus I let the topic go.
    Recently I decided that it was time to finally tame the
    VIPER
    LAYERS!?
    14 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  15. I decided it was enough and that I should
    charm the VIPER
    15 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  16. We already covered the pretty basic definition of what VIPER is; just
    Clean Code architecture applied to iOS and that its a backronym
    for VIEW, Interactor, Presenter, Entity & Routing
    Ok, time to get our hands dirty.
    VIPER and/or Clean Code Architecture takes a lot of
    inspiration from the «onion» architecture
    The basic principle of Onion Architecture is to follow the boundaries
    of these layers – the inner layer canʼt depend on its outer layer but
    can depend on layers beneath. For example, domain layer canʼt
    depend on Infrastructure layer, but Infrastructure layer can depend
    on Domain layer.
    16 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  17. OK, we now know where does the architecture takes
    inspiration from, what it stands for and a little bit of how
    the dependency flow goes.
    Now lets try to dig a little bit depper into each layer
    An Interactor, which contains the business logic as
    specified by a use case.
    A Presenter, which contains view logic for preparing
    content for display (as received from the Interactor) and
    reacting to user inputs (by requesting new data from the
    Interactor).
    A View, which displays what it is told to by the Presenter
    and relays user input back to the Presenter.
    VIPER
    ▸ View: displays what it is told to by the Presenter and
    relays user input back to the Presenter.
    ▸ Interactor: business logic.
    ▸ Presenter: contains view logic
    17 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  18. Entity
    Entities are the model objects manipulated by an Interactor. Entities are only
    manipulated by the Interactor. The Interactor never passes Entities to the
    presentation layer (i.e. Presenter).
    Wireframe
    Routing handles navigation from one screen to another as defined in the
    wireframes created by an interaction designer. A Wireframe object in
    responsible for routing. The Wireframe object owns the UIWindow,
    UINavigationController, etc. It is responsible for creating an Interactor,
    Presenter and View/ViewController and installing the ViewController in the
    window. Since the Presenter contains the logic to react to user inputs, it is the
    Presenter that knows when to navigate to another screen. The Wireframe
    knows how to navigate. So, the Presenter is a client of the Wireframe.
    VIPER
    ▸ Entity: model objects manipulated by an Interactor
    ▸ Wireframe/Routing: handles navigation from one
    screen to another
    18 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  19. Here we have a diagram of how all the components
    in VIPER interconnect with each other
    19 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  20. The basic principle of Onion Architecture is to follow the boundaries
    of these layers – the inner layer canʼt depend on its outer layer but
    can depend on layers beneath. For example, domain layer canʼt
    depend on Infrastructure layer, but Infrastructure layer can depend
    on Domain layer.
    So; how do we overcome this limitation when implementing VIPER.
    If you have your outer layers depending on the inner ones but not the
    oppsite is hard to send messages from the outmost part of the
    «onion» and then wait for the results to be notified back
    DEPENDENCY
    FLOW
    20 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  21. In order to circumvent this we need to use heavy usage of
    the concept of the «Dependency Inversion» principle.
    DEPENDENCY
    Inversion
    21 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  22. If you don't know or can't remember; fortunately for
    you I have copied the definition from Wikipedia
    …the conventional dependency
    relationships established from
    high-level… modules to low-level,
    dependency modules are
    reversed….
    — Dependency Inversion - Wikipedia 2
    2 https://en.wikipedia.org/wiki/Dependency_inversion_principle
    22 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  23. As you can see; by defining interfaces (or in our case protocols) we can then invert
    the dependencies and decouple the outer layers from the inner layers without losing
    the ability for the inner layers to communicate results outwards of the «onion».
    23 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  24. Let's start diving a little bit into my own experience with VIPER; as soon as you read the
    dependency flow present in VIPER you'll quickly realise that the only way then to achieve
    the direction flow of the dependencies is via defining «boundary» protocols in
    between the layers; that way you can actually reference outer layers from inner layers
    when notifying back the results from your computations
    This will give us a lot of benefits; not only for the Dependency Inversion Principle
    mentioned before; which is needed anyways in order to achieve this architecture; but it
    will allow us to better test our app.
    HERE BE
    protocols
    24 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  25. Yesterday Dave «rained on TDD's parade» by mentioning many good
    reasons why you should be pragmatic about what to test
    One of Clean Code Architecture «dogmas» is to TDD, or better
    put, the ability to easily TDD your code
    Also by defining your protocols first you start thinking about how the
    API for the components is going to look, then your unit tests will
    become the first consumers of your API and they can show you how
    good or bad an API is written even before there's an actual
    implementation.
    By doing this you avoid coding a component just to then reach the next
    component that's dependent on the previous to find out that you either
    need to pass some extra information, you need a different set of values
    returned or a combination of all the possibilities.
    This wasn't my first application developed relying on TDD but this was by
    far the easiest to set following this development process.
    Also super important to mention: By defining all your dependencies as
    protocols you set yourselve in the right path to mock said
    dependencies and then inject them when writing your tests to better
    control the setup of your unit tests
    TDD &
    protocols
    25 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  26. I'm not advocating that you should go back home and
    TDD the heck out of all your future apps.
    I'm just telling you what VIPER does to help you
    achieve certain things; like in this case, do TDD in a nice
    way
    TDD &
    protocols
    ⾠ DISCLAIMER⾠
    26 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  27. Now that we have settle the foundations for what VIPER is; how it
    works, how everything interconnects with each other, how to overcome
    the dependencies problem is time to «Learn through experience»
    Learning
    through
    EXPERIENCE™
    27 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  28. For the sakes of me learning VIPER I
    decided to do a fairly simple app
    Our APP
    28 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  29. Which is NOT a TODO app
    Decided against a TODO app because it seems like all the tutorials and
    articles on internet are based on building a TODO app; and while this would
    have seemed good because that means more documentation available,
    more examples, maybe same patterns that you could use to learn and
    improve it would somehow mean to me a «hindrance» in my learning
    experience by limiting the creative process
    Our APP
    NOT A TODO APP
    29 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  30. Instead I decided that I wanted to build a simple app that could
    connect to an endpoint A SINGLE endpoint, retrieves a JSON, parses
    said JSON to Entities, loads the images that are referenced in the
    Entities and let's the user tap and show the «detail» image
    User can enter a search criteria that will be used to as a seed to search
    for images in ther server and display results.
    IMAGEGRAM
    30 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  31. Let's enumerate then and identify each part
    First we'll need a networking component that
    will be able to connect to a server and
    consume an endpoint in said server.
    IMAGEGRAM
    1. Connect to an endpoint (Network)
    31 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  32. Then we want to be able to convert the information
    retrieved from the endpoint to valid entities that we
    could manipulate in our application.
    We'll be getting JSON back from the server so we'll need
    a way to transform that into our objects
    IMAGEGRAM
    1. Connect to an endpoint (Network)
    2. Parse JSON to Entities
    32 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  33. This step is pretty trivial in terms of coding; we need to load
    some images that are referenced in the Entities as URLs
    Just need to remember Dave's comment about mega-
    corps interview questions; do not load on the main thread
    and don't force unwrap and we'll be good to go
    IMAGEGRAM
    1. Connect to an endpoint (Network)
    2. Parse JSON to Entities
    3. Load the images in the Entities
    33 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  34. This goes hand in hand with the previous point
    IMAGEGRAM
    1. Connect to an endpoint (Network)
    2. Parse JSON to Entities
    3. Load the images in the Entities
    4. Display the images in a grid
    34 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  35. And the last bit; we'll want to be able to identify when a user wants to
    see a detailed view of an image; so whenever a user taps on a cell/
    image we'll load a new VC showing the image but bigger
    IMAGEGRAM
    1. Connect to an endpoint (Network)
    2. Parse JSON to Entities
    3. Load the images in the Entities
    4. Display the images in a grid
    5. Show a detail view
    35 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  36. Now is time to identify what we need to achieve and see how
    that maps into VIPER
    First of all the Network here is a Data Store.
    The Data Store can be a web service, database, etc & is
    responsible for providing Entities to an Interactor.
    As anInteractor applies its business logic it will typically
    retrieve Entities from the Data Store, manipulate the
    Entities and then put the updated Entities back in the
    Data Store. The Data Store manages the persistence of
    the Entities. Entities do not know about the Data
    Store, so Entities do not know how to persist themselves.
    IMAGEGRAM
    1. Connect to an endpoint (Network) ✅
    2. Parse JSON to Entities ✅
    3. Load the images in the Entities
    4. Display the images in a grid
    5. Show a detail view
    36 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  37. This 2 sounds like Presenter & View related tasks. The Interactor will ask the Data Store for images;
    it will then notify the Presenter when its done loading the Entities and passes then a «subset» structure;
    never the Entities up to the Presenter. In this case we'll send the URLs and a title for each Image retrieved
    from the server; then the Presenter can easily tell the View that it should display the loaded elements
    IMAGEGRAM
    1. Connect to an endpoint (Network)
    2. Parse JSON to Entities
    3. Load the images in the Entities ✅
    4. Display the images in a grid ✅
    5. Show a detail view
    37 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  38. Last but not least we have to show a detailed view; this means a work between the
    Presenter reacting to the User's request, the Routing creating the next VC and
    pushing it into the stack and then a different Presenter telling the new view to load a
    bigger or better resolution version of the desired image
    IMAGEGRAM
    1. Connect to an endpoint (Network)
    2. Parse JSON to Entities
    3. Load the images in the Entities
    4. Display the images in a grid
    5. Show a detail view ✅
    38 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  39. The network layer is pretty trivial and non-important to
    the whole «learning exercise».
    Let's see this as an implementation detail.
    We'll just make use of NSURLSession and call an API
    From what I could get from VIPER is that the Use
    Case is the heart of it; it will encompass everything
    that you need to do in order to achieve a «feature».
    The use case will hold the spec and criteria for your
    feature.
    Network
    INTERACTOR
    39 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  40. So I defined my Interactor's protocol or interface like this; nothing too mind
    blowing and its ok to not read this; we'll come back to it later; but while I was
    defining my interface I bumped into my first moment of doubt; or questioning
    SearchImagesInteractor
    internal protocol SearchImagesInteracting {
    var output: SearchImagesInteractingOutput? { get set }
    func retrieveImagesMatching(criteria: String)
    func loadMoreImages()
    }
    40 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  41. There's an «important» part of the interface and
    perhaps in a way; of VIPER itself.
    How do you communicate back to the outer
    layers when you want to notify that you are done?
    I guess good old way would be via delegation
    pattern
    !
    41 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  42. According to how it should work; you basically want to tell your inner layers to do something,
    and then wait for them to do so and get the results back later; the way this is achieved (as far
    as I could tell from the definition and articles) is via defining some sort of «delegate» and then
    you just let the outer layer know via this «delegate»
    ALTHOUGH IS
    NOT REALLY
    delegation
    42 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  43. So heading back to my interactor definition we can see that I
    defined said delegate as an output for my interactor; and here's
    how I defined said output
    SearchImagesInteractor
    internal protocol SearchImagesInteracting {
    var output: SearchImagesInteractingOutput? { get set }
    43 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  44. Two important things to notice; 1 is that here Image is not a UIImage and
    is neither an Entity but a pretty simplified struct that will be used to
    pass information up to the Presenter
    And the second and to me the most important thing is; do we really need or
    want to define the «outputs» as delegates?
    I've had this discussion with some coworkers; somehow the naming feels
    wrong because it will entail that you are delegating an object to do
    something rather than just notifying that something is done and more
    importantly; at least to me, it feels a little bit outdated.
    Like I said before I'm in no ways a VIPER expert and knew only the basics
    but given that we have closures now in Swift and even blocks in
    ObjC wouldn't it make more sense to perhaps have a completion parameter
    at the end of each method in our Interactor protocol?
    SearchImagesInteractor
    internal protocol SearchImagesInteractingOutput: class {
    func didLoad(images: [Image])
    func didFinishLoadingImages(with error: APIError)
    }
    internal protocol SearchImagesInteracting {
    var output: SearchImagesInteractingOutput? { get set }
    44 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  45. Here I over simplified the code to make it fit in the slides but the idea is the
    same.
    I guess if we do it like this we also get rid of the need for the inner layers to be
    able to reference the outer layers?
    Would this make it any less easy to read or understand, or perhaps even test?
    I don't really have an answer; while I was experimenting with my sample app I
    did stop for some time to ponder on this; I was not able to find good
    resources that explain why there's a need for the delegates or outputs vs
    blocks or closures; perhaps it was all due to timing, when Clean Code
    was «ported» to iOS perhaps we didn't have this resources available and
    thus the architecture was defined as it is now and we kept on carrying the
    same pattern even when new features were made available in ObjC and
    even with new languages like Swift
    Also for the sakes of learning I ended up following the good old proven
    already established pattern and created protocols for my outputs and
    views but thought it was worth mentioning
    SearchImagesInteractor
    internal protocol SearchImagesInteracting {
    func retrieveImagesMatching(criteria: String, completion: (Result) -> Void)
    func loadMoreImages(_ completion: (Result) -> Void)
    }
    45 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  46. Let's wrap up the «code» part; not going to show you the
    full implementation, that's out of the scope of the talk.
    So let's try to skim over the surface of the other
    components
    !
    46 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  47. I just defined 2 protocols for each component
    One that will define its interface (used this a lot
    for when I was mocking and writing tests)
    And the other one to expose a way to
    communicate up when I was done processing
    tasks
    ! protocols
    1. Defining the component's interface
    2. Defining the component's output
    Basically a way to communicate the results to the
    upper layers
    47 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  48. The app is pretty simple and only had 1 Entity and 1 sub-
    struct that was passed from Interactor -> Presenter
    ! models
    48 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  49. Finally we are getting to the important bits of
    the talk
    The talk was not supposed to be centered
    around me explaining how VIPER works or
    not but what I learned while implementing it.
    Now I'll try to enumerate the benefits here
    Benefits
    49 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  50. Because I was focusing so much in defining my interfaces and outputs
    ahead of time I was able to test my components really well.
    I started defining from the lowest or internal components outwards of
    the «onion»; which meant that the very first component that I wrote
    didn't really had any dependencies; but the next component out
    depended on the component that I just wrote; which had its nice
    interface already defined and thus, it was super easy to mock.
    Basically by the time you reach the outer layer you are fairly certain that
    everything should work separately as expected.
    Benefits
    «EASIEST» TDD
    PROCESS EVER
    50 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  51. Once I was done with my app and I was satisfied with what I learned and did I
    decided to spice things up a little and change some things; changed the API
    endpoint, the JSON response and then added another feature to save the
    history of searches so that users could see their past searches.
    Thanks to how it was structured changing the endpoint and the JSON response
    required changes in the Data Store layer only; which is the one in charge of
    turning the JSON into Entities; and everything else stayed the same
    Benefits
    EASY TO INTERATE
    51 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  52. Because every component is pretty well defined and does one thing and one thing only
    and even sometimes classes function only as «proxies» you end up having pretty small
    classes with pretty well defined «pure functions» (another point to easily testable)
    Benefits
    SMALL CLASSES
    52 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  53. Now that we have covered the good; let's talk about the bad.
    Obviously this is not a perfect solution so it has to had its drawbacks
    Problems
    53 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  54. I've read this in many places; there are so many layers to VIPER that it may feel overwhelming; and while is
    true that you don't NEED to implement all of the components for each feature; only the ones that make
    sense but even doing so you might end up with lots of layers; and to build up on the last of the Benefits;
    some classes end up being proxies which in the end would make sense to get rid of them but then again;
    wouldn't that mean «defining a different architecture» in a way?
    Problems
    LAYERS
    54 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  55. The first time that I tried VIPER it felt a little bit overwhelming due to all the
    separate pieces. You might feel lost as to where things should go.
    Most things are pretty straighforward and you know right out of the bat
    where a function should be but sometimes is hard to figure out.
    For example, when loading the detail the Presenter will tell the Routing
    but at this point, should the Presenter ask the Interactor for some
    sort of ID, etc that identifies the desired Entity; pass that to the Routing
    and then the Routing will use that ID to pass it to the next Presenter so
    that the new Presenter can ask its Interactor for the sub-set Entity
    that should be displayed?
    Or the subset entity that you pass from the Interactor to the
    Presenter should already contain all the information needed for this
    scenario?
    Problems
    HARD TO FIGURE
    WHERE THINGS GO
    55 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  56. I want to tie in a little bit with yesterday's talk about
    architecture; I've been saying but for some time now.
    There's simply not a fix all architecture
    I don't think that VIPER is a silver bullet solution for
    EVERYTHING.
    It does a lot of nice things and helps you with some
    stuff.
    SO… VIPER
    THEN?
    56 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  57. Gracias!
    !"
    57 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide

  58. GITHUB | TWITTER
    @esttorhe
    ESTEBANTORR.ES
    58 — Esteban Torres • NSSpain 2017 • @esttorhe

    View Slide