Swiftにおけるオブジェクト指向・ジェネリクスによる実装と関数型的実装の比較とそのハイブリッド方式の実装の検討

  • 39
    Like
  • 0
    Comment
More than 1 year has passed since last update.

Swiftにおいてオブジェクト指向とジェネリクスを利用した実装方法と関数型的実装方法を、簡単なコードであるが、比較してみる。あるクラスから別のクラスに変換する場合にオブジェクト指向と関数型のどちらが利用しやすいかを調べるのが目的である。

以下はPersonとそれを表示するために利用するDisplayPersonの構造体である。そしてPerson配列を作成してある。そのPersonからDisplayPersonへの変換を2つの方法で実装してみる。

struct Person {
    let firstName: String
    let lastName: String
    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }
}

struct DisplayPerson {
    let name: String
    init(name: String) {
        self.name = name
    }
}

let people = [Person(firstName: "Ryoichi", lastName: "Izumita"), Person(firstName: "Taro", lastName: "Yamada")]

オブジェクト指向とジェネリクスを利用した実装方法

まずオブジェクト指向とジェネリクスを利用した実装を行う。

class Translator<S, D> {
    typealias Source = S
    typealias Destination = D
    func translate(source: Source) -> Destination {
        fatalError()
    }
}

class PersonToDisplayPersonTranslator: Translator<Person, DisplayPerson> {
    var firstNameFirst: Bool

    init(firstNameFirst: Bool = true) {
        self.firstNameFirst = firstNameFirst
    }

    override func translate(source: Source) -> Destination {
        let name = firstNameFirst ? source.firstName + " " + source.lastName : source.lastName + " " + source.firstName
        return DisplayPerson(name: name)
    }
}

Translatorクラスは抽象クラスである。Translatorとして具象クラスを他のクラスに渡すようにすれば具象クラスを入れ替えることができるので、DIコンテナであつかいやすくなる。

Translatorの具象クラスはPersonToDisplayPersonTranslatorである。First NameもしくはLast Nameのどちらを先に表示するかを選択できるようになっている。firstNameFirstをプロパティで持たずtranslateメソッドに渡してカリー化する事も考えられるが、よりオブジェクト指向的にするためにプロパティとした。

以下のようにして利用する。

let translator = PersonToDisplayPersonTranslator(firstNameFirst: true)
let displayPeople = people.map(translator.translate)

関数型的な実装

次に関数型的な実装を行ってみる。(「的」というのは、私が関数型の実装を正しく理解していない可能性があるからである。)

func fullName(firstNameFirst: Bool = true) -> Person -> String {
    return {
        person in
        if firstNameFirst {
            return person.firstName + " " + person.lastName
        } else {
            return person.lastName + " " + person.firstName
        }
    }
}

上記はPersonから氏名文字列を作成する関数である。カリー化してある。

let translate = DisplayPerson.init  fullName(false)

•演算子はSwiftzのものである(SwiftzはScalazのSwift版のフレームワークである。)。fullName関数とDisplayPersonのイニシャライザメソッドを•演算子によって合成している。合成して作成したtranslate関数はPersonを入力するとDisplayPersonを出力する関数になっている。(ちなみに•はOption+8で入力する。)

以下のように利用する。

let displayPeople = people.map(translate)

比較してみて

オブジェクト指向での実装は氏名の作成とDisplayPersonの作成が同じメソッドに記述されており、それを分けるとすると氏名作成を別メソッド、さらには別クラスに分ける必要が発生する可能性がある。さらに抽象クラス・具象クラスが存在する。つまり冗長であると感じた。しかしAppleのSDKはオブジェクト指向でありアプリ全体的にはオブジェクト指向での設計に寄せた方が書きやすいので親和性が高く、特にDIを行う場合にはあつかいやすい。

関数型的な実装はミニマムである。機能的にも分割されている。DIコンテナではあつかいにくいがコンテナを利用しないのであれば、利用するクラスや関数に合成関数を注入することでDIを行うことも可能である。ただ、上記のように広域関数として定義することはせず、モジュールとして分割するためにstruct内にstaticとして定義するようになるのではないかと考えられる。となると、ミニマムであっても結局structを作成することになり記述量は増える可能性を秘めている。

一長一短あるようである。ではそれぞれの方法を混ぜるとどうなるのかを考えてみた。

ハイブリッド方式

オブジェクト指向と関数型のハイブリッド方式は以下のようになった。

struct Translator<S, D> {
    typealias Source = S
    typealias Destination = D
    let translate: Source -> Destination
    init(_ translate: Source -> Destination) {
        self.translate = translate
    }
}

struct NameManipulation {

    static func fullName(firstNameFirst: Bool = true) -> Person -> String {
        return {
            person in
            if firstNameFirst {
                return person.firstName + " " + person.lastName
            } else {
                return person.lastName + " " + person.firstName
            }
        }
    }

}

let translator = Translator(DisplayPerson.init  NameManipulation.fullName(false))
let displayPeople = people.map(translator.translate)

Translator抽象クラスが具象クラスになり、structのstaticメソッドにしたfullNameをDisplayPersonのイニシャライザと合成してTranslatorのイニシャライザに渡している。この実装であれば抽象クラスと具象クラスを分ける必要がなく、機能の分割も行いやすく、おそらくDIコンテナでもあつかいやすくなっている。関数のモジュール化はstructで行うしかないが、それ以外は2つの実装方法の良いとこ取りをできているようである。

結論

以上のようにオブジェクト指向と関数型的な実装を比較してそのハイブリッド方式での実装を試してみた。オブジェクト指向と関数型的な実装には利点・欠点があったが、ハイブリッドにすることでお互いに欠点を補うことができるようである。全ての場面でこのように補い合えるかは分からないが、一つの方法として記憶しておこうと思う。