UZABASE Tech Blog

〜迷ったら挑戦する道を選ぶ〜 株式会社ユーザベースの技術チームブログです。

Go言語でオブジェクト指向プログラミングの基本(型編)

こんにちは、SPEEDA開発チームの @tkitsunai です。

最近リリースされたプロダクションコードにようやくGo言語が採用されました。嬉しい。

今回はGo言語でオブジェクト指向プログラミングで型表現についてのテクニックや考え方の基礎を紹介します。もっとGopherが増えて欲しい!

対象読者

  • Go言語をこれから始める人
  • Go言語の型宣言で表現力を高めたい人
  • オブジェクト指向プログラミングに向き合いたい人

型を定義する

オブジェクト指向プログラミングの基本として「型」で表現を体現します。structはGoの値であり型です。

例題:

  • エンジニアだけが所属できる、SPEEDAプロダクトというユニットに、tkitsunaiという名前のエンジニアが所属している。
  • デザイナーだけが所属できる、SPEEDAデザインというユニットに、hiranotomokiという名前のデザイナーが所属している。

例題を悪い例として実装します。

type (
    Unit struct {
        Name string
        Members []string
    }
)

利用する実装コードは以下です。

   tkitsunai := "tkitsunai"
    hiranotomoki := "hiranotomoki"

    speedaProductUnitName := "SPEEDAプロダクトユニット"
    speedaDesignUnitName := "SPEEDAデザインユニット"

    speedaProductUnit := Unit{
        Name:    speedaProductUnitName,
        Members: []string{
            tkitsunai,
        },
    }

    speedaDesignUnit := Unit{
        Name:    speedaDesignUnitName,
        Members: []string{
            hiranotomoki,
        },
    }

型で厳格さを作り、その型を洗練させる

上の構造体定義はプリミティブな型で表現されています。例題に対して型でほとんど表現できておらず悪い例といえます。

  • ユニットが変数名だけで表現されており、デザインユニットにエンジニアが入ってしまう可能性がある
  • tkitsunaiがそもそも人ではない可能性がある
  • tkitsunaiかhiranotomokiという文字だけでは、エンジニアかデザイナーか判断できない
  • etc...

プリミティブ型を多用することによって起きる問題を、型の厳格さを加えることで排除します。

改良1: プリミティブ型をなくす

Go言語には型宣言によって、基本型もしくは定義された構造体を別名で再定義することが可能です。改良を加えてみます。

type (
    UnitName string
    PersonName string

    Person struct {
        Name PersonName
    }

    Unit struct {
        Name UnitName
        Members []Person
    }
)

利用する実装コードは以下です。

   tkitsunai := Person{
        Name: PersonName("tkitsunai"),
    }
    hiranotomoki := Person{
        Name: PersonName("hiranotomoki"),
    }

    speedaProductUnitName := UnitName("SPEEDAプロダクトユニット")
    speedaDesignUnitName := UnitName("SPEEDAデザインユニット")

    speedaProductUnit := Unit{
        Name: speedaProductUnitName,
        Members: []Person{
            tkitsunai,
        },
    }

    speedaDesignUnit := Unit{
        Name: speedaDesignUnitName,
        Members: []Person{
            hiranotomoki,
        },
    }

少しだけ良くなりました。この改良でProductUnitに人以外が混在することはなくなりました。

また、人の名前とユニットの名前が混在することもなくなりました。

改良2: Unitの特性を型で表現する

例題を見直すと、プロダクトユニットにはそれぞれ特性があります。プロダクトユニットならばエンジニア、デザインユニットならばデザイナーである必要があります。

現在のままでは、tkitsunaiという人がデザインチームに入れられるかもしれません(それはそれで楽しそうですが)。これも型で制限していきます。

type (
    UnitName string
    PersonName string

    Person struct {
        Name PersonName
    }
    Engineer Person // <- Person型をwrapしたEngineer型として定義する
    Designer Person // <- Person型をwrapしたDesigner型として定義する

    ProductUnit struct {
        Name UnitName
        Members []Engineer
    }

    DesignUnit struct {
        Name UnitName
        Members []Designer
    }
)

利用する実装コードは以下です。

   tkitsunai := Engineer{
        Name: PersonName("tkitsunai"),
    }
    hiranotomoki := Designer{
        Name: PersonName("hiranotomoki"),
    }

    speedaProductUnitName := UnitName("SPEEDAプロダクトユニット")
    speedaDesignUnitName := UnitName("SPEEDAデザインユニット")

    speedaProductUnit := ProductUnit{
        Name: speedaProductUnitName,
        Members: []Engineer{
            tkitsunai,
        },
    }

    speedaDesignUnit := DesignUnit{
        Name: speedaDesignUnitName,
        Members: []Designer{
            hiranotomoki,
        },
    }

この改良によって、各ユニットで所属できる型が制限されたので、Engineerであるtkitsunaiはデザインユニットに入ることはできなくなりました(残念)。

型に厳格さを求める完全コンストラクタを活用する

現時点では、PersonNameやUnitNameにはどのような名前も許されていますが、オブジェクト生成時にルールを適用させることで、型を利用する場合にエラーとできるようにします。いわゆる完全コンストラクタを目指します。Go言語にはコンストラクタはありませんが、慣習としてPrefixにStruct名にNewをつけた関数を用意し、structとerrorを返すようにします。

func NewUnitName(unitName string) (UnitName, error) {
    if unitName == "" {
        return "", errors.New("ユニット名が必要です")
    }

    if strings.HasSuffix(unitName, "ユニット") {
        return "", errors.New("ユニット名は必要ありません")
    }
 
    return UnitName(unitName), nil
}

