Help us understand the problem. What is going on with this article?

命名の大事さを遠回りに体感してみる

命名の大事さを遠回りに体感してみる

メディアドゥ アドベントカレンダーの記事です。

私は医者でも学者でもありませんが、今回は認識とかそういったものから、分かりやすい命名とは、というものを遠回りに体感してみます。

はじめに

私は、文字を読むことが苦手です。気合いを入れれば人並みの速度で読むことができます。例えば、紙に印刷された文字などは、歪みが発生すると、読むことが困難になります。そのため、技術書は電子版で購入することがほとんどです。また、マンガを読むことは大変です。絵と、吹き出しの中の文字と、オノマトペの文字が入り混じり、そのうちに絵と文字の区別がつかず、なかなか苦労をします。アクション要素強めのマンガでも、単行本1冊に3時間などかかりますが、ジョジョの奇妙な冒険は、6部まで読みました。現在7部の途中です。ジョナサン・ジョースターが最も好きなキャラクターです。絵柄は2部が好きなため、初期のゴツい絵柄とキャラクターが、私の趣向にあっているのだと思います。

ただ、プログラムのコードは、日々触れていることによる慣れからか、読むことに苦労を覚えません。プログラムのコードは、英数字と、使用可能な記号で、構成されています。

そうして日々を過ごし、チームで開発する上で、可読性を意識したコードの事を考えますが、人にとって読みやすい命名というのはどういうことかを、改めて考えてみると、本記事の内容が頭の中に浮かびました。

文字の認識

プログラムのコードは文字ですが、私達が普段触れ合う文書と同じ用に脳の中では扱われるのでしょうか。

音声化

人間は、文字から意味を認識する際に、文字から意味へと直接マッピングしているわけではないようです。文字を視覚により認識した後、音声化が行われ、介して意味へとつながっているということらしいです。言われてみれば、確かにそのように思います。

文字の順番

タイポグリセミアというものがあるようです。

タイポグリセミアとは Wikipediaリンク

タイポグリセミア(Typoglycemia)は、文章中のいくつかの単語で最初と最後の文字以外の順番が入れ替わっても正しく読めてしまう現象である。

試しに、本記事の文章の一部を単語ごとで分割し、バラバラにしてみます。

ジョョ冒の妙奇ジな険 は 、 6ま部読でましみた。 現7の途在中部です 。 ジナジサンョ・ターョスー が 最好きもなキタクャラーです 。 絵は部2柄がきた好なめ 、 初ゴ期ツのい絵と柄キクラャターが 、 私にの趣あって向るだいとの思まいす。

ひらがなでやってみます。

じじょょ の きょみう な ぼけうん は ろくぶ まで よましみた。げざんい ななぶ の とゅでうちす。じさなょん じすょーたー が もっすきともな きくゃらたー です。えらがは にぶ が すたきなめ しきょの ごつい えがら と きゃらたくー が わしたの しこゅう に あっている の だと おいまもす。

何となく読めるといえば読めますが、しっくりくるかというとそうでもないと感じます。ひらがなの方が、単語の区切りで分けていることもあり、読みやすく感じます。上述の音声化を加味すると、文字の順番がバラバラであっても、それを脳内で再構成し、意味の通る順番へ並べ替え、音声化する際には、好ましい順番に直した状態になっていると考えられます。

ここで言えること

  • 文字は頭の中で音声化されている (と言える)
  • 文字の順番はそれほど重要ではない (と言える)

プログラム コードへの転用

サンプルに、挨拶するプログラムを思うままに書きました。本来であれば、エンティティと値オブジェクトと、処理はサービスとして分けて・・・とコード自体をパッケージやファイルで分割するので1ファイルあたりはエディタのスクロールが発生しないレベルまで小さくします。今回は掲載が記事であることから、一望しやすいように一つのファイルにまとめています。

// あいさつプログラム

package main

import "fmt"

func main() {
    f := &greeterFactory{}
    g := &greeting{
        from: f.makeHuman("kent"),
        to:   f.makeDog(shiba),
    }
    g.start()
    g = &greeting{
        from: f.makeDog(golden),
        to:   f.makeDog(dalmatian),
    }
    g.start()
}

