avatarCong Le

Summary

The provided content outlines the implementation of SOLID design principles in Objective-C to create robust and maintainable iOS code.

Abstract

The web content delves into the application of SOLID design principles—Single Responsibility Principle (SRP), Open/Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), and Dependency Inversion Principle (DIP)—within the context of Objective-C programming for iOS development. It offers practical examples and code refactoring to demonstrate how adhering to these principles can lead to more modular, extensible, and testable code. The article emphasizes the importance of these principles for maintaining clean code practices and facilitating easier codebase management. It also provides links to a more detailed accompanying article and a GitHub repository for readers to explore the concepts further.

Opinions

  • The author advocates for the Single Responsibility Principle by showing how separating concerns into distinct classes simplifies code maintenance and extension.
  • The Open/Closed Principle is presented as a means to allow for the extension of a codebase without modifying existing, well-functioning code, thus reducing the risk of introducing new bugs.
  • The Liskov Substitution Principle is highlighted as crucial for ensuring that subclass objects can replace their superclass counterparts without altering the expected behavior of the system.
  • The Interface Segregation Principle is emphasized to prevent classes from being forced to depend upon interfaces they do not use, thereby avoiding unnecessary coupling.
  • The Dependency Inversion Principle is recommended for decoupling high-level modules from low-level module implementations, leading to more flexible and testable code architectures.
  • The author's use of GitHub repositories for further exploration suggests a commitment to practical, hands-on learning and the importance of real-world code examples in understanding abstract design principles.

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.

Objective C
iOS Development
Design Pattern Ios
Recommended from ReadMedium
avatarManuel Meyer
Agile Architecture in Swift

44 min read