Android設計模式之Builder模式在實際項目的運用

背景

Builder模式是一種設計模式,Android源碼中AlertDialog就是使用Build設計模式,這種模式的主要特點就是鏈式的,方便使用者的調用,使用者無需關心內部如何實現就可以方便調用。

爲什麼要用?

首先了解一下定義:

建造者模式(Builder Pattern):將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。建造者模式是一種對象創建型模式。

使用場景:
  • 相同的方法,不同的執行順序,產生不同的事件結果時;

  • 多個部件或零件,都可以裝配到一個對象中,但是產生的運行結果又不相同時;

  • 產品類非常複雜,或者產品類中的調用順序不同產生了不同的效能,這個時候使用建造者模式非常合適;

  • 當初始化一個對象特別複雜,如參數多,且很多參數都具有默認值時。

UML圖解:

5315387-16848739f754e53b

在建造者模式結構圖中包含如下幾個角色:

● Builder(抽象建造者):它爲創建一個產品Product對象的各個部件指定抽象接口,在該接口中一般聲明兩類方法,一類方法是buildPartX(),它們用於創建複雜對象的各個部件;另一類方法是getResult(),它們用於返回複雜對象。Builder既可以是抽象類,也可以是接口。

● ConcreteBuilder(具體建造者):它實現了Builder接口,實現各個部件的具體構造和裝配方法,定義並明確它所創建的複雜對象,也可以提供一個方法返回創建好的複雜產品對象。

● Product(產品角色):它是被構建的複雜對象,包含多個組成部件,具體建造者創建該產品的內部表示並定義它的裝配過程。

● Director(指揮者):指揮者又稱爲導演類,它負責安排複雜對象的建造次序,指揮者與抽象建造者之間存在關聯關係,可以在其construct()建造方法中調用建造者對象的部件構造與裝配方法,完成複雜對象的建造。客戶端一般只需要與指揮者進行交互,在客戶端確定具體建造者的類型,並實例化具體建造者對象(也可以通過配置文件和反射機制),然後通過指揮者類的構造函數或者Setter方法將該對象傳入指揮者類中。

Builder模式的優缺點

優點:

  • 良好的封裝性,使用建造者模式可以使客戶端不必知道產品內部組成的細節
  • 建造者獨立,容易擴展

缺點:

  • 會產生多餘的Builder對象以及Director對象,消耗內存

變種Builder模式

變種Builder模式在現實Android開發中會經常用到。現實開發中,Director角色會經常被省略。而直接使用一個Builder來進行對象的組裝,這個Builder通常爲鏈式調用,它的關鍵點是每個setter方法都返回自身,即return this。調用如下:

new TestBuilder().setA("A").setB("B").create()

好處:

目的在於減少對象創建過程中引入的多個重載構造函數、可選參數以及setter過度使用導致不必要的複雜性。

運用實例可以在最下文的參考資料3中查看關於User對象屬性的例子,此處不在贅述。

再舉一例:

參加面試的同學可能會有被問到StringStringBufferStringBuilder的區別,這裏就有StringBuilder這個字符串構建器,我們來簡單看一下這個構建器的用法吧!

  String sb = new StringBuilder().append("I ")
                .append("am ")
                .append("student.").toString();

	//我們看一下這裏用的append()函數的源碼
	@Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

可知這個append()將傳入的str通過調用父類的方法加到了該對象字符串的後面,並將該對象返回了,這樣就可以連續的調用append()方法,並且保持對象的唯一性(String每次都會創建一個新的對象)。

