iT邦幫忙

2022 iThome 鐵人賽

DAY 26
0
Software Development

Kotlin on the way系列 第 26

Day 26 設計模式 工廠建構的細節

  • 分享至 

  • xImage
  •  
  • Factory
    • simple factory
    • Companion object factory
    • unlock companion object
    • Extension factory function
    • factory function

工廠 Factory

第二個要講的是工廠模式

和單例的設計取向不同,單例著重在全域的唯一實體,而工廠模式著重在物件建構的彈性,也很好的處理了物件依賴的關係

client 會了解介面類型,透過工廠幫他產生實體,使其不需知道物件的建構方式,將建構方式限制在工廠內,再更上一層的抽象,還能將工廠也抽象化,使工廠也得以替換

fun main() {
   GinFactory().apply{
       println(
           makeDrink(GinType.BRITISH_GIN).drinkType.getSpice()
       )
       println(
           makeDrink(GinType.HENDRICKS_GIN).drinkType.getSpice()
       )
   }
}

class GinFactory :DrinkFactory {
    override fun makeDrink(expectGin:GinType):Drink{
        return when(expectGin){
            GinType.BRITISH_GIN -> {
                Drink(BritishGin(), 100)
            }
            GinType.HENDRICKS_GIN -> {
                Drink(HendricksGin(), 200)
            }
        }
    }
}

interface DrinkFactory {
    fun makeDrink(expectGin:GinType):Drink
}

interface Gin{
    fun getSpice():List<String>
    fun getPrice():Int
	fun getContent():String
}

class BritishGin:Gin {
    override fun getSpice():List<String> {
        return listOf("alo", "fuck","regret")
    }
    override fun getPrice():Int = 100
	override fun getContent():String = "I am british gin"
}


class HendricksGin:Gin {
    override fun getSpice():List<String> {
        return listOf("test", "kdnf.", "fgfes")
    }
    override fun getPrice():Int = 20000000
	override fun getContent():String = "I am hendrick's gin"
}

data class Drink(
	val drinkType:Gin,
    val price:Int
)

enum class GinType{
	BRITISH_GIN, HENDRICKS_GIN;
}

Companion object factory function

工廠函式最常見的寫法就是放在共生物件裡面,就像是靜態工廠方法

class MyLinkedList<T>(
   val head: T, 
   val tail: MyLinkedList<T>?
) {

   companion object {
      fun <T> of(vararg elements: T): MyLinkedList<T>? {
          /*...*/ 
      }
   }
}

// Usage
val list = MyLinkedList.of(1, 2)

我們也能用介面來實現ㄌ

class MyLinkedList<T>(
   val head: T, 
   val tail: MyLinkedList<T>?
): MyList<T> {
   // ...
}

interface MyList<T> {
   // ...
  
   companion object {
       fun <T> of(vararg elements: T): MyList<T>? {
           // ...
       }
   }
}

// Usage
val list = MyList.of(1, 2)

factory function

但並非一定要用工廠模式才能得到好處,以函式做物件建構(factory function),有時會更適合,首先來比較建構子和建構函示的差異

  • 和建構式不同,函數擁有名稱
    • 命名可以說明物件如何被建立,以及傳入參數用途
    • 命名可以解決傳入參數相同的衝突
  • 函式可以回傳類型和其子類型
    • 可以用介面隱藏實體
  • 函式不必在每次呼叫時都回傳一個新的實體
    • 可以搭配快取
    • 可以回傳 null
  • 函式可以提供還沒被建構出的實體
    • 被使用 annotation 為主的套件來說很常見
  • 當我們在物件外面定義建構函式時,可以控制其可見性
  • 可以 inline 使類型可以被 reifield
  • 可以負責建構複雜的實體
  • 類別必須立刻呼叫父類別的建構式,但函式可以延後

比如鏈接列表,只透過建構子會變得很麻煩

class MyLinkedList<T>(
   val head: T, 
   val tail: MyLinkedList<T>?
)

val list = MyLinkedList(1, MyLinkedList(2, null))

如果透過建構函式,可以讓我們在呼叫方更加方便

fun <T> myLinkedListOf(
   vararg elements: T
): MyLinkedList<T>? {
   if(elements.isEmpty()) return null
   val head = elements.first()
   val elementsTail = elements
       .copyOfRange(1, elements.size)
   val tail = myLinkedListOf(*elementsTail)
   return MyLinkedList(head, tail)
}

val list = myLinkedListOf(1, 2)

用法

此處的建構方法,有著固定的命名習慣

  • from,型別轉換,從A轉換成 b
  • of,聚合函式,將同類型資料聚合起來,並回傳一個集合
  • value of,上面兩者的攏長版
  • instance, getInstance,用於單例模式,已獲得同一個實例
  • createInstance, newInstance,每次都提供新一個實例
  • getType
    • 和 getInstance一樣,用於在不同類別編寫工廠函式,此處的 type 需要將其命名為目標類別, ex. val fs: FileStore = Files.getFileStore(path)
  • newType
    • 和 newInstance一樣,用於在不同類別編寫工廠函式,此處的 type 需要將其命名為目標類別, ex. val br: BufferedReader = Files.newBufferedReader(path)

