はじめに
if分岐を減らすコードの有用性⇒なぜか三項演算子の話に置き換えられるってのをTwitterで見かけたので、実例を挙げてみました。
言語に寄らない知識なので使う言語はなんでもいいのですが、私が今使っているgolangで説明させていただきます。
なんで条件分岐が少ないと嬉しいのか?
個人的には、頭に優しくなるのが一番のメリットだと思っています。
- 頭に優しい
- どこに分岐があるのかを考える箇所が減る
- 今なんの分岐中なのか?を考える箇所が減る
もう少し客観的な理由をまとめます。
影響範囲の複雑さを減らす
分岐処理がいたるところにあると、分岐処理が増えた時にいたるところに手を入れる必要があります。
しかし、これが分岐を減らして特定の場所に分岐がまとまっていくと、結果分岐に影響するコードの範囲が減ります。
疎結合なコードを作るのは大事ですよね
テストの複雑さを減らす
テスト設計、皆さんどうしていますか?
テスト観点には以下があります。
- 中身を気にしないブラックボックステストの網羅性(入力パターンを如何に網羅するか?)
- 中身ベースのホワイトボックステストの網羅性
ホワイトボックステストの網羅性には、命令網羅、分岐網羅、条件網羅、複合条件網羅といったものがあります。順にテストパターンが増えていきます。
説明は省略しますが、命令網羅以外は全て条件分岐が関わってきているのがポイントです。
そのため、上記の網羅性を保ったテストをするには、条件分岐が関わる。つまり条件分岐が減るほど、少ないテストでのパターン網羅がしやすいということになります。
というわけで、テストに関する指標に分岐が関わってくるわけです。
コードカバレッジもこの網羅性が関わってきますね。
- もちろんカバレッジが高い=品質がいいと決まるわけではないです。参考: テストカバレッジ100%を追求しても品質は高くならない理由と推奨されるカバレッジの目標値について
メトリクスで見る複雑さを減らす
コードの複雑さをはかる指標は世の中に沢山あり、その数値を測定するツール(メトリクス計測)も沢山あります。
その中の指標の一つにサイクロマティック複雑度があるんですが、これもテストの時と同じく分岐が増えるほど複雑さが増える仕組みになっています。
(算出方法はテストの時と同じ網羅性ですが)
対象外: 1行。でも分岐が減っていない三項演算子
例えば以下は、条件分岐を三項演算子を使って一行で記載しています。golangは三項演算子が無いのでC言語の例。
これにより分岐が無くなり、コードの網羅性を簡単に担保できるようになりました!やったぜ
#include <stdio.h>
/*
if (a==0) {
return "value is 0\n";
} else if ((a)==1) {
return "value is 1\n";
} else {
"value is not 0 or 1\n";
}
の3条件による分岐が下記1行で書かれている
*/
#define TERNARY_OPERATOR(a) ((a)==0)?"value is 0\n":((a)==1)?"value is 1\n":"value is not 0 or 1\n"
int main(void){
// Your code here!
printf(TERNARY_OPERATOR(0));
printf(TERNARY_OPERATOR(1));
return 0;
}
ってそうじゃないですよね。これはただ分岐を一行に書いただけです。上記ならTERNARY_OPERATORが0, 1以外のパターンを網羅できていないですし。
その1. Mapを使う
Mapと呼ばれる、key/valueのペアになっている配列を使います。
C++やGolang, Rubyだとmap。JavaならHashMap, pythonならdictとか、言語によって名前が違います。
ただ同じ機能はほぼ全ての言語にあるので、言語+mapとか、言語+配列+key辺りのキーワードで検索すれば引っかかると思います。Cはないけど
package main
import "fmt"
// Your code here!
var sayings = map[string]string {
"dog": "woof",
"cat": "meow",
"elephant": "toot",
}
//map以外のkeyはないものとする
func crying(animal string) {
fmt.Println(animal + " is " + sayings[animal])
}
func main(){
crying("dog")
crying("cat")
crying("elephant")
crying("no animal")
}
その2. interface classを使う。strategyパターン
戦略を意味する言葉で、interfaceクラスの実体を差し替えることで、処理は変えずに中身の戦略を変えるという考え。
RPGがイメージしやすいかもしれません。戦闘時にキャラクターを選んで戦闘へ。同じ「戦え!」という選択に対する行動はキャラクターによって変わります。呪文を使ったり特殊能力を使ったり。
コードだとこんな感じすかね。Comparatorの部分
package main
import "fmt"
//interface定義
type RPGCharactor interface {
Action()
}
//各実体を定義
type Fighter struct {
//男は黙って 拳
}
func (f *Fighter) Action() {
fmt.Printf("武道家の攻撃!男は黙って 拳!\n")
}
//実体の中に持っているデータは自由。Actionが異なる
type Warrior struct {
Sword string//型は適当
}
func (w *Warrior) Action() {
fmt.Printf("戦士の攻撃!%sで切り付ける!\n", w.Sword)
}
type Witch struct {
Spell string//型は適当
}
func (w *Witch) Action() {
fmt.Printf("魔法使いの攻撃!%sの呪文を唱えた!\n", w.Spell)
}
Comparatorを扱う側の部分。コンストラクタ等で違いが作れます。
type Party struct {
Member RPGCharactor
}
func (p *Party) Attack() {
p.Member.Action()
}
func (p *Party) Join(member RPGCharactor) {
p.Member = member
}
func main(){
party := Party{}
//武道家の場合
party.Join(&Fighter{})
party.Attack()
//戦士の場合
party.Join(&Warrior{"エクスカリバー"})
party.Attack()
//魔法使いの場合
party.Join(&Witch{"メドローア"})
party.Attack()
}
その3. interface classを使う。stateパターン
Strategyと似てますが、前者はinterfaceを投入して処理を差し替えるイメージ。こちらは対応するinterfaceを持っておいて、内部で状態にあわせてパチパチ切り替えるイメージだと思っています。関数テーブルとかのイメージに近い。
こんな風に状態毎のActionを実装して、
package main
import "fmt"
//状態
const (
NORMAL = "normal"
MP0 = "mp0"
DYING = "dying"
DEAD = "dead"
DEFAULT = NORMAL
)
//interface定義
type RPGCharactorAction interface {
Action()
}
type Witch struct {
Spell string//型は適当
State string
acts map[string]RPGCharactorAction
}
func NewWitch(spell string) Witch{
w := Witch{}
//状態毎の行動を登録
w.acts = make(map[string]RPGCharactorAction, 0)
w.acts[NORMAL] = &WitchNormal{spell}
w.acts[MP0] = &WitchMP0{}
w.acts[DYING] = &WitchDying{}
w.acts[DEAD] = &WitchDead{}
//初期状態を登録
w.State = DEFAULT
return w
}
func (w *Witch) Action() {
w.acts[w.State].Action()
}
func (w *Witch) ChangeState(state string) {
//一応無効stateをチェック
if _, ok := w.acts[state]; !ok {
fmt.Printf("無効なstate\n")
return
}
w.State = state
}
//各実体を定義
type WitchNormal struct {
Spell string//型は適当
}
func (w *WitchNormal) Action() {
fmt.Printf("魔法使いの攻撃!%sの呪文を唱えた!\n", w.Spell)
}
type WitchMP0 struct {
}
func (w *WitchMP0) Action() {
fmt.Printf("魔法使いの攻撃!杖で叩いた!\n")
}
状態を変えると実際の動作が変わるようになっています。
type Party struct {
Witch Witch
}
func main(){
party := Party{NewWitch("メドローア")}
party.Witch.Action()
fmt.Printf("=====調子に乗ってMPを使い切った!=====\n")
party.Witch.ChangeState(MP0)
party.Witch.Action()
fmt.Printf("=====味方がMP回復!=====\n")
party.Witch.ChangeState(NORMAL)
party.Witch.Action()
}
コード全文
package main
import "fmt"
//状態
const (
NORMAL = "normal"
MP0 = "mp0"
DYING = "dying"
DEAD = "dead"
DEFAULT = NORMAL
)
//interface定義
type RPGCharactorAction interface {
Action()
}
type Witch struct {
Spell string//型は適当
State string
acts map[string]RPGCharactorAction
}
func NewWitch(spell string) Witch{
w := Witch{}
//状態毎の行動を登録
w.acts = make(map[string]RPGCharactorAction, 0)
w.acts[NORMAL] = &WitchNormal{spell}
w.acts[MP0] = &WitchMP0{}
w.acts[DYING] = &WitchDying{}
w.acts[DEAD] = &WitchDead{}
//初期状態を登録
w.State = DEFAULT
return w
}
func (w *Witch) Action() {
w.acts[w.State].Action()
}
func (w *Witch) ChangeState(state string) {
//一応無効stateをチェック
if _, ok := w.acts[state]; !ok {
fmt.Printf("無効なstate\n")
return
}
w.State = state
}
//各実体を定義
type WitchNormal struct {
Spell string//型は適当
}
func (w *WitchNormal) Action() {
fmt.Printf("魔法使いの攻撃!%sの呪文を唱えた!\n", w.Spell)
}
type WitchMP0 struct {
}
func (w *WitchMP0) Action() {
fmt.Printf("魔法使いの攻撃!杖で叩いた!\n")
}
type WitchDying struct {
}
func (w *WitchDying) Action() {
fmt.Printf("魔法使いは回復呪文を使った!しかし効果がなかった。\n")
}
type WitchDead struct {
}
func (w *WitchDead) Action() {
fmt.Printf("魔法使いは死んでしまった…\n")
}
type Party struct {
Witch Witch
}
func main(){
party := Party{NewWitch("メドローア")}
party.Witch.Action()
fmt.Printf("=====調子に乗ってMPを使い切った!=====\n")
party.Witch.ChangeState(MP0)
party.Witch.Action()
fmt.Printf("=====味方がMP回復!=====\n")
party.Witch.ChangeState(NORMAL)
party.Witch.Action()
fmt.Printf("=====モンスターの攻撃を受けた!=====\n")
party.Witch.ChangeState(DYING)
party.Witch.Action()
fmt.Printf("=====あ、MPが尽きた。。。=====\n")
party.Witch.ChangeState(MP0)
party.Witch.Action()
fmt.Printf("=====え、なんで攻撃してんの。。あ。瀕死の行動を優先してない。。。=====\n")
party.Witch.ChangeState(DEAD)
party.Witch.Action()
fmt.Printf("=====Game Over=====\n")
}
また、状態が移動するイベントx状態の組み合わせで処理が変わるように工夫すると、状態遷移図(StateMachine)をコードで表せるので便利です。
その4. 番外編 NullObjectパターン
限定的だけど割とよくある話。想定外なコードを書いとくべきだけど、そのケースでは特に何もしなくていい時。
エラーを握りつぶすとかではなくて、特定のケースだけログを表示して後は無言とか、mapのvalidationで範囲外の為の実装を毎回したくない!とか、そんな時に何もしない!をするクラスを作ります。(「何もない」があるのよ というよつばと風香のセリフを思い出す)
さっきのStateパターンの例だと、ChangeStateで無効なstateが入らないようにしてAction関数内のw.acts[w.state]
が範囲外にならないことを保証していました。
…でもそれって大丈夫?ChangeStateがActionと依存しているし、そもそも w.State
がいじられたら?とか考えると、Action内で範囲チェックしたくなります。
func (w *Witch) Action() {
w.acts[w.state].Action()
}
func (w *Witch) ChangeState(state string) {
//一応無効stateをチェック
if _, ok := w.acts[state]; !ok {
fmt.Printf("無効なstate\n")
return
}
w.state = state
}
じゃあAction関数で範囲チェックする?とならないよう、処理本体のクラスを取得してきて実行するようにする。その実行は絶対に失敗しないように何もしないObjectを渡すというのがNullObjectパターンの発想です。各所のNullチェックの代わりに、Objectを作るところだけに分岐を入れてますね
func (w *Witch) Action() {
w.getActor().Action()
}
func (w *Witch) getActor() RPGCharactorAction {
//一応無効stateをチェック
act, ok := w.acts[w.State]
if !ok {
//無効な値なら何もしないObjectを返す
return &NullAction{}
}
return act
}
type NullAction struct {
}
func (n *NullAction)Action() {
//何もしない
}
コード全文
package main
import "fmt"
//状態
const (
NORMAL = "normal"
MP0 = "mp0"
DYING = "dying"
DEAD = "dead"
DEFAULT = NORMAL
)
//interface定義
type RPGCharactorAction interface {
Action()
}
type Witch struct {
Spell string//型は適当
State string
acts map[string]RPGCharactorAction
}
func NewWitch(spell string) Witch{
w := Witch{}
//状態毎の行動を登録
w.acts = make(map[string]RPGCharactorAction, 0)
w.acts[NORMAL] = &WitchNormal{spell}
w.acts[MP0] = &WitchMP0{}
w.acts[DYING] = &WitchDying{}
w.acts[DEAD] = &WitchDead{}
//初期状態を登録
w.State = DEFAULT
return w
}
func (w *Witch) Action() {
w.getActor().Action()
}
func (w *Witch) getActor() RPGCharactorAction {
//一応無効stateをチェック
act, ok := w.acts[w.State]
if !ok {
//無効な値なら何もしないObjectを返す
return &NullAction{}
}
return act
}
type NullAction struct {
}
func (n *NullAction)Action() {
//何もしない
}
func (w *Witch) ChangeState(state string) {
w.State = state
}
//各実体を定義
type WitchNormal struct {
Spell string//型は適当
}
func (w *WitchNormal) Action() {
fmt.Printf("魔法使いの攻撃!%sの呪文を唱えた!\n", w.Spell)
}
type WitchMP0 struct {
}
func (w *WitchMP0) Action() {
fmt.Printf("魔法使いの攻撃!杖で叩いた!\n")
}
type WitchDying struct {
}
func (w *WitchDying) Action() {
fmt.Printf("魔法使いは回復呪文を使った!しかし効果がなかった。\n")
}
type WitchDead struct {
}
func (w *WitchDead) Action() {
fmt.Printf("魔法使いは死んでしまった…\n")
}
type Party struct {
Witch Witch
}
func main(){
party := Party{NewWitch("メドローア")}
party.Witch.Action()
fmt.Printf("=====無効な状態!=====\n")
party.Witch.ChangeState("Unknown")
party.Witch.Action()
fmt.Printf("=====調子に乗ってMPを使い切った!=====\n")
party.Witch.ChangeState(MP0)
party.Witch.Action()
fmt.Printf("=====味方がMP回復!=====\n")
party.Witch.ChangeState(NORMAL)
party.Witch.Action()
fmt.Printf("=====モンスターの攻撃を受けた!=====\n")
party.Witch.ChangeState(DYING)
party.Witch.Action()
fmt.Printf("=====あ、MPが尽きた。。。=====\n")
party.Witch.ChangeState(MP0)
party.Witch.Action()
fmt.Printf("=====え、なんで攻撃してんの。。あ。瀕死の行動を優先してない。。。=====\n")
party.Witch.ChangeState(DEAD)
party.Witch.Action()
fmt.Printf("=====Game Over=====\n")
}
最後に
分岐を減らすってのは、単純にif文やswitch文を別の書き方で書くってわけではないよ。って例をいくつか記載しました。
これが正解!って決まった手段があるわけではないですし、プロジェクト事情で出来ること出来ないことはあると思います。
そういった中で理屈を持ってコード構成を考えるのが、コーディングの面白いところの一つだと思います。
全部を見直すことは無理でも、出来るところから条件分岐を減らしていきましょう!
参考
記事を書いたきっかけクソコード動画「switch文」解説