Skip to content

Latest commit

 

History

History
266 lines (171 loc) · 10.4 KB

CodeStructure.md

File metadata and controls

266 lines (171 loc) · 10.4 KB

#Code structure

In this document we describe how our code base is organized. Since we introduced this guide to an existing code base it is very likely that you will encounter structures that do not comply with rules presented here. If you find code that does not correspond to these rules, please refactor it!

Table of Contents

##Folder Structure

The code for the iOS Raumfeld Control is organized by topics such as Zones, Content, Playback, or UPnP:

  • RaumfeldControl/
    • Zones/
    • Content/
    • Playback/
    • UPnP/

Each topic is organized by this folder structure:

  • Topic/
    • View/
    • ViewController/
    • Model/
    • Controller/

This folder structure reflects our view on the app's architecture.

##App Architecture

The app is composed of four layers:

The purpose of this architecture is to clearly separate responsibilities and ease unit testing.

###View Layer

All of your UICollectionView, UITableView and custom view classes and XIB's go here.

###ViewController Layer

Yeah, you got it: all subclasses of UIViewController go here. ViewControllers should be as lightweight as possible. A viewController's task is to do view-management: construct views from domain model objects and present those views to the user. A viewController should not download anything, not persist anything, or perform any other business logic. This is the task of classes in the controller layer or model layer.

###Model Layer

Classes in this layer represent your domain model and all of your business logic.

The names of classes in this layer refer to domain concepts like content item, content cache, or zone configuration, zone, room:

@class ContentItem
@class ContentCache

@class RFZoneConfig
@class RFZone
@class RFRoom

Model layer classes should not download anything, not persist anything, communicate with other systems, or contain third party code. Classes in this layer are only allowed to reference classes from the controller layer. The reason for this restriction is to keep this layer as independent as possible from any changes in iOS versions, or third party libraries. Our underlying assumption is:

We control the application domain. Others control the technology.

Responsibilities like communicating with a remote system, or storing objects persistently is delegated to classes in the controller layer.

###Controller Layer

This layer contains all of your efforts to store objects in databases, do encryption, or communicate to other systems. The integration of third libraries like AFNetworking, FMDB, or CocoaSecurity happens here.

Classes in this layer translate technical concepts to domain concepts.

As an example let's have a look at the RFZoneController protocol and the class RFZoneControllerImpl that conforms to this protocol and translates an XML snippet into an instance of RFZoneConfig. The protocol defines how to access the app's zone configuration - by accessing the property zoneConfig:

@protocol RFZoneController <NSObject>

@property (nonatomic, readonly) RFZoneConfig *zoneConfig;

@end

This protocol does not reveal how a zone configuration is retrieved. And this does not matter for users of this protocol. It is irrelevant to users wether the zone configuration is downloaded as an XML file via HTTP or loaded via NSKeyedUnarchiver from the device's local storage.

It is the class RFZoneControllerImpl that knows how to get the zone configuration. Nobody else. It knows that the zone configuration is downloaded as an XML snippet via an instance of URLPoll and parsed by RFZoneConfigParser:

//RFZoneControllerImpl.h

@interface RFZoneControllerImpl : NSObject <RFZoneController>
@end
//RFZoneControllerImpl.m

#pragma mark - private

@interface RFZoneControllerImpl () <URLDownloadDelegate>

@property (nonatomic, strong) URLPoll *zoneDownload;
@property (nonatomic, strong) RFZoneConfigParser *xmlParser;

@end

#pragma mark -

@implementation

//....

@end

By defining the responsibility of the class RFZoneControllerImpl via the protocol RFZoneController we have achieved two things:

  1. Loose coupling of the model layer and the controller layer
  2. Better testability of the model layer

###Loose coupling

The model layer only uses the protocol RFZoneController. It does not need to know how this protocol is implemented. If we decide to switch to JSON as a transport format between host and app, the model layer is not affected. The model layer is not only loosely coupled with the implementation of this protocol, it is independent of the technology that we use. This eases maintenance of this layer significantly.