// モデルとなる、インターフェースと構造体の定義
type greeter interface {
    greet() string
}

type human struct {
    name string
}

func (h *human) greet() string {
    return fmt.Sprintf("こんにちは、わたしは %s です", h.name)
}

type dog struct {
    roar string
}

func (d *dog) greet() string {
    return fmt.Sprintf("%s だワン!", d.roar)
}

// ファクトリーの定義
type breed int

const (
    shiba breed = iota + 1
    golden
    dalmatian
)

type greeterFactory struct{}

func (g *greeterFactory) makeHuman(n string) greeter {
    return &human{name: n}
}

func (g *greeterFactory) makeDog(b breed) greeter {
    switch b {
    case shiba:
        return &dog{roar: "ワン"}
    case golden:
        return &dog{roar: "ガオー"}
    case dalmatian:
        return &dog{roar: "ニャー"}
    default:
        return &dog{roar: "我は何者"}
    }
}

// ロジックの定義
type greeting struct {
    from greeter
    to   greeter
}

func (g *greeting) start() {
    fmt.Println("あいさつ かいし!")
    fmt.Println(g.from.greet())
    fmt.Println(g.to.greet())
    fmt.Println("あいさつ おわり!")
}
実行結果
あいさつ かいし!
こんにちは、わたしは kent です
ワン だワン!
あいさつ おわり!
あいさつ かいし!
ガオー だワン!
ニャー だワン!
あいさつ おわり!

それほどの処理もないため、変化させても効果は乏しいような気がしますが、色々と変えて実験してみます。

タイポグリセミア化

読みにくくなるだけですが、タイポグリセミア化してみます。

// あさいつプラグロム

package main

import "fmt"

func main() {
    f := &greteerFcatroy{}
    g := &greeting{
        form: f.mkaeHmaun("knet"),
        to:   f.mkaeDog(sbhia),
    }
    g.sartt()
    g = &greeting{
        form: f.mkaeDog(gloedn),
        to:   f.mkaeDog(damiatlan),
    }
    g.sartt()
}

// モデルとなる、インーフターェスと構造体の定義
type geterer interface {
    geret() string
}

type hmaun struct {
    nmae string
}

func (h *hmaun) geret() string {
    return fmt.Sprintf("こちんには、わしたは %s です", h.nmae)
}

type dog struct {
    raor string
}

func (d *dog) geret() string {
    return fmt.Sprintf("%s だワン!", d.raor)
}

// フクァリトーの定義
type beerd int

const (
    sbhia beerd = iota + 1
    gloedn
    damiatlan
)

type greteerFcatroy struct{}

func (g *greteerFcatroy) mkaeHmaun(n string) geterer {
    return &hmaun{nmae: n}
}
func (g *greteerFcatroy) mkaeDog(b beerd) geterer {
    switch b {
    case sbhia:
        return &dog{raor: "ワン"}
    case gloedn:
        return &dog{raor: "ガオー"}
    case damiatlan:
        return &dog{raor: "ニャー"}
    default:
        return &dog{raor: "我何は者"}
    }
}

// ロッジクの定義
type greeting struct {
    form geterer
    to   geterer
}

func (g *greeting) sartt() {
    fmt.Println("あさいつ かいし!")
    fmt.Println(g.form.geret())
    fmt.Println(g.to.geret())
    fmt.Println("あさいつ おわり!")
}
実行結果
あさいつ かいし!
こちんには、わしたは knet です
ワン だワン!
あさいつ おわり!
あさいつ かいし!
ガオー だワン!
ニャー だワン!
あさいつ おわり!

意外と読めますね。fromがformになる辺りは、単語として成り立ったままのため、困惑しますが、いけそうです。文字の順番より、音より何より、言語構文のおかげで読むことができるという感じがしますが。関数や構造体の名前などが重要でなければ、変数名だけでなく、それらも1文字でも良さそうです。

全て1文字にする

アルファベットですと使える文字数が少なく、重複によりコンパイルエラーとなるため、一部は2文字にしています。頭の中では音声化する必要もありません。

