Intro To Object-Oriented Design: Part 2/2
This tutorial series will teach you the basics of object-oriented design. In this final part: polymorphism, factory methods, and singletons. By Ellen Shapiro.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Intro To Object-Oriented Design: Part 2/2
30 mins
- Polymorphism
- The Decorator Pattern
- Implementing the Decorator Pattern
- The Adapter Pattern
- Implementing the Adapter Pattern
- Additional Object-Oriented Patterns
- Class Factory Methods
- Implementing the Vehicle Class Factory Method
- Implementing the Car Class Factory Method
- Implementing the Motorcycle Class Factory Method
- Implementing the Truck Class Factory Method
- The Singleton Pattern
- Where To Go From Here?
Implementing the Car Class Factory Method
Go to Car.h and declare the following factory method:
//Factory Method
+(Car *)carWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear powerSource:(NSString *)powerSource numberOfDoors:(NSInteger)numberOfDoors convertible:(BOOL)isConvertible hatchback:(BOOL)isHatchback sunroof:(BOOL)hasSunroof;
Since you need all the information for the Vehicle other than the number of wheels, you’ve added parameters to your method for all the other Vehicle data along with the Car-specific properties.
Go to Car.m and replace init
with the following implementation of the factory method:
#pragma mark - Factory Method
+(Car *)carWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear powerSource:(NSString *)powerSource numberOfDoors:(NSInteger)numberOfDoors convertible:(BOOL)isConvertible hatchback:(BOOL)isHatchback sunroof:(BOOL)hasSunroof
{
//Create the car object using the superclass factory method.
Car *newCar = [Car vehicleWithBrandName:brandName modelName:modelName modelYear:modelYear powerSource:powerSource wheels:4];
//Set the car-specific properties using the passed-in variables.
newCar.numberOfDoors = numberOfDoors;
newCar.isConvertible = isConvertible;
newCar.isHatchback = isHatchback;
newCar.hasSunroof = hasSunroof;
//Return the fully instantiated Car object.
return newCar;
}
Note that as a general rule of thumb, you don’t have to choose between an init
method and a factory method; however, in this case you’re not going to be using the init
method directly and the factory method now does everything your custom init
method used to do. It makes sense at this point to get rid of the old code since you won’t be needing it anymore.
Next, go into VehicleListTableViewController.m and update the setupVehicleArray
method to use the new factory method on each of the Car objects you’re creating, as follows:
//Create a car.
Car *mustang = [Car carWithBrandName:@"Ford" modelName:@"Mustang" modelYear:1968
powerSource:@"gas engine" numberOfDoors:2 convertible:YES hatchback:NO sunroof:NO];
//Add it to the array
[self.vehicles addObject:mustang];
//Create another car.
Car *outback = [Car carWithBrandName:@"Subaru" modelName:@"Outback" modelYear:1999
powerSource:@"gas engine" numberOfDoors:5 convertible:NO hatchback:YES sunroof:NO];
//Add it to the array.
[self.vehicles addObject:outback];
//Create another car
Car *prius = [Car carWithBrandName:@"Toyota" modelName:@"Prius" modelYear:2007
powerSource:@"hybrid engine" numberOfDoors:5 convertible:YES hatchback:YES sunroof:YES];
//Add it to the array.
[self.vehicles addObject:prius];
Build and run your application; everything looks the same as it did before, but you know that underneath the hood you’re using far less code to creating your Vehicle array. You can now take this same pattern and apply it to the Motorcycle and Truck classes.
Implementing the Motorcycle Class Factory Method
In Motorcycle.h, add the following new declaration of the factory method
//Factory Method
+(Motorcycle *)motorcycleWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear engineNoise:(NSString *)engineNoise;
In this case, you’re adding the specific parameters for creating a new instance of Motorcycles.
Now open up Motorcycle.m and replace the init
method with your factory method’s implementation, as below:
#pragma mark - Factory Method
+(Motorcycle *)motorcycleWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear engineNoise:(NSString *)engineNoise
{
//Create a new instance of the motorcycle with the basic properties by calling the Factory
//method on the superclass.
Motorcycle *newMotorcycle = [Motorcycle vehicleWithBrandName:brandName modelName:modelName modelYear:modelYear powerSource:@"gas engine" wheels:2];
//Set the Motorcycle-specific properties.
newMotorcycle.engineNoise = engineNoise;
return newMotorcycle;
}
Implementing the Truck Class Factory Method
Open Truck.h and add the following factory method declaration:
//Factory Method
+(Truck *)truckWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear powerSource:(NSString *)powerSource wheels:(NSInteger)numberOfWheels cargoCapacityCubicFeet:(NSInteger)cargoCapacityCubicFeet;
Just as before, you’re including parameters that are specific to your new Vehicle instance — in this case, Truck.
Open Truck.m, and add the following factory method implementation (in this case, there isn’t an existing init
to replace):
#pragma mark - Factory Method
+(Truck *)truckWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear powerSource:(NSString *)powerSource wheels:(NSInteger)numberOfWheels cargoCapacityCubicFeet:(NSInteger)cargoCapacityCubicFeet
{
//Create a new instance using the superclass's factory method.
Truck *newTruck = [Truck vehicleWithBrandName:brandName modelName:modelName modelYear:modelYear powerSource:powerSource wheels:numberOfWheels];
newTruck.cargoCapacityCubicFeet = cargoCapacityCubicFeet;
//Return the newly created truck instance.
return newTruck;
}
Now that you’ve created your factory methods for Motorcycle and Truck, head back to VehicleDetailsViewController.m and update your code to use these new factory methods as shown below:
//Add a motorcycle
Motorcycle *harley = [Motorcycle motorcycleWithBrandName:@"Harley-Davidson"
modelName:@"Softail" modelYear:1979 engineNoise:@"Vrrrrrrrroooooooooom!"];
//Add it to the array.
[self.vehicles addObject:harley];
//Add another motorcycle
Motorcycle *kawasaki = [Motorcycle motorcycleWithBrandName:@"Kawasaki"
modelName:@"Ninja" modelYear:2005 engineNoise:@"Neeeeeeeeeeeeeeeeow!"];
//Add it to the array
[self.vehicles addObject:kawasaki];
//Create a truck
Truck *silverado = [Truck truckWithBrandName:@"Chevrolet" modelName:@"Silverado"
modelYear:2011 powerSource:@"gas engine" wheels:4 cargoCapacityCubicFeet:53];
[self.vehicles addObject:silverado];
//Create another truck
Truck *eighteenWheeler = [Truck truckWithBrandName:@"Peterbilt" modelName:@"579"
modelYear:2013 powerSource:@"diesel engine" wheels:18 cargoCapacityCubicFeet:408];
[self.vehicles addObject:eighteenWheeler];
Build and run your application; again, everything works exactly the same on the surface. However, you’ve shortened and simplified your code under the hood and moved often-repeated code into reusable factory methods.
Factory methods add a level of convenience and guard against the possibility of inadvertently leaving off a required property. You’ll see them in common places such as NSString’s stringWithFormat:
or UIButton’s buttonWithType:
– and now you’ve added them to your own vehicle class and subclasses!
The Singleton Pattern
One very specific, very useful type of Class Factory method is the Singleton. This ensures that a particular instance of a class is only initialized once.
This is great for items that need to only have a single instance — for instance, the UIApplication singleton sharedApplication
— or for those classes that are expensive to initialize, or which store small amounts of data which need to be accessed and updated throughout your app.
In the case of your Vehicles app, you can see there’s one piece of data that might need to be accessed and updated all over the place: your list of Vehicles. The list also violates MVC rules by letting VehicleListTableViewController manage its creation and existence. By moving the list of vehicles into its own singleton class, you gain a lot of flexibility for the future.
Go to File\New\File\Objective-C Class and create a subclass of NSObject called VehicleList. Open VehicleList.h and add the following class method declaration for the singleton instance along with a property to store the array of vehicles under the @interface
line:
//The list of vehicles.
@property (nonatomic, strong) NSArray *vehicles;
//Singleton Instance
+ (VehicleList *)sharedInstance;
Next, open VehicleList.m and add the following implementation of the singleton factory method:
+ (VehicleList *)sharedInstance
{
//Declare a static instance variable
static VehicleList *_vehicleList = nil;
//Create a token that facilitates only creating this item once.
static dispatch_once_t onceToken;
//Use Grand Central Dispatch to create a single instance and do any initial setup only once.
dispatch_once(&onceToken, ^{
//These are only invoked the onceToken has never been used before.
_vehicleList = [[VehicleList alloc] init];
_vehicleList.vehicles = [VehicleList initialVehicleList];
});
//Returns the shared instance variable.
return _vehicleList;
}
Notice that you’re declaring the _vehicleList
instance variable and onceToken
GCD token as static
variables. This means that the variable exists for the entire lifetime of the program. This helps with creating a singleton in two ways:
- Instead of checking the null/not null status of the
_vehicleList
instance variable, GCD can more quickly test whether theonceToken
has been executed or not in order to decide whether it needs to create the_vehicleList
instance. Using GCD to perform this check is also thread-safe, sincedispatch_once
ensures that when it’s called from multiple threads, each thread will finish before the next one is allowed to try and create the instance. - You can’t accidentally overwrite the
_vehicleList
instance. Since static variables can only be initialized once, if someone accidentally adds another[[VehicleList alloc] init]
call once the_vehicleList
variable has an initialized object, it won’t have any effect on your existing VehicleList object.
Next, you need to move your vehicle creation over from the VehicleListTableViewController to the VehicleList class.
First, import the Car, Motorcycle, and Truck classes at the top of VehicleList.m:
#import "Car.h"
#import "Motorcycle.h"
#import "Truck.h"
Next, add the following class method to VehicleList.m:
+ (NSArray *)initialVehicleList
{
//Initialize mutable array.
NSMutableArray *vehicles = [NSMutableArray array];
//Create a car.
Car *mustang = [Car carWithBrandName:@"Ford" modelName:@"Mustang" modelYear:1968
powerSource:@"gas engine" numberOfDoors:2 convertible:YES hatchback:NO sunroof:NO];
//Add it to the array
[vehicles addObject:mustang];
//Create another car.
Car *outback = [Car carWithBrandName:@"Subaru" modelName:@"Outback" modelYear:1999
powerSource:@"gas engine" numberOfDoors:5 convertible:NO hatchback:YES sunroof:NO];
//Add it to the array.
[vehicles addObject:outback];
//Create another car
Car *prius = [Car carWithBrandName:@"Toyota" modelName:@"Prius" modelYear:2007
powerSource:@"hybrid engine" numberOfDoors:5 convertible:YES hatchback:YES sunroof:YES];
//Add it to the array.
[vehicles addObject:prius];
//Add a motorcycle
Motorcycle *harley = [Motorcycle motorcycleWithBrandName:@"Harley-Davidson" modelName:@"Softail"
modelYear:1979 engineNoise:@"Vrrrrrrrroooooooooom!"];
//Add it to the array.
[vehicles addObject:harley];
//Add another motorcycle
Motorcycle *kawasaki = [Motorcycle motorcycleWithBrandName:@"Kawasaki" modelName:@"Ninja"
modelYear:2005 engineNoise:@"Neeeeeeeeeeeeeeeeow!"];
//Add it to the array
[vehicles addObject:kawasaki];
//Create a truck
Truck *silverado = [Truck truckWithBrandName:@"Chevrolet" modelName:@"Silverado" modelYear:2011
powerSource:@"gas engine" wheels:4 cargoCapacityCubicFeet:53];
[vehicles addObject:silverado];
//Create another truck
Truck *eighteenWheeler = [Truck truckWithBrandName:@"Peterbilt" modelName:@"579" modelYear:2013
powerSource:@"diesel engine" wheels:18 cargoCapacityCubicFeet:408];
[vehicles addObject:eighteenWheeler];
//Sort the array by the model year
NSSortDescriptor *modelYear = [NSSortDescriptor sortDescriptorWithKey:@"modelYear" ascending:YES];
[vehicles sortUsingDescriptors:@[modelYear]];
return vehicles;
}
The above method can be called at any time to either create or reset the initial list of vehicles.
You’ll notice that most of this code has been moved over from VehicleListTableViewController, but now the vehicles are added to the newly created local vehicles
array instead of VehicleListTableViewController‘s self.vehicles
.
Now you can go back to VehicleListTableViewController.m and remove three things that are no longer needed:
- Delete the entire
setupVehiclesArray
method and the call to it inawakeFromNib
. - Delete the
vehicles
instance variable and the call to initialize it inawakeFromNib
. - Delete the
#imports
forCar.h
,Motorcycle.h
, andTruck.h
Your private interface for VehicleListTableViewController and awakeFromNib
implementation should now look like this:
@interface VehicleListTableViewController ()
@end
@implementation VehicleListTableViewController
#pragma mark - View Lifecycle
- (void)awakeFromNib
{
[super awakeFromNib];
//Set the title of the View Controller, which will display in the Navigation bar.
self.title = @"Vehicles";
}
You’ll notice that Xcode shows you have three errors, since there are three places where you used the vehicles property to feed the UITableViewDataSource and segue handling methods. You’ll need to update these to use your new singleton instead.
First, import the VehicleList class at the top of VehicleListTableViewController.m so you can access the singleton:
#import "VehicleList.h"
Then, find the three spots where Xcode indicates an error and update the code to use the VehicleList singleton’s array of vehicles
instead, as shown below:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [[VehicleList sharedInstance] vehicles].count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
Vehicle *vehicle = [[VehicleList sharedInstance] vehicles][indexPath.row];
cell.textLabel.text = [vehicle vehicleTitleString];
return cell;
}
#pragma mark - Segue handling
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:@"showDetail"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
Vehicle *selectedVehicle = [[VehicleList sharedInstance] vehicles][indexPath.row];
[[segue destinationViewController] setDetailVehicle:selectedVehicle];
}
}
Build and run your application; you’ll see the same list as you did before, but you can sleep better knowing that the code behind the app is clean, concise, and easily extensible.
With all the changes above, you’ll be able to easily add new Vehicles to this list in the future. For instance, if you were to add a new UIViewController that allows the user to add their own Vehicle, you’d only need to add it to the singleton’s Vehicles array.
Or, if you wanted to allow the user to edit a Vehicle, you could make sure you sent all the data back without needing to implement a delegate
for the VehicleListViewController.
There’s one tricky thing to watch out for with singletons: they will stay alive for the entire duration of your app’s lifecycle, therefore you don’t want to load them down with too much data. They can be great for lightweight data storage or to make objects accessible throughout your application.
If you’re storing a lot of data in your app, you’ll want to look at something more robust like Core Data to handle the data storage and retrieval of your objects.