In the Internet age, THE S.O.L.I.D principle has long been influential. It is found in computer programming languages and platform features. It also guides the design and coding of software engineering. IOS platform software development is also a branch of software development, S.O.L.I.D principles are also effective for iOS software development, and to be a better iOS software developer, you may have to understand S.O.L.I.D principles more deeply and put them into practice.

The S.O.L.I.D principles are essentially five guiding principles for object-oriented programming (OOP). When designing and coding classes or modules, following S.O.L.I.D principles can make software more robust and stable.

  • Single Responsibility Principle
  • Open/ Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle
  • Dependency Inversion Principle (DIP)

Single Responsibility Principle (SRP)

** The single responsibility principle (SRP) ** states that a class or module has one and only one responsibility. A class is like a container that can add any number of properties, methods, and so on. However, if a view makes a class do too much, it can quickly become unwieldy. Any small change can result in changes to the class, repeated full tests, and so on. But if you follow SRP, classes will remain clean and flexible, and each class will be responsible for a single problem, task, or concern, which is the least expensive way to develop tests. The core of SRP is to break the whole problem into smaller modules, and each module will be implemented and responsible by a separate class.

Often during development, it is easy to violate SRP principles because of their simplicity. ** The biggest phenomenon is small features or small features. ** Small features tend to get development into trouble, especially in team battles where you add a small feature to one class and someone else adds another small feature, and the class becomes more and more features.Ultimately, how to use the class and how to optimize and refactor it becomes the biggest struggle.

IOS developers, by contrast, are probably the most likely to violate SRP, because UIViewController is something we can’t avoid because of the peculiarity of the iOS architecture.

Basically, UIViewController is a group of views on the screen, like table views, image views, etc., and UIViewController also acts as a navigation between UIViewControllers, and sometimes, network requests, etc. UIViewController has 12 responsibilities. This is an iOS component that violates the SRP principle. In iOS software development, UIViewControllers are called Massive View controllers. The large-scale view controller.

This is why almost every iOS developer is reluctant to make arbitrary changes to the ViewController. Due to its many responsibilities and small features, an incomplete or poorly thought out change can result in an application not working properly or behaving as expected.

How to deal with it?

First, never add small features or features to an existing class for the sake of simplicity, but instead think in terms of modules, components, or apis. In the development process, we need to get rid of the tinkering mentality or hacker mentality and consider the solution in the form of class library for the sake of software life integrity and scalability. Build as small a class as possible to complete only one task or solve only one problem. If the problem is relatively large, try breaking it down into smaller problems, then writing a solution class for each of the smaller problems, and eventually building a class to assemble the smaller classes and solve the larger problem.

Review the ViewController in your project. If the class is too heavy, try breaking down the different functions in the class to make the ViewController light. A good example is the assembly method of UITableView provided by iOS SDK, which uses delegate and dataSource to separate actions and data sources, so that the implementation of TableView is clear, simple and fast. Organizing Data using a Data Source is something that any class can do, not just for viewControllers.

Open and Closed Principle (OCP)

** The open Closed principle (OCP) ** states that a class should be open for extension and closed for modification. This means that once you have created a class and other parts of the application start using it, you should not modify it. Why is that? Because if you change it, chances are your change will cause the system to crash. If you need some extra functionality, you should extend the class rather than modify it. With this approach, existing systems do not see the impact of any new changes. In the meantime, you only need to test the newly created class.

Let’s say we have a class UserFetcher that fetches user data, and in that class we have a method fetchUsers, as follows:

class UserFetcher {
    func fetchUsers(onComplete: @escaping ([User]) -> Void) {
        let session = URLSession.shared
        let url = URL(string: "")!
        session.dataTask(with: url) { (data, _, error) in
            guard let data = data else {
                print(error!)
                onComplete([])
                return
            }
            
            let decoder = JSONDecoder(a)let decoded = try? decoder.decode([User].self, from: data) onComplete(decoded ?? [])}}}Copy the code

