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

同じことをしているのに(文脈の大切さと難しさ)

More than 1 year has passed since last update.

ここで言う「文脈」とは?

この言葉自体が非常に難しく
はっきりと「これだ」ということはできませんが

ここでは

作成者の意図(こうして欲しいという思い)

ということで話を進めていきたいと思います。

はじめに

新しくプロジェクトに参加した時や新しい機能が追加された時、
なるべく早く内容を理解したいと思います。
そんな時にコードの文脈がはっきりしているかしていないかによって
理解をする早さも変わってくると思います。

もちろん理解の仕方や捉え方は人それぞれで異なるので
何が正しいかということは言えませんが
今回は
文脈の有無や明確さによってどう変わってくるのか
どうしたらもっと明確になるのか

などを考えていきたいと思います。

環境

使用言語: Swift5.0
IDE: Xcode10.2.1

文脈がないと...

コードを読んでいる時に
文脈がないと何をしたいのかが見えづらくなってしまいます。

例えば、下記のコードを見てみてください。

struct Something {
    let hoges: [Bool] = {
        var temp = [Bool]()
        var isHoge = false
        for i in 1...8 {
            for j in 1...8 {
                temp.append(isHoge)
                isHoge = !isHoge
            }
            isHoge = !isHoge
        }
        return temp
    }()

    func render() -> String {
        var result = ""
        hoges.enumerated().forEach { index, isHoge in

            if isHoge {
                result += "■"
            } else {
                result += "□"
            }

            if (index + 1) % 8 == 0 {
                result += "\n"
            }
        }
        return result
    }
}

これは何を表しているでしょうか?

おそらくパッと見ただけではわからないと思います。

では、下記の場合はどうでしょうか?

struct Chessboard {
    let boardColors: [Bool] = {
        var temporaryBoard = [Bool]()
        var isBlack = false
        for i in 1...8 {
            for j in 1...8 {
                temporaryBoard.append(isBlack)
                isBlack = !isBlack
            }
            isBlack = !isBlack
        }
        return temporaryBoard
    }()

    func render() -> String {
        var result = ""
        boardColors.enumerated().forEach { index, isBlack in
            if isBlack {
                result += "■"
            } else {
                result += "□"
            }

            if (index + 1) % 8 == 0 {
                result += "\n"
            }
        }
        return result
    }
}

structの名前からもわかるように
これはチェスのボードを表していました。

どちらもやっていることは全く同じなのに
どんなことをしているのかの明確さは全然違います。

このように
文脈があるかないかで
理解のしやすさや早さは全然変わってくるのではないでしょうか。

※ ちなみにこのサンプルはSwiftの公式のガイドから引用しています。
https://docs.swift.org/swift-book/LanguageGuide/Initialization.html#//apple_ref/doc/uid/TP40014097-CH18-ID232

文脈を伝えようとしても適度な量が難しい

上記のメソッドは明らかに文脈を伝えようとしていませんでした。

一方で文脈を伝えようとしたとしても
思ったように伝わらないこともあるのかなと思うことがあります。

先ほどとは別の例を挙げてみます。

ちょっと情報が多い?

まず出てくる内容として
買い物かごと商品を表すstructがあるとします。

struct Product {
    let name: String
    let price: Int
}

struct ShoppingBasket {
    var products: [Product] = []
}

ここで、買い物かごに商品を追加するメソッドについて
考えてみます。

extension ShoppingBasket {
    mutating func add(product: Product) {
        products.append(product)
    }
}

これは一見良さそうに見えます。

しかし
使う側から見るとどうでしょうか?

var basket = ShoppingBasket()
let product = Product(name: "商品", price: 100)
basket.add(product: product)

意見が分かれるところかもしれませんが
ShoppingBasketProductしか持っておらず
何を渡すのかが明らかであるので
引数ラベルはいらないのではないでしょうか?

読む時は良いかもしれませんが
例えば何回も使う必要がある場合は
引数ラベルをいちいち書くのは
ちょっと面倒だなと私は思ってしまいました。

なので下記の方が良いように感じます。

extension ShoppingBasket {
    mutating func add(_ product: Product) {
        products.append(product)
    }
}

var basket = ShoppingBasket()
let product = Product(name: "商品", price: 100)
basket.add(product)

※ Swiftでは上記のように引数ラベルの前にアンダーバーをつけることで
呼び出し側で引数ラベルを省略することができます。

ちょっと情報が足りない?

もう一つ別の例を考えてみます。
今度は買い物の合計金額を計算したいと思います。

その際にある特定の郵便番号に対しては
送料を追加する仕様があるとします。

extension ShoppingBasket {
    func calculateTotalAmount(_ shippingPostcode: String) -> Int {
        var amount = products.map{ $0.price }.reduce(0, +)
        if postcode.starts(with: "1") {
            amount += 100
        }
        return amount
    }
}

特に問題はなさそうに見えます。

では
これを使う側から考えてみます。

struct User {
    let name: String
    let postcode: String
    let address: String
    let phoneNumber: String
}

let user = User(name: "fuga",
                postcode: "1234567",
                address: "東京都〇〇区〇〇XXXXXX",
                phoneNumber: "090XXXXXXXX")

basket.calculateTotalAmount(user.postcode)

user.postcodeについてどう思われますでしょうか?

例えば
初めてコードを見て
ちょっと処理の流れをざっと確認しようかなと思っている場合でも

