golangでコマンドラインツールを作る #9 Compositeパターン

f:id:masato47744:20150816150451p:plain

その8の続き

PBXGroup

Xcodeは実際のファイル構造じゃなくて好きな構造で表示できる。例えば実際のファイルは全部一つのフォルダに入ってたとしても、階層構造の表示できる。それを表現してるsectionがPBXGroup。で、PBXGroupはchildrenを持ってて、そこにはPBXGroupかPBXFileReferenceを持ってるって訳。

f:id:masato47744:20150816220244p:plain

ん、確かこのパターンどこかで見たことあるぞ・・あっ!これ進研ゼミで習ったやつだ!!

ってことで、Compositeパターンを思い出したのでやってみた。

Compositeパターン

ディレクトリ構造とか再帰的に辿る的なやつのときに使うやつ。 これ→11. Composite パターン | TECHSCORE(テックスコア)

今回は、GroupっていうtypeとFileReferenceっていうtypeを同一視するために、interfaceを宣言する。

interface名悩むなぁ。Xcodeだと、グループとかファイルの情報ってFile Inspectorっていうペインで見れる。 FileInspectorっていう名前か、Fileか、それか参考サイトみたいにDirectoryEntryにするか、うーむ。 よし、GruopEntryにしよう。

GroupEntryはグループかどうかを教えてくれるIsGroup()と、その子供達を返すChildren()、そのEntryの情報を返してくれるDescription()あたりがあればよさそう。

とりあえずそんな流れでいこう。

// GroupEntry describes xcode project's entry
// it is file or group
type GroupEntry interface {
    IsGroup() bool
    Children(p Pbxproj) []GroupEntry
    Description() string
}

Goの場合、interfaceを定義したらあとは同一視したいtype、今回でいうと、GroupFileReferenceでこのメソッドを実装してあげれば2つとも全然違うtypeなのに、GroupEntityとして処理できる。すばらしい。 実装の仕方は例えばIsGroupのやつはこんな感じ。レシーパーとしてそのstructを指定する。

func (g Group) IsGroup() bool {
    return true
}

func (f FileReference) IsGroup() bool {
    return false
}

ここで、ポインタをつけた方がいいのかつかなくてもいいのかがよく分からなかった・・ 紹介されてる記事だとよくポインタついてたりして、その辺ちゃんと理解しないといけなそう。(そのときがきたら考える)

再帰処理をどうする?

で、再帰処理をどう書くか悩んでて、GoはOSSだったこと思い出した! filepath.Walkと同じことやればいいやと思って実装パクることにした。 いやー、OSSってすばらしい。普段、iOS書いてるからすごく素晴らしく感じる。

filepath.Walkの処理はここに書いてある。

go/path.go at master · golang/go · GitHub

今回のやつの場合、丁寧にerror返すみたいなところまでやってないからその辺削ってこんな感じで実装した。 表示するときに階層構造表したいから、levelも渡すようにした。

// WalkFunc is the type of the function called for each fileReference or
// group visited by Walk.
type WalkFunc func(entry GroupEntry, level int)

// walk recursively descends group entry
func (p Pbxproj) walk(entry GroupEntry, level int, walkFn WalkFunc) {
    walkFn(entry, level)
    for _, c := range entry.Children(p) {
        p.walk(c, level+1, walkFn)
    }
}

// Walk walks the xcode project tree rooted at root
// calling walkFn for each group or file in the tree, including root
func (p Pbxproj) Walk(walkFn WalkFunc) {
    for _, g := range p.groups {
        if g.isRoot() {
            rootLevel := 0
            p.walk(g, rootLevel, walkFn)
        }
    }
}

で、コマンドで利用する側は、こんな感じで好き勝手できる。

proj.Walk(func(entry pbxproj.GroupEntry, level int) {
    if level == 0 {
        return
    }
    for i := 1; i < level; i++ {
        fmt.Print("  ")
    }
    if entry.IsGroup() {
        fmt.Println("+ " + entry.Description())
    } else {
        fmt.Println("  " + entry.Description())
    }
})

結果は、こんな感じ。それっぽい。今回は、グループの場合は、+表示してるけど、ほんとは色とか変えたり絵文字とか表示したりしたらXcodeでみてるやつっぽくなるかも?swiftだったら鳥の絵文字表示するとかw

+ Sample
    AppDelegate.swift
    ViewController.swift
    Images.xcassets
  + Supporting Files
      Info.plist
+ SampleTests
    SampleTests.swift
  + Supporting Files
      Info.plist
+ Products
    Sample.app
    SampleTests.xctest

ということである程度完成した。

ここまでの感想

  • ポインタがよくわかんなくなってきた。つけてもつけなくても動いてるっぽいのなぜ
  • 標準パッケージのソース見てたら意外と長い名前つけてる。forの変数は1文字にしないとと思ってたけどそんなことなさそう
  • 調べ物してるときにforの中でselect使ってる構文があったけどなんだろうか、あれは。
  • パッケージ名と変数名がかぶってたときってどうするんだ?
  • classがないっていうの、ポリシーというか設計者の思想が分かればすっと理解できるかな?
  • Enumないの?とふと思ってさらっと見てみたけど構文としてはなさそう