###Better testability

Because the model layer is independent of the technology that we use, it is easier to test. Let's consider a class from the model layer that needs a zoneController:

@interface AModelLayerClass

- (instancetype)initWithZoneController:(id<RFZoneController>)zoneController NS_DESIGNATED_INITIALIZER;

@end

In order to use and test this class we need to initialize it with a zoneController. Or expressed in another way: we need to inject the dependency to an object conforming to RFZoneController.

###Dependency Injection

Let's have a look at the class AModelLayerClass and how it is used.

This might be our first try to instantiate it in a method:

@implementation SomeClass

- (NSString *) computeString
{
	id<RFZoneController> zoneController = [[RFZoneControllerImpl alloc] init];
	AModelLayerClass *instance          = [[AModelLayerClass alloc] initWithZoneController:zoneController];
	
	NSString *result = nil;
	//do something with instance and compute result
	
	return result;
}

@end

This couples the method computeString tightly to the class RFZoneControllerImpl. If we want to test computeString, we are going to instantiate RFZoneControllerImpl and probably trigger side effects like starting an URLPoll:

- (void)testComputeString
{
	SomeClass *sut   = [[SomeClass alloc] init];
	NSString *result = [sut computeString];
	
	XCTAssertTrue([result isEqualToString:@"dependency hell"]);
}

This test is not isolated. This might work. But as your code base grows, you will have plenty of unit tests full of side effects that will not only run slow but influence each other.

One of your thoughts might be "Let's mock the zone controller to isolate the test!". You are right. This is a possible way and there are powerful mocking libraries like OCMock and OCMockito at hand. Instead of mocking the code as it is, we propose a different way of isolating unit tests and avoiding side effects.

Let's have a look at a new implementation of computeString:

@implementation SomeClass

- (NSString *) computeString
{
	id<RFZoneController> zoneController = [RFFactory zoneController];
	AModelLayerClass *instance          = [[AModelLayerClass alloc] initWithZoneController:zoneController];
	
	//...
	
	return result;
}

@end

computeString just asks the RFFactory for an instance of RFZoneController. The RFFactory returns an instance - there is no need to create a specific instance of RFZoneController. The method is independent of the implementation of the RFZoneProtocol. The unit test for computeString does not change. The RFFactory is configured to return a mock for zoneController when a unit test is run. Thus, the test is isolated.

To illustrate the possibilities of dependency injection a bit more, we refactor computeString one more time:

@implementation SomeClass

- (NSString *) computeString
{
	AModelLayerClass *instance = [RFFactory aModelLayerClassInstance];
	
	//...
	
	return result;
}

@end

RFFactory gives us a fully configured instance of AModelLayerClass. It takes care of creating the appropriate instance of RFZoneController and injects it into instance.

The last refactoring shows that by applying dependency injection we get rid of instantiation logic and object graph wiring. This is done by our RFFactory which can be called a "dependency injection container" (DIC).

By delegating object instantiation and object graph wiring to a dependency injection container we get a huge benefit with regard to testability of our app: dependencies can be controlled at a central location. This location is the configuration of a DIC. In case of our app, this is the RFAssembly:

@interface RFAssembly

- (WebService *)webService;
- (ControlContext *)controlContext;
- (id<RFZoneController>)zoneController;
//...

@end

RFAssembly lists the app's components that are instantiated and wired by our RFFactory. RFFactory is configured with an RFAssembly.

To get an instance of ControlContext, use the RFFactory:

ControlContext *controlContext = [RFFactory controlContext]; 

####No Singletons

The use of singletons in the app is deprecated. Don't do this in your code:

+ (id)sharedInstance {
    static MyClass *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

If your class has singleton semantics - only one instance is required in production - let its lifecycle be manged by RFFactory (see RFFactoryGuide). The above code creates a single instance that will live throughout all unit tests. It will aggregate state and hinder you from writing isolated unit tests. If the class' lifecycle is managed by RFFactory a new instance will be created for every unit test which gives you fully isolated unit tests.