Implementing SOLID Design Principles in Objective-C
Mastering Robust and Maintainable iOS Code with SOLID Architecture Patterns for Objective-C
If you’re keen to delve into the SOLID principles within iOS development, I invite you to read the accompanying article that discusses these concepts in detail. Below, you’ll find a practical demonstration of each principle, meticulously implemented in Objective-C.
1. Responsibility Principle (SRP)
Here’s an example demonstrating the Single Responsibility Principle (SRP) in Objective-C, using a class that handles user profile information.
Before applying SRP:
Imagine we have a UserProfileManager
class that handles user profile display, data fetching, and user preferences saving, all in one class.
// UserProfileManager.h
@interface UserProfileManager : NSObject
- (void)displayUserProfile;
- (void)fetchUserData;
- (void)saveUserPreferences;
@end
// UserProfileManager.m
@implementation UserProfileManager
- (void)displayUserProfile {
// Code to display user profile in the UI
}
- (void)fetchUserData {
// Code to fetch user data from the server
}
- (void)saveUserPreferences {
// Code to save user preferences to disk
}
@end
This UserProfileManager
class has multiple responsibilities, which violates SRP.
After applying SRP:
We can refactor the code into separate classes, each handling a single responsibility.
// UserProfileDisplayer.h
@interface UserProfileDisplayer : NSObject
- (void)displayUserProfile;
@end
// UserProfileDisplayer.m
@implementation UserProfileDisplayer
- (void)displayUserProfile {
// Code to display user profile in the UI
}
@end
// UserDataFetcher.h
@interface UserDataFetcher : NSObject
- (void)fetchUserData;
@end
// UserDataFetcher.m
@implementation UserDataFetcher
- (void)fetchUserData {
// Code to fetch user data from the server
}
@end
// UserPreferencesSaver.h
@interface UserPreferencesSaver : NSObject
- (void)saveUserPreferences;
@end
// UserPreferencesSaver.m
@implementation UserPreferencesSaver
- (void)saveUserPreferences {
// Code to save user preferences to disk
}
@end
// Usage in a ViewController
UserProfileDisplayer *displayer = [[UserProfileDisplayer alloc] init];
[displayer displayUserProfile];
UserDataFetcher *fetcher = [[UserDataFetcher alloc] init];
[fetcher fetchUserData];
UserPreferencesSaver *saver = [[UserPreferencesSaver alloc] init];
[saver saveUserPreferences];
In this refactored example, we have separated the responsibilities into three classes: UserProfileDisplayer
, UserDataFetcher
, and UserPreferencesSaver
. Each class has a single responsibility, making the code easier to maintain and extend, and adhering to the Single Responsibility Principle.
For a comprehensive demonstration of this principle implemented in Xcode, feel free to explore my GitHub repository.
2. Open/Closed Principle (OCP)
Here’s an example demonstrating the Open/Closed Principle (OCP) in Objective-C, using a class hierarchy for shapes.
Before applying OCP:
Imagine you have a Shape
class and a ShapeCalculator
class that calculates the area of different shapes. Initially, it only supports rectangles.
// Shape.h
@interface Shape : NSObject
@end
// Rectangle.h
@interface Rectangle : Shape
@property (nonatomic) CGFloat width;
@property (nonatomic) CGFloat height;
@end
// ShapeCalculator.h
@interface ShapeCalculator : NSObject
- (CGFloat)calculateAreaOfRectangle:(Rectangle *)rectangle;
@end
// ShapeCalculator.m
@implementation ShapeCalculator
- (CGFloat)calculateAreaOfRectangle:(Rectangle *)rectangle {
return rectangle.width * rectangle.height;
}
@end
If you want to add support for circles, you’d have to modify the ShapeCalculator
class:
// Circle.h
@interface Circle : Shape
@property (nonatomic) CGFloat radius;
@end
// ShapeCalculator.h
@interface ShapeCalculator : NSObject
- (CGFloat)calculateAreaOfRectangle:(Rectangle *)rectangle;
- (CGFloat)calculateAreaOfCircle:(Circle *)circle;
@end
// ShapeCalculator.m
@implementation ShapeCalculator
- (CGFloat)calculateAreaOfRectangle:(Rectangle *)rectangle {
return rectangle.width * rectangle.height;
}
- (CGFloat)calculateAreaOfCircle:(Circle *)circle {
return M_PI * circle.radius * circle.radius;
}
@end
This violates the Open/Closed Principle because you are modifying the existing ShapeCalculator
class to add a new feature.
After applying OCP:
To comply with OCP, you can define a protocol for shapes that can have their area calculated and then extend each shape to conform to this protocol:
// Shape.h
@protocol Shape <NSObject>
- (CGFloat)area;
@end
// Rectangle.h
@interface Rectangle : NSObject <Shape>
@property (nonatomic) CGFloat width;
@property (nonatomic) CGFloat height;
@end
// Rectangle.m
@implementation Rectangle
- (CGFloat)area {
return self.width * self.height;
}
@end
// Circle.h
@interface Circle : NSObject <Shape>
@property (nonatomic) CGFloat radius;
@end
// Circle.m
@implementation Circle
- (CGFloat)area {
return M_PI * self.radius * self.radius;
}
@end
// ShapeCalculator.h
@interface ShapeCalculator : NSObject
- (CGFloat)calculateAreaOfShape:(id<Shape>)shape;
@end
// ShapeCalculator.m
@implementation ShapeCalculator
- (CGFloat)calculateAreaOfShape:(id<Shape>)shape {
return [shape area];
}
@end
Now, ShapeCalculator
is closed for modification (you don't need to change it when adding new shapes) but open for extension (you can add new shapes by conforming to the Shape
protocol). If you want to add a new shape like a triangle, you just need to create a new Triangle
class conforming to Shape
without altering ShapeCalculator
:
// Triangle.h
@interface Triangle : NSObject <Shape>
@property (nonatomic) CGFloat base;
@property (nonatomic) CGFloat height;
@end
// Triangle.m
@implementation Triangle
- (CGFloat)area {
return 0.5 * self.base * self.height;
}
@end
This way, the ShapeCalculator
class adheres to the Open/Closed Principle, making the codebase more robust and easier to maintain.
For a comprehensive demonstration of this principle implemented in Xcode, feel free to explore my GitHub repository.
3. Liskov Substitution Principle (LSP)
Here’s an example demonstrating the Liskov Substitution Principle (LSP) in Objective-C, using a class hierarchy related to transportation vehicles.
Before applying LSP:
Imagine you have a base class Vehicle
with a method refuel
. You then have a subclass ElectricCar
that doesn't use traditional fuel, so it would need to override this method in a way that might not make sense.
// Vehicle.h
@interface Vehicle : NSObject
- (void)refuel;
@end
// Vehicle.m
@implementation Vehicle
- (void)refuel {
NSLog(@"Vehicle refueled");
}
@end
// ElectricCar.h
@interface ElectricCar : Vehicle
@end
// ElectricCar.m
@implementation ElectricCar
- (void)refuel {
// Electric cars don't refuel, they recharge
NSLog(@"ElectricCar refueled (incorrect behavior)");
}
@end
// Usage
Vehicle *car = [[Vehicle alloc] init];
[car refuel]; // Works fine
Vehicle *tesla = [[ElectricCar alloc] init];
[tesla refuel]; // Incorrect behavior, violating LSP
After applying LSP:
To adhere to LSP, you can create a more generic method that can be correctly implemented by both the base class and the subclass.
// Vehicle.h
@interface Vehicle : NSObject
- (void)prepareForNextTrip;
@end
// Vehicle.m
@implementation Vehicle
- (void)prepareForNextTrip {
NSLog(@"Vehicle refueled");
}
@end
// ElectricCar.h
@interface ElectricCar : Vehicle
@end
// ElectricCar.m
@implementation ElectricCar
- (void)prepareForNextTrip {
NSLog(@"Electric car recharged");
}
@end
// Usage
Vehicle *car = [[Vehicle alloc] init];
[car prepareForNextTrip]; // Works fine, the vehicle is refueled
Vehicle *tesla = [[ElectricCar alloc] init];
[tesla prepareForNextTrip]; // Works fine, the electric car is recharged
In the refactored code, the prepareForNextTrip
method in the Vehicle
class is a more generic action that all vehicles perform to get ready for their next use. The Vehicle
class assumes a traditional refueling, while the ElectricCar
subclass correctly implements this method to indicate recharging instead. Both the Vehicle
object and the ElectricCar
object can now be used interchangeably when it comes to preparing for the next trip, adhering to the Liskov Substitution Principle.
Another code example:
Here’s an example demonstrating the Liskov Substitution Principle (LSP) in Objective-C, using a class hierarchy related to birds.
Before applying LSP:
Imagine you have a base class Bird
with a method fly
. You then have a subclass Penguin
that cannot fly, so it overrides this method with an exception or some unexpected behavior.
// Bird.h
@interface Bird : NSObject
- (void)fly;
@end
// Bird.m
@implementation Bird
- (void)fly {
NSLog(@"This bird is flying");
}
@end
// Penguin.h
@interface Penguin : Bird
@end
// Penguin.m
@implementation Penguin
- (void)fly {
@throw [NSException exceptionWithName:@"CannotFlyException" reason:@"Penguins cannot fly" userInfo:nil];
}
@end
// Usage
Bird *bird = [[Bird alloc] init];
[bird fly]; // Works fine
Bird *penguin = [[Penguin alloc] init];
[penguin fly]; // Throws an exception, violating LSP
After applying LSP:
We can refactor the classes to ensure that Penguin
can be used as a substitute for Bird
without any unexpected behavior.
// Bird.h
@interface Bird : NSObject
- (void)move;
@end
// Bird.m
@implementation Bird
- (void)move {
NSLog(@"This bird is moving");
}
@end
// FlyingBird.h
@interface FlyingBird : Bird
@end
// FlyingBird.m
@implementation FlyingBird
- (void)move {
NSLog(@"This bird is flying");
}
@end
// Penguin.h
@interface Penguin : Bird
@end
// Penguin.m
@implementation Penguin
- (void)move {
NSLog(@"This penguin is swimming");
}
@end
// Usage
Bird *bird = [[FlyingBird alloc] init];
[bird move]; // Works fine, the bird flies
Bird *penguin = [[Penguin alloc] init];
[penguin move]; // Works fine, the penguin swims
In the refactored code, we’ve replaced the fly
method with a more generic move
method. The FlyingBird
subclass overrides move
to specify that the bird is flying, while the Penguin
subclass specifies that the penguin is swimming. Both subclasses can now substitute the Bird
class without any unexpected behavior, adhering to the Liskov Substitution Principle.
For a comprehensive demonstration of this principle implemented in Xcode, feel free to explore my GitHub repository.
4. Interface Segregation Principle (ISP)
Certainly! Here’s an example demonstrating the Interface Segregation Principle (ISP) in Objective-C, using protocols to define specific functionalities.
Before applying ISP:
Imagine you have a protocol Worker
that includes a wide range of responsibilities.
// Worker.h
@protocol Worker <NSObject>
- (void)doCoding;
- (void)doDesigning;
- (void)doTesting;
@end
// Programmer.h
@interface Programmer : NSObject <Worker>
@end
// Programmer.m
@implementation Programmer
- (void)doCoding {
NSLog(@"Programming...");
}
- (void)doDesigning {
// Programmer should not implement designing
}
- (void)doTesting {
NSLog(@"Testing code...");
}
@end
This design forces the Programmer
class to implement methods that it doesn't need, like doDesigning
, which violates ISP.
After applying ISP:
You can refactor the large interface into smaller, more specific interfaces (protocols).
// Coding.h
@protocol Coding <NSObject>
- (void)doCoding;
@end
// Testing.h
@protocol Testing <NSObject>
- (void)doTesting;
@end
// Designing.h
@protocol Designing <NSObject>
- (void)doDesigning;
@end
// Programmer.h
@interface Programmer : NSObject <Coding, Testing>
@end
// Programmer.m
@implementation Programmer
- (void)doCoding {
NSLog(@"Programming...");
}
- (void)doTesting {
NSLog(@"Testing code...");
}
@end
// Designer.h
@interface Designer : NSObject <Designing>
@end
// Designer.m
@implementation Designer
- (void)doDesigning {
NSLog(@"Designing...");
}
@end
In this refactored example, we have three protocols: Coding
, Testing
, and Designing
. Each protocol represents a specific responsibility. The Programmer
class now conforms to only the Coding
and Testing
protocols, which are relevant to its role. The Designer
class conforms to the Designing
protocol. This adheres to ISP, allowing each class to depend only on the interfaces that are relevant to their function.
For a comprehensive demonstration of this principle implemented in Xcode, feel free to explore my GitHub repository.
5. Dependency Inversion Principle (DIP)
Certainly! Here’s an example demonstrating the Dependency Inversion Principle (DIP) in Objective-C, using a scenario where a high-level class depends on an abstraction rather than a concrete class for sending messages.
Before applying DIP:
Imagine you have a MessageSender
class that a CommunicationManager
class uses to send messages. The CommunicationManager
directly depends on the concrete MessageSender
class.
// MessageSender.h
@interface MessageSender : NSObject
- (void)sendMessage:(NSString *)message;
@end
// MessageSender.m
@implementation MessageSender
- (void)sendMessage:(NSString *)message {
// Send message logic
NSLog(@"Message sent: %@", message);
}
@end
// CommunicationManager.h
@interface CommunicationManager : NSObject
@property (strong, nonatomic) MessageSender *sender;
- (void)sendCommunication:(NSString *)message;
@end
// CommunicationManager.m
@implementation CommunicationManager
- (void)sendCommunication:(NSString *)message {
[self.sender sendMessage:message];
}
@end
The CommunicationManager
is tightly coupled to the MessageSender
class, which violates DIP.
After applying DIP:
To adhere to DIP, you would define an abstraction (protocol) for sending messages and make CommunicationManager
depend on this abstraction.
// MessageSending.h
@protocol MessageSending <NSObject>
- (void)sendMessage:(NSString *)message;
@end
// MessageSender.h
@interface MessageSender : NSObject <MessageSending>
@end
// MessageSender.m
@implementation MessageSender
- (void)sendMessage:(NSString *)message {
// Send message logic
NSLog(@"Message sent: %@", message);
}
@end
// CommunicationManager.h
@interface CommunicationManager : NSObject
@property (strong, nonatomic) id<MessageSending> sender;
- (void)sendCommunication:(NSString *)message;
@end
// CommunicationManager.m
@implementation CommunicationManager
- (void)sendCommunication:(NSString *)message {
[self.sender sendMessage:message];
}
@end
Now, CommunicationManager
depends on the MessageSending
protocol, an abstraction, rather than the concrete MessageSender
. This allows you to easily swap out MessageSender
with another message-sending mechanism that conforms to the MessageSending
protocol without changing the CommunicationManager
code. It makes the code more flexible and easier to test.
For example, you could create a mock sender for testing:
// MockMessageSender.h
@interface MockMessageSender : NSObject <MessageSending>
@end
// MockMessageSender.m
@implementation MockMessageSender
- (void)sendMessage:(NSString *)message {
// Mock send message logic for testing
NSLog(@"Mock message sent: %@", message);
}
@end
// Usage
MockMessageSender *mockSender = [[MockMessageSender alloc] init];
CommunicationManager *manager = [[CommunicationManager alloc] init];
manager.sender = mockSender;
[manager sendCommunication:@"Hello, World!"];
By following DIP, the CommunicationManager
becomes more modular, easier to test, and adheres to good software design principles.
For a comprehensive demonstration of this principle implemented in Xcode, feel free to explore my GitHub repository.