isyumi_netブログ

isyumi_netがプログラミングのこととかを書くブログ

急遽個別対応するときはコンパイルエラーを出しながら元に戻せるようにしよう

あなたはショッピングサイトを運営していたとする。ある日急に担当者から「来週からセールを始めるので13:00になったらこの告知バナーを表示してほしい」と依頼された。しかし、既存のシステムにはセール時にバナーを差し込むようなレイヤーは用意していない。そこで、少々お行儀は悪いがテンプレートに直接<img>タグを差し込むことにした。

void topPage(HttpResponse res) {
// ここからセール対応 セールが終わったら消し忘れないで><
var isSale = new DateTime.now().isAfter(new DateTime(2017, 10, 21, 13));
var saleTag = isSale ? "<img src='sale.png'/>" : "";
// ここまでセール対応

res.write("""
<div>
<h1>●●ショップへようこそ</h1>
${saleTag}
</div>
""");
}

 その後担当者から追加の依頼があった。「商品一覧画面にも出して><!」

void productListPage(HttpResponse res) {
// ここからセール対応 セールが終わったら消し忘れないで><
var isSale = new DateTime.now().isAfter(new DateTime(2017, 10, 21, 13));
var saleTag = isSale ? "<img src='sale.png'/>" : "";
// ここまでセール対応

res.write("""
<div>
${saleTag}
<h1>防寒具一覧</h1>
</div>
""");
}

 

 さらにその後担当者から依頼があった。「マイページにも出して(:3_ヽ)_」

void myPage(HttpResponse res) {
// ここからセール対応 セールが終わったら消し忘れないで><
var isSale = new DateTime.now().isAfter(new DateTime(2017, 10, 21, 13));
var saleTag = isSale ? "<img src='sale.png'/>" : "";
// ここまでセール対応

res.write("""
<div>
${saleTag}
<h1>購買履歴</h1>
</div>
""");
}

 

 さてあなたはここで不安になる。自分はセールが終わったときに正しくすべての個別対応を除去できるだろうか。消し忘れたら恥ずかしい。あなたにはいくつかの選択肢がある。

これらも間違ってはいないと思うが、もう少し便利な方法を紹介したい。元に戻すときにすべての該当箇所にコンパイルエラーが出せるようにしておこう。方法はたくさんあるが一番手軽な方法として、セールが始まっているかどうか判定する関数を一個作っておこう。そして個別対応した場所はすべてその関数を参照するようにしよう。

 

 

bool isSale() {
return new DateTime.now().isAfter(new DateTime(2017, 10, 21, 13));
}

void myPage(HttpResponse res) {
// ここからセール対応 セールが終わったら消し忘れないで><
var saleTag = isSale() ? "<img src='sale.png'/>" : "";
// ここまでセール対応

res.write("""
<div>
${saleTag}
<h1>購買履歴</h1>
</div>
""");
}

 今回はisSaleという関数を定義し、myPageやその他バナーを張ったページから呼び出している。

すると、セールが終わった後にisSale関数を削除するとこのようにコンパイルエラーが出る。

f:id:isyumi-net:20171020202125p:plain

赤い波線が出てわかりやすい



f:id:isyumi-net:20171020202317p:plain

IDEコンパイルエラー一覧を見れる



 

連想配列をclassでくるもう

連想配列を使う時の些細な一手間について。連想配列は自由度が高い機能だ。反面、その連想配列をどう使っているのかがコードを書いた本人にしかわからない。そこで、コードの中で連想配列の使い方を明示する方法を紹介する。

あなたは刻々と流れてくる売り上げ情報を集計し、リアルタイムな商品ごとの売り上げを出したいとする。次のような流れのコードを書こうと思う。

アウトライン

  • 連想配列のkeyを商品IDにする
  • valueを通算の売り上げにする
  • 商品の売り上げ情報が流れてくるごとにvalueに値を追加する
  • ただし初めてそのIDの商品が来た時に足し算をするとエラーになるので0を代入する

まず、このようなコードを書いた。

Map<String,int> sales = {};

