Architecture in Context
Garo Hussenjian at Swift Summit 2017
I have always loved this conference, and it's a huge honor to be here today. We've spent the last year and change at Tinder working on cleaning up a code base that was never genuinely prepared to be as successful as it was. It's a legacy code base, so we have a lot of code that works correctly and is really difficult to work on, so it's difficult to change. We've heard a lot of talks about this today. I think it's because it's such a common recurring theme in the work that we do. Swift was an incredible opportunity, served as a catalyst, I think, for rebuilding and improving our app architecture. When we started to look at how we could achieve this, one thing we ruled out, because I know it's really tempting to rewrite the whole thing ... Tinder seems like a very simple application. It's actually pretty complex. Internally, there's a lot going on. Part of the reason is that; very few screens, a lot of features.
First thing I wanna mention is some bad advice. Don't do this. We don't wanna jump in and say, "Okay, we're gonna rewrite this in Swift." So, start rewriting it in Swift. I think it was really helpful to take some time and really talk about how we wanted to approach it, and this is a quote that's floated around for, essentially, as long as I've been thinking about programming. It's a great quote. What sucks about it is it's actually true, but obviously, we don't wanna take that advice. But there's a lot we have to think about. We're putting together these apps that host an incredible amount of complexity, in very little real estate, and I think the very natural inclination is to do ... I've heard it referred to as "functional decomposition." This is different from functional programming.
It's actually kind of a bad thing, and this bad thing is, because I have networking and data processing, I'm gonna slice up the application into these layers. Layers are good, but if we do that just based on these functional criteria, we end up with maybe a small app that nicely factors out all of these responsibilities, and as we add features, what we're doing is we're adding on top of each of these things, another piece. So we're going back and adding to our data processing to support our new models, or adding on our networking to support our new endpoints, and this ends up creating, effectively, what I like to refer to as "Mt. Olympus architecture." Brandon earlier was talking about singletons. These generally up looking like singletons, and you get this very familiar dependency graph. We don't want this. I think everybody knows that.
But we're definitely needing something else. So our process, planning out this architecture revamp, was to start with what we know. So ... Sorry, let me back up. Essentially, we wanted to break it down into modules. The answer to not doing functional decomposition is to do modular decomposition. What's the difference? Modules are sort of high-level. They're use case-oriented. This is the theme that I'll be coming back to very often, certainly the most important thing that I'll talk about today. The features or use cases, I'll kind of use those interchangeably, and the library stuff, what I call platform, share components, dependencies. We wanna think of those as modules, and then within the modules, I wanna think about layers now. Whereas before I started with the layers and thought of those as these functional things, I'm now thinking of the layers as really the presentation business logic, and some sort of services within a module. So we're slicing this a little bit differently.
Roles are the things you're probably most familiar with, is the thing that we name our class. A view controller, that kind of thing. They're units of single responsibility with a thin layer within a module. Cool? These are just kind of my working definitions. I'm sure people think of these things in different ways. And then lastly, the dependencies, collaboration between these roles. So we're gonna talk about the various ways we can mix and match these things.
Great, so now I can get to what we know, MVC. I definitely don't want to start this talk by saying MVC is bad. We don't do MVC ... Actually, MVC, if you look at it as what it is intended to be, is a UI architecture, like a control architecture, kind of referring to an individual component, and the idea that we can separate the domain ... what I'm calling the "domain" is this business logic, what we refer to as kind of the model, which I'll talk about at length ... and then the presentation, which is what we're mostly very familiar with, obviously, if you use view controllers. But this does have a couple of limitations. Because it's not an application architecture, when used to model an application, certainly runs into problems, and the most common one is the massive view controller.
But I would argue that there's a different problem here that's maybe a little bit more important, which I haven't heard a lot of people talk about, which is: Really, the model in MVC is kind of undefined. I would say that if I'm interested in the domain architecture and I see this diagram, it's really not telling me anything about the model. All it's telling me is I have a model, and I don't know if I'm referring to a domain object, like a single thing. Is it a collection of objects? Is it the object plus all the machinery around it that makes things happen? I don't know, so I think that's an important question. Again, we'll get back to that.
And so, obviously, people were looking for improvements here, and one of those was the idea of MVVM, a view model. So we add a view model and we kind of extract a lot of the presentation logic into something that's more testable, decoupled from a view controller, has all these nice benefits. It really helps. View models are great, and essentially further separating concerns. Maybe our view controller has one less responsibility. Absolutely it’s gonna help with massive view controller. It might also help with the undefined model problem, in that now we essentially have this view model sitting between the view controller view, which we'll commonly refer to as a "view" if we're doing MVVM, and now the model. So actually, if you look at the pattern, it's identical to MVC in the sense that the view model is still sitting between a view and a model, or the model and this actually introduces a little bit of ambiguity now. I no longer am sure, what is my domain code and what is my presentation code?
So, this kind of puts something in between, which is also useful, but possibly blurs the line a little bit between the layers, which we're also interested in preserving. But MVC is really great and I'll talk about it some more.
What's interesting is if you look at Clean architecture, the view model's a really small piece. It's the output of a presenter. Presenter's the thing that now is concerned with updating the view. The view model flows out of a presenter, and then as input flows in from the view to a controller and generates a request model and interactor, the interactor does the work of manipulating the entities in your model. It's working with this new thing called a gateway. This is a great concept we can inject behind some protocol, something that represents an external dependency. It could be a database, could be the network, anything that I would consider I/O would sit behind a gateway, and we do also talk about these as services, and I'll talk about that a little bit more.
Clean really starts to answer the question of; what does the model look like? This interactor thing, it's actually referred to as a use case interactor, and starts to really put attention on the behavior that's happening. How is the data changing as the user interacts? So we're really concerned with data interaction, and this is really great. If you do Clean, there's a Clean swift.org site. This same pattern is referred to as a VIP, also, and it's a pretty powerful pattern. People that do it really love it. It does a couple things that are a little bit risky, and I'll talk about that a little bit more later, again. I'm leading up to this thing, but I'll say for now that when you create a module in VIP, you create a scene, and the scene gives you the view controller, the interactor and the presenter, and it's effectively creating a unit of code that's still based on a view. So everything we've seen up until now is, I would say, a view-based architecture. It's looking at the screen, and then kind of hanging everything off of that screen. This is something that is a little bit concerning and I'll get to that. So Clean is awesome. Everything has protocols in between. This is very well extracted, very testable. This is something we really drew a lot of inspiration from and appreciate.
Okay, so there's a version of Clean, something that was derived from Clean, which is Viper. Viper's interesting. It's moving the presenter kind of to the control position, sort of the driver, if you will, and it's still more or less thinking about a particular view. If there's navigation, it's introducing the concept of a wireframe or router, and this is making the new view controllers, presenting them. If you had a multi-step flow, this might be doing the individual steps and transitioning between steps, and there may be some cases where, as you move from screen to screen, you're gonna be using a series of different interactors. This is not a requirement, but I think it's a very common structure.
Something that I noticed that I really was interested in, was the idea of this use case consolidation, or the coherence of this use case. So, can I go in my code and find the implementation for one use case, and can it look like a script that I can read? What is the story of this particular use case? We ran into the same issue just thinking about Viper, that that story's not very well-represented anywhere in the code. It's a problem with a lot of object-oriented systems, is that you have to kind of piece together the story from all over the code base. So I think a big goal in a lot of modern architectures is to try to pull these together. I'm just curious, show of hands: Who's heard of DCI? Cool, at least one. Two, three. It's kind of a small number. I expected it not to be a large number.
This is a very, very interesting piece of work. The first thing about it I think I should mention is that, while we as a community have been sort of frowning upon MVC, the creator of MVC has sort of answered our concerns with DCI. DCI is the complement to MVC. It's the model side of the MVC architecture equation. This models behaviors in the system, and the use case context is the primary sort of container for system behavior that actually executes a use case. Anything that's gonna happen in the application is gonna happen in a context. Now, that actually sounds really good. Just talking about it, it makes a lot of sense. If I'm doing something, I'm in this context, and there might be states associated with this thing that I'm doing. I think of it as a process, almost, that's running. I could think of an application as a collection of these running processes, and a context is a really beautiful representation for that.
It's also on the domain side, so because it's over there, we can actually write our contexts so that they represent all of the internal system behavior, data transformation that takes place within the app, and then I can reuse this in different apps, or in different views. Or I can have a many-to-many relationship between my views and these contexts that happen to be around. So these wouldn't be singletons. These would be instances that we inject in different ways. That's more of a detail. This is more high-level, but effectively, DCI answers the question of, how do we solve massive view controller? That answer was provided by the creator of MVC. I think that's worth a second look at MVC also.
The other thing I wanna mention about DCI that I really think is important, and the one thing I can say definitively, is that Swift is the best language in existence to implement DCI. The reason is, these object roles over here ... What these are, are these are all the methods to the data that we're considering to be basically done. I didn't mention this earlier, but from Clean onward, you could argue in MVVM it's the same, that the data is really ... There's no behavior in data, so anything we've learned about structs, immutable data types, they're basically just collections of data. It's very simple. They're inert. They don't behave. This is a great thing. So all of these examples are building on that idea.
DCI's a little interesting in that it actually does keep sort of a register of methods, but separate from the data, and attaches it to the data in a context. Now, this is kind of cool. If I have a user in one context, it might have a certain behavior associated with it like, let's say, I'm in Tinder and I see a user and it's a recommendation. Maybe in this context, the user wants to show the photos or whatever, but in a chat context, maybe the user wants to send me a message. Well, if I'm working with that user object in a recommendation context, I'm not gonna necessarily see the sent message on a user for example. I'm not saying we should have these methods on users, but in DCI, we're able to utilize object roles and attach methods to objects. That sounds a lot like protocols and protocol extensions, and in fact, we wanna make heavy use of these protocols and protocol extensions to implement what is the interaction in DCI, is the object roles. That's how we build the relationships between data and other data, and how we express it in a context.
So, again, this is all sounding very ... For me, very natural language. As I talk about a use case, I'm talking about the roles. I'm talking about data. I'm putting this stuff together in a context and it flows. When I'm reading the code, I'm understanding what's going on. The number one mission of DCI is to map the programmer's mental model into the system the way MVC mapped the user's mental model into the system, and to kind of converge the user and programmer mental models into one sort of unified language. This is also a very common theme in domain-driven design. Very, very useful and powerful technique.
So, you can sort of mix these architectures. We love DCI. We had a problem, actually. The context would invariably ... because they're representing use cases and use cases are complex, they have many steps ... and many of these involve presentation. Suddenly we're trying to ... We're making effort. Like before, we're trying to keep our model out of our view controllers. Now we're actually trying to keep our UI out of our context, and this is actually pretty awkward. We had to adapt a bit to make this work in a way that I think is very natural in iOS. For example, I might still like to have a view model, and there's nothing to say that I can't use MVVM with DCI, because if MVC is compatible with DCI and MVVM's very similar to MVC, but allowing me to write something that I can test in a view model ... maybe this view model isn't going to be handling all of our user input, which surely is being routed to a context, but maybe this will ... doing the sort of data binding to the view and implementing observation and these kinds of things. So it's still really useful. We don't have to use them, but they're available to us. It's another tool. All of these are valid roles in the system.
I wanna mention one other thing, which is the service, again, you saw it as a gateway in Clean. The services ... Little things, they're tiny. This would be like the HTP service or an API service. It wouldn't know of any of your application at all, so it would be really a couple of methods that just implement a protocol that you can inject. So we're injecting only this minimum amount of external dependency, and we're not cluttering these things with your domain objects, your endpoints, your APIs. These would live inside of a context, but that gets actually a little bit messy. That's a lot of code to put in there, right? You can say, "Wait a minute. I had an API thing. I had a data thing." We were talking about those functional roles earlier, and so you wanna stuff all that stuff into a context, this thing's gonna be a new massive object, right? Instead of one of them, we'll have one per use case, but that's not gonna necessarily help enough. And so, we combined a few more of these ideas.
So everything I've talked about is great in different ways, and they each have some problem. Some of them minor, some of them a little less minor. We really wanted to have an important part of our architecture in the domain that I can move around, that I could reuse, that I could think about and reason about, and these contexts were great on the view side. We had a lot of options, but they were a little bit ... I think there's one more problem here. The code that wants to be in the context to model a use case is code that really needs to also exist and it needs to be first class. In fact, I think it's actually above a context.
So this is what we are doing. We call it Discover, and we take what we've talked about, everything we talked about, and we introduce a flow. So flow is also ... and I didn’t have a slide for this, but you've probably heard of coordinators. You've heard of a flow controller. You've heard of wireframes and Viper, and you probably know what a presenter does. If you put those things together, you basically get a flow. Now, when I think about a flow, I'm thinking about screenshots handed down from a product manager or designer. It's a sequence again, but some of these things are on one screen. Some of them involve navigation to other screens, and if these are actually part of a single use case, I actually wanna put them together in a flow. Flows are the single control point for our application. So we still have view models if we need them. We can use an observer on a context if we wanna do reactive stuff, if we wanna use RX, whatever. This is totally framework-independent. There is no GitHub repository, download this SDK ... It's not like that. This is just how we structure some code, and internally, we can choose our dependencies and we can choose our ... Maybe our view architecture might be a little different, but the two important aspects of this is that we take our use case behavior, and we put it in a context, and then we take our user experience and we put it in a flow.
It's that simple. If you do those two things, I would argue you're doing Discover. Now, you can call it anything. That was actually something that one of our writers thought of just because it fit a bunch of the roles, so I thought it was kind of cool.
The other thing I didn't mention is interactors in this world. The interactors pull out of a context the stuff that I was talking about earlier, which is a lot of minutia, and it's also extremely transactional. This is how you should write an interactor. There's a request, there's a response. It's stateless. If you're doing functional programming, this is a great place to concentrate all of your transactions and transformations. If you're using some sort of persistence library like Realm or Core Data, this is a great place to concentrate all of this minutia. I kind of think of context as these meta-objects and I think of interactors as these meta-methods. They're actually like objects that perform work, but they're units of work, and ... Who's heard the talks, a lot of people talked about ... I think Andy's gonna talk later on, he's mentioned on many occasions, the functional core and imperative shell? Hands? This is a very, very, very interesting, powerful pattern, and I think context interactors implement this very beautifully. The context would hold some state, could just be data. We're doing transformations on interactors. We're holding that result in a context, and we can use, again, any patterns that we like to achieve this from object-oriented patterns to functional patterns. It's wide open to interpretation.
So one thing is, this is just one module. It's just one use case. This only exists in the context of an application. There's an interesting thing also which is, that application itself was one of my modules. If you remember, there's that sort of app module, and this is one use case module, so actually presentation, use case domain, are part of that same module. There's a funny thing, which is the service is actually part of the application domain also, so if I'm putting together the application, all those high-level ... or I should say low-level application-independent dependencies are inside of these services.
I think of this as a matrix, and this is kind of what you get when you extract it out to the app level. This is just one use case on the bottom. There's a flow of ... Maybe there's a view model, there's a view. You've got a context interactor. Maybe it's taking ... I should fill in the dependencies. Yeah, so now you can kind of see how we would build this, and this is really interesting because we would always think of presentation and domain, and then we might think about an application and use case, but if you primarily slice on either one of these, it's actually not quite right. They're a little bit orthogonal. There's actually an application presentation layer. There's an application domain layer. There's a use case presentation. There's a use case domain. So this is actually just a very simple sort of map for me to think about, where does this code belong? Is it part of use case? Is it part of presentation? And I can think along these two independent axes. I think this was a really helpful tool.
And if you notice on the top, I've added this sort of extra layer which is "session container," which can be like your navigation container or tab something. This is when the user logs in, and they're done logging in, what they would get. So that would kind of host the application UI. A session flow might decide whether to provide a login screen or the application, for example, and you might have a session context that deals with a lot of your off and keeps track of your tokens and that kind of thing. So again, it's just giving us a very clean way to break apart and organize all of these things that constitute the application. So that's the pattern.
I'm gonna go really quickly through the roles because I'm basically out of time. The data structure, as you know, these are tree value semantics. Inputs, outputs, and state. Interactors are the functional core, state transformations. Services are lightweight. They don't know about your app, but they might be networking frameworks or external dependencies. We can inject these as protocols. The context is the imperative shell, state or behaviors relating to one use case. Observers emit data. Basically they can be used anywhere, but they're really helpful for us to build a unidirectional data flow that I can mutate the data and the view can just update as it's changed. So that kind of decouples the reading and the writing when interacting with the context. The view, anything you like. The view can be MVC, it can be MVVM, it could be your functional reactive UI abstraction. Whatever makes you happy. I think it's really important on a team to have a view architecture that everyone likes. Maybe you're in a team where everyone's all in on RX, and that's what you should be using there. If you're a team where you wanna do a new thing, that's what you wanna do. And then if you're on a mixed team, maybe you wanna use the lowest common denominator. You can actually do MVC and it works really well.
Entity, I won't really get into, but if you're using an external, like a Realm or Core Data, this is sorta where you keep your object graph. And then the flow, which is actually the most important thing, should probably be first, but it would destroy the acronym. High-level UX presentation, navigation, modals, deep linking. That's a lot for a flow, so one of the benefits here, it's very modular. It's composed around use cases and behaviors rather than presentation. It implements a functional core imperative shell. This is a very powerful pattern. It's very testable. I've found we can unit test almost everything. Some of the things we'll integration-test if we don't want to create extra protocols, and I think there is such a thing as protocol fatigue ... Although I love protocols, I do think they should be used in the right places. It works really well in the services. It works really well kind of at the major architectural boundaries, but you don't need them everywhere internally, so we do a lot of integration testing. But these are synchronous tests and they're very easy to write. I would call them almost unit tests, for just a larger unit. Therefore, integration tests.
Observers can be done in pure Swift, it could be done with an external framework. You don't have to use observer. There's a standard flow that would just eliminate that layer. Bring your own view architecture, mix and match. And then flows. Flows are really the key to this. They tell the complete story. They tell it one method at a time, so you could imagine starting a view controller with a closure instead of a delegate, getting back a completion event, and firing analytics event. Actually we could implement all of our analytics in a single flow. This is very, very powerful. I think there are other examples of this, like coordinators, flow controllers ... Viper does a pretty good job in the presenter to achieve this kind of similar thing, so we're just kind of branding it as a Flow, and people like how that sounds. And routing, navigation, deep linking you achieve via composition of Flows.
So there's gonna be some sort of enumeration, and a tree of the things that a user can do where they are, and when you wanna do some sort of state preservation, we're gonna sort of capture that state, and then the app flow can build, the session flow can build, the UI flows as needed to get you to the right place. This last one alone I think is a complete talk. I'd love to elaborate on that, so we'll be hopefully sharing on our tech blog some of the implementation for this. And that's what I got for you today. Thank you very much.