// あいさつプログラム

package main

import "fmt"

func main() {
    f := &f{}
    g := &gg{
        f: f.h("kent"),
        t: f.d(bs),
    }
    g.s()
    g = &gg{
        f: f.d(bg),
        t: f.d(bd),
    }
    g.s()
}

// モデルとなる、インターフェースと構造体の定義
type g interface {
    g() string
}

type h struct {
    n string
}

func (h *h) g() string {
    return fmt.Sprintf("こんにちは、わたしは %s です", h.n)
}

type d struct {
    r string
}

func (d *d) g() string {
    return fmt.Sprintf("%s だワン!", d.r)
}

// ファクトリーの定義
type b int

const (
    bs b = iota + 1
    bg
    bd
)

type f struct{}

func (f *f) h(n string) g {
    return &h{n: n}
}

func (f *f) d(b b) g {
    switch b {
    case bs:
        return &d{r: "ワン"}
    case bg:
        return &d{r: "ガオー"}
    case bd:
        return &d{r: "ニャー"}
    default:
        return &d{r: "我は何者"}
    }
}

// ロジックの定義
type gg struct {
    f g
    t g
}

func (g *gg) s() {
    fmt.Println("あいさつ かいし!")
    fmt.Println(g.f.g())
    fmt.Println(g.t.g())
    fmt.Println("あいさつ おわり!")
}

割と読むことができます。このコードを作成している間、半分ほど過ぎたところでスラスラと書くことができました。頭の中に「〜〜〜は***のコード」と、辞書が出来上がってくれば、難なく読み書きできました。この場合、コメントがかなり助けていると考えられます。初めてこのコードを読むとコメントがないと全くわかりません、特にmain関数は呪文です。しかし、文字数が少ないため、前述のコードよりも、この1文字〜2文字の名前で構成されたコードのほうが、字面はスッキリとしています。

日本語にする

(私にとって)母国語であれば、最も読みやすそうです。

// あいさつプログラム

package main

import "fmt"

func main() {
    製造機 := &挨拶さん製造機{}
    処理 := &挨拶処理{
        先に言う人: 製造機.人間を作る("kent"),
        後に言う人: 製造機.犬を作る(柴犬),
    }
    処理.始める()
    処理 = &挨拶処理{
        先に言う人: 製造機.犬を作る(ゴールデンレトリバー),
        後に言う人: 製造機.犬を作る(ダルメシアン),
    }
    処理.始める()
}

// モデルとなる、インターフェースと構造体の定義
type 挨拶さん interface {
    挨拶する() string
}

type 人間 struct {
    名前 string
}

func (この人の *人間) 挨拶する() string {
    return fmt.Sprintf("こんにちは、わたしは %s です", この人の.名前)
}

type  struct {
    鳴き声 string
}

func (この犬の *) 挨拶する() string {
    return fmt.Sprintf("%s だワン!", この犬の.鳴き声)
}

// ファクトリーの定義
type 犬種 int

const (
    柴犬 犬種 = iota + 1
    ゴールデンレトリバー
    ダルメシアン
)

type 挨拶さん製造機 struct{}

func (この製造機 *挨拶さん製造機) 人間を作る(渡す名前 string) 挨拶さん {
    return &人間{名前: 渡す名前}
}

func (この製造機 *挨拶さん製造機) 犬を作る(渡す犬種 犬種) 挨拶さん {
    switch 渡す犬種 {
    case 柴犬:
        return &{鳴き声: "ワン"}
    case ゴールデンレトリバー:
        return &{鳴き声: "ガオー"}
    case ダルメシアン:
        return &{鳴き声: "ニャー"}
    default:
        return &{鳴き声: "我は何者"}
    }
}

// ロジックの定義
type 挨拶処理 struct {
    先に言う人 挨拶さん
    後に言う人 挨拶さん
}

func (この挨拶処理の *挨拶処理) 始める() {
    fmt.Println("あいさつ かいし!")
    fmt.Println(この挨拶処理の.先に言う人.挨拶する())
    fmt.Println(この挨拶処理の.後に言う人.挨拶する())
    fmt.Println("あいさつ おわり!")
}