unlock companion object

共生物件可以繼承介面,使其對類別擴長

abstract class ActivityFactory {
   abstract fun getIntent(context: Context): Intent

   fun start(context: Context) {
       val intent = getIntent(context)
       context.startActivity(intent)
   }

   fun startForResult(activity: Activity, requestCode: 
Int) {
       val intent = getIntent(activity)
       activity.startActivityForResult(intent, 
requestCode)
   }
}

class MainActivity : AppCompatActivity() {
   //...

   companion object: ActivityFactory() {
       override fun getIntent(context: Context): Intent =
           Intent(context, MainActivity::class.java)
   }
}

// Usage
val intent = MainActivity.getIntent(context)
MainActivity.start(context)
MainActivity.startForResult(activity, requestCode)

這裡的抽象共生物件工廠,可以保存狀態,也可以為測試做出假建構

Extension factory function

有時我們無法更改介面或是需要再另外的檔案裡建立工廠函式,可以使用拓展的方式

//無法更改的介面
interface Tool {
   companion object { /*...*/ }
}

為其寫出拓展

fun Tool.Companion.createBigTool( /*...*/ ) : BigTool {
   //... 
}
``` kotlin

At the call site we can then write:

``` kotlin
Tool.createBigTool()

此種方法非常方便,能夠讓我們快速地替外部套件寫出工廠方法,唯一的限制就是他需要原本的套件有寫 Companion object

Top level object

頂層工廠函式也很常見,像是 listOf, mapOf 等等,但使用頂層函式也要非常小心,因為會讓ide 提示變得混亂,尤其是top level 和 class 命名相似時更容易搞混

fake constructor