なぜ合計金額を計算するのに郵便番号が必要になるのだろうか?
もしかしたら何か間違っているのでは?

という疑問を持ってしまい
使い方などを調べ始めてしまう気がするなと私は思いました。

こういう場合には
もう少し文脈を明確にするために引数ラベルを付け

basket.calculateTotalAmount(shippingPostcode: user.postcode)

とした方が良いのかなと思います。

何を渡す必要があるのか、なぜその値を渡すのか

といったことがわかりやすくなったのではないかと感じられます。

こうして見ていくと
文脈を伝える中でも
適度な量を探していく必要もあるのかなと思います。

良さそうだけど、もっと文脈をはっきりさせたい

※ ここから先はコンパイラなど静的な型のチェックができることを前提をしています。

上記の例では引数ラベルを明示することで
わかりやすくなりましたが
もしかしたらこんなことが起きるかもしれません。

basket.calculateTotalAmount(shippingPostcode: user.phoneNumber)

郵便番号を入れるところに間違えて電話番号を入れてしまいました。

文脈としては間違いですが
型としてはどちらもStringなので問題ありません。

もちろんメソッドの中で値のチェックを行うべきではありますが
そのチェック処理が増えた分
何か不具合を起こしてしまう可能性も生まれてしまいます。

では
下記のようにするとどうでしょうか?

struct Postcode {
    let code: String
}

extension ShoppingBasket {
    func calculateTotalAmount(_ shippingPostcode: Postcode) -> Int {
        var amount = products.map{ $0.price }.reduce(0, +)
        if shippingPostcode.code.starts(with: "1") {
            amount += 100
        }
        return amount
    }
}

let user = User(name: "fuga",
                postcode: Postcode(code: "1234567"),
                address: "東京都〇〇区〇〇XXXXXX",
                phoneNumber: "090XXXXXXXX")

basket.calculateTotalAmount(user.postcode)

この場合、Postalcodeに決まっているため
他の型の値が入る余地がなくなり
チェックをする必要がなくなります。

また
型が何を渡すのかを明示しているため
引数ラベルも不要になるでしょう。

※ 今回は例のためにPostcodeのみを型にしましたが
他の項目も同じように型を使った方がより良くなると思っております。

もう一つ例を見てみたいと思います。

今度は買い物カゴの中の商品を表示するメソッドがあるとします。
設定によってアニメーションをするかしないかの指定ができます。

func presentProduct(_ product: Product,
                    animated: Bool,
                    duration: TimeInterval = 0.3,
                    curve: UIView.AnimationCurve = .easeInOut,
                    completion: (() -> Void)? = nil) {

    // 何かの表示ロジック
}

※ 引数の後ろの値はデフォルト引数と呼ばれ
これを設定することで引数の省略をすることができます。

アニメーションさせたい場合は
下記のように指定します。

presentProduct(product, animated: true)

これもどんな値を設定すれば明示されており
良さそうな感じがします。

しかし
下記のような場合はどうでしょうか?

presentProduct(product, animated: false, duration: 0.4)

使う側が意図的に値を指定していますが
アニメーションをfalseにしているのに
durationが設定されているため
結局どうしたいのかがわかりません。

ここで不整合な状態が生まれてしまいました。

このようなケースも型を活用してみるとどうでしょうか。

struct Animation {
    var duration: TimeInterval = 0.3
    var curve: UIView.AnimationCurve = .easeInOut
    var completion: (() -> Void)? = nil
}

func presentProduct(_ product: Product,
                    animation: Animation? = nil) {

    // 何かの表示ロジック
}

※ Swiftでは型の後ろに?をつけることでOptionalであることを示します。

こうすることで
animatedというBoolが不要になり、
animationがnil(null)であるかどうかを見て
アニメーションをさせたいのかどうかがより明確になったと感じられます。

このように型を活用することで
文脈をより明確にすることができるのに加えて
「使う側で文脈に合うように気をつけて実装する」ではなく
「そもそも文脈に合うようにしか使えなくする」ことができるため
こちらの方が良いのではないかなと私個人としては思っています。

まとめ

いくつか文脈ということを見てきましたが
開発環境の方針や個々人の好みなど色々な要因があり
明確な答えというものは見つかりません。

しかし

「これを他の人が見た時にどう見えるのか?」
「どうしたら伝わるだろうか?」

を考えることで文脈の伝わりやすさを高めることができ
よりスムーズな開発環境の構築に繋がっていくのではないかなと
私は思っています。

もし「もっとこうしたら良いのでは?」などの
ご意見がございましたらぜひ教えていただけますと嬉しいです:smiley:

参考

SwiftにはAPIデザインガイドラインがあります。
https://swift.org/documentation/api-design-guidelines/

これはSwiftを書く時のマニュアルにもなり
Swiftを使っている方は一度は目を通しておいた方が良い内容になっています:point_up:

また、「リーダブルコード」という本があります。
https://www.oreilly.co.jp/books/9784873115658/

名前の付ける時にどう考えていくのかについて
とてもわかりやすく書いてあります。
そんなに分厚い本ではないので
空いている時間に読んでみるのも良いかもしれません:point_up:

shiz
iOSエンジニア 受託開発会社でモバイルアプリからインフラの構築まで行う雑多なエンジニアでしたが、 Swiftへの関心が特に強く、ご縁をいただきiOSエンジニアへと転身。日々勉強中。 開発言語経験: Swift, kotlin, Javascript(Angular, Node.js), C#, PHP, Java
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