MVP 中试用面向接口编程优化代码

问题出现的背景

当我们需要规定一类抽象行为的时候,是使用基类继承的方式还是组合的方式呢?
在使用 MVP 架构进行编码时,我碰到了下面的这个情况用以探讨并记录。

循序渐进,抽象出行为至协议的过程

假设一个 app 每次启动后都需要登录。

它最开始的登录方式只支持:密码登录

MVP 架构中, P 用于执行业务逻辑,并驱动 V 层作相应显示。

就上面的例子而言,每次登录包含的业务逻辑,P 层需要做下面几件事:

  • 检查更新
  • 检查当前网络的安全性
  • 登录后保存当前用户名

V 层提供了展示的接口

  • 展示网络无效的弹窗
  • 展示检查更新的结果


业务拓展,增加需求

他的登录方式拓展到了有三种:

三种登录方式.png
  • 密码登录
  • 生物识别登录(指纹/面容 ID)
  • 九宫格绘制图形解锁登录

三种登录方式都需要执行密码登录中的业务逻辑。
我们在设计的过程中需要把这个业务逻辑抽象出来。

传统的做法自然而然会想到使用基类,但是问题来了,由于我们使用了 MVP 的架构模式,业务逻辑主要分布在 P 层。而业务逻辑的过程中,势必需要驱动 controller(V 层)的变化,如果使用基类,那么在基类中又会耦合 V 的代码。

我们其实需要做的事是,希望有一个类,提供业务逻辑相关的行为供 P 调用,只管业务逻辑的事。至于页面的变化和驱动,仍交由各个 具体的 P 类自己完成。

那么你肯定会问,如果这样的话,让 P 的基类具备这些功能供子类调用就好了啊?
可是如果是这样,你觉得 这个类和 P 还是父子关系吗?

最好的方法,就是需要引入协议拓展 —— Swift 中的 protocol extension

协议拓展

//
//  LoginPresenterProtocol.swift
//  OSSApp
//
//  Created by Chen Defore on 2019/7/30.
//  Copyright © 2019 IGG. All rights reserved.
//

import UIKit

protocol LoginPresenterProtocol: class {
    func checkUpdate(onSuccess: @escaping CallbackWithNoParams)
    func checkNetworkPermission(onSuceess: @escaping CallbackWithNoParams, onFailed: @escaping CallbackWithNoParams)
    func handleLoginSuccess(with successInfo: LoginSuccessModel, onComplete: @escaping CallbackWithNoParams)
}

extension LoginPresenterProtocol {
    // MARK:【检查更新】
    func checkUpdate(onSuccess: @escaping CallbackWithNoParams) {
        // 使用静默检查,不添加 Loading 动画
        AppUpdateManager.shared.checkIfNeedUpdateAppSilently { [weak self] (checkResult) in
            onSuccess()
            self?.handleCheckUpdateResult(checkResult)
        }
    }
    
    // MARK:【处理检查更新的弹窗】
    private func handleCheckUpdateResult(_ result: CheckUpdateAppResult) {
        let model = GeneralAlertDialogViewModel(content: result.updateLog, actionButtonTitle: myLocal("update_now"))
        model.isContentScrollable = true
        model.contentViewMinHeight = 230
        
        var dialog: GeneralAlertDialogView?
        switch result.updateAppType {
        case .forcedUpdate:
            model.cancelButtonTitle = myLocal("exit_app")
            model.isTapBackgroundCloseEnabled = false
            dialog = GeneralAlertDialogView(model: model, onClose: {
                exit(0) // 左边按键是 退出应用
            }, onPressActionButton: {
                guard let url = result.updateURL else { return }
                UIApplication.shared.open(url, options: [:], completionHandler: { (_) in
                    exit(0)
                })
            })
        case .unforcedUpdate:
            model.cancelButtonTitle = myLocal("update_later")
            dialog = GeneralAlertDialogView(model: model, onClose: nil, onPressActionButton: {
                guard let url = result.updateURL else { return }
                UIApplication.shared.open(url, options: [:], completionHandler: { (_) in
                    exit(0)
                })
            })
        case .noUpdate:
            break
        }
        
        guard let _ = dialog else { return }
        DispatchQueue.main.async {
            UIApplication.shared.keyWindow?.addSubview(dialog!)
        }
    }
    
    // MARK:【检查网络权限】
    func checkNetworkPermission(onSuceess: @escaping CallbackWithNoParams,
                                onFailed: @escaping CallbackWithNoParams) {
        
        print("检查权限")
        IGGTransaction.shared.checkNetworkPermission { (result) in
            switch result {
            case .success(_):
                onSuceess()
            case .failure(let failure):
                if let code = failure.errorCode,
                    let errorType = IGGErrorType(rawValue: code),
                    errorType == .withoutPermission {
                    
                    // 提示用户无权限
                    onFailed()
                } else {
                    onFailed()// TODO 表示的是请求失败的场景 网络异常
                }
            }
        }
    }
    
