Syndicode
Contact Us
Maryna Medushevska
May 16, 2023

Benefits of Clean Coding in Mobile Development: Real Code Examples

This post was written by Oleksandr Mamchich, head of mobile development at Syndicode. 

With years of experience as a senior iOS developer, Oleksandr readily shares his knowledge with his colleagues. He is also an avid learner, always on the lookout for changes and new technologies.

This article will discuss high-performance mobile applications that involve real-time image, video, sound, and signal processing, as well as artificial intelligence and augmented reality. 

These processes require engineers to come up with optimized solutions to handle the heavy workload and meet user experience requirements. To achieve this, engineers must be creative in squeezing every bit of performance out of the hardware.

But before we delve into actual examples of high-performance code, let’s take a moment to discuss the definition of clean code and why it’s essential.

Principles of clean code

Clean code is a common term in software development, but what exactly does it entail? In essence, clean code is code that is

  • Easy to read;
  • Easy to understand;
  • Easy to test;
  • Easy to maintain.

Clean code adheres to a set of principles and practices that make it more resilient, flexible, and versatile.

The KISS principle (“Keep It Simple, Stupid!) is fundamental to clean code. It advocates for breaking down larger problems into smaller, more manageable parts. This helps reduce the risk of bugs and errors, simplifies code maintenance and updating over time, and ensures that others can easily understand and work with the code.

Consistency is another crucial principle of clean code. It involves adhering to a set of agreed-upon conventions and standards for naming, formatting, and code structure. By doing so, developers can ensure that their code is easy to understand and navigate. It is essential to remember that code is written once but read multiple times.

Testability is the next principle of clean code, emphasizing the importance of designing code that can be easily tested. This involves using automated testing tools and techniques to verify that the code is working correctly and designing code in a way that makes it easy to test.

Lastly, the maintainability principle focuses on designing code that is easy to update and modify without introducing new bugs or issues. Achieving this requires careful planning and attention to detail, along with continuous testing and monitoring to ensure that code changes do not negatively impact the entire system.

SOLID approach

SOLID is a popular approach to adhering to the clean code principles. The SOLID acronym represents five design principles that are widely recognized as a foundation of good software design, promoting flexibility, maintainability, and scalability.

Let’s go over the concepts in the SOLID approach and see how they help produce better code.

The Single Responsibility Principle (SRP)

This principle states that a class should have only one reason to change, meaning that it should have only one responsibility or job to do. If a class has multiple responsibilities, it becomes large, unwieldy, and may have unnecessary relations between logically unrelated sub-services.

For example, a class that handles all image-related tasks would be too big and inefficient, violating the SRP.

class ImageManager {
    // If you have to name something 'Manager' - consider decomposition
    
    public func uploadImage(_ image: UIImage) {
    }
    
    public func resizeImage(_ image: UIImage, size: CGSize) -> UIImage {
    }
    
    public func fromResource(resourceId: String) -> UIImage {
    }
  public func addAlphaChannel(_ image: UIImage) -> UIImage {
    }
}

Currently, the `ImageManager` class has multiple unrelated responsibilities, such as uploading, resizing, retrieving, and adding an alpha channel to images. This makes it difficult to test each function individually and modify or replace them. 

To improve the code’s maintainability, let’s break it into dedicated single-purpose classes.

class ImageUploader {
    public func uploadImage(_ image: UIImage) {
    }
}


class ImageTools {
    public func resizeImage(_ image: UIImage, size: CGSize) -> UIImage {
    }
    
    public func addAlphaChannel(_ image: UIImage) -> UIImage {
    }
}


class ResourceBundle {
    public func fromResource(resourceId: String) -> UIImage {
    }
}

Now that the code is modular, changes to one class will not affect the others. This approach makes it easier to reuse the code in other projects.

The Open/Closed Principle (OCP)

This principle suggests that a class should be open for extension but closed for modification. Over time, the interpretation of OCP has evolved alongside programming languages, and it now can be stated as follows: “New functionality should be able to be added without the need to change the existing source code.” 

Additionally, this principle emphasizes that polymorphism is always preferable to if/switch statements. 

Consider the following code:

class NetworkTransport {
    enum Proto {
        case TCP
        case UDP
    };
    
    private let _proto : Proto;
    
    init(_ proto: Proto) {
        _proto = proto;
    }
    
    func sendMessage(message: String) {
        switch (_proto) {
        case .TCP: // Send over TCP
            break
        case .UDP: // Send over UDP
            break
            
        }
    }
}

The `NetworkTransport` class deals with two different transport protocols. However, even if we ignore the issue of violating the SRP, there is still a problem. If we want to add a new transport protocol, such as WebSocket, we would need to modify the class in multiple places. 

To address this problem, we can break the class into three separate classes. This approach will make it easier to add a new transport protocol by simply adding a new class instead of modifying the existing ones.

class NetworkTransport {
    init() {
    }
    
    func sendMessage(message: String) {
    }
}


