Let's all go to the Circus!

[Note: This is a copy of the article on my website. I'll be updating and following up with that article there.]

So, I had a problem.

Like every mobile project before, this project was suffering.

The views (UIViewControllers on iOS or Activities on Android) lifecycles were wreaking havoc with the logic. The views were like waves....they'd arrive causing my logic to start, and they'd go tearing down the logic sandcastles as they left.

The code invariably had train wrecks scattered throughout: object.attribute.field.method().anotherMethod(). On Android it would all crash horribly in a nullPointerException fire. On iOS, it would just quietly disappear like a mafia hit.

The code was so tightly integrated, you needed testing magic, mocking doubles, stubs, libraries, Robolectric or such to try to test the code. These often came with limitations that were worse than the untested code.

So, we're back at manual testing bug whack-a-mole: knock one bug down, and another one rises. Knock the second bug down, and the first one returns. Squish both, and a yet-undiscovered bug pops up.

The UI state for each view was this flexible thing...when a button was clicked, the textfield was enabled. Except when the 'Bad Password' screen was showing, because then the cursor would show through. So we had to special-case that. Of course, when the requirement came down to show a 'forgotten password' dialog, that had to be catered for as well, and...

Enough!

I had to be missing something fundamental here.  

At a previous company, I was floundering towards a solution...  

I had a class called a 'Brain'. This Brain was a wrapper for the logic states, while the views were the OS-specific visible part. The brain could be in a different state, and the corresponding views would be shown.

Oh, this raised issues to be sure. Both iOS and Android want their framework to be the center of your application. After all, it makes an iOS app near-impossible to translate to Android, and vice-versa, unless you're using a multi-platform framework (Zen, PhoneGap, etc). I had these states, but the interface was more than 3 methods (it had 5 per state) that people coming to learn it thought it was crazy.

Hrm...time for more research, I guess...  

I ran across this article(https://hannesdorfmann.com/android/model-view-intent) by Hannes Dorfmann. Brilliant article, and it felt like I was on to something. It's for Android, obviously, but I'm sure the concepts will translate with a bit of work.

Then I downloaded the code...and felt trapped back in the same box. Not being completely up-to-speed on RXJava, it felt like there were things just out the corner of my eye that I didn't understand or couldn't even completely see. I didn't see how it would handle the lifecycle issues I'd been facing.

So, then, I thought: why not rewrite it using the most boring, stupid java possible? 

That's when I arrived at 'Circus'.

Before I tell you what Circus is, let me ask you a question: What /is/ your app?  

Is it the views? Is it the database? Is it the network?

I'd say it's the *logic* of your app that makes it special. How you choose to do whatever it is your app does.

What are the features I'm looking to implement?

1) Eliminate lifecycle gyrations from my logic.

2) Isolate the storage, network, and all other library calls I'm not writing.

3) Provide an easy event-recording mechanism.

4) Ensure that all of my UI, my logic, and my plugins can be written using TDD /without/ having to use crazy testing frameworks.

5) On Android, ensure that my tests run in the JVM, so they're crazy fast, enabling real TDD again.

6) Split the UI, Logic, and Plugins into pieces so that multiple people can work on the same codebase in a *clean* way?

Sound too good to be true?

Okay, so lots of (jagged) lines. How's this really a solution for anything?

Well, let's simplify the views...the views only ever do two things: they send an event to the back end, and they render new states. They do *not* alter global state. They could be killed, and reconstituted, and still be just as good as before, as they do not maintain their own state.

Okay, let's also simplify the back end things. In fact, let's break the back-end things into component parts. There's a 'network' plugin that provides the 20 or so network calls we'll make to the backend. There's a 'database' call that handles all storage of temporary state. And so on. Any code that we aren't writing gets wrapped in a plugin. All of it.

What's left? Our logic, of course. This is the thing that makes each view of our app a view of OUR app.

How do these all talk to each other? In true Uncle-Bob fashion, though Interfaces (or your language-equivalent). This means that there's one (and only one) way from a view to send an event to our logic. There's only one way for our logic to send a new state to the front end for rendering. There's only one way for our logic to kick off a network call, or for that network call to return its result.

This means that testing is a doddle, as we can simply write any old object that implements this interface, and all of a sudden, it's that kind of object. We don't need Mockito, Robolectric or any other framework to test...it's all just Our Code!

Our tests can accept events, and send new states to the UI through the UI Interface. Our tests can send events to the Back classes and see what states pop out. The plugins can be tested using the Plugin Interface.

And threading? The UI reports events through a single call. It receives new states through a single call. When UI events are reported, they're simply put onto a background thread, and when a new state is sent to the views to be rendered, it's shifted to the foreground thread. That means that our code *never* needs to bother with foreground/background, etc. Our logic can simply block and wait, as we're by definition on a background thread.

Will this be a solution for all ills? I don't know.

In my next few articles, I'm going to demonstrate these principles, and how I might implement all this and more. What I'm aiming to deliver is a simple framework that we can use to get our jobs done in a way that allows for full (and easy) TDD in a mobile context.

My needs are a fairly simple Android application, so I'll be writing it in Java. The concepts would hold on iOS as well with slight variations.

In fact, if I manage the abstractions cleanly enough, the logic won't care a bean what the UI is doing, nor will the UI care what the backend is...

So, enough for today. Read my next article for first implementations...

-Ken


要查看或添加评论,请登录

Ken Corey的更多文章

社区洞察

其他会员也浏览了