// 売り上げイベントが流れてくる
void onSales(String productID , int price) {
// この商品IDが初めてきた時は0を代入する
if(!sales.containsKey(productID)) {
sales[productID] = 0;
}
// 商品代金を売り上げに加算する
sales[productID] += price;
}

 

コードが短いのであまり効果が実感できないかもしれないが、このコードの改善できる点として

  • 上にあげたアウトラインがコードで表現できていない
  • 連想配列をどのように使うかのルールがコメントと書いた人の頭の中にしかない
  • コードを書いている最中にそのルールを覚えていなければいけない

もっと複雑なデータ構造(例えばMapのなかにMapがあり、その中にListがあるような状況)であればこの問題は大きくなる。では、どう改善できるだろうか。連想配列を包んだクラスを定義しよう。そしてそのクラスのメソッド経由で連想配列を操作しよう。

class ProductSales {
Map<String, int> map = {};

void productInitialize(String productID) {
if (map.containsKey(productID)) {
map[productID] = 0;
}
}

void addSales(String productID, int price) {
map[productID] += price;
}

}

final productSales = new ProductSales();

// 売り上げイベントが流れてくる
void onSales(String productID, int price) {
productSales.productInitialize(productID);
productSales.addSales(productID,price);
}

 

余計長くなってしまったが 、それでも

  • 連想配列をどう使うかプログラムで表現できた
  • 一番上流のメソッドから連想配列の具体的な操作を除去でき、代わりに分かりやすい名前のメソッド呼び出しが出現した
  • onSalesを書いてる時(と読んでいる時)は連想配列の存在や使い方についてのことを忘れていられる

と言う効果が実感できると思う。普段はこんなことをしなくてもいいが、すごく長くて複雑なメソッドができてしまった場合にぜひ使ってみて欲しい小手先のテクニックだ。

再代入は初期化の直後に限ろう

下のコードのように一度変数に値を代入した後に別の値を代入することを再代入という。

例:

 

 

func main() {
i := hoge()
i = "iii"
log.Print(i)
}

まず、極力再代入を減らすように心がけよう。再代入を減らすとバグが減るしデバッグの手間も減る。このブログで何度も言ってきたことだが、変数はとにかく早く値を確定させよう。下のコードは悪い例だ。関数の上のほうで定義された途中で書き換えられていることに気づいただろうか。

 

func main() {
i := hoge()
a = 3 * 7
b = a % 2
c := fuga(i)
if c == 0 {
a = functionA(d * c)
i = "iii"
}
for x := 0; x < 10; x ++ {
piyo(len(i) * x)
}
log.Print(i)
}

 このようなコードはiの値はなんだっけと考えてしまう。変数を最初に宣言してから最後に代入するまでの間は脳のワーキングメモリを消耗しながらコードを読んでいると考えてほしい。逆に、その間の距離を極力短くすることができれば疲れないコードになったといえる。さらに、変数の再代入を一切排除できればそれだけいいコードといえる。下はいい例だ。

func main() {

i := hoge()
if c == 0 {
i = "iii"
}
// この時点でiの値は確定している

fuga(i)
a = 3 * 7
b = a % 2
if c == 0 {
a = functionA(b * c)
}
for x := 0; x < 10; x ++ {
piyo(len(i) * x)
}
log.Print(i)
}
 

上のコードのように最初にiが宣言されてから即座に再代入をしていればあまり心が消耗することがない。このルールを適用すべき場面として二つあげたい。

  • デフォルト値を入れる
  • for文で計算する

一つ目のデフォルト値だが、関数を実行して戻り値を得るがもしエラーになったらデフォルト値を入れるというコードを書くことは多い。その場合も

i, err := hoge()
if err != nil {
i = ""
}

 

 のように即再代入をすることを心がけよう。決して隙間にほかの処理を挟まないようにしてほしい。

i, err := hoge()
// ここに処理を一切書かない
if err != nil {
// ここにデフォルト値を入れるために必要な処理以外を一切書かない
i = ""
// ここに処理を一切書かない
}

 

