SOLID Principles in Swift

 SOLID is a coding principle and we can write flexible, dynamic, testable and effective codes with SOLID principles.

SOLID has five principle. They are

  1. Single responsibility
  2. Open-closed
  3. Liskov substitution
  4. Interface segregation
  5. Dependency inversion

Let’s examine these in detail.

1. Single Responsibility Principle

If our classes or functions are handling multiple responsibilities, we can say that this is mistake. Because each class or function should have single responsibility.

There is a CategoriesVC class in this example. And there are codes to setup the view model and navigation title in viewDidLoad function. But these codes have different responsibilities.

final class CategoriesVC: UIViewController {

//MARK: - PROPERTIES
var viewModel: CategoriesVMProtocol? {
didSet {
viewModel?.delegate = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel?.fetchCategories()
navigationItem.title = "Categories"
}

}

I created setupViewModel and setupNavigationBar functions in this example. Because these functions have different responsibilities and each function have own responsibility in this example.

final class CategoriesVC: UIViewController {

//MARK: - PROPERTIES
var viewModel: CategoriesVMProtocol? {
didSet {
viewModel?.delegate = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
setupViewModel()
setNavigationTitle()
}

//MARK: - PRIVATE FUNCTIONS
private func setupViewModel(){
viewModel?.fetchCategories()
}
private func setNavigationTitle() {
navigationItem.title = "Categories"
}
}

2.Open-Closed Principle

In this principle, every class and structure should closed for modification and open for extension. For instance we want to create a class to calculate the area of any shape. In this case we have to create a parent class named Shape

I created rectangles and triangles array in AreaCalculator class. I had to create two variable this is because they have different types. And I created two for loop to sum to areas of shapes.

final class Rectangle {
let width: Double
let height: Double

init(width: Double, height: Double) {
self.width = width
self.height = height
}

func calculateArea() -> Double {
return width * height
}
}

final class Triangle {
let base: Double
let height: Double

init(base: Double, height: Double) {
self.base = base
self.height = height
}

func calculateArea() -> Double {
return (base * height) / 2.0
}
}

final class AreaCalculator {

let rectangles: [Rectangle] = [
Rectangle(width: 5, height: 3),
Rectangle(width: 7, height: 4)
]

let triangles: [Triangle] = [
Triangle(base: 5, height: 3),
Triangle(base: 7, height: 4)
]

func calculateTotalArea() -> Double {
var totalArea: Double = 0.0

for rectangle in rectangles {
totalArea += rectangle.calculateArea()
}

for triangle in triangles {
totalArea += triangle.calculateArea()
}

return totalArea
}

}

I created Shape protocol. Because rectangle objects and triangle objects are same type. I mean they are just a shape. So I gave inheritance to Rectangle and Triangle classes. This way I used just one array and one for loop to calculate the area of shapes.

protocol Shape {
func calculateArea() -> Double
}

final class Rectangle: Shape {
let width: Double
let height: Double

init(width: Double, height: Double) {
self.width = width
self.height = height
}

func calculateArea() -> Double {
return width * height
}
}

final class Triangle: Shape {
let base: Double
let height: Double

init(base: Double, height: Double) {
self.base = base
self.height = height
}

func calculateArea() -> Double {
return (base * height) / 2.0
}
}

final class AreaCalculator {

let shapes: [Shape] = [
Rectangle(width: 5, height: 3),
Triangle(base: 7, height: 4)
]

func calculateTotalArea() -> Double {
var totalArea: Double = 0.0

for shape in shapes {
totalArea += shape.calculateArea()
}

return totalArea
}

}

3. Liskov Substitution Principle

It is a principle that aims for classes work together in a meaningful way. So the parent classes should use to features of the child classes without change.

We have Dog and Fish classes in this example. This classes inherited from Animal class. But fish objects shouldn’t have run speed value.

class Animal {
let name: String
let runSpeed: Int
let swimSpeed: Int
init(name: String, runSpeed: Int, swimSpeed: Int) {
self.name = name
self.runSpeed = runSpeed
self.swimSpeed = swimSpeed
}
}

final class Dod: Animal {

}

final class Fish: Animal {

}

I created CanSwim and CanRun protocols. And I created Animals class to hold the name of animal. This is because every animal have a name but every animal can’t swim or can’t run. Now our parent classes can’t change the features of sub classes.

protocol CanSwim {
var swimSpeed: Int { get set }
}

protocol CanRun {
var runSpeed: Int { get set }
}

class Animal {
let name: String
init(name: String) {
self.name = name
}
}

final class Dod: Animal, CanRun {
var runSpeed: Int
init(name: String, runSpeed: Int) {
self.runSpeed = runSpeed
super.init(name: name)
}
}

final class Fish: Animal, CanSwim {
var swimSpeed: Int
init(name: String, swimSpeed: Int) {
self.swimSpeed = swimSpeed
super.init(name: name)
}
}

4. Interface Segregation Principle

This principle aims to separate the different features of interfaces into different interfaces. Because client shouldn’t depend on they don’t need.

In this example, the Animals protocol forces every animal to have both the eat and fly methods. However, the Dog class doesn't have the ability to fly, yet it is required to implement the fly method, leaving it empty. This violates the Interface Segregation Principle because each class should only have the functionality it needs.

protocol Animals {
func eat()
func fly()
}

class Bird: Animals {
func eat() {
print("I can eat")
}

func fly() {
print("I can fly")
}
}

class Dog: Animals {
func eat() {
print("I can eat")
}

func fly() {
print("I cannot fly")
}
}

In this example, specific protocols like Flyable and Feedable are used, allowing each animal class to only implement the features it requires. The Bird class implements both the fly and eat methods, while the Dog class only implements eat. This approach adheres to the Interface Segregation Principle because each class only contains the methods it needs. By doing so, we increase flexibility and keep the code cleaner and more maintainable.

protocol Flyable {
func fly()
}

protocol Feedable {
func eat()
}

class Bird: Flyable, Feedable {
func eat() {
print("I can eat")
}

func fly() {
print("I can fly")
}
}

class Dogs: Feedable {
func eat() {
print("I can eat")
}
}

5. Dependency Inversion Principle

This principle state that high-level classes shouldn’t depend on low-level classes. Also this principle abstract high-level classes and low-level classes using protocols. Therefore, the dependency between them becomes minimalized.

In this example, the UserManager class directly depends on the Networking class. Any changes in Networking would require modifications in UserManager as well. This violates the Dependency Inversion Principle because high-level modules like UserManager should not depend on low-level modules like Networking. This tight coupling makes the code harder to maintain and extend.

class Networking {
func fetchData() {
print("Fetching data from the server")
}
}

class UserManager {
let networking = Networking()

func fetchUserData() {
networking.fetchData()
}
}

let userManager = UserManager()
userManager.fetchUserData()

In this example, we use a NetworkingProtocol to decouple the UserManager and Networking classes. UserManager now depends on the protocol instead of the concrete Networking class. This makes the code more flexible and easier to maintain since changes in Networking won’t affect UserManager.

protocol NetworkingProtocol {
func fetchData()
}

class Networking: NetworkingProtocol {
func fetchData() {
print("Fetching data from the server")
}
}

class UserManager {
let networking: NetworkingProtocol

init(networking: NetworkingProtocol) {
self.networking = networking
}

func fetchUserData() {
networking.fetchData()
}
}

let networking = Networking()
let userManager = UserManager(networking: networking)
userManager.fetchUserData()

Flutter-Clean Architecture

What is the Clean Architecture ?

Clean Architecture is a design principle that promotes separation of concerns. Aims to create a modular, scalable and testable code base. Clean Architecture provides instructions on how to structure the code base. It also includes guidelines on how to define dependencies between different layers of an application. It is not specific to Flutter.

The clean architecture consists of three layers. These are the Presentation LayerDomain Layer and Data Layer. Let’s examine these now.

