Recently I’ve been rethinking my android architecture project, trying to adapt it to a more “clean architecture”, specifically, the kind of design suggested by “Uncle Bob”.
Which it involves several layers of abstractions, a nice isolation of responsibilities and a very strong dependency inversion achieved by dependency injection; which, ultimately, leads to a very decoupled-portable system. A perfect candidate to be tested by unit testing and integration testing.
In my android implementation I’ve ended up having three different modules or layers:
-domain: entities, interactors, presenters (pure java module)
-data: (acts as a repository to supply the data to the domain) (android library module)
-presentation: ui related stuff, fragments, activities, views, etc (android application module)
So, I’m trying to figure out what would be the best approach on the iOS ecosystem. I’ve tried creating a project with multiple targets to achieve the same solution:
-domain: command line target (which seems very weird but I think is the most pure swift target available)
-data: cocoa touch framework
-presentation: cocoa touch framework
With this approach I can use these targets in the way I did with android modules. But the first caveat I’ve found it is that I need to add manually every new file to the dependent target.
But my knowledge is very limited in projects with multiple targets. I mean I’ve never created an iOS application with multiple targets. So I don’t know even if the solution would be use a framework (cocoa touch/cocoa) as a target instead of a command line module for the domain layer.
Any thought would be really appreciate.
Thanks!
Traits of a Clean architectureThe business rules can be tested without the UI, database, web server, or any other external element. Independent of UI. The UI can change easily, without changing the rest of the system. A web UI could be replaced with a console UI, for example, without changing the business rules.
In a nutshell, MVC is the only viable and thus the best architecture for an iOS app.
Architecture of IOS is a layered architecture. At the uppermost level iOS works as an intermediary between the underlying hardware and the apps you make. Apps do not communicate to the underlying hardware directly. Apps talk with the hardware through a collection of well defined system interfaces.
Note: You can combine Clean Architecture with the model-view-presenter (MVP) architecture as well. But since Android Architecture Components already provides a built-in ViewModel class, we are going with MVVM over MVP—no MVVM framework required!
Uncle Bob's Clean Architecture absolutely applies to iOS, Swift, and Obj-C. Architecture is language agnostic. Uncle Bob himself codes mostly in Java but in his talks he rarely mentions Java. All his slides do not even show any code. It is an architecture meant to be applied to any project.
Why am I so sure? Because I've studied MVC, MVVM, ReactiveCocoa, and Clean Architecture for 2 years. I like Clean Architecture the best, by far. I tested it by converting 7 Apple sample projects to using the Clean Architecture. I've used this approach exclusively for over a year. It works out better every time.
Some of the benefits are:
We also added a router component so we can use multiple storyboards. No more conflicts.
Writing unit tests is greatly simplified too because I only need to test the methods at the boundaries. I don't need to test private methods. On top of that, I didn't even need any mocking framework because writing your own mocks and stubs becomes trivial.
I've written my experience for my last 2 years studying iOS architecture at Clean Swift I also put together some Xcode templates to generate all the Clean Architecture components to save a ton of time.
UPDATE - To answer @Víctor Albertos's question about dependency injection in the comment below.
This is a really great question and demands a long detailed answer.
Always keep the VIP cycle in mind. In this case, the doSomethingOnLoad()
method is not a boundary method. Rather, it is an internal method invoked only within CreateOrderViewController
. In unit testing, we test a unit's expected behavior. We give inputs, observe outputs, then compare the outputs with our expectations.
Yes, I could have made doSomethingOnLoad()
a private method. But I chose not to. One of the goals of Swift is to make it easy for developers to write code. All the boundary methods are already listed in the input and output protocols. There is really no need to litter the class with extraneous private modifiers.
Now, we do need to test this behavior of "The CreateOrderViewController
should do something on load with this request data" somehow, right? How do we test this if we can't invoke doSomethingOnLoad()
because it is a private method? You call viewDidLoad()
. The viewDidLoad()
method is a boundary method. Which boundary? The boundary between the user and view controller! The user did something to the device to make it load another screen. So how do we invoke viewDidLoad()
then? You do it like this:
let bundle = NSBundle(forClass: self.dynamicType)
let storyboard = UIStoryboard(name: "Main", bundle: bundle)
let createOrderViewController = storyboard.instantiateViewControllerWithIdentifier("CreateOrderViewController") as! CreateOrderViewController
let view = createOrderViewController.view
Simply calling the createOrderViewController.view
property will cause viewDidLoad()
to be invoked. I learned this trick a long time ago from someone. But Natasha The Robot also recently mentioned it too.
When we decide what to test, it is very important to only test the boundary methods. If we test every method of a class, the tests become extremely fragile. Every change we make to the code will break many, many tests. A lot of people give up because of this.
Or, think about it this way. When you ask how to mock CreateOrderRequest
, first ask if doSomethingOnLoad()
is a boundary method that you should write test for. If not, what is? The boundary method is actually viewDidLoad()
in this case. The input is "when this view loads." The output is "call this method with this request object."
This is another benefit of using Clean Swift. All your boundary methods are listed at the top of the file under explicitly named protocols CreateOrderViewControllerInput
and CreateOrderViewControllerOutput
. You don't need to look elsewhere!
Think about what happens if you were to test doSomethingOnLoad()
. You mock the request object, then assert that it equals to your expected request object. You are mocking something and comparing it. It's like assert(1, 1)
instead of var a=1; assert(a, 1)
. What's the point? Too many tests. Too fragile.
Now, there is a time when you do mock CreateOrderRequest
. After you've verified the correct CreateOrderRequest
can be generated by the view controller component. When you test CreateOrderInteractor
's doSomething()
boundary method, you then mock CreateOrderRequest
using interface dependency injection.
In short, unit testing is not about testing every unit of a class. It is about testing the class as a unit.
It is a mindset shift.
Hope that helps!
I have 3 series of draft posts in Wordpress on different topics:
Which one of these do you want to hear more first? Should I bump up the series on testing?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With