二つ目のfor文だが、for文で変数の値を順番に書き換えることは多い。その場合も、書き換える専用のfor文を用意しよう。

i := 0
for x := 0 ; x < 100 ; x ++{
i += x
}

 

このようにこの四行以外に余計な処理が一切なければコードを読んだ人は「ああ、この四行一固まりでiの値が確定するんだ」と理解できる。これも同じようにコードの隙間に別の処理を差し込まないようにしたい。

i := 0
// ここに処理を書かない
for x := 0; x < 100; x ++ {
// ここにiを上書きするために必要な処理以外を書かない
i += x
// ここに処理を書かない
}

 ぜひ心掛けてほしい。

処理の流れを型で表現しよう

もしかしたらあなたは型をめんどくさいものだと感じているかもしれない。型宣言は動くようにするために仕方なくするものと思っていないだろうか。もしそうなら型を使って頭を整理する習慣について知ってほしい。きっとあなたは型を極力狭く定義したほうが捗ることに気付くだろう。

このコードは二つの数を掛け算するコードだ。掛け算する前の構造体のsumには0を入れてある。このコードは間違いなく動く。

type Sum struct {
Num1 int
Num2 int
Sum int
}

func sum() {
s := Sum{
Num1: 3,
Num2: 4,
Sum: 0,
}

s.Sum = s.Num1 + s.Num2

}

しかし、このように計算前の構造体と計算後の構造体を用意すればどうだろう。コードを読む人は一目で「ああ、ここまでは計算結果が出ていないんだな」ということが分かるし、計算する前の0という値を間違って使ってしまうこともなくなる。

type Operand struct {
Num1 int
Num2 int
}
type Result struct {
Num1 int
Num2 int
Sum int
}

func sum() {
s := Operand{
Num1: 3,
Num2: 4,
}

r = Result{
Num1: s.Num1,
Num2: s.Num2,
Sum: s.Num1 + s.Num2,
}

}

このように、処理の大雑把な流れを型で表現しよう。入ってくる値の型は何で出て行く型の価は何か、その中間はどんなデータ構造か。そして型と型を橋渡しするようにコードを書いていくと疲れにくい。

コマンド化して副作用を追い出そう

多くのプログラムにはこのような外の世界にデータを書き込む処理が登場します。

  • メール送信
  • DBに保存
  • ファイルに書き込み
  • HTTPリクエストにレスポンス

このような処理をここではIOと呼びます。IOが混じるコードのテストは複雑です。多くの場合実機確認することになります。DBに狙った通りにデータが書き込まれたか確認するフレームワークなどありますが、難易度が高いです。また、多くの場合このようなフレームワークは動作が遅いです。手早くシンプルにテストをかけるようにするためにどうすればいいでしょうか。

まず考え方ですが、IOの混じるコードをIO処理の部分とそれ以外の部分に分けて考えます。そして

  • 何を書き込むか計算する処理をテストできるように書く
  • DBやファイルへきちんと書き込めているかは実機確認する

という方針でコードを切り分けていきます。下のコードを見てください。

void main() {
doOperation();
}

void doOperation() {
// なんか複雑な処理
var x = calc(); // なんか複雑な計算
// 複雑な処理
file.put(createData()); // 何かのデータをファイルに保存

db.insert(x);// DBになんか保存
}

calc() {
}

 

 短くてメリットが分かりづらいですが、//なんか複雑な処理 にはすごく複雑な処理が書かれているとしてください。doOperation関数内でファイルへの書き込みとDBへの書き込みが実行されています。この場合、動作確認時にxの値やcreateDataの戻り値を確認するにはDBを見なければいけません。そこで、DBやファイルへの書き込みをコマンド化したオブジェクトを導入し、それを戻り値とします。そして、戻した先で実際のIO処理をします。

void main() {
// 戻り値をコマンドで取得
var commands = doOperation();

// コマンドを一個ずつ実行していく
for (var command in commands) {
if (command is DBInsertCommand) {
db.insert(command.data);
} else if (command is FileWriteCommand) {
file.put(command.data);
}
}
}

