HiveBrain v1.2.0
Get Started
← Back to all entries
patternModerate

Dependency Inversion / Injection? - Networking code in model classes

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
networkingdependencycodeclassesinjectioninversionmodel

Problem

@implementation User

- (void)refreshProperties
{
    assert(!self.isSyncing);

    NetworkOperation *op = [[NetworkOperation alloc] initWithEndPoint:GetUserDetails parameters:nil];
    [self setupCompletionForRefresh:op];

    [self setNetworkOperation:op];
    [self setSyncing:YES];

    [[NetworkManager sharedManager] enqueueOperation:op];
}


The above code is used by a UIViewController subclass to update the properties of a User instance (i.e. the view controller calls [aUser refreshProperties] in pull-to-refresh).

Concerted efforts have been made to make sure there are clear lines defining the separation of concerns:

  • Network Operation class only deals with getting data from the network



  • Network Manager class deals with enqueuing operations and watching


reachability

  • User class houses the strings, numbers, bools, etc that


make up a User instance

However, there are some problems with the above method. First and foremost is it's hard to test. There isn't any way to mock a network operation or network manager and get them inside the method call without swizzling. Also, it couples the User class with the NetworkOperation and NetworkManager classes.

While the above code works, I would like to refactor it.

Looking through GoF, it looks like there are some ways to remove the dependency between User and Network-Operation/Manager: adapter, dependency inversion (using a protocol), etc. From what I can tell, that code would look something like:

[aUser setNetworkOperation:];
[aUser setNetworkManager:];
[aUser refreshProperties];


But... isn't this just shifting the dependency? The above calls would happen in the view controller, and now it has to know about and use three classes instead of just the main one it was originally concerned with - User.

Thoughts, discussion, resources, or code examples are greatly appreciated!

Solution

In GitHub for Mac, we use a combination of AFNetworking and our own Mantle and ReactiveCocoa frameworks for API requests.

AFNetworking makes it trivial to create your own "network manager" type object as a subclass of AFHTTPClient. We enqueue requests on it, ask them to hand back a certain kind of MTLModel subclass, and then subscribe to the resulting signal.

The resulting code looks something like this in the caller:

GHGitHubClient *client = [GHGitHubClient clientForUser:loggedInUser];
[[[client enqueueRequestWithMethod:@"GET" path:@"user" parameters:nil resultClass:GHGitHubUser.class]
    deliverOn:RACScheduler.mainThreadScheduler]
    subscribeNext:^(GHGitHubUser *user) {
        // Do something with the fetched user here.
    } error:^(NSError *error) {
        // Handle errors here.
    }];


There are several advantages to this:

  • You're building on the strength of AFNetworking. There's really no reason to write networking code from scratch anymore – this framework is validated in many, many applications, and can do almost anything you'd need.



  • MTLModel provides a way to parse model objects knowing only the desired class. GHGitHubClient can drive the parsing without being coupled to the specific model types.



  • Signals in ReactiveCocoa are composable, so this code can be easily modified to manipulate and transform the GHGitHubUser, or even perform other fetches and combine the results, before delivering to the main thread.



  • Your domain model isn't doing network activity. I firmly believe that domain models should care only about data, not the mechanics of persisting or fetching it.

Code Snippets

GHGitHubClient *client = [GHGitHubClient clientForUser:loggedInUser];
[[[client enqueueRequestWithMethod:@"GET" path:@"user" parameters:nil resultClass:GHGitHubUser.class]
    deliverOn:RACScheduler.mainThreadScheduler]
    subscribeNext:^(GHGitHubUser *user) {
        // Do something with the fetched user here.
    } error:^(NSError *error) {
        // Handle errors here.
    }];

Context

StackExchange Code Review Q#19307, answer score: 14

Revisions (0)

No revisions yet.