At first glance, this approach looks like a good way to load data from the network, parse it, and return parsed data. However, suppose you have another task that needs to load Article data from the network. If you do this, you need to rebuild a class that loads Article data. This looks fine, but the problem is that the method for loading User is 99% the same as the method for loading Article. Then the amount of code will double and repeat. Another serious problem is that if the loading protocol is changed, every class that loads data needs to be modified, potentially turning into an exceptional disaster. So what’s a better way to write it?

class Fetcher<T: Decodable> {
    func fetch(onComplete: @escaping ([T]) -> Void) {
        let session = URLSession.shared
        let url = URL(string: "")!
        session.dataTask(with: url) { (data, _, error) in
            guard let data = data else {
                print(error!)
                onComplete([])
                return
            }
            
            let decoder = JSONDecoder(a)let decoded = try? decoder.decode([T].self, from: data) onComplete(decoded ?? [])}}}Copy the code

For the data loading class is reconstructed, define a support for any Decodable protocol class Fetcher, that is, define a support for generics class, the modified class can support all the same return value of the data join and parse etc. Such as:

typealias UserFetcher = Fetcher<User>
typealias ArticleFetcher = Fetcher<Article>
Copy the code

** Open Closed Principles (OCP) ** well followed can save developers’ time and allow the project to evolve quickly. The above examples may not be enough to fully illustrate the importance of the open closed principle, but in the process of continuous thinking and practice, it is recommended that the open closed principle be deliberately brought into the process of software development, there will be unexpected good results.

Richter’s Substitution Principle (LSP)

The Richter’s substitution principle (LSP) states that a derived subclass should be fungible to a base class, meaning that wherever a base class appears, a subclass must appear. It is worth noting that when you implement polymorphic behavior through inheritance, a derived class that does not follow LSP can cause an exception to the system. Use inheritance with caution. Use inheritance only when you are sure that the relationship is IS-IS – A. In addition, LSP means that any method function used with a class should also be used with any subclass of those classes, and if the method is overridden, the user of that method should not see the difference between the method corresponding to the base class and the method overridden by the subclass.

For example, in the above example, ArticleFetcher loads data from the network, parses it, and returns the results, but at some point, the Article data may not need to be loaded from the network, but from the local file system, in which case a good solution would be to override the Fetch method, for example:

class FileFetcher<T: Decodable> :Fetcher<T> {
    override func fetch(onComplete: @escaping ([T]) -> Void) {
        let json = try? String(contentsOfFile: "article.json")
        guard letdata = json? .data(using: .utf8)else {
            return
        }
        
        let decoder = JSONDecoder(a)let decoded = try? decoder.decode([T].self, from: data) onComplete(decoded ?? [])}}Copy the code

The quick method, rewritten, looks like it’s all right, but there’s a serious mistake here. The way the base class works is that if an error occurs, it returns an empty array and completes program processing, whereas the overridden method does nothing if an error occurs. In this way, the UI interface using this method will not be updated, nor will there be a prompt, etc.

1 / / way
let fetcher = FileFetcher<Article>()
fetcher.fetch { articles in
    self.articles = articles
    self.tableView.reloadData()
}
2 / / way
if fetcher is FileFetcher {
    tableView.reloadData()
}
Copy the code

In fact, neither of these ways is correct or rigorous. Either way, the ultimate goal is not to change the base class on the basis of the subclass to complete the implementation of the same behavior as the base class, to achieve the same results. Method 1 seems to be fine, but the subclass behavior is implemented ignoring the behavior of the program when an error occurs. Method 2 can be considered lazy. Although the fetcher object is indeed FileFetcher, this method completely loses the purpose of subclassing and the meaning of subclassing. Just like using proxy callbacks and Block callbacks.

Interface Isolation Principle (ISP)

The interface Isolation principle (ISP) ** states that classes should not be forced to rely on methods they do not use, that an interface should have as little behavior as possible, and that the implementation of the interface should be lean and functional. Let’s say that after getting the data for Article above and showing it in a list, we also need to get the details after the user clicks on the list item. As a protocol-oriented programmer, you can use the protocol approach here to solve this problem.

