As we complete our development tasks, we always want to deliver high-quality code. There are many ways to measure code quality, among which scalability and reusability are two indexes. The theory of design pattern can guide the code design very effectively, but talking about these theories is very abstract, this article is aimed at downloading this scene, combined with some theory of design pattern, talk about how to design a more reasonable structure of the download module.

Step.1 Take your time and do a “needs review”

Before you start coding, identify functional requirements, technical requirements, and then do some preliminary thinking.

Start from the goal

Starting from a goal can help define the focus of the design process. For the download scenario, it is intuitive to imagine that the steps involved in file manipulation, persistent storage, and so on are frequent in a project. So I would expect a lot of code written for the download module to be well reusable. At the same time, it can be predicted that the download scenario is very prone to subsequent changes or additions. One day, it may only download video, and the next day, it may need to add support for audio and ZIP files. For the database storage framework, you may be using FMDB now and then WCDB later. Therefore, the expansibility and easy modification of this module are also required.

With a little bit of theory

There are several principles of design patterns that we find difficult to grasp at first. For they are as brief as a few words of truth, while the actual scenes are a thousand thousand. So, let’s start with the most understandable “single responsibility principle”. Simply put, a single module should only be responsible for a single task, and the finer the granularity of the task, the less coupling it has with other modules, and the easier it is to reuse. However, following the “dependency inversion principle” can effectively improve the code’s ease of modification. For example, for database modules, a layer of interface classes is abstracted on top of the implementation classes that actually use a database framework to access operations. Only the methods provided in the interface class are used in the download process, and the concrete implementation of the methods in the interface class is done by the lower implementation class. Thus, when we replace the database framework with WCDB instead of FMDB, we only need to make changes to the code of the implementation class. The goal of the changes is to implement the methods declared in the interface class again using the new framework. This is called “programming for the interface” instead of “programming for the implementation.” The benefits are obvious: in the replacement of the database framework, the top-level business code does not need to be changed at all **, just the implementation classes of the database operations.

Purpose of modularization

One thing to be clear is that the “modularity” we talk about is not always reusable for all modules in any scenario. Because modules can be divided into business modules and general modules, the general module strives to achieve reuse in any scenario, while the business module focuses on completing a certain demand scenario. Although the word “download” is used in many projects, the definition of it varies from project to project. Some “downloads” simply refer to downloading a single file, while others refer to the local cache of all content in a particular scenario.

In this article, I have a scenario in which a download task will include various specific subtasks. For example, a download task might consist of three video files, two audio files, three images, and two JSON-formatted results of a network request.

Therefore, I will refer to the “download” in this article as a business module, which does not seek to be reusable in any scenario, but does a good job of downloading in this more complex scenario. However, the specific steps such as file download, image cache and file operation contained in this business module are actually irrelevant to business, so they can be classified as a general module. The image caching module here can be used in other image caching scenarios, and the file manipulation module here can be used in other file manipulation scenarios. A detailed analysis of them is described below.

Step.2 gives the design scheme

Combined with the analysis of the first part of the article, start to design the scheme.

Downloading is not the only thing

Generally speaking, download refers to the process of obtaining resources from the cloud to the local disk. For iOS apps, the purpose of downloading is to display something offline. A complete download process should consist of the following steps:

  • File operations

For the downloaded file, you need to determine its local storage path; Given a certain key value, you need to obtain the corresponding file storage path. For a specified path, operations such as checking file existence and integrity are performed. In the downloading process, files are written continuously. Deleting downloaded content involves file deletion and directory deletion. In addition, there are general operations such as obtaining various system directories, obtaining disk space data, etc. If security requirements are involved, there will be file encryption and decryption operations. Therefore, encapsulating file operations as a separate module is a wise choice. File operations will not only occur in the download scenario, so the implementation of this module should be stripped of business related content as much as possible, and strive to become a general tool module.

  • Database operations

Based on the scenario given in the first part of this article, the download task here should be structured data. Downloaded content is displayed regardless of network conditions, so download records should be stored persistently. Based on the above two points, the use of database is a natural choice. It should be made clear that the database stores records of downloaded tasks, or logs, not downloaded files. Given the diversity of database frameworks in iOS and the continued pursuit of database performance by business parties, it’s easy to see how database frameworks will be replaced in the future. Therefore, this module is also analyzed above, which is divided into abstract interface class and concrete implementation class according to the principle of dependency inversion.

  • Large volume file download

Large files such as video, audio, and ZIP files are common for downloading. Therefore, a download module for only large files is necessary. It does not involve any specific business details, its task is only according to the given file URL and local storage path, complete the download of the file. It is relatively easy to achieve the high cohesion of this module, so it is highly recommended that this section be packaged as a generic module to meet the file download requirements in any scenario. In order to reduce the horizontal dependence between common modules, one idea is that the local path is obtained by the upper-level business module calling the file operation module, and then passed to the module, rather than the module directly calling the file operation module. For file writing operations, you can use the system’s NSFileManager directly. Another way to think about it is that the dependency between large file downloads and file operations is natural and acceptable, allowing the download module to depend on the file operations module. There’s no right answer, you can choose.

  • Image download

