文字列連結 vs ヒアドキュメント(リーダブルコードを読んで)

#はじめに

リーダブルコードを読んで感動しました。
で、ヒアドキュメントの賛否についてわかりませんでした。
リーダブルコードを見直してみましたが、それらしい記述を見つけられません。

ならば自分なりの答えを出してみようと思いました。

#時間のない人向け
長い文字列を組み立てるときは、文字列連結とヒアドキュメントを使い分けましょう。

#ヒアドキュメントとは
[PHPの例を書いてくださっている方の記事](https://qiita.com/fkm10pa/items/eefabe15aeb4ea4dc105)を紹介させて頂きます。

#例題

SQLのクエリをPowershellから組み立てる、という例で考えてみたいと思います。

流すクエリは以下の内容とします。

```sql:sample_query
select A.col1, A.col2, A.col3, A.col4, A.col5, B.col1, B.col2, B.col3, B.col4, B.col5 from example_table_1 A join example_table_2 B on A.col1 = B.col1 where A.col1 is not null and A.col2 is not null and A.col3 is null
```

#整形して読みやすく
少しずつ順を追って私なりの読みやすさを追求していきます。

##まず改行で整形
横に長くてひたすら読みづらいです。
まずはsqlの時点で改行を入れて読みやすく整形します。

```sql:改行で整形
select
A.col1, A.col2, A.col3, A.col4, A.col5,
B.col1, B.col2, B.col3, B.col4, B.col5
from example_table_1 A
join example_table_2 B
on A.col1 = B.col1
where A.col1 is not null
and A.col2 is not null
and A.col3 is null
```

## 字下げで整形

```sql:字下げで整形
select
  A.col1, A.col2, A.col3, A.col4, A.col5,
  B.col1, B.col2, B.col3, B.col4, B.col5
from example_table_1 A
join example_table_2 B
  on A.col1 = B.col1
where A.col1 is not null
  and A.col2 is not null
  and A.col3 is null
```

これだけでかなり見やすくなりました。

# 整形した弊害

このように整形したクエリは、プログラム内に1行で記述することができなくなります。
ここで登場するのが前述したクエリの組み立てです。
一時的な変数に整形したクエリを1行ずつ連結することでクエリを組み立てます。

powershellでは+=で文字列を連結することができます。
文字列連結で記述すると、以下のようになります。

```powershell:文字列連結
$SQL = "select "
$SQL += "  A.col1, A.col2, A.col3, A.col4, A.col5, "
$SQL += "  B.col1, B.col2, B.col3, B.col4, B.col5 "
$SQL += "from example_table_1 A "
$SQL += "join example_table_2 B"
$SQL += "  on A.col1 = B.col1 "
$SQL += "where A.col1 is not null "
$SQL += "  and A.col2 is not null "
$SQL += "  and A.col3 is null "

Write-Host $SQL
```

私としては、この書き方に以下のデメリットがあると考えます。

- 1行目と以降の行で=(代入) と+=(追記) を使い分けているので、コピペによる構文エラーの可能性がある
- 全ての連結する文字列の末尾に (半角スペース)を入れないと構文エラーになる
- このクエリをそのままSQL実行の環境にコピペしたいのに出来ない

特に2つ目なんかは経験者が多いのではないかと思います。
なぜ構文エラーになるんだ…と小一時間悩んだ挙句、FROM句とJOIN句の間にスペースがありませんでしたー、なんて経験はありませんか？
実際に5行目の末尾の空白をリーダブルコード風にあえて忘れてみました。

## 1行目と以降の行の記述を揃える

以下のような書き方であれば、問題を1つクリアできます。

```powershell:文字列連結
$SQL = ""
$SQL += "select "
$SQL += "  A.col1, A.col2, A.col3, A.col4, A.col5, "
$SQL += "  B.col1, B.col2, B.col3, B.col4, B.col5 "
$SQL += "from example_table_1 A "
$SQL += "join example_table_2 B "
$SQL += "  on A.col1 = B.col1 "
$SQL += "where A.col1 is not null "
$SQL += "  and A.col2 is not null "
$SQL += "  and A.col3 is null "

Write-Host $SQL
```

これが最も普及している書き方なのではないでしょうか。
実際に、この記事を書くきっかけになったソースは、VB.NETでこの書き方が使われていました。

# そんなに困る？

何がそんなに困るんだろう？と思われる方がいらっしゃるかと思います。
慣れていらっしゃる方も大勢いて、疑問に思わないと思いますし、個人の考え方によって違いがあるかと思います。
私の実体験をもとに説明させて頂きます。

実際のプログラムは、Microsoft.Jet.OLEDB.4.0データプロバイダーなるもので、CSVファイルをあたかもデータベースかのようにSELECTして使っています。
(すごく古いソースなもので…まだそんなの使ってるのかよ等の突っ込みは無しでお願いします…)

こんな仕組みがあったなんて目からウロコです。
でも、この仕組み、デバッグができなくて最悪でした。
列がどのような型で認識されているのかわからず、whereなどの条件をいじるときに型違いが連発しました。
Jetなんたらに慣れている方なら、デバッグツールなどがある、とあっさりと教えて頂けるかもしれませんが、周囲にも私にもそんな知識はなく…。

結果、Jet素人がたどり着いた結論は、**どうやら構文がAccessみたいだから、Accessにクエリを貼り付けてデバッグしよう**という物でした。

## Accessでデバッグ

ちょっと状況がニッチ過ぎて、本題から離れた気がしますが、もう少しお付き合いください。(ちょっと小芝居っぽくなります)
ちなみにAccessもほぼ触ったことがありません…。

AccessにCSVをインポートしてtable化することはあっという間にできました。
よーし、クエリ画面にクエリを貼り付けて整形だ！

ぺたー…！
![1.PNG](https://qiita-image-store.s3.amazonaws.com/0/369361/7bf96862-229a-5766-70fe-74c4802cccc1.png)

あぁ、そうか連結のところ消さなきゃ矩形選択…？
なんだこのクエリエディタ！編集機能が皆無じゃないか！
もういいよ、実行結果から拾ったクエリを張り付けて、整形…？
クエリの整形機能もないのかよ！
という一人芝居をうっておりました。

この時、締め切りまで3日ほどしかなく、絶望を感じたのは言うまでもありません。

## Accessから逃亡

エディター上で加工して貼り付けてデバッグ…。
ってこれデバッグ終わったクエリをAccessから貼り付けなおすときもまた加工し直し…。
まだ4つもクエリ残ってるのに…。
編集ミス…デバッグ済みのクエリの編集ミス…。
何とか無編集で相互に貼り付けあえるようにしたい…。

# ヒアドキュメントを使おう

で、行き着いたわけです。
クエリ長ったらしいし、いっぱいあるから、すべてを書き換えるのはめんどくさいけど、ヒアドキュメントで書き直そう、と。
リーダブルコードに書いてありました。
少しずつ、続けることが大事なのだと。

## ヒアドキュメントで書き直す

ヒアドキュメントで書き直すと以下のようになります。

```powershell:ヒアドキュメント
$SQL = @'
select
  A.col1, A.col2, A.col3, A.col4, A.col5,
  B.col1, B.col2, B.col3, B.col4, B.col5
from example_table_1 A
join example_table_2 B
  on A.col1 = B.col1
where A.col1 is not null
  and A.col2 is not null
  and A.col3 is null
'@

Write-Host $SQL
```

字下げも改行も反映されるし、コピペも楽々！
なんでこの書き方をしないんだろう！

しかし、ヒアドキュメントにも弱点はあります。

- ネストの深い箇所で使用した場合、インデントがすべて反映されてしまうので、対象とする文字列次第では、ヒアドキュメントが使えない
- 各行にコメントを書くことができない

でも、こんなに便利な書き方を使わないのはもったいないです。

# まとめ: リーダブルコードにならって

ヒアドキュメントの字下げが深い場合は、メソッドなりに切り出してしまうほうがいいと思います。

各行にコメントを書けないのは致命的なデメリットだと思います。
解決策として、メソッド名や戻り値を受け取る変数の名前でクエリを追わなくても何のクエリなのかわかるようにするほうがいいと思います。
IDEの定義を参照する機能を使えば、クエリもすぐそばで確認できます。

**汚い仕事を外に押し付けて、説明変数で人にやさしく。**

- 文字列連結
  - 列が20個以上あるとか、コメントを各行に入れないとつらい場合にオススメ

- ヒアドキュメント
  - 基本的にこちらがオススメ
  - 弱点があるので、メソッド化、IDEの力を借りる、コメントまとめて書くなどの工夫で弱点を克服

これが私の出した結論です。

以下、クエリを意図に沿うように変えていますが、メソッド化の例です。

```powershell:関数名長すぎ…だけど好き
function getQueryStringForPurchaseNumbersWithNameInActiveUser() {
    $SQL = @'
select
   P.no
  ,U.name
from user U
join purchase P
  on U.id = P.user_id
where U.activate = '1'
  and U.disabled = '0'
'@
    return $SQL
}

$purchase_numbers = getQueryStringForPurchaseNumbersWithNameInActiveUser

write-host $purchase_numbers
```

ちょっとやりすぎかもしれませんが、クエリの意味を知ればクエリを追わなくて済むので、こちらのほうが私は好きです。

クエリ文字列を返す軽量アクセサを表現するために、あえてgetを使っています。

実際には、〇〇レコードを取得する、などの共通認識のあるプログラムでしたので、getQueryStringFor〇〇Recordなどとしました。

異論は全面的に認めます！

#余談

## ただの文字列でも大丈夫
powershellではヒアドキュメントを使わなくても、大丈夫でした。

```powershell:文字列
$SQL = '
select
  A.col1, A.col2, A.col3, A.col4, A.col5,
  B.col1, B.col2, B.col3, B.col4, B.col5
from example_table_1 A
join example_table_2 B
  on A.col1 = B.col1
where A.col1 is not null
  and A.col2 is not null
  and A.col3 is null
'

Write-Host $SQL
```

##カンマの位置を調整

カラムなどの区切りの,(カンマ)を頭に置くスタイルは構文エラーを生みにくくメンテナンスがしやすいですね。最初に考えた人すごい！

```powershell:カンマを先頭に
$SQL = '
select
  A.col1
 ,A.col2
 ,A.col3
 ,A.col4
 ,A.col5
 ,B.col1
 ,B.col2
 ,B.col3
 ,B.col4
 ,B.col5
from example_table_1 A
join example_table_2 B
  on A.col1 = B.col1
where A.col1 is not null
  and A.col2 is not null
  and A.col3 is null
'

Write-Host $SQL
```

## WHEREの改善

以下のリンクで「5. おわりに」の直前に、さらっと触れられていますが、大変見やすいクエリです。す、すばらしい…。
[Oracle 津島博士のパフォーマンス講座です](https://www.oracle.com/technetwork/jp/database/articles/tsushima/tsm09-1598259-ja.html)

```powershell:WHEREの改善
$SQL = '
select
  A.col1
 ,A.col2
 ,A.col3
 ,A.col4
 ,A.col5
 ,B.col1
 ,B.col2
 ,B.col3
 ,B.col4
 ,B.col5
from (
    select * from example_table_1
    where col1 is not null
      and col2 is not null
      and col3 is null
  ) A
join example_table_2 B
  on A.col1 = B.col1
'

Write-Host $SQL
```