class TCPTransport: NetworkTransport {
    override init() {
        super.init()
    }
    
    override func sendMessage(message: String) {
        // Send over TCP
    }
}


class UDPTransport: NetworkTransport {
    override init() {
        super.init()
    }
    
    override func sendMessage(message: String) {
        // Send over UDP
    }
}


// To add new transport you need a new class inherited from NetworkTransport

The Liskov Substitution Principle (LSP)

The LSP principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. This implies that a subclass should be able to replace its superclass without breaking the application. Essentially, inherited classes should behave the same way as the superclass.

For example, consider the implementation of orientation services that adhere to the SRP and OCP. However, the orientation method returns orientation in different formats in different inherited classes. 

This violates the LSP, as inherited classes should operate in the same manner, and can lead to unexpected results if a program uses objects of `Device1` and `Device2` in place of objects of `OrientationDevice.`

class OrientationDevice {
    init() {
    }
    
    func getOrientation() -> vector_float3 {
        return simd_make_float3(0, 0, 0);
    }
}


class Device1: OrientationDevice {
    override init() {
        super.init()
    }
    
    override func getOrientation() -> vector_float3 {
        // Returns Euler angles
    }
}


class Device2: OrientationDevice {
    override init() {
        super.init()
    }
    
    override func getOrientation() -> vector_float3 {
        // Returns Roll/Pitch/Yaw
    }
}


// You can not replace Device1 with Device2 and vice versa
// Because they return orientation in different formats

READ ALSO: AI-POWERED FACIAL RECOGNITION APP DEVELOPMENT.

The Interface Segregation Principle (ISP)

This principle suggests that a client should not be compelled to rely on interfaces it does not use. Thus, an interface should contain only the methods that the client needs. 

The delegate pattern is one of the most commonly used in iOS development. However, at times, delegation between two entities can become excessively complicated, leading to bloated and difficult-to-read code. This is especially true when it is used to communicate between multiple entities. An example of this is shown in the code below.

protocol FatUglyDelegate {
    func onEntityCreated(_ entity: SomeEntity);
    func onEntityUpdated(_ entity: SomeEntity);
    func onEntityDestroyed(_ entity: SomeEntity);
    
    func onUploadFileRequested(path: String);
    func onUploadFileCancelled(path: String);
    
    func onImagePicked(_ image: UIImage);
    func onImagePickerError();
}

This interface is responsible for supporting the lifecycle of multiple entities, leading to overloading. Breaking down the interface into three separate interfaces, each catering to a specific entity, would reduce complexity and increase modularity.

protocol SomeEntityCRUDDelegate {
    func onEntityCreated(_ entity: SomeEntity);
    func onEntityUpdated(_ entity: SomeEntity);
    func onEntityDestroyed(_ entity: SomeEntity);
}


protocol FileUploadDelegate {
    func onUploadFileRequested(path: String);
    func onUploadFileCancelled(path: String);
}


protocol ImagePickerDelegate {
    func onImagePicked(_ image: UIImage);
    func onImagePickerError();
}

The Dependency Inversion Principle (DIP)

It emphasizes that modules of higher and lower levels should not depend on each other. Instead, both should rely on abstractions. Abstractions should be independent of details, whereas details should depend on abstractions. 

For instance, consider the OpenGLES renderer on Android devices, where the context is reset when the app goes to the background or the device orientation changes. Therefore, it is crucial to continuously check the integrity of the OpenGLES context.

class Renderer {
    enum API {
        case None
        case Metal
        case OpenGL
    };
    
    init() {}
    func getAPI() -> API { return .None }
    func restoreContext() {}
    func render() {}
}


class MetalRenderer: Renderer {
    override init() {
        super.init()
    }
    override func getAPI() -> API { return .Metal }
    override func restoreContext() {}
    override func render() {}
}


class OpenGLRenderer: Renderer {
    override init() {
        super.init()
    }
    override func getAPI() -> API { return .OpenGL }
    override func restoreContext() {}
    override func render() {}
}


func renderSomething(renderer: Renderer) {
    if (renderer.getAPI() == .OpenGL) {
        // This also breaks LSP because
        // The user of Renderer has to alter behavior in order
        // to renderer implementation
        renderer.restoreContext();
    }
    
    renderer.render();
}

The code above violates the DIP because the high-level module `RenderSomething()` depends on the lower-level module `Renderer.` Additionally, the `Renderer` class contains implementation details for specific graphics APIs (`Metal` and `OpenGL`), which violates the “abstractions should not depend on details” part of the DIP. 

To adhere, we should move the context restoration logic inside the renderer implementation. This would make the code more flexible, easier to maintain and test, and much more reusable.

Final words

And there you have it, the five principles of the SOLID approach, which is a part of the clean code philosophy, illustrated by real examples of working code. By applying these principles, developers can make their code easier to understand, saving considerable time when picking up development after a different team.

The next blog post, however, will provide some examples of when following clean code philosophy can actually do more harm than good. Stay tuned!