Sometimes image downloads are included in the download task, and by size, it is not unreasonable to classify image downloads as files. However, image caching is a long-standing topic in the development of iOS. We have YYWebImage, SDWebImage and other excellent image caching frameworks. What’s the reason to repeat a wheel that may not have better performance? In addition, the two image frames mentioned above are basically used in the vast majority of iOS web apps, so it’s likely that images that have already been downloaded will be loaded with the above image frames at an unrelated point in the project. If the image download is implemented using the caches of these frameworks, then in the above scenario, the ** framework will find the target image from the local cache, avoiding repeated cloud downloads, achieving effective and obvious optimization results. Based on the principle of locality, the hit rate of this scenario is not negligible. ** Therefore, it is recommended to split the download of images into an image cache with an internal implementation using the framework described above.

  • Caching of network request results

In some download scenarios, network requests need to be cached. The result of the network request is mostly JSON data, which is small and belongs to the lightweight download content. My implementation is the network request cache and picture cache as a part of the cache module, the whole package of a cache module. You can also separate the two and modularize them, depending on your specific business needs.

  • Service modules downloaded in a specific scenario

The modules listed above can basically be widely reusable to the general module efforts. As mentioned above, modularity also includes business modules that focus on specific scenarios. In this article’s business scenario, I have encapsulated a business module. Its job is to persistently maintain a list of downloaded and downloading tasks; According to the download tasks submitted in a fixed format, a structured task structure is resolved. For different types of subtasks, the corresponding generic modules are used to complete the download. At the same time, responsible for coordinating the synchronization relationship among sub-tasks; After all subtasks are downloaded, check the file integrity of the entire structure; After the integrity check is complete, store the download logs in the database. The module is also responsible for downloading task status updates throughout the activity cycle.

Overall structure of modules

Through the analysis of the whole download process, we split out several modules. According to the principle of single responsibility, the responsibility of each module is divided to a more appropriate granularity, which can achieve a certain degree of reuse. For modules whose expansion may be high, a layer of interface class is abstracted according to the principle of dependency inversion to avoid the influence of future modification of the bottom layer on the upper layer of business code. In the modular application, also achieved a clear purpose, reasonable separation.

Below is the schematic diagram of the whole:

Step.3 complete the implementation!

In fact, after writing the second part, the writing purpose of this paper has almost been achieved. You can feel from the title, this paper focuses on the “download” this scene to use some theoretical guidance for a more reasonable code structure design. However, in order to finish what you started — “start with theoretical analysis and end with concrete implementation”, this section discusses the implementation details and provides some “dry goods”. These solutions will have different advantages and disadvantages in different scenarios, just for reference.

  • File manipulation module

This part of my implementation is the use of the system’s NSFileManager file existence judgment and other basic operations. For the destination path of the local storage, the generated rule performs MD5 operations for the URL of the file and then adds the suffix of the file type. In a scenario with high security, all downloaded files are from its own server. In this case, the back end can partially support file correctness verification. For example, the back end returns a specific verification value for each file.

  • Database module

My advice on what fields need to be stored in the database is this: For a specific file, store basic information such as the initial URL, where the file was stored locally, the file size, and the update time. For structured entire download records, the fields required to restore the original download task are stored. To be specific, the submission of the initial download task mostly uses the data type of the business side, such as the model for the presentation of a microblog or an article. After the download task is submitted to the download module, we will convert the initial data type to the specified data format of the download module. In the case of breakpoint continuation and other scenarios, there will be the reverse transformation from the data format used for downloading modules obtained from the database to the data format of the initial business party after the APP restarts. At this time, all necessary state information of the initial task is needed to carry out on-site recovery and continue downloading.

As mentioned above, the download management service module needs to maintain the list of downloaded and downloaded tasks. What is used to distinguish the status? My implementation is to add a field indicating whether the download record is completed or not, so that after the APP restarts, all download records will be obtained from the database. If a record is marked as incomplete, it will be the record that needs to be restored to the initial download task and will be included in the list of downloads.

  • Large volume file download module

There has been a lot of discussion about this section, which will not be covered in this article. It is worth mentioning that this generic component still faces the problem of the underlying implementation change or version upgrade, so the idea of abstracting the interface layer by dependency inversion still applies here.

  • The cache module

Image caching has been discussed in detail above. For network request results in JSON format, iOS generally uses NSDictionary to store them, which supports NSCoding protocol. Therefore, YYCache, EGOCache and other cache frameworks can be used. The interface design of this part is straightforward. It caches the value corresponding to the specified key, returns the corresponding cached value based on the given key, and removes the content corresponding to the given key. The idea of an abstract interface layer applies as usual.

  • Download the management service module