  1. Presentation Layer:
    The presentation layer is the layer where the user interacts with the application. The presentation layer consists of two main parts. These are Widgets and Presentation Logic Holders. The Widgets section contains widgets, screens and views. In the Presentation Logic Holders section, there are classes such as bloc and getx, where we combine the interface and data.
  2. Domain Layer:
    The Domain Layer represents the business logic of the feature. Domain Layer includes Use CasesEntities and Repositories. The domain layer should be agnostic of any specific framework or technology.
  3. Data Layer:
    Data layer is the data retrieval and data storage part. The data layer consists of three parts. These are ModelsRepositories and Data Sources. The data sources part can be APIs, local databases, or other external data providers.

Why Clean architecture ?

Clean architecture benefits us in many areas. Firstly it facilitates testability. Because the layers can be tested independently. The code becomes easier to make changes and additions as the application grows. The codebase becomes more modular as each layer has different responsibilities.

Filling Example

Let’s imagine an application where users come from API and are listed on the screen. First, let’s create the “features” folder for the features of the application. Then create a “users” folder on the “features” folder and add the folder required for our layers.

I created two folders, bloc and screens, inside the Presentation Layer. Then I created the entities, repository and usecases folder for the Domain Layer. Finally I created data_sources, models and repository folder for the Data Layer.

Code Example

I will explain the Entities, Use Case and Bloc parts with more detail in my other posts. For now, let’s go through the example.

To analyze the state of the data, I created the data_state.dart file in the core/resources/ location.

abstract class DataState<T> {
final T? data;
final FlutterError? error;

const DataState({this.data, this.error});
}

class DataSuccess<T> extends DataState<T> {
const DataSuccess(T data) : super(data: data);
}

class DataFailed<T> extends DataState<T> {
const DataFailed(FlutterError error) : super(error: error);
}

Since this is a simple example, I did not customize the error types. I recommend customizing your error types in your own project.

Then I created my main UseCase. Use cases implement the basic business logic of the project and connect the Presentation and Domain layers.

abstract class UseCase<Type,Params> {
Future<Type> call({Params params});
}

Now we can start coding the layers. Since I have a block and listing screen in my presentation layer, I will not explain it in detail. We can start with the domain layer.

I created the User Entity class in the domain/entity/ for users’ information as follows.

class UserEntity extends Equatable {
final int? id;
final String? firstName;
final String? lastName;
final int? age;

const UserEntity({
this.id,
this.firstName,
this.lastName,
this.age,
});

@override
List <Object?> get props => [id, firstName, lastName, age];
}

We create the UserRepository class and add the necessary functions.

abstract class UserRepository {
Future<DataState<List<UserEntity>>> getUsers();
}

Since I will only be fetching users, I created a UseCase called GetUsersUseCase. Here we call the getUsers() function on the repository.

class GetUsersUseCase implements UseCase<DataState<List<UserEntity>>,void> {
final UserRepository _usersRepository;
GetUsersUseCase(this._usersRepository);

@override
Future<DataState<List<UserEntity>>> call({void params}) {
return _usersRepository.getUsers();
}
}

We have completed the domain layer. First, I create the UserModel for the data layer. UserModel inherits from the UserEntitiy class. Here I added the functions of converting the data coming from the API into a model and converting the model into an entity.

class UserModel extends UserEntity {

const UserModel({
int? id,
String? firstName,
String? lastName,
int? age,
}) : super(
id: id,
firstName: firstName,
lastName: lastName,
age: age,
);

factory UserModel.fromJson(Map<String,dynamic> map) => UserModel(
firstName: map['firstName'],
lastName: map['lastName'],
age: map['age'],
);

factory UserModel.fromEntity(UserEntity entity) => UserModel(
id: entity.id,
firstName: entity.firstName,
lastName: entity.lastName,
age: entity.age,
);
}

UsersRemoteDataSource class is the class from which we retrieve data from the Api. Here we convert the data from the API into a list of models. It would be better to customize the data conversion and error throwing parts according to the API from which you pull the data.

import 'package:http/http.dart' as http;

abstract class UsersRemoteDataSource {
Future<List<UserModel>> getUsers();
}

class UsersRemoteDataSourceImpl extends UsersRemoteDataSource {
@override
Future<List<UserModel>> getUsers() async {
var url = Uri.parse("xxx/getUsers");
Map<String, String> header = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
var response = await http.get(url, headers: header);
if (response.statusCode == 200) {
var data = json.decode(response.body);
List<UserModel> result = data['users'].map<UserModel>((dynamic i) => UserModel.fromJson(i as Map<String, dynamic>)).toList();
return result;
} else {
throw Error();
}
}
}

The UserRepositoryImpl class inherits from the UserRepository class we created previously.

class UserRepositoryImpl implements UserRepository {
final UsersRemoteDataSource _newsRemoteDataSource;
UserRepositoryImpl(this._newsRemoteDataSource);

@override
Future<DataState<List<UserEntity>>> getUsers() async {
try{
var result = await _newsRemoteDataSource.getUsers();
return DataSuccess(result);
} on FlutterError catch(error) {
return DataFailed(error);
}
}
}

Finally, we call GetUsersUseCase in UsersBloc and update the UserState according to the returned result.

part 'users_event.dart';
part 'users_state.dart';

class UsersBloc extends Bloc<UsersEvent,UsersState> {

final GetUsersUseCase _getUsersUseCase;
UsersBloc(this._getUsersUseCase) : super(UsersInitial()) {
on<GetUsers>(getUsers);
}

Future<void> getUsers(event, emit) async {
emit(UsersLoading());
final result = await _getUsersUseCase.call();
if(result.error != null){
emit(UsersLoadFailure(result.error!.message));
} else if(result.data != null){
emit(UsersLoaded(result.data!));
}
}
}

How to extract filename from Uri?

Now, we can extract filename with and without extension :) You will convert your bitmap to uri and get the real path of your file. Now w...