英語で書かれている部分は、言語ルール上必ずそうなる部分や、外部パッケージであり、日本語である部分は自前で書いた部分ということがすぐに読み取ることができます。読みやすいかどうかはわかりませんが、「この〜の.〜.〜()」と、日常の言葉のように流れているあたりは意図が伝わりやすそうです。字面は、ごちゃごちゃとしています。形のシンプルさではアルファベットにかないません。ロジック中の処理は、流れがある方がわかりやすく、定義など独立している部分は、字面がスッキリしていることが良さそうです。

説明をつける

ここまでで下記のことが大事であることがわかりました。

  • 頭の中に定義の辞書が出来上がっていること
  • 字面はスッキリしている方が良い

それであれば、命名が極端に短縮されたものでも、一定のルールがあり、そのルールが説明されていれば、すんなりと読むことができるかを試してみます。

/*
あいさつプログラム

型 命名、関数 命名

    型名 末尾 F : インスタンス生成のための、ファクトリを表します
        返却する型名がそのまま関数名になっています
    型名 末尾 L : 処理本体である、ロジックを表します
        処理の実行はdo関数です
*/

package main

import "fmt"

func main() {
    f := &greeterF{}
    g := &greetL{
        from: f.human("kent"),
        to:   f.dog(shiba),
    }
    g.do()
    g = &greetL{
        from: f.dog(golden),
        to:   f.dog(dalmatian),
    }
    g.do()
}

// モデルとなる、インターフェースと構造体の定義
type greeter interface {
    greet() string
}

type human struct {
    name string
}

func (h *human) greet() string {
    return fmt.Sprintf("こんにちは、わたしは %s です", h.name)
}

type dog struct {
    roar string
}

func (d *dog) greet() string {
    return fmt.Sprintf("%s だワン!", d.roar)
}

// ファクトリーの定義
type breed int

const (
    shiba breed = iota + 1
    golden
    dalmatian
)

type greeterF struct{}

func (g *greeterF) human(n string) greeter {
    return &human{name: n}
}

func (g *greeterF) dog(b breed) greeter {
    switch b {
    case shiba:
        return &dog{roar: "ワン"}
    case golden:
        return &dog{roar: "ガオー"}
    case dalmatian:
        return &dog{roar: "ニャー"}
    default:
        return &dog{roar: "我は何者"}
    }
}

// ロジックの定義
type greetL struct {
    from greeter
    to   greeter
}

func (g *greetL) do() {
    fmt.Println("あいさつ かいし!")
    fmt.Println(g.from.greet())
    fmt.Println(g.to.greet())
    fmt.Println("あいさつ おわり!")
}

特に読みやすさは変わらないように感じます。また、それほど字面もスッキリしませんでした。

まとめ

有名なオープンソースのコードなどを見ても、上述したような妙な命名は殆どありませんし、GoDocでパッケージや型に説明をつけるため、結局は普通が一番ということになります。

ありきたりな言葉になりますが、つまるところ重要なのは・・・。

  • シンプルであること
  • 意図が説明されていること
  • 全体で一貫したルールがあること

ということであり、それをチームで遵守することと言えます。命名で迷ったら、それらの基準に立ち返ると、悩みも解決できるかもしれません。

なお、ドメイン駆動設計の場合は、ドメイン知識を中心に命名をするため、そもそもそういった議論はユビキタス言語の策定のフェーズで殆ど終わります。

そういうところで、あえて命名を普段ではやらない形をいくつか試してみましたが、改めて大事さを体感することができました。

mediado
私たちメディアドゥは、電子書籍を読者に届けるために「テクノロジー」で「出版社」と「電子書店」を繋ぎ、その先にいる作家と読者を繋げる「電子書籍取次」事業を展開しております。業界最多のコンテンツラインナップとともに最新のテクノロジーを駆使した各種ソリューションを出版社や電子書店に提供し、グローバル且つマルチコンテンツ配信プラットフォームを目指しています。
https://mediado.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away