返回首页

Swift Concurrency Series 08|Asynchronous code organization in real projects

What is really difficult is whether the entire asynchronous link can remain clear for a long time.

When there are only one or two asynchronous requests in the project, a lot of the code doesn’t look too bad. A real watershed usually occurs when the following situations occur simultaneously:

  • The same page has first loading, pull-down refresh, retry, and filter switching
  • Local cache and remote requests need to be involved together
  • When leaving the page, cancel some tasks and keep others
  • Multiple modules share certain concurrent resources

At this time, you will find that the biggest difficulty of asynchronous code is already:

How to organize the entire asynchronous link so that it does not become more scattered and difficult to change as it is written.

In this article, I don’t want to talk about a single API, but I want to talk about the organizational principles that I value more in real projects.

1. What I share first is responsibility.

When asynchronous code is messed up, the first reaction is often to “dismantle the function”. Splitting functions is certainly useful, but if the responsibilities are already mixed together, splitting them usually just involves splitting the mess into several small files.

I’m more concerned with separating the responsibilities first. Usually divided into at least three layers:

1. Page layer

The page layer is responsible for:

  • Trigger user intent
  • Display page status
  • Respond to interactive changes

It knows “what should be loaded now”, but it should not be responsible for “how to orchestrate the entire asynchronous process”.

2. State layer / ViewModel

The state layer is responsible for:

  • Translate user intent into tasks
  • Decide whether tasks should be parallelized, replaced or canceled
  • Manage page semantics such as loading, loaded, failed, etc.
  • Determine which results are still eligible to write back the page

It is the true end point of asynchronous processes.

3. Service layer

The service layer is responsible for:

  • Adjust interface
  • read cache
  • Combine multiple data sources
  • Provide domain capabilities

It shouldn’t know what the page looks like, nor should it sneak in UI state semantics.

A lot of asynchronous code is messed up because once these three layers are mixed, any change in requirements will affect the UI, process, status and data source at the same time.

2. The most important principle of the page layer: know less about asynchronous details

I don’t like having the page do too many asynchronous steps on its own. Because once the page knows too much, it will start to take on these things:

  • Request sequence control
  • Mistakes
  • Filter results
  • Cancellation policy
  • loading subdivision semantics

When changing a requirement like this, the UI and process are often affected together.

So I prefer to have the page express only these things:

  • “I need to refresh now”
  • “The user clicked to try again”
  • “Filter conditions changed”

As for what’s behind it:

  • Read the cache first or open the interface first?
  • Do you want to stop the old tasks?
  • Whether the result has expired
  • Should the first load and refresh share the same link?

Put as much as possible into the status layer closure.

3. The page status should be explicit, don’t rely on a bunch of scattered bits and pieces to spell semantics.

Many asynchronous pages will look like this in the later stage:

  • isLoading
  • isRefreshing
  • hasError
  • showRetry
  • isEmpty
  • items

These values are reasonable when viewed individually, but are prone to self-contradiction when combined:

  • Loading with old errors
  • Both display the empty state and retain the old list
  • Refreshing, but the first load logo is still there

So I value “page semantic state” more than “many small states that can be freely combined”.

Because what’s really important about an asynchronous page is whether it can tell clearly what stage the page is currently in.

4. Task boundaries must be clear, otherwise everything will just be “run first”

Whether the asynchronous structure is stable or not depends on whether it can answer these questions:

  • Who owns this mission? -Does it continue when the page is left?
  • When a new task comes, will the old task become invalid immediately?
  • Is this a stand-alone task or part of a longer process?

If these questions cannot be answered clearly, the following code will definitely enter a state:

  • Everywhere seems to be self-explanatory
  • But no one can put together a complete retelling of how this link works.

If a piece of asynchronous code is difficult to repeat in natural language, it is usually difficult to maintain it stably later.

5. I will try my best to build “result validity” into the process

Many pages are messed up. On the surface, it looks like the result has failed, but it is actually closer:

  • Old results returned successfully
  • but it no longer corresponds to the current page context

This type of problem is especially likely to occur when:

  • Search
  • Filter switch
  • Pagination
  • Quickly access the exit page

If the effectiveness of the results is not designed into the design, the page will sooner or later develop these strange phenomena:

  • Content rollback
  • loading ends inexplicably
  • Error prompts overwrite the current success status

So I care about:

  • Who will judge whether the result has expired?
  • Whether each call point must be judged by yourself
  • Still close the interface uniformly at the status layer

My preferences are very clear: **Try to be consistent in closing, and don’t let each view branch judge by itself “whether the result this time still counts”. **

6. Don’t sneak page semantics into the service layer

A lot of code is messed up. On the surface, it seems that the service layer will not make requests. In fact, it is closer to the service layer and begins to slowly incorporate page concepts.

For example, naming or logic like this begins to appear in the service layer:

  • “Special request for first load of home page”
  • “Details page is empty and full of logic”
  • “Wrong copywriting for this page”

Once these are mixed in, subsequent structures will become increasingly difficult to reuse. Because the service layer begins to know what the page looks like, and the page layer begins to know what implementation details the service layer has, the boundaries are quickly blurred.

The service layer is more suitable to focus on:

-What data did you get?

  • How to combine data sources
  • What is the caching strategy?

Rather than “how should this page behave?”

7. I care about one principle: the asynchronous process must be clearly recited

This is a commonly used criterion when I do code reviews myself.

If you pick up a piece of asynchronous code, it is already difficult to repeat it in natural language:

  • What is the entrance?
  • What is the main process?
  • Which tasks will be cancelled?
  • Which results will be discarded
  • Which states are changed by which layer?

Even if this code can run today, it will most likely become more and more difficult to evolve in the future.

The real fear of asynchronous code is that it is unclear. Once you can’t tell clearly, any subsequent iterations are just a matter of luck.

8. An organizational idea that is closer to implementation

If I wanted to sum it up in a more practical sentence, I would organize it like this:

  • Page layer: expressing intent
  • State layer: closing tasks and state semantics
  • Service layer: providing capabilities
  • Shared resource layer: Use Actor or other isolation means to manage shared state when necessary

Then clarify these relationships in the state layer:

  • Which tasks are mutually exclusive
  • Which tasks are parallel
  • Which results can be discarded
  • Which states must be updated by the main actor

A common situation is that this “seems like an extra layer”, but in truly complex projects, these layers are to prevent asynchronous logic from directly spreading to each page event.

9. Conclusion: The core of asynchronous organization in real projects is the boundary

The most important thing in a real project is never whether it will be Task, async let or Actor. What really determines whether the code can evolve in the long term is whether these boundaries are clear:

  • Boundary between page layer and process layer
  • Boundary between state layer and service layer
  • Boundary between shared state and normal state
  • The boundary between current valid results and expired results

So if I had to sum up this article in one sentence, I would say:

The key to asynchronous code organization in real projects is not “how to write an asynchronous API”, but “whether the four boundaries of tasks, status, results, and responsibilities are clearly drawn.”

Only when these boundaries are clear can asynchronous code truly change from “can run” to “can be maintained for a long time.”

FAQ

读完之后,下一步看什么

如果还想继续了解,可以从下面几个方向接着读。

Related

继续阅读

这里整理了同分类、同标签或同类问题的文章。