建構式和 top level一樣,是全局可見的,唯一的差別就是大小寫,而 fake constructor 就是利用這點,使函式看起來像是建構式,而這樣的好處在於

  • 使介面可以擁有建構式(偽
  • 可以 reifield 參數
public inline fun <T> List(
   size: Int, 
   init: (index: Int) -> T
): List<T> = MutableList(size, init)

public inline fun <T> MutableList(
   size: Int, 
   init: (index: Int) -> T
): MutableList<T> {
   val list = ArrayList<T>(size)
   repeat(size) { index -> list.add(init(index)) }
   return list
}

除了上面兩點,偽建構式應該和建構式行為一樣,如果要實作快取,就應該以具名函式,nullable的方式,如前面共生物件的方式實現

除上面做法外,還能以 operator fun invoke 得方式實現假建構,但並不建議

summary

那建構函式(factory function)會取代建構式嗎?
不會,建構函式依仍需要建構式,但他相對於建構式更有彈性、獨立於類別且可空

比較工廠模式和工廠函式,工廠模式多出了能持有狀態的好處,他們可以擁有屬性,並利用屬性去對建構作出調整

English

  • Factory
    • simple factory
    • Companion object factory
    • unlock companion object
    • Extension factory function
    • factory function

Factory

The second pattern we gonna discuss is factory

Unlike the core idea of singleton, singleton focuses on the only instance globally, but factories focus on the flexibility of the object constructor, and also deal with class dependency well.

Check the image above, client know what interface them will receive, and using factory to build the instance for them, so client won't know the how to build the instance, restrict the constructor method inside factory, we can add more abstract on factory by making abstract for factory as well

fun main() {
   GinFactory().apply{
       println(
           makeDrink(GinType.BRITISH_GIN).drinkType.getSpice()
       )
       println(
           makeDrink(GinType.HENDRICKS_GIN).drinkType.getSpice()
       )
   }
}

class GinFactory :DrinkFactory {
    override fun makeDrink(expectGin:GinType):Drink{
        return when(expectGin){
            GinType.BRITISH_GIN -> {
                Drink(BritishGin(), 100)
            }
            GinType.HENDRICKS_GIN -> {
                Drink(HendricksGin(), 200)
            }
        }
    }
}

interface DrinkFactory {
    fun makeDrink(expectGin:GinType):Drink
}

interface Gin{
    fun getSpice():List<String>
    fun getPrice():Int
	fun getContent():String
}

class BritishGin:Gin {
    override fun getSpice():List<String> {
        return listOf("alo", "fuck","regret")
    }
    override fun getPrice():Int = 100
	override fun getContent():String = "I am british gin"
}


class HendricksGin:Gin {
    override fun getSpice():List<String> {
        return listOf("test", "kdnf.", "fgfes")
    }
    override fun getPrice():Int = 20000000
	override fun getContent():String = "I am hendrick's gin"
}

data class Drink(
	val drinkType:Gin,
    val price:Int
)

enum class GinType{
	BRITISH_GIN, HENDRICKS_GIN;
}

Companion object factory function

It is common to write build method inside companion object, this is what we call static factory methods

class MyLinkedList<T>(
   val head: T, 
   val tail: MyLinkedList<T>?
) {

   companion object {
      fun <T> of(vararg elements: T): MyLinkedList<T>? {
          /*...*/ 
      }
   }
}

// Usage
val list = MyLinkedList.of(1, 2)

we can also using interface to implement

class MyLinkedList<T>(
   val head: T, 
   val tail: MyLinkedList<T>?
): MyList<T> {
   // ...
}

interface MyList<T> {
   // ...
  
   companion object {
       fun <T> of(vararg elements: T): MyList<T>? {
           // ...
       }
   }
}

// Usage
val list = MyList.of(1, 2)

factory function

It doesn't require factory to get the benefit, sometimes using function to build class is better opinion, but first check out pros and cons

  • unlike constructor, function have name
    • name can explain when the class built, and the purpose of variable usage
    • name can fix the conflict of same argument
  • function can return type and its sub-type
    • using interface to hide instance
  • function doesn't require return new instance
    • allow cache
    • allow null
  • function can provide instance not build yet
    • common in library using annotation
  • when we define constructor function outside class, we can control its visibility
  • using inline can be ratified
  • could use to build complexity instance
  • class have to call constructor of its parent, but function could call later

like linkedlist, using constructor only annoying yourself

class MyLinkedList<T>(
   val head: T, 
   val tail: MyLinkedList<T>?
)

val list = MyLinkedList(1, MyLinkedList(2, null))

but if we choose constructor function, it will be more convenience from caller

fun <T> myLinkedListOf(
   vararg elements: T
): MyLinkedList<T>? {
   if(elements.isEmpty()) return null
   val head = elements.first()
   val elementsTail = elements
       .copyOfRange(1, elements.size)
   val tail = myLinkedListOf(*elementsTail)
   return MyLinkedList(head, tail)
}

val list = myLinkedListOf(1, 2)

usage of factory function

The constructor methods has rule for its naming

  • from
    • transfer type, from A transfer into B
  • of
    • aggregate function, aggregate same type of data, and return a collection
  • value of
    • longer version above
  • instance, getInstance
    • use for singleton, get the same instance
  • createInstance, newInstance
    • provide new instance each times
  • getType
    • like getInstance, use for factory function for different class, the type here should be the target class
    • ex. val fs: FileStore = Files.getFileStore(path)
  • newType
    • like newInstance一樣, use for factory function for different class, the type here should be the target class
    • val br: BufferedReader = Files.newBufferedReader(path)

unlock companion object

companion object can implement interface, to extend the class

abstract class ActivityFactory {
   abstract fun getIntent(context: Context): Intent

   fun start(context: Context) {
       val intent = getIntent(context)
       context.startActivity(intent)
   }

   fun startForResult(activity: Activity, requestCode: 
Int) {
       val intent = getIntent(activity)
       activity.startActivityForResult(intent, 
requestCode)
   }
}

class MainActivity : AppCompatActivity() {
   //...

   companion object: ActivityFactory() {
       override fun getIntent(context: Context): Intent =
           Intent(context, MainActivity::class.java)
   }
}

// Usage
val intent = MainActivity.getIntent(context)
MainActivity.start(context)
MainActivity.startForResult(activity, requestCode)

The abstract companion factory here, can save state, also build mock contract for test

Extension factory function

Sometimes we can't change interface or we need to write factory function in different file, we can choose extend

//interface can't change
interface Tool {
   companion object { /*...*/ }
}

using extend function

fun Tool.Companion.createBigTool( /*...*/ ) : BigTool {
   //... 
}
``` kotlin

At the call site we can then write:

``` kotlin
Tool.createBigTool()

This is very convenience, we can write factory function quickly, but it require the source interface use Companion object

Top level object

Top level factory function is common, like listOf mapOf , but be careful when using top level function, since it make ide prompt being confused

fake constructor

Constructor just like top level, is can see from global, the different is capitalize, and fake constructor using this point to make function look like constructor, to get the following benefit

  • allow interface look like having constructor
  • allow reifield
public inline fun <T> List(
   size: Int, 
   init: (index: Int) -> T
): List<T> = MutableList(size, init)

public inline fun <T> MutableList(
   size: Int, 
   init: (index: Int) -> T
): MutableList<T> {
   val list = ArrayList<T>(size)
   repeat(size) { index -> list.add(init(index)) }
   return list
}

Besides above, fake contractor should behavior as constructor, if you need cache, using named function, nullable

summary

Will factory functions replace constructor?
No, factory function still require constructor, but it is more flexible, independence and allow nullable

Compare factory pattern and factory function, factory pattern got the advantage of holding state, they are allow to have properties, and using property to adjust constructor


上一篇
Day 25 設計模式 單例模式的細節 Design pattern - Singleton Creational pattern
下一篇
Day 27 設計模式 裝飾和代理的細節 Proxy pattern and Decorator pattern Structural
系列文
Kotlin on the way31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言