A,

In the payment process of App, add a variety of payment methods. Different payment methods will have different operations. Some will jump to a new WebView, some will call the system browser, some will enter a new form page, and so on.

The payment mode that can be added is also uncertain and dynamically delivered by the background.

As shown below:

According to the above UI, the execution process is explained:

  1. Click different add payment method item

  2. Enter the corresponding process of adding payment method (form page, WebView, popup box, etc.)

  3. The third party callback performs different operations depending on the payment method

  4. Call the background interface to check whether adding is successful

  5. Display different successful or failed UIs based on the interface results

Two, the previous implementation

Hosted by an Activity, all of the above processes are in the Activity. The Activity includes a list display, implementation of multiple payment methods, and a UI.

The pseudocode is as follows:

class AddPaymentListActivity : AppCompatActivity(R.layout.activity_add_card) {

    private val addPaymentViewModel : AddPaymentViewModel = ...

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        addPaymentViewModel.checkPaymentStatusLiveData.observer(this) { isSuccess ->
            // Check whether the database is successfully added based on the background result
            if (isSuccess) {
                addCardSuccess(paymentType)
            } else {
                addCardFailed(paymentType)
            }
        }
    }

    private fun clickItem(paymentType: PaymentType) {
        when (paymentType) {
            PaymentType.ADD_GOOGLE_PAY -> // Execute the add Google Pay process
            PaymentType.ADD_PAY_PEL-> // Execute the add PayPel payment process
            PaymentType.ADD_ALI_PAY-> // Execute the process of adding Alipay payment
            PaymentType.ADD_STRIPE-> // Execute the add Stripe payment process}}override fun onActivityResult(requestCode: Int, resultCode: Int.data: Intent?). {
        super.onActivityResult(requestCode, resultCode, data)
        when (resultCode) {
            PaymentType.ADD_GOOGLE_PAY -> {
                // Get the key based on the result of the third-party callback
                // Invoke the background Api interface based on the key to check whether the IP address is added successfully
            }
            PaymentType.ADD_PAY_PEL -> / / same as above
            // ...}}private fun addCardSuccess(paymentType: PaymentType){
        when (paymentType) {
            PaymentType.ADD_GOOGLE_PAY -> // Add the corresponding payment method successfully, display the successful UI, and proceed to the next step
            PaymentType.ADD_PAY_PEL-> / / same as above
            // ...}}private fun addCardFailed(paymentType: PaymentType){
        when (paymentType) {
            PaymentType.ADD_GOOGLE_PAY -> // Add the corresponding payment method failed and display the failed UI
            PaymentType.ADD_PAY_PEL-> / / same as above
            // ...}}enum class PaymentType {
        ADD_GOOGLE_PAY, ADD_PAY_PEL, ADD_ALI_PAY, ADD_STRIPE
    }

}
Copy the code

While it may seem logical enough based on paymentType, the complexity is much greater.

  • Different payment methods lead to different pages

  • The callback fetch of the results also varies widely, and not all of them are in onActivityResult

  • Success and failure can not be dealt with uniformly in fact, which contains a lot of if… else… judge

  • If the payment method is delivered dynamically in the background, there is more logic to process

And then there’s the biggest problem: scalability.

When a new payment method, such as wechat pay, is introduced, the code changes are very large, basically the entire Activity code changes. The above approach has zero scalability and is simply a matter of rubbing the code together.

Third, the optimized code

The easiest way to achieve high cohesion and low coupling is to apply a common design pattern, and recall that the strategy pattern + simple factory pattern is a good fit for this scenario.

Take a look at the optimized code:

class AddPlatformActivity : BaseActivity() {
    
    private var addPayPlatform: IAddPayPlatform? = null

    private fun addPlatform(payPlatform: String) {
        // Turn the payment platform string returned in the background into an enumeration class
        valplatform: PayPlatform = getPayPlatform(payPlatform) ? :return
        addPayPlatform = AddPayPlatformFactory.getCurrentPlatform(this, platform) addPayPlatform? .getLoadingLiveData()? .observe(this@AddPlatformActivity) { showLoading ->
                if (showLoading) startLoading() elsestopLoading() } addPayPlatform? .addPayPlatform(AddCardParameter(platform)) }override fun onActivityResult(requestCode: Int, resultCode: Int.data: Intent?). {
        super.onActivityResult(requestCode, resultCode, data)
        // Forward the onActivityResult callback to the policy class to listen onaddPayPlatform? .thirdAuthenticationCallback(requestCode, resultCode,data)}}Copy the code

4. Strategic mode

Quote rookie tutorial on a simple introduction:

Intent: Define a set of algorithms, encapsulate them one by one, and make them interchangeable.

In the case of similar algorithms, if… Else is complex and difficult to maintain.

When to use: A system has many, many classes that are distinguished only by their immediate behavior.

How to solve it: Encapsulate these algorithms into classes and replace them arbitrarily.

Key code: implement the same interface.

Advantages: 1. The algorithm can be switched freely. 2. Avoid using multiple conditional judgments. 3. Good expansibility.

Disadvantages: 1. Policy classes will increase. 2. All policy classes need to be exposed.

Usage scenarios: 1. If there are many classes in a system that are distinguished only by their behavior, then using the policy pattern can dynamically let an object choose one behavior among many behaviors. A system needs to dynamically choose one of several algorithms. 3. If an object has many behaviors, these behaviors can be implemented using multiple conditional selection statements without appropriate patterns.

5. Goals to be achieved

5.1 Decouple host activities

The current host Activity code is too heavy, including multiple payment implementations, list UI displays, network requests, and so on.

The goal now is to break up the code in the Activity so that the host Activity is small and light.

If the product says to add a payment method, it can be done easily with very few code changes.

5.2 Extraction into independent modules

Because there are likely to be multiple projects in the organization, the layering of the payment module should be at the reusable level, and it is likely that it will be encapsulated into a separate Mouble for use by different projects.

Now the code is all in the Activity, and if you extract a Mouble later, you’ll need to redo the entire requirement.

5.3 Component Black Box

The term “component black box” is one of my own definitions. General meaning:

Make a View or a class highly encapsulated, exposing as few public methods as possible to external use.

A business side can use a business component directly in a black box, regardless of the logic.

The business side only needs a simple operation, such as clicking a button to invoke a method, and then the logic is implemented inside the component, which handles all operations of external events, such as Loading, requesting the network, success or failure.

The business side does not need to know the internal operations of the component, thus isolating the host from the component.

Of course, this kind of processing also needs to be considered in different scenarios. One of the key points is whether the component is business-oriented or function-oriented, that is, whether the business logic should be integrated into the component. Only after thinking clearly about this problem can we develop a business component. Excerpt from Xu ‘Yi’ Sheng blog

Since adding payment method is a business-oriented function, my design idea is as follows:

External Activity click add corresponding mode of payment, payment will be enumerated by passing payment type and related parameters, and then the strategy of different class add components to perform their own logic, and then through a layer of callback will third-party payment callback switching over from the Activity and strategy of each kind of internal processing callback operation, Specific policy classes themselves maintain successful or failed UIs.

Six, concrete implementation

6.1 Defining a Top-level Policy Interface

interface IAddPayPlatform {

    fun addPayPlatform(param: AddCardParameter)

    fun thirdAuthenticationCallback(requestCode: Int? , resultCode:Int? .data: Intent?).

    fun addCardFailed(message: String?).

    fun addCardSuccess(a)
}
Copy the code

6.2 General Payment Parameter classes

open class AddCardParameter(val platform: PayPlatform)

class AddStripeParameter(val card: Card, val setPrimaryCard: Boolean, platform: PayPlatform)
    : AddCardParameter(platform = PayPlatform.Stripe)
Copy the code

Because there are many additional payment methods, different payment methods have different parameters.

So first create a general card parameter base class AddCardParameter, different payment methods to achieve different specific parameters. So the policy interface can just write a method to add the card addPayPlatform(param: AddCardParameter).

6.3 Handling Loading

Since I want to achieve the effect of a black box component, the loading of all the added cards is also encapsulated in each policy implementation class.

Loading appears and disappears In several common ways:

  • Pass a reference to BaseActivity, because my loading method is in BaseActivity, which is simple but coupled to BaseActivity.

  • Using a message EventBus, such as EventBus, is highly decouple, but message events are difficult to control and require additional dependency libraries

  • With LiveData, add a method to the common interface of the policy to return Loading LiveData and let the host Activity implement it itself

    interface IAddPayPlatform {
        // ...
        fun getLoadingLiveData(a): LiveData<Boolean>?
    }
    Copy the code

To extract 6.4BaseAddPayStrategy

Since there will be a lot of the same code for each strategy to add cards, HERE I extract a BaseAddPayStrategy to hold the template code.

In order to achieve the effect of the black box component, the host Activity does not need to worry about whether there is a network request in the process of adding payment method, so the network request is also packaged in each policy implementation class.

abstract class BaseAddPayStrategy(val activity: AppCompatActivity, val platform: PayPlatform) : IAddPayPlatform {

  private val loadingLiveData = SingleLiveData<Boolean> ()protected val startActivityIntentLiveData = SingleLiveData<Intent>()

  override fun getLoadingLiveData(a): LiveData<Boolean> = loadingLiveData

  protected fun startLoading(a) = loadingLiveData.setValue(true)

  protected fun stopLoading(a) = loadingLiveData.setValue(false)

  private fun reloadWallet(a) {
      startLoading()
      // Refresh wallet data after adding cards
  }

  override fun addCardSuccess(a) {
      reloadWallet()
  }

  override fun addCardFailed(message: String?). {
      stopLoading()
      if (isEWalletPlatform(platform)) showAddEWalletFailedView() else showAddPhysicalCardFailedView(message)
  }

   /** * Failed to add physical card to display UI */
  private fun showAddPhysicalCardFailedView(message: String?). {
       showSaveErrorDialog(activity, message)
  }
  
   /** * Add physical card successfully display UI */
  private fun showAddPhysicalCardSuccessView(a) {
      showCreditCardAdded(activity) {
          activity.setResult(Activity.RESULT_OK)
          activity.finish()
      }
  }

  private fun showAddEWalletSucceedView(a) {
      // Add the e-wallet after successful execution
      activity.setResult(Activity.RESULT_OK)
      activity.finish()
  }

  private fun showAddEWalletFailedView(a) {
      // Failed to add electronic wallet after execution
  }
  
  // -- default empty implementation, some payment methods do not need these methods --
  override fun thirdAuthenticationCallback(requestCode: Int? , resultCode:Int? .data: Intent?). = Unit

  override fun getStartActivityIntent(a): LiveData<Intent> = startActivityIntentLiveData
}
Copy the code

6.5 Implementation of specific policy classes

The AppCompatActivity reference is passed to get AddPaymentViewModel, an instance of the ViewModel that adds the card, and then calls a network request to see if the card has been added successfully.

class AddXXXPayStrategy(activity: AppCompatActivity) : BaseAddPayStrategy(activity, PayPlatform.XXX) {

  protected val addPaymentViewModel: AddPaymentViewModel by lazy {
      ViewModelProvider(activity).get(AddPaymentViewModel::class.java)
  }

  init {
      addPaymentViewModel.eWalletAuthorizeLiveData.observeState(activity) {

          onSuccess { addCardSuccess()}

          onError { addCardFailed(it.detailed) }
      }
  }

  override fun thirdAuthenticationCallback(requestCode: Int? , resultCode:Int? , result:Intent?). {
      valuri: Uri = result? .data? :return
      if (uri.host == "www.xxx.com") {
          uri.getQueryParameter("transactionId")? .let { addPaymentViewModel.confirmEWalletAuthorize(platform.name, it) } } }override fun addPayPlatform(param: AddCardParameter) {
      startLoading()
      addPaymentViewModel.addXXXCard(param)
  }
}
Copy the code

Seven, simple factory optimization

Because I don’t want to refer to every concrete policy class in my Activity, I just want to refer to the abstract interface class IAddPayPlatform, which is optimized with a simple factory.

object AddPayPlatformFactory {

    fun setCurrentPlatform(activity: AppCompatActivity, payPlatform: PayPlatform): IAddPayPlatform? {
        return when (payPlatform) {
            PayPlatform.STRIPE -> AddStripeStrategy(activity)
            PayPlatform.PAYPAL -> AddPayPalStrategy(activity)
            PayPlatform.LINEPAY -> AddLinePayStrategy(activity)
            PayPlatform.GOOGLEPAY -> AddGooglePayStrategy(activity)
            PayPlatform.RAPYD -> AddRapydStrategy(activity)
            else -> null}}}Copy the code

Add another method of payment

If you add another payment method, you can leave the code in the host Activity unchanged. You just need to create a new policy class that implements the top-level policy interface.

This way, whether deleting or adding a payment method, it is easy to maintain.

The benefits of the strategic model are clear.

After the