protocol ArticleFetcher {
    func getArticles(onComplete: ([Article]) -> Void)
    func getArticle(id: String, _: ([Article]) -> Void)}Copy the code

At this point, build a class that gets the details and implement the ArticleFetcher protocol. While this solves the above problem, the problem is that you don’t need getArticle on the list page, and you don’t need getArticles on the details page. The way the protocol approach is defined above directly adds to confusion and noise by providing an unwanted approach, which also goes against all the issues discussed in ** Single Responsibility Principle (SRP) **.

To solve this problem, the above protocols can be split into two, providing a single responsibility, decouple protocol definition method, for example:

protocol ArticlesFetcher {
    func getArticles(onComplete: ([Article]) -> Void)}protocol ArticlesFetcher {
    func getArticle(id: String, _: ([Article]) -> Void)}Copy the code

Once defined separately, the previous implementation does not need to be modified again, and the same class can implement both protocols. Using an instance of ArticlesFetcher in the list controller without causing additional clutter not only adds functionality to the way in which the details are retrieved, but also doesn’t cause trouble for users of the class.

That’s why Swift has protocols for Decodable, Encodable, and Codable. But such a design may not be everyone’s design, and not everyone needs features. However, a good design that conforms to SRP is more beneficial to the stability and robustness of software.

Dependency Inversion Principle (DIP)

The dependency inversion principle (DIP) ** states that high-level modules should not rely on low-level modules; instead, they should rely on abstract classes or interfaces. In module design, specific low-level modules should not be used in high-level modules. Because of this, the higher-level modules will become tightly coupled to the lower-level modules. If the underlying module is changed, the higher-level module will also be modified. According to the DIP principle, high-level modules should rely on abstract classes or interfaces, as should low-level modules. By programming toward interfaces (abstract classes), tight coupling is eliminated.

So what is a high-level module and what is a low-level module? Typically, we instantiate the dependent objects (low-level modules) inside a class (high-level modules). This inevitably leads to tight coupling, and any changes to the dependent objects will cause changes to the class.

The dependency inversion principle states that both high-level modules and low-level modules depend on abstractions. If we call the protocol defined above the Fetchable protocol, then the Fetchable protocol should be used in the view controller, not the Fetcher class.

The reason is that ** reduces coupling. ** Strong coupling occurs when one class relies heavily on another class’s implementation, with many methods called, assumptions made about the inner workings of the class, variable names used to bind it to a particular class, and so on.

As a direct result of strong coupling, optimization and refactoring of the code base becomes more difficult. For example, if you are using CoreDataService for database use and need to switch to RealmService due to business development, the best case scenario is that the view controller does not rely heavily on CoreDataService.

The best practice to solve this problem is to use the same base protocol, such as DatabaseService, and build different database utility classes to implement that protocol.

protocol DatabaseService {
    func getUsers(a)- > [User]}class CoreDataService: DatabaseService {
    // ...
}


let databaseService: DatabaseService = CoreDataService(a)Copy the code

Protocol instances are used in view controllers because there are fewer protocols than classes. A class has certain names and certain methods. In addition, the protocol is abstract. Multiple classes can implement the same protocol, making it ideal for reducing coupling.

If you want to switch to RealmService, all you need to do is create a class that conforms to the same protocol, because there is no dependency on any particular implementation, so you don’t need to change the code in the attempt controller, saving a lot of time.

In the process of software development, it is best to think ahead about the organization of the code, with low coupling and high cohesion reflected in each implementation, and ultimately the stability and robustness of the software will pay off for you.

conclusion

These are the S.O.L.I.D principles, a complete regression of five important software development best practices, but to be clear, these principles, while very useful, are not rules, they are tools to help you make your software more efficient, more stable, and more robust. “Their statement was’ An apple a day to keep the doctor away, ‘” notes Robert C. Martin, creator of the S.O.L.I.D principle. So keep them in mind, but compromise.

Happy coding!