// メソッドの中で複雑な計算結果をfileに書き込んだりDBに保存したりする
List<Command> doOperation() {
var commands = [];

// なんか複雑な処理
// 複雑な処理

// 何かのデータをファイルに保存するコマンドを作成
commands.add(new FileWriteCommand()..data = createData());

var x = calc(); // なんか複雑な計算
// DBにInsertするコマンドを作成
commands.add(new DBInsertCommand()..data = calc());

return commands;
}

class Command {}

class DBInsertCommand extends Command {
int data;
}

class FileWriteCommand extends Command {
int data;
}

 

ここではDBInsertCommandとFileWriteCommandをListに入れて戻し、mainメソッド内で実行しています。この一手間によりxやcreateDataの値を簡単にテストできるようになりました。

このようにコマンド化して副作用を追い出していきましょう。

 

補足

command is DBInsertCommand

command is FileWriteCommand

と書いてあるところですがこれはダウンキャストが成功するかどうかでコマンドの方を確認しています。

オプショナル型や代数的データ型がある言語はいい感じに書けると思います。

補足2

一度Listに入れてまとめて返すというのはあまりパフォーマンスが良くないです。その言語のStream型やジェネレータ構文などがないか調べて見ましょう。

正規化した状態変数を展開する関数を作ろう

 

blog.isyumi.net

 

前回、状態を管理する変数は正規化しようという話をした。これは状態を管理するうえで役に立つ。しかし、その変数を使うときにめんどくさくなる。そこで、正規化した変数を非正規化する専用の関数を一つ作ろう。状態変数を正規化することと、正規化した変数をもとに戻す関数をセットで作ることによって安全かつ簡単なプログラミングができるようになる。以後この関数を展開関数と呼ぶ。これはRDBのcreate viewに相当する。まず、展開関数の役割について具体的に考えよう。

  • 展開関数は正規化した状態変数を使いやすい型に変換する関数
  • 戻り値は「後続の処理で使いやすいか?」が絶対正義
  • 値の重複を削除した型から値の重複する型へ変換する
  • 「いえること」もここで計算する
  • 戻り値はサイクルの中で使い捨てにする

そして、展開関数でやってはいけないこともはっきりさせておこう。

  • 状態変数以外の変数の読み取り(定数なら可)
  • 展開関数の外にある変数の書き込み

次に、この関数を何に使うかだ。

  • 新しいサイクルが始まったら、まず状態変数を読み取って展開関数にかける
  • その後の処理は展開関数の戻り値を使う(状態変数に触らない)
  • 何かをシミュレーションするような処理に使ってもいい
  • テスト

シミュレーションというのは、例えば展開関数の中に現在の所持金をもとにある商品を購入可能か判定する式があったとする。あるユーザーが現在100円を持っており、商品Aはお金が足らず買えなかったとする。もし、ユーザーが150円の給料を得たとき、商品を購入可能か判定したくなった。その場合も、展開関数自体は副作用を一切持たないので適当にダミーの状態変数をこしらえて展開関数にかければ、どこにも影響を出さず結果を判明させられる。このように、もし○○な状態だったらどうなるか簡単に確かめるのも展開関数の使途の一つだ。

では実例を見よう。コツだが、引数と戻り値の数が増えすぎたらめんどくさいので、とりあえず登場する状態変数と展開後の変数(ここではModelと命名しました)を一つのオブジェクトツリーにまとめよう。

//ここから状態変数
class State {
DateTime now;
List<StateVideo> videos = [];
List<StateUser> users = [];
List<StateHistory> historyItems = [];
}

class StateUser {
String userID;
DateTime birthday;
}

class StateVideo {
String videoID;
}

class StateHistory {
String videoID;
String userID;
}

//ここから展開後の変数
class Model {
List<ModelVideo> videos = [];
List<ModelUser> users = [];
}

class ModelVideo {
String videoID;
List<String>playedUsers = [];
}

class ModelUser {
String userID;
List<String> playedVideos = [];
}

後は順番に計算していくだけだ。

 

