above
I’m sure you’re already familiar with Okhttp’s chain of responsibility mode, but today I’m going to introduce another interesting gameplay.
Let me describe the scenario to you, so that you don’t say THAT I over-designed the code. I’ll take Google Pay as an example. Google Pay encapsulates most invocation scenarios with asynC callback.
- Set up a link with Google Pay. If the link fails, the connection ends after three retries.
- Query whether there is no verification of the order, if there is processing, if there is no downward execution.
- Query THE Sku data. If the query fails, the operation ends. After the query succeeds, the logical execution continues.
- The payment is made based on the Sku data, then the payment result is asynchronously retrieved, and the return value is used to determine whether to proceed down.
- Call the Api to notify the backend of the result of the transaction, retry if it fails, and terminate if it still fails.
- Check whether the order has goods in stock, if so, call the write-off Api, if not, continue down.
- Determine whether the order is a subscription or not. If it is a subscription, the confirmation API needs to be called and the entire payment process is finally terminated according to the asynchronous result.
This time you have to say, isn’t it just a little disgusting, I top I also can ah. There are several ways to deal with it.
-
Asynchronous big guy, asynchronous set asynchronous set asynchronous, a shuttle ha is dry, amazing is 7 layers nested?
-
I can convert all asynchrony to synchronization as long as the child thread waits for the value while true.
-
RxJava or coroutine master, this is not a simple chain operation, each chain each function is only responsible for their related operations, but asynchronous conversion to RxJava may be a little disgusting, but not a big problem.
Raises the question, how does RxJava implement sequential chained execution? Do you feel a bit like OkHttp’s chain of responsibility? Massa card!
An example to understand the Rxjava event flow conversion principle, interested students can read this article analysis.
The body of the
Our solution this time is to transform the code with the execution order in a chain of responsibility way. First of all, I would like to thank the author of WmrOuter, a Microsoft boss, whose code gave me a good idea. It was from WmrOuter that I basically mastered this interesting design pattern.
For the Demo, you can refer to the routing project I wrote earlier, which is also based on this asynchronous responsibility chain.
Route the Demo project link
Introduction to responsibility chain
Chain of responsibility mode is a design mode. In the chain of responsibility pattern, many objects are connected in a chain by each object’s reference to its next parent. Requests pass along the chain until one of the objects on the chain decides to process the request. The client making the request does not know which object on the chain ultimately handles the request, allowing the system to dynamically reorganize and assign responsibilities without affecting the client. (Refer to Baidu Encyclopedia)
I stole two web maps to introduce okHTTP’s responsibility chain, which is relatively graphic.
The overall interceptor flow is shown below.
Simply put, a chain of responsibility is a process that goes down in a particular order. Most of you know about okHTTP’s chain of responsibilities. So let’s start with this, and compare the two parts.
fun interface Interceptor {
@Throws(IOException::class)
fun intercept(chain: Chain): Response
interface Chain {
fun request(a): Request
@Throws(IOException::class)
fun proceed(request: Request): Response
}
}
Copy the code
The responsibility Chain for OkHttp starts with an Interceptor and a Chain interface. The Chain’s proceed method then executes the code in the next interceptor. Since the PROCEED method returns a value, you must ensure that the next interceptor has a return value. This means that the PROCEED method must be a synchronous method (one that returns a value on the spot).
Responsibility chain with no return value
This time you said, so since the next responsibility chain must return, then how to implement asynchronous responsibility chain ah? Let’s use the previous routing project to introduce this part, after all, Google Pay is the company code (I’m fired, you keep me?? , not particularly convenient open source.
interface Interceptor {
@Throws(RouteNotFoundException::class)
fun intercept(chain: Chain)
interface Chain {
val url: KRequest
val context: Context
@Throws(RouteNotFoundException::class)
fun proceed(a)}}Copy the code
In comparison, the biggest difference between the two responsibility chains is whether the Intercept method has a specific return value. Proceed, on the other hand, merely tells the current interceptor whether it has been executed downwards. In short, this is where the biggest difference between the two chains of responsibility lies.
class LogInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain) {
Log.i("LogInterceptor"."LogInterceptor")
chain.proceed()
}
}
Copy the code
Under normal circumstances, our interceptor logic will be like the above code, but due to the current method is not return a value, that is to say, we have an asynchronous task in possession of the chain of instances, so when we have the results of the asynchronous callback, continue to carry out our proceed method is under a chain call is ok. Let’s look at the last asynchronous implementation.
class DelayInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain) {
GlobalScope.launch {
// Create a Google link
delay(5000)
withContext(Dispatchers.Main) {
chain.proceed()
}
}
}
}
Copy the code
I use coroutine delay in this interceptor, delay means using a Google link mentioned above, then we schedule to the main thread and call the next one in the responsibility chain. Since fun has no return value and we hold a reference to chain, we can call the next responsibility chain in any asynchrony, thus completing an asynchrony chain encapsulation.
To achieve Chain
Using my route’s chain of responsibility as an example, the PROCEED method is pretty straightforward: call the next interceptor with index+1, execute its code, and return if the list is empty.
class RealInterceptorChain internal constructor(private val interceptors: List<Interceptor>, override val url: KRequest,
override valhostParams: Map<String, HostParams>? .private val index: Int.override
val context: Context) : Interceptor.Chain {
@Throws(RouteNotFoundException::class)
override fun proceed(a) {
proceed(url)
}
@Throws(RouteNotFoundException::class)
fun proceed(request: KRequest) {
if (index >= interceptors.size) {
return
}
val next = RealInterceptorChain(interceptors, request, hostParams,
index + 1, context)
val interceptor = interceptors[index]
interceptor.intercept(next)
}
}
Copy the code
The other is the need for a place to put the entire chain Call interceptor in a list, using the Call class.
class RealCall(private val hostMap: Map<String, HostParams>, private val config: RouteConfiguration) {
private val cachedRoutes: HashMap<String, RouterParams> = hashMapOf()
@Throws(RouteNotFoundException::class)
fun open(request: KRequest, context: Context) {
getParamsWithInterceptorChain(request, context)
}
@Throws(RouteNotFoundException::class)
private fun getParamsWithInterceptorChain(request: KRequest, context: Context) {
val interceptors: MutableList<Interceptor> = ArrayList()
if (config.lazyInitializer) {
interceptors.add(LazyInitInterceptor())
}
interceptors.addAll(config.interceptors)
interceptors.add(CacheInterceptor(cachedRoutes))
interceptors.add(RouterInterceptor(cachedRoutes))
val chain: Interceptor.Chain = RealInterceptorChain(interceptors, request, hostMap, 0, context)
chain.proceed()
}
}
Copy the code
RealCall with such a List and the exposed open method, we can ensure that the chain of responsibilities is executed down the chain. And since the Chain described above notifies the next Chain to be executed using the proceed method. The whole chain will be connected.
In Google Pay, because they’re not really an interceptor, they’re a processor, so I call this part Handler. Each Handler handles the part of the logic it currently needs and passes it on to the next Handler when it’s done.
This part can be introduced to you in pseudocode.
Handler{async{if(async{if(async{if(async){next Handler}else{terminate}) }} Other handlers...Copy the code
I’ve written two pseudocodes that represent the Handler logic for this part of the process, and the rest of the process is similar. The advantage of this is that each Handler is responsible for its own logic, so that the subsequent maintenance process will be relatively simple, and other processors can be inserted in the middle, or adjust the sequence to facilitate code reuse, etc.
It’s also possible to avoid callback hell, where the code only has successful callbacks, and if there are exceptions, it’s a mess. Worst of all, in case I get the hell out of here, the people behind me can easily see the implementation just by looking at the logic in the responsibility chain.
That could go wrong
So what’s the downside of this?
Because everything is asynchronous and asynchronous means holding references, there is a risk of memory leaks without a binding to LifeCycle. For example, a page is destroyed and the result is called back, and then a memory leak occurs because the callback is held.
At this time, we provide another termination instruction to help us optimize the leakage situation. When the current chain of responsibility terminates, all Handler references are cleared.
interface Chain {
val context: Context
val lifecycleOwner: LifecycleOwner?
fun proceed(a)
fun onComplete(a)
}
Copy the code
class RealCall:Handler.Chain.LifecycleObserver{
override fun onComplete(a) {
// Clear your list of responsibilities
handlers.clear()
}
}
Copy the code
And because it’s all asynchronous, it makes the code harder to understand, but I personally think it’s better overall than asynchronous nesting.
Since the method does not return a value, passing parameters is not particularly friendly, and the result can only be sent asynchronously to the user. Although it avoids a lot of callback nesting, it is still necessary to give the user a callback to give the final processing result.
When applied to scenarios like routing, the flexibility I feel is far better than interceptors with return values.
conclusion
If I can write this part of the code using coroutines, I feel I can do it without any pressure. However, because it is an SDK side, it is necessary to consider whether the user has a class library and is willing to minimize the dependency granularity when outputting the code. Therefore, this design mode can only be adopted to reduce the dependency of the code and make the maintainability of the code higher.
This part of the code I did not think of at the beginning, I was first hand to write a luk again, and then thought, if after a while let me to take over this part of the code, I estimate will want to die. So I redesigned the realization of this part of the code, more thinking live learning live, do not need to copy.