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.
1. Define requirements
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.
Two, give 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:
Three, complete the concrete 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.
// Business model@class OriginModel; @interface DownloadManager: NSObject // Obtain the downloaded managed object (singleton) + (instanceType)sharedInstance; - (NSArray<OriginModel *> *)downloadingItems; // Get the downloaded task - (NSArray<OriginModel*> *)downloadedItems; - (OriginModel *)downloadedItemForId:(id<NSCopying>)itemId; - (BOOL)didDownloadedItem:(id<NSCopying>)itemId; - (void)downloadItems:(NSArray<OriginModel*> *)items; // pause downloading - (void)pauseDownloadForItem:(id<NSCopying>)itemId; - (void)resumeDownloadForItem:(id<NSCopying>)itemId; CancelDownloadForItem :(id<NSCopying>)itemId; @endCopy the code
@implementation DownloadManager - (void)downloadItems:(NSArray<OriginModel *> *)items { MissionStruct *oneStruct = [self analyzeMission]; for (MissionItem *item in oneStruct) { [self.missionList pushItem:item]; }... // If not empty, fetch the task element if (! [self.missionList isEmpty]) { MissionItem *oneMission = [self.missionList pop]; [self handleMission:oneMission]; }} - (void)handleMission:(MissionItem *)mission {// Call the database module and insert a new record [DatabaseManager insertMission:mission]; dispatch_group_t downloadGroup; // Download video for (videommission) {dispatch_group_enter(downloadGroup); / / call the file management module, access to the url corresponding to the file path targetPath = [FileManager pathForURL: videoMission. Url]; // Call the big file download module, Download the video [FileDownloadManager downloadFile: videoMission url targetPath: targetPath success: ^ () { dispatch_group_leave(downloadGroup); }]; } // Download audio for (audiommission. Audios) {dispatch_group_enter(downloadGroup); / / call the file management module, access to the url corresponding to the file path targetPath = [FileManager pathForURL: audioMission. Url]; // Call the big file download module, Download the audio [FileDownloadManager downloadFile: audioMission url targetPath: targetPath success: ^ () { dispatch_group_leave(downloadGroup); }]; } for (imageMission in mission.images) {dispatch_group_enter(downloadGroup); / / call the image cache module, cache the image [ImageCacheManager cacheImage: imageMission. Url success: ^ () {dispatch_group_leave (downloadGroup);}]. } for (contentMission in mission.contents) {dispatch_group_enter(downloadGroup); // Call the network request cache module, Cache the network request [RequestCacheManager cacheRequest: contentMission url success: ^ () {dispatch_group_leave (downloadGroup);}]. }... Dispatch_group_notify (downloadGroup, dispatch_get_global_queue(0, 0), ^ {/ / through the integrity check if ([self verifyAllSubMission: mission]) {/ / call the database module, update the download record [DatabaseManager updateMission: mission]; } else {DatabaseManager removeMission:mission]; }}); } @endCopy the code