Model mapToModel(State state) {
return new Model()
..videos = mapToVideos(state.videos, state.historyItems)
..users = mapToUsers(state.users, state.historyItems, state.now);
}

// ビデオ一覧を作成
// ビデオインスタンスに視聴ユーザー一覧も入れる
List<ModelVideo> mapToVideos(List<StateVideo> videos,
List<StateHistory> historyItems) {
List<ModelVideo> results = [];

for (var v in videos) {
var mv = new ModelVideo()
..videoID = v.videoID
..playedUsers = filterPlayedUserIDs(historyItems, v.videoID);
results.add(mv);
}
return results;
}

// ユーザー一覧を作成
// ユーザーインスタンスに視聴ビデオ一覧も入れる
// 誕生日と現在時刻から年齢を計算する
List<ModelUser> mapToUsers(List<StateUser> users,
List<StateHistory> historyItems, DateTime now) {
List<ModelUser> results = [];
for (var u in users) {
var mu = new ModelUser()
..userID = u.userID
..age = culcAge(u.birthday, now)
..playedVideos = filterPlayedVideos(historyItems, u.userID);
results.add(mu);
}
return results;
}


 計算する処理を書いたら、

  1. 現在の状態の取り出し
  2. 展開関数で変換
  3. 使用
  4. 最新の状態に更新

を順番にやっていこう。

void onPlay(String userID, String videoID) {
// とりあえず変換する
var model = mapToModel(state);

// 使いたいように使う
print("ユーザーごとの視聴履歴は ${model.users}");
print("動画ごとの視聴ユーザーは ${model.videos}");

// 最後にstateを更新する
var newHistoryItem = new StateHistoryItem()
..userID = userID
..videoID = videoID;
var newHistoryItems = new List.from(state.historyItems)
..add(newHistoryItem);
state = new State()
..users = state.users
..videos = state.videos
..historyItems = newHistoryItems
..now = new DateTime.now();
}

 

 

状態を管理する変数は正規化しよう

先日のブログで「あなたの書いたプログラムには状態を保持した変数がある。そこが散らかるとめんどくさいことになるのできちっと管理しよう」という話をした。 

blog.isyumi.net

 

おさらいだが、状態変数というのはサイクルをまたぐ変数だ。博士のメモだ。管理するための色々な技法があるので一つ紹介しよう。RDBと同じように状態変数も正規化しよう。正規化はデータ同士の不整合が起こらないようにするために、重複した値を削除していくことだ。データの矛盾にはシステム外との矛盾とシステム内での矛盾がある。あなたのシステムに20歳と登録されている人が本当は30歳だった、というような矛盾は防ぎづらい。しかし、システムのある場所には20歳と登録されているのに、他の場所には30歳と登録されるような間違いは正しいプログラミングをすれば防げるはずだ。

ここに動画一覧とユーザー一覧がある。ここでuser1がvideo2を再生したとしよう。動画エンティティの視聴ユーザー一覧にuser2を追加した。

正規化していない

しかし、user1の視聴動画一覧にvideo2を追加するのを忘れてしまった。これでは状態変数が矛盾してしまう。そこで動画一覧とユーザー一覧に加えて視聴履歴一覧を追加した。これで視聴履歴一覧に値を挿入するだけでよくなった。

正規化した

小さな点だが勘違いしないようにしたいのは、正規化の目的は手数を減らすことではなく矛盾が発生しないようにすることだ。データ構造を考えるときはどうすればより矛盾が発生しにくいデータ構造になるかを考えよう。

また、重複には「言えること」も含まれる。例えば、四角形の縦と横と面積を持つ変数を考えてほしい。

class Rectangle {
int height;
int width;
int area;
}

四角形の面積は縦 * 横である。つまり、縦・横・面積の三つのうち2個判明していれば最後の1個は不要である。状態として持つのではなく、面積が必要になった時に計算すればいい。

class Rectangle {
int height;
int width;
}

他の例をあげれば

  • 誕生日と現在時刻と年齢
  • Aさんの位置とBさんの位置と距離
  • 税抜き価格と税率と税込価格

などがある。このような変数も重複と考えて削除していこう。