読者です 読者をやめる 読者になる 読者になる

struct に依存しない処理は function に切り出すのか、method に切り出すのか

golang

以前少し考えて自己解決して終わったんだけど、
ちょっとしたきっかけがあったのでアウトプットしてみる。

以下のような Person があって、
Hello(), Goodbye() には全く同じ「複雑な処理」がある場合・・・

type Person struct {
    name string
}

func (p *Person) Goodbye(input string) {
    //複雑な処理のつもり
    fmt.Println("common logic " + input)

    fmt.Println("goodbye " + p.name)
}

func (p *Person) Hello(input string) {
    //複雑な処理のつもり
    fmt.Println("common logic " + input)

    fmt.Println("hello " + p.name)
}



以前はこれを以下のように commonLogic() として method で切り出していた。

type Person struct {
    name string
}

func (p *Person) Goodbye(input string) {
    p.commonLogic(input)

    fmt.Println("goodbye " + p.name)
}

func (_ *Person) commonLogic(input string) {
    fmt.Println("common logic " + input)
}

func (p *Person) Hello(input string) {
    p.commonLogic(input)

    fmt.Println("hello " + p.name)
}



ここでポイントになるのが、
commonLogic() では receiver である Person を利用していない点。
commonLogic() は Person の field に紐づくわけではないが、
Person 内で完結する処理なので、
Person の method として切り出している。

自分は php, java をメインで利用していて、
struct = class のようなイメージがある。
そして、クラスのfieldに紐付かない重複処理であっても、
以下のようにクラスに属する private なメソッドとして切り出すことが多かった。
多かったというか、普通はこうすると思う。

<?php
class A {
    public function xxx() {
    }

    public function yyy() {
    }

    // 切り出した処理
    private function commonLogic() {
    }
}



なので、golang でも struct に属する package private な method として切り出していたが、
net/http/transport.go を読んだ時に、
こういった共通処理を function で切り出しているのを偶然見つけ・・・
https://github.com/golang/go/blob/release-branch.go1.6/src/net/http/transport.go#L345

golang では struct に紐付かない処理を切り出す場合は、
method よりも function で切り出した方がいいのかな?
と思ったので少し考えてみた。


重複処理を method に切り出した場合、struct からしか method を呼べない。
「その struct に関係する処理だから、その struct に実装する」
というのは問題ないと思う。

ただ、golang の場合
package private な method は package 内で実行できてしまう。
以下の Person は・・・

type Person struct {
    name string
}

func (_ *Person) commonLogic(input string) {
    fmt.Println("common logic " + input)
}



package 内であれば commonLogic() を実行できてしまう。

p := &Person{"pospome"}
p.commonLogic("my_input") //同じ package 内であれば、commonLogic() が呼べる

この commonLogic() は一見 Person に紐づいているが、
Person の値を利用することはない。
Person に関係ない処理が Person に紐づくのは違和感がある。

php, java の場合はクラス単位で private になるので、
同一 package 内であっても、commonLogic() は呼べない。
クラスの利用者は常にクラスに紐付いた振る舞いを呼ぶことになるので、
違和感なく利用できる。


golang は struct 単位の private がないので、
package 内ではこういった自然な振る舞いを提供できない。
「struct に紐付いていない振る舞いは struct に実装する必要がない」
というのは自然な考えのように思える。

function の面倒なところは名前が package 内でユニークになるという点。
function が増えすぎると、prefix, safix が多くなったり、
他の function と区別するために命名が冗長になるかもしれない。

ただ、function が増えて管理しづらいということは、
その package が大きすぎる可能性がある。
function や struct の性質によって package を分割することを考えた方がいいかもしれない。

また、function は値と振る舞いを一緒に持たないので、
function が増えて管理できないということは、
本来 struct, primitive type にすべきものを見落としている可能性がある。

例えば、以下の Person でいうと・・・

type Person struct {
    name string
}

func (p *Person) Goodbye(input string) {
    p.commonLogic(input)

    fmt.Println("goodbye " + p.name)
}

func (_ *Person) commonLogic(input string) {
    fmt.Println("common logic " + input)
}

func (p *Person) Hello(input string) {
    p.commonLogic(input)

    fmt.Println("hello " + p.name)
}



以下のように string の input を primitive type にして、
commonLogic() を実装した方がいいのかもしれない。

type Input string

func (i Input) commonLogic() {
    fmt.Println("common logic " + i)
}

type Person struct {
    name string
}

func (p *Person) Goodbye(i Input) {
    i.commonLogic()

    fmt.Println("goodbye " + p.name)
}

func (p *Person) Hello(input string) {
    i.commonLogic()

    fmt.Println("hello " + p.name)
}


https://github.com/golang/go/blob/release-branch.go1.6/src/net/http/transport.go#L345

逆に言うと、上記の checkTransportResend() のロジックは
引数になっている Request, persistConn のどちらかに対して実装すると
責務として不自然になってしまうのだと思う。
かといって、Request, persistConn を持ち、
checkTransportResend() を実装するような struct を作るまでもない、
もしくは、仕様上そのような概念が存在しないので、
function として切り出しているんだと思う。
http.checkTransportResend() というのが自然な文脈になる。
DDD でいうところのドメインサービス的なイメージかな。

ちなみに、struct に紐付かなくても、
以下のように特定の struct に対する固定値を実装する場合に method を利用するのは問題ないと思います。

type A struct {
}

func (_ *A) isXxx() bool {
    return true
}


結論

今回の内容は transport.go を見た時に、
他の標準パッケージやdockerのソースもサラッと見て、
こういった傾向があるのかなと思って考えたものですが、
全てのコードを調べたわけでもないので、
「function を利用する」と言い切れるものではないです。
*もし、private mehtod に切り出している処理があれば教えてください。

記事中の例では「共通処理を切り出すケース」について説明しましたが、
長いメソッドを切り出す際にも
function として切り出すのか、
method として切り出すのかを意識して書く必要があると思います。

皆さんはどうお考えでしょうか?

何か意見があれば、
コメント or twitter にメンションください。