利用するコード

   speedaProductUnitName, err := NewUnitName("") // ユニット名はひとまず入れなくていーや
    if err != nil {
        panic(err)
    }

実行結果

panic: ユニット名が必要です

エラーを握りつぶされてしまったら元も子もないですが、これでUnitNameのルールを生成時に確認でき、空文字なユニット名は作ることはできなくなりました。

このように、UnitNameを持つProductUnitおよびDesignUnitは、UnitNameの完全コンストラクタを通じて、型とそのルールが表現されていきます。

ファーストクラスコレクションと型定義によってメンバーに対する操作を制限する

ProductUnitのフィールドであるMembersは[]Engineer型で表現されています。この状態では、ProductUnitがこのコレクションを直接操作しなければならず、コレクションを壊しかねません。型を抜き出して、ファーストクラスコレクションとしてstructを再定義し、操作を制限させることでコレクションの壊れにくさを作ります。

type Engineers struct {
    list []Engineer
}

func (p Engineers) Add(newEngineer Engineer) Engineers {
    engineers := make([]Engineer, len(p.list)+1)
    for i, engineer := range p.list {
        engineers[i] = engineer
    }
    engineers[len(p.list)] = newEngineer
    return Engineers{
        list: engineers,
    }
}

func (p *ProductUnit) JoinNewMember(engineer Engineer) {
    p.Members = p.Members.Add(engineer) // 増やす操作しかできない
}

上記の実装によって、増やすメソッドは存在するが、減らすメソッドを定義しないことで、コレクションに対する操作を制限できました。一度入ったら出られないプロダクトユニットへようこそ!

なお、上記の例ではストイックにstructを定義していますが、厳格さをそこまで求めないのならばEngineers []Engineerとしても良いかもしれません。

列挙型で表現力を高める

プログラム言語を持ったエンジニアを定義するような値とそれを持った型を作るならば、型表現とコレクションを応用して表現力を高めることができそうです。

type ProgrammingLanguage int
type ProgrammingLanguages []ProgrammingLanguage

const (
    Golang ProgrammingLanguage = iota
    Java
    JavaScript
    Clojure
    Elixir
    Rust
)

type Engineer struct {
    Person
    Skills ProgrammingLanguages
}

func (e Engineer) LetsGoProgramming() {
    for _, lang := range e.Skills {
        if lang == Golang {
            fmt.Println("OK, I will typing `func main() { //blablabla }`")
        }
    }
}

これもただの列挙型ではなく、型に別名をつけることで厳格化させます。

構造体埋め込みで表現をする

構造体埋め込みを利用して型定義に幅を持たせます。

package engineer

import "fmt"

type (
    FirstName string
    LastName  string
    person    struct {
        FirstName FirstName
        LastName  LastName
    }
    GoEngineer struct {
        person
    }
)

func (p person) Greet() {
    fmt.Println(fmt.Sprintf("hello I am %s", p.fullName()))
}

func (p person) fullName() string {
    return fmt.Sprintf("%s %s", p.FirstName, p.LastName)
}

func NewGoEngineer(firstName FirstName, lastName LastName) GoEngineer {
    return GoEngineer{
        person: person{
            FirstName: firstName,
            LastName:  lastName,
        },
    }
}

利用する実装コードは以下です。

package main

import (
    "engineer" // <= 擬似コード:engineerパッケージを読み込んだことにします
)

func main() {
    tkitsunai := engineer.NewGoEngineer(engineer.FirstName("takayuki"), engineer.LastName("kitsunai"))
    tkitsunai.Greet()
}

GoEngineer型のフィールドperson型定義によって型の恩恵を受けられます。同時にperson型はパッケージ外に公開していないため、完全コンストラクタを経由してGoEngineer型を利用すれば、personの詳細実装が外に漏れずに済みます。

構造体の埋め込みは普段活用していませんが、型をまとめるためだけでなく、制限範囲を限定することもできるため、応用がしやすいと思います。

まとめ

今回はGo言語における型表現について、ほんの一部だけ紹介しました。

  • プリミティブな型を作らない
  • 最初から汎用的な型を作らない
  • 完全コンストラクタで型が示す表現力を高める
  • ファーストクラスコレクションで操作を制限する
  • 列挙であるiotaも別名で型定義して利用する
  • 構造体埋め込みを上手く利用する

関連しているオブジェクト指向エクササイズも面白いので、気になる方はぜひ調べてみてください。

型を厳格にする意義

型を定義するという行為は、ルールや制限を明確かつ厳格化することであり、表現力を豊かにする行為そのものです。

オブジェクトには何かしらのルールが存在し、型そのものや、型自体で知識やルールを表現/体現する効果があります。

最近はドメイン駆動設計が随分と盛り上がりを見せており、ドメインレイヤの実装では、ドメインを型と型のメソッドで知識を表現/体現します。まさにオブジェクト指向プログラミングです。

型の恩恵を受けられるチャンスを増やしていきましょう。

最後に、この言葉をおくります。

ゆるゆるな型を使うだけでは、せっかくの型がかたなし

ありがとうございました!


SPEEDAプロダクトユニットではエンジニアを募集しています

まずは気軽に話を聞いてみたい!という方はtwitter @tkitsunai 宛にリプライをしてみてください。型が大好きな方もこれから取り組みたい方も、様々な強みを持った弊チームで働きませんか?

応募はこちらから。

apply.workable.com