    // MARK:【处理登录成功】
    func handleLoginSuccess(with successInfo: LoginSuccessModel, onComplete: @escaping CallbackWithNoParams) {
        // 与服务端约定好的接口
        UserInformation.shared.sessionID = successInfo.sessionID
        UserInformation.shared.realName = successInfo.realName
        UserInformation.shared.avatarURL = successInfo.avatarURL
        LanguageHelper.syncLanguage(from: successInfo.language)
        
        cleanCacheIfUserIDChanged(id: successInfo.userID) {
            UserInformation.shared.userID = successInfo.userID
            onComplete()
        }
    }
    
    // 如果用户 ID 发生了变化,为了避免新用户还能访问上一个用户的信息,需要将所有的缓存数据清空
    private func cleanCacheIfUserIDChanged(id: String?, onSuccess: @escaping CallbackWithNoParams) {
        guard let currentUserID = UserInformation.shared.userID,
            let newUserID = id, currentUserID != newUserID else {
                
                onSuccess()
                return
        }
        
        print("User ID 改变了!")
        // 用户变更,则生物识别失效
        LocalAuthenticationLoginCredential.shared.removeCredential()
        IGGCacheCenter.shared.cleanAllCache {
            // 清空缓存时,必须要同时清空单例中的数据,否则其中保存的数据在同步时仍然会再写入缓存
            HistoricalRecordManager.shared.removeAllRecords()
            OpenedFunctionRecordManager.shared.removeAllRecords()
            
            onSuccess()
        }
    }
}

具体的 P,生物识别登录

class LocalAuthenticationLoginPresenter: BasePresenter, LoginPresenterProtocol {
    private weak var controller: LocalAuthenticationLoginViewController?
    private var enterForegroundNotificationToken: NotificationToken?
    private var hasCheckUpdate: Bool = false
    
    init(controller: LocalAuthenticationLoginViewController) {
        self.controller = controller
        super.init()
        enterForegroundNotificationToken = NotificationCenter.default.igg.observe(name: UIApplication.willEnterForegroundNotification, object: nil, queue: nil) { [weak self] (_) in
            
            self?.checkNetworkPermission() // 每次进入前台,都检测权限
        }
    }
    
    // MARK: - 【登录操作】
    func dologin() {
        // 授权
        LocalAuthenticationHelper.authenticate { [weak self] (isSuccess, error) in
            if isSuccess {
                let currentTimestamp = Int(ServiceSignatureHelper.generateTimestamp()) ?? 0
                let expiredTime = LocalAuthenticationLoginCredential.shared.accessTokenExpiredTime
                
                if currentTimestamp > expiredTime { // 超过了保质期
                    self?.refreshToken()
                } else {
                    self?.requestSessionID()
                }
            } else {
                let errorMessage = LocalAuthenticationHelper.checkUnavailableErrorInDetail(evaluateError: error!)
                self?.controller?.showAlert(with: errorMessage)
            }
        }
    }
    
    private func requestSessionID() {
        let accessToken = LocalAuthenticationLoginCredential.shared.accessToken ?? ""
        
        IGGToast.shared.showLoading()
        IGGTransaction.shared.localAuthenticationLogin(accessToken: accessToken) { [weak self] (result) in
            IGGToast.shared.dismiss()
            switch result {
            case .success(let loginSuccessModel):
                self?.handleLoginSuccess(with: loginSuccessModel)
            case .failure(let error):
                if let errorCode = error.errorCode,
                    let errorType = IGGErrorType(rawValue: errorCode),
                    errorType == .expiredToken {
                    
                    // 如果上面判断过期的步骤不准确,服务端会返回一个 token 过期的信息,这时候去发送刷新 token 的请求
                    self?.refreshToken()
                    return
                }
                
                let code = error.errorCode ?? ""
                let message = error.errorMessage ?? "\(myLocal("unknow_error")) \(code)"
                IGGToast.shared.showTip(message: message)
            }
        }
    }
    
    // Token 过期那么去刷新 token,
    func refreshToken() {
        let refreshToken = LocalAuthenticationLoginCredential.shared.refreshToken ?? ""
        print("刷新 token,刷新前的 token = \(refreshToken)")
        IGGToast.shared.showLoading()
        IGGTransaction.shared.refreshLocalAuthenticationLogonToken(refreshToken: refreshToken) { [weak self] (result) in
            IGGToast.shared.dismiss()
            switch result {
            case .success(let refreshedLoginModel):
                LocalAuthenticationLoginCredential.shared.saveCredential(from: refreshedLoginModel)
                print("刷新 token,刷新后的 token = \(refreshedLoginModel.refreshToken ?? "")")
                self?.requestSessionID()
            case .failure(let error):
                if let codeString = error.errorCode, let code = Int(codeString), code < 0 {
                    print("表示是 NSURLErrorDomain 这类本地网络错误")
                    let code = error.errorCode ?? ""
                    let message = error.errorMessage ?? "\(myLocal("unknow_error")) \(code)"
                    IGGToast.shared.showTip(message: message)
                    return
                }
                
                self?.handleRefreshTokenInvalid()
            }
        }
    }
    
    // 极端情况, refreshToken 无效的处理
    private func handleRefreshTokenInvalid() {
        /* 其他错误是服务端返回的错误信息,表示刷新 token 失败,此时需要将 生物识别凭证清空,
         提示用户生物识别失效提示,并进入密码登录页
         */
        LocalAuthenticationLoginCredential.shared.removeCredential()
        controller?.changeToPasswordLogin()
        IGGToast.shared.showTip(message: myLocal("invalid_refresh_token"), duration: 3) // 提示时间长一些,避免一闪而过
    }
    
    // MARK:【检查网络权限】
    func checkNetworkPermission() {
        IGGToast.shared.showLoading()
        checkNetworkPermission(onSuceess: { [weak self] in
            IGGToast.shared.dismiss()
            self?.controller?.dismissInvalidNetworkDialog()
            self?.checkUpdate()
        }) { [weak self] in
            IGGToast.shared.dismiss()
            self?.controller?.showInvalidNetworkDialog()
        }
    }
    
    // MARK:【处理登录成功】
    func handleLoginSuccess(with successInfo: LoginSuccessModel) {
        handleLoginSuccess(with: successInfo) { [weak self] in
            self?.addCookiesManually()
            self?.controller?.dismiss(animated: false, completion: nil)
            self?.controller?.enterMainPageOnLoginSucess()
        }
    }
    
    /* 因生物识别登录的步骤,没有密码登录 WKWebView 浏览器自己注入 Cookies 的操作(网页发送了请求后 response header 中 set-cookies 的操作)
     因此在这里需要手动注入 cookies,否则后续网页的相关请求都不带 cookies,造成会话失效
     
     ios 10 和 ios 11 的注入方式不尽相同
     */
    private func addCookiesManually() {
        if #available(iOS 11.0, *) {
            let cookie = HTTPCookie(properties: [
                .domain: "support.igg.com",
                .path: "/",
                .name: "__USER__",
                .value: "\(UserInformation.shared.sessionID ?? "")",
                .secure: "TRUE",
                .expires: NSDate(timeIntervalSinceNow: 3600)
                ])!
            
            WebViewManager.shared.configuration.websiteDataStore.httpCookieStore.setCookie(cookie,
                                                                                           completionHandler: nil)
        } else {
            let sessionID = UserInformation.shared.sessionID ?? ""
            let cookieScript = WKUserScript(source: "document.cookie = '__USER__=\(sessionID);path=/;secure=true'",
                injectionTime: .atDocumentStart,
                forMainFrameOnly: false)
            
            WebViewManager.shared.configuration.userContentController.addUserScript(cookieScript)
        }
    }
    
    // MARK:【检查更新】
    func checkUpdate() {
        guard hasCheckUpdate == false else { return }
        checkUpdate { [weak self] in
            self?.hasCheckUpdate = true
        }
    }
}

Controller 层的协议拓展

// MARK: -【登录页面 View 协议】
protocol LoginViewControllerProtocol: class {
    var invalidNetworkDialog: GeneralAlertDialogView? { get set }
    
    func showInvalidNetworkDialog()
    func showInvalidSessionDialog()
    func dismissInvalidNetworkDialog()
    func enterMainPageOnLoginSucess()
}

extension LoginViewControllerProtocol {
    func initInvalidNetworkDialog() -> GeneralAlertDialogView {
        let model = GeneralAlertDialogViewModel(content: myLocal("invalid_network"), actionButtonTitle: myLocal("go_to_myigg"))
        model.cancelButtonTitle = myLocal("exit_app")
        model.needHeader = false
        model.isTapBackgroundCloseEnabled = false
        model.contentViewMinHeight = 109
        let dialog = GeneralAlertDialogView(model: model, onClose: {
            print("退出应用")
            exit(0)
        }) {
            if let url = MyIGGSchemeURL.igg.obtainValidURL() {
                // 如果支持,那么打开 MyIGG
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            } else {
                // 不支持,那么打开 MyIGG 的下载地址
                guard let MyIGGDownloadURL = MyIGGDownlowdURL.igg.obtainValidURL() else { return }
                UIApplication.shared.open(MyIGGDownloadURL, options: [:], completionHandler: nil)
            }
        }
        
        return dialog
    }
    
    func showInvalidNetworkDialog() {
        guard let dialog = invalidNetworkDialog else { return }
        UIApplication.shared.keyWindow?.addSubview(dialog)
    }
    
    func showInvalidSessionDialog() {
        let model = GeneralAlertDialogViewModel(content: myLocal("invalid_session"), actionButtonTitle: myLocal("confirm"))
        let dialog = GeneralAlertDialogView(model: model, onClose: nil) {
        }
        
        DispatchQueue.main.async {
            UIApplication.shared.keyWindow?.addSubview(dialog)
        }
    }
    
    func dismissInvalidNetworkDialog() {
        DispatchQueue.main.async {
            self.invalidNetworkDialog?.removeFromSuperview()
        }
    }
    
    func enterMainPageOnLoginSucess() {
        DispatchQueue.main.async {
            UIApplication.shared.keyWindow?.rootViewController = MainViewController()
        }
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容