why just #import a header file when you can import the object itself?
Objective-C Ruby
Switch branches/tags
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
Hypo
HypoTest
Hypo.podspec
LICENSE
README.md

README.md

Hypo helps Cocoa coders write loosely-coupled classes. That is, classes that use the services of other classes but try to minimize assumptions.

By minimizing assumptions we write software that is easier to change and test.

Instead of #importing (or now @import-ing) classes and instantiating them directly, Hypo allows you to work at a slightly higher level of abstraction and indicate declaratively object instances your class needs to collaborate with to get its work done.

Let's make this concrete by rewriting a traditional class that finds a Mac app's Application Support folder and rewrite it to use Hypo.

Here's the traditional code:

//
// AppSupportFolder.h
//

@interface AppSupportFolder : NSObject
- (NSURL*)calculatedURL;
@end

//
// AppSupportFolder.m
//

#import "AppSupportFolder.h"

@implementation AppSupportFolder
- (NSURL*)calculatedURL {
    NSArray *urls = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory
                                                           inDomains:NSUserDomainMask];
    
    NSURL *baseURL = ([urls count] > 0)
        ? [urls objectAtIndex:0]
        : [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
    
    NSString *appName = [[[NSBundle mainBundle] infoDictionary] objectForKey:(id)kCFBundleNameKey];
    return [baseURL URLByAppendingPathComponent:appName isDirectory:YES];
}
@end

And you'd use AppSupportFolder like so:

NSLog(@"%@", [[AppSupportFolder new] calculatedURL]);

As it stands now AppSupportFolder relies upon NSFileManager and NSBundle to figure out the correct path to the application's support folder. Let's declare those collaborations:

//
// AppSupportFolder.h
//

@import Foundation;

@interface AppSupportFolder : NSObject
@property(nonatomic, strong)  NSFileManager  *fileManager_hypo;
@property(nonatomic, strong)  NSBundle       *mainBundle_hypo;

- (NSURL*)calculatedURL;
@end

Objective-C doesn't have anything like Java or C#'s annotations, so we use the _hypo suffix naming convention hack to "annotate" properties requiring instantiation.

Here's the updated implementation code:

//
// AppSupportFolder.m
//

#import "AppSupportFolder.h"
#import "Hypo.h"

@implementation AppSupportFolder

- (void)hypo_awakeFromNew {
    if (!self.mainBundle_hypo) {
        self.mainBundle_hypo = [NSBundle mainBundle];
    }
}

- (NSURL*)calculatedURL {
    NSArray *urls = [self.fileManager_hypo URLsForDirectory:NSApplicationSupportDirectory
                                                           inDomains:NSUserDomainMask];
    
    NSURL *baseURL = ([urls count] > 0)
        ? [urls objectAtIndex:0]
        : [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
    
    NSString *appName = [[self.mainBundle_hypo infoDictionary] objectForKey:(id)kCFBundleNameKey];
    return [baseURL URLByAppendingPathComponent:appName isDirectory:YES];
}
@end

As you can see instead of instantiating NSFileManager directly, we rely upon Hypo's default behavior of creating a new instance.

NSBundle is a special case, however, since it's singleton. We could ask every client provide us with that singleton, but since that's common case we leverage -hypo_awakeFromNew to supply that default if it's not otherwise supplied.

You instantiate Hypo-participating classes a little differently:

NSLog(@"%@", [[AppSupportFolder hypo_new] calculatedURL]);

+hypo_new is the bottleneck that allows Hypo to examine the new instance at runtime and figure out what properties need to be filled out.

Now our little class is testable:

//
// AppSupportFolderTest.m
//

#import <XCTest/XCTest.h>
#import "AppSupportFolder.h"
#import "Hypo.h"
#import "OCMock.h"

@interface AppSupportFolderTest : XCTestCase
@end

@implementation AppSupportFolderTest

- (void)testCalculatedURL {
    NSURL *userAppSupportFolder = [NSURL fileURLWithPath:@"/Users/wolf/Library/Application Support" isDirectory:YES];
    
    AppSupportFolder *asf = [AppSupportFolder hypo_new:^(AppSupportFolder *instance) {
        {{
            OCMockObject *fileManagerMock = [OCMockObject mockForClass:[NSFileManager class]];
            [[[fileManagerMock stub] andReturn:@[userAppSupportFolder]] URLsForDirectory:NSApplicationSupportDirectory
                                                                               inDomains:NSUserDomainMask];
            
            instance.fileManager_hypo = (NSFileManager*)fileManagerMock;
        }}
        {{
            OCMockObject *mainBundleMock = [OCMockObject mockForClass:[NSBundle class]];
            [[[mainBundleMock stub] andReturn:@{(id)kCFBundleNameKey: @"MyAppName"}] infoDictionary];
            instance.mainBundle_hypo = (NSBundle*)mainBundleMock;
        }}
    }];
    
    NSURL *expectedURL = [userAppSupportFolder URLByAppendingPathComponent:@"MyAppName"
                                                               isDirectory:YES];
    NSURL *actualURL = [asf calculatedURL];
    XCTAssertEqualObjects(expectedURL, actualURL, @"");
}

@end

Our test class takes advantage of the other version of +hypo_new, +hypo_new: which takes a block. The block is passed the instance that's being filled out. Any property that isn't filled out when the block returns will be filled out with Hypo's default behavior of creating a new instance.

TODO

  • Document HypoClass.