There are many places in the project where you might need to know the status of the currently downloaded module, so using a singleton implementation here is a good choice. At the beginning of the whole download process, it resolves the specific subtask type according to the submitted initial task data and calls the corresponding submodule to complete the download of the subtask. Sub-tasks under the same download task should be asynchronous between them, so the Dispatch group is an intuitive choice. The relationship between all the initial tasks submitted sequentially is synchronous, which can be managed using a queue-like structure. A schematic diagram is given below:

For download, downloaded the distinction between these two kinds of state, here provide a improvement ideas: before an initial task really started to download, just download a new record into the database, set the status field is not completed, and when all the subtasks are accomplished by the integrity check, update the status field to finish.

Finally, a sample pseudocode of the business module is provided to show the entire download process.

// Download the interface list of the management business module.

// Business model
@class OriginModel;

@interface DownloadManager : NSObject
// Obtain the downloaded managed object (singleton)
+ (instancetype)sharedInstance;
// Get the task in the download
- (NSArray<OriginModel *> *)getDownloadingItems;
// Get the downloaded task
- (NSArray<OriginModel*> *)getDownloadedItems;
// Get the downloaded item by id
- (OriginModel *)getDownloadedItemById:(id<NSCopying>)itemId;
// Whether the specified item has been downloaded
- (BOOL)didDownloadedItem:(id<NSCopying>)itemId;
// Batch download
- (void)downloadItems:(NSArray<OriginModel*> *)items;
// Pause the download
- (void)pauseDownloadForItem:(id<NSCopying>)itemId;
// Resume the download
- (void)resumeDownloadForItem:(id<NSCopying>)itemId;
// Cancel the download
- (void)cancelDownloadForItem:(id<NSCopying>)itemId;
@end
Copy the code
// Download the main implementation of management business module

@implementation DownloadManager

- (void)downloadItems:(NSArray<OriginModel *> *)items {
    
// Parse the task structure and push all tasks into the task queue
    MissionStruct *oneStruct = [self analyzeMission];
    for (MissionItem *item in oneStruct) {
        [self.missionList pushItem:item]; }...// If not empty, fetch the task element from the task queue
    if(! [self.missionList isEmpty]) {
        MissionItem *oneMission = [self.missionList pop];
        [selfhandleMission:oneMission]; }} - (void)handleMission:(MissionItem *)mission {
    
    // Call the database module to insert a new record
    [DatabaseManager insertMission:mission];
    dispatch_group_t downloadGroup;
    
    // Download the video
    for (videoMission in mission.videos) {
        dispatch_group_enter(downloadGroup);
        // Call the file management module to obtain the file path corresponding to the URL
        targetPath = [FileManager pathForURL:videoMission.url];
        // Call the big file download module to download the video
        [FileDownloadManager downloadFile:videoMission.url
                               targetPath:targetPath
                                  success:^(){
                                      dispatch_group_leave(downloadGroup);
                                  }];
    }
    
    // Download the audio
    for (audioMission in mission.audios) {
        dispatch_group_enter(downloadGroup);
        // Call the file management module to obtain the file path corresponding to the URL
        targetPath = [FileManager pathForURL:audioMission.url];
        // Call the big file download module to download the audio
        [FileDownloadManager downloadFile:audioMission.url
                               targetPath:targetPath
                                  success:^(){
                                      dispatch_group_leave(downloadGroup);
                                  }];
    }
    
    // Cache images
    for (imageMission in mission.images) {
        dispatch_group_enter(downloadGroup);
        // Call the image cache module to cache the image
        [ImageCacheManager cacheImage:imageMission.url
                                  success:^(){
                                      dispatch_group_leave(downloadGroup);
                                  }];
    }
    
    // Cache network requests
    for (contentMission in mission.contents) {
        dispatch_group_enter(downloadGroup);
        // Call the network request caching module to cache the network request[RequestCacheManager cacheRequest:contentMission.url success:^(){ dispatch_group_leave(downloadGroup); }]; }...// All subtasks completed
    dispatch_group_notify(downloadGroup, dispatch_get_global_queue(0.0), ^ {// Pass the integrity check
        if ([self verifyAllSubMission:mission]) {
            // Call the database module to update the download record
            [DatabaseManager updateMission:mission];
        } else {
            // Failed to pass the integrity check[DatabaseManager removeMission:mission]; }}); }@end
Copy the code

Knowledgeset is a team official account, mainly targeting at the mobile development field, sharing mobile development technology, including iOS, Android, applets, mobile front end, React Native, WEEX, etc. Original articles will be shared every week, and our articles will be published on our official account. Stay tuned for more.