在實際項目的案例:

  • 網絡請求時通用參數配置
 	private const val DEFAULT_CONNECT_TIMEOUT = 5_000L
    private const val OTHER_TIME_OUT = 5_000L    

	/**
     * 測試環境網絡請求配置
     */
    fun testConfig() {
        val httpConfig = HttpConfig.Builder().baseUrl(CommonApi.apiBaseUrl)
            // 打印使用http請求日誌
            .setLogLevel(HttpLoggingInterceptor.Level.BODY)
            // 設置全局超時時間
            .connectTimeoutMillis(DEFAULT_CONNECT_TIMEOUT)
            .readTimeoutMillis(OTHER_TIME_OUT)
            .writeTimeoutMillis(OTHER_TIME_OUT).build()
        HttpUtil.initHttpConfig(httpConfig)
    }

	/**
     * 網絡請求配置構建者
     */
    class Builder {
        private var baseUrl = ""
        private var interceptors: ArrayList<Interceptor> = ArrayList()
        private var networkInterceptors: ArrayList<Interceptor> = ArrayList()
        private var defaultConnectTimeout = 10_000L
        private var defaultReadTimeout = 10_000L
        private var defaultWriteTimeout = 10_000L
        private var retryOnConnectionFailure = false
        private var isUseCookie = false
        private var isUseCache = false
        private var logLevel = HttpLoggingInterceptor.Level.NONE
        private val commonHeaders = ArrayMap<String, String>()
        private val commonParams = ArrayMap<String, String>()
        private var sslParam: SSLParam = HttpsUtil.getSslSocketFactory()
        private var hostnameVerifier: HostnameVerifier = HttpsUtil.UnSafeHostnameVerifier

        fun baseUrl(url: String): HttpConfig.Builder {
            baseUrl = url
            return this
        }

        fun addInterceptor(interceptor: Interceptor): HttpConfig.Builder {
            interceptors.add(interceptor)
            return this
        }

        fun addNetworkInterceptor(interceptor: Interceptor): HttpConfig.Builder {
            networkInterceptors.add(interceptor)
            return this
        }

        /**
         * 連接超時時間
         * @param millis 單位是毫秒(默認10秒)
         */
        fun connectTimeoutMillis(millis: Long): HttpConfig.Builder {
            if (millis <= 0) {
                throw IllegalArgumentException("connect timeout must Greater than 0")
            }
            defaultConnectTimeout = millis
            return this
        }

        /**
         * 讀取超時時間
         * @param millis 單位是毫秒(默認10秒)
         */
        fun readTimeoutMillis(millis: Long): HttpConfig.Builder {
            if (millis <= 0) {
                throw IllegalArgumentException("read timeout must Greater than 0")
            }
            defaultReadTimeout = millis
            return this
        }

        /**
         * 寫入超時時間
         * @param millis 單位是毫秒(默認10秒)
         */
        fun writeTimeoutMillis(millis: Long): HttpConfig.Builder {
            if (millis <= 0) {
                throw IllegalArgumentException("write timeout must Greater than 0")
            }
            defaultWriteTimeout = millis
            return this
        }

        /**
         * 連接失敗時是否重新進行網絡請求
         * @param retryOnConnectionFailure 默認爲false
         */
        fun retryOnConnectionFailure(retryOnConnectionFailure: Boolean): HttpConfig.Builder {
            this.retryOnConnectionFailure = retryOnConnectionFailure
            return this
        }

        /**
         * 是否開啓cookie
         * @param isUseCookie 默認爲false
         */
        fun useCookie(isUseCookie: Boolean): HttpConfig.Builder {
            this.isUseCookie = isUseCookie
            return this
        }

        /**
         * 是否使用緩存
         * @param isUseCache 默認爲false
         */
        fun useCache(isUseCache: Boolean): HttpConfig.Builder {
            this.isUseCache = isUseCache
            return this
        }

        /**
         * 設置日誌級別,參考[HttpLoggingInterceptor.Level]
         * @param level 默認爲[HttpLoggingInterceptor.Level.NONE]
         */
        fun setLogLevel(level: HttpLoggingInterceptor.Level): HttpConfig.Builder {
            logLevel = level
            return this
        }

        /**
         * 設置通用請求header
         * @param key header鍵
         * @param value header值
         */
        fun commonHeader(key: String, value: String): HttpConfig.Builder {
            commonHeaders[key] = value
            return this
        }

        /**
         * 設置通用請求參數
         * @param key 參數鍵
         * @param value 參數值
         */
        fun commonParam(key: String, value: String): HttpConfig.Builder {
            commonParams[key] = value
            return this
        }

        /**
         * 配置ssl
         * @param param ssl參數,默認不對證書做任何檢查
         */
        fun sslSocketFactory(param: SSLParam): HttpConfig.Builder {
            sslParam = param
            return this
        }

        /**
         * 主機名驗證
         * @param verifier 默認允許所有主機名
         */
        fun hostnameVerifier(verifier: HostnameVerifier): HttpConfig.Builder {
            hostnameVerifier = verifier
            return this
        }

        fun build(): HttpConfig {
            return HttpConfig(
                baseUrl, interceptors, networkInterceptors, defaultConnectTimeout
                , defaultReadTimeout, defaultWriteTimeout, retryOnConnectionFailure, isUseCookie
                , isUseCache, logLevel, commonHeaders, commonParams, sslParam, hostnameVerifier
            )
        }
    }
  • MaterialDialog對話框
   private fun showSavePromptDialog() {
        MaterialDialog(this)
            .message(text ="消息提示標題")
            .positiveButton("保存") {
               // do something
            }
            .negativeButton(“取消”) {
               // do something
            }
            .showByCZConfig()
    }

/**
*MaterialDialog擴展
*/
fun MaterialDialog.showByCZConfig(): MaterialDialog {
    show()
    doAsync {
        SystemClock.sleep(50L)
        uiThread {
         val positiveButton = findViewById<AppCompatButton>(R.id.md_button_positive)
         val negativeButton = findViewById<AppCompatButton>(R.id.md_button_negative)
         val neutralButton = findViewById<AppCompatButton>(R.id.md_button_neutral)

         positiveButton.setTextColor(ResourcesUtil.getColor(R.color.common_font_red))
         negativeButton.setTextColor(ResourcesUtil.getColor(R.color.common_font_black))
         neutralButton.setTextColor(ResourcesUtil.getColor(R.color.common_font_black))
        }
    }
    return this
}

其他:如開源的圖片框架ImageLoader就是通過ImageLoaderConfig進行配置等等。

小結

在開發中,當遇到一個類的構造器或者靜態工廠中具有多個參數,特別是大多數參數是可選的時候,可以考慮使用Builder模式,避免過多的setter方法。Builder模式比較常見的實現形式是通過調用鏈實現,這樣使得代碼更簡潔、易懂。

參考資料:

1.設計模式系列——建造者模式-Builder Pattern

2.創建型設計模式之Builder模式

3.Android : Builder模式 詳解及學習使用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章