「わかる!ドメイン駆動設計〜もちこちゃんの大冒険〜」を読んだ

わかる!ドメイン駆動設計〜もちこちゃんの大冒険〜を読む。 社内のアーキテクチャや設計はDDD的な要素が入っているので知らないままではいられない。

DDD

DDDでやること

  • 開発者とドメインエキスパートがドメインモデルを作る
    • ドメインとは、ソフトウェアを適用する対象領域。
      • e.g. 会計ソフトならドメインは会計業務、など
    • ドメインエキスパートとは、その業務に最も精通した人
      • e.g. 業務知識に長けた人、業務の流れについてよく分かっている人、営業担当
    • ドメインモデルとは、業務ドメインに特化したソフトウェアモデルのこと
      • 業務は目に見えるものだけでなく概念や関係性なども含まれる
        • e.g. 役割や権限などは概念
      • ドメインを反映したモデルとは、ドメインを構成する物・概念・振る舞い・関係性を表現したもの
        • これらのドメインを構成する物・概念・振る舞い・関係性を表現したものは、開発者に対して明白になっていることは殆ど無い
        • 下記2つのプラクティスを実現しつつ、開発者はドメインエキスパートの頭の中にあるものを引き出していく
          • イテレーティブな開発
          • 開発者とドメインエキスパートの密接な関係性

DDDのメリット

  • ドメインエキスパートと開発者を同じ土俵に乗せることで、プログラマーの視点だけでなく業務側の視点も入れられる
  • ソフトウェアを理解している人を一部だけという状況をなくせる
  • ドメインエキスパートとソフトウェア開発者、そしてソフトウェアそのものとの一切の通訳が不要になる
  • 設計がコードであり、コードが設計になる
    • 知識の継承、ビジネス要件に伴う機能変更にすばやく対応できる

DDDの要素

  • 戦略的設計戦術的設計に分類される
    • 戦略的設計 -> 概念的な要素
      • ユビキタス言語
      • 境界づけられたコンテキスト
      • コンテキストマップ
    • 戦術的設計 -> 技術的な要素
  • 戦術的設計だけでもDDDの恩恵は受けられるが両者が組み合わさったときのような大きな利益は生まれない

ユビキタス言語

  • ユビキタス言語とは、ドメインエキスパートやソフトウェア開発者を含めたチーム全体で作り上げる共通言語のこと
    • 誰かが規定するものではなく、チーム内で議論し合意の上で最善の用語を見つけていく
    • ユビキタス言語は、チーム内での会話・ドキュメント・ソースコード全てに適用を徹底すべきもの
  • ユビキタス言語の見つけ方
    • 現在のドキュメントを調べ、ユビキタス言語の候補になりそうなもの、問題がありそうなものをピックアップする
      • 次の場合には要注意
        • 同じものを表す用語が複数ある
        • 概念はあるのに名前がない
        • ビジュアルはあるのに名前はない
    • ドメインエキスパートの話す用語を観察し、用語やフレーズをピックアップする
    • ピックアップした用語やフレーズをチームで議論してユビキタス言語として確立する
    • 名前のない概念、曖昧な概念をはっきりさせ、名前をつけてユビキタス言語として確立する
  • ユビキタス言語
  • 一度決めたらそれっきりではなく、ユビキタス言語も育っていく

ドメインモデル

  • ドメインを反映したモデル
    • 名前・関係性・振る舞いなどが表現されていて、チームメンバー全員が同じものを思い浮かべるもの
  • DDDでは設計がコードであり、コードが設計である
    • ドメインモデルはドキュメントに残されるものではなく、コードで表現されたドメインモデルが最も長期間ユビキタス言語とドメインの現状を正確に表現し続ける
    • ドメインモデルをコードに落とし込む段階で、考慮漏れやより良いモデル、ユビキタス言語を見つけることがある。この場合には、ドメインモデルやユビキタス言語にフィードバックし、常にコードが正確にドメインモデルを表現するようにする
  • ドメインドメインモデル、ソフトウェアモデル、オブジェクトモデルの関係性
  • ドメインモデル貧血症
    • ドメインモデル(と呼んでいるもの)が下記のような状態にあることを指す
      • getter/setterのようなものしかない、ただの値を保持するだけのオブジェクトである
      • モデルを利用する側がほとんどのロジックを抱えている
    • ドメインモデル貧血症となっているコードにはほとんど意図が反映されていない

ドメインとは

境界づけられたコンテキスト

  • ドメインモデルがどこに属するかを表すもの
  • 新規のプロジェクトなどは1つのサブドメインに対して1つの境界づけられたコンテキストになるが、そうではないケースもある
    • 対象としている領域は同じサブドメインだが、クライアント側とサーバー側に別れている場合などはチームの境界が(ユビキタス)言語の境界にもなっていることが多くこの場合は異なる境界づけられたコンテキスト
  • 境界づけられたコンテキストのサイズを間違える原因として多いもの
    • 言語的な境界ではなく技術的な境界を考えてしまっている
    • チームの開発要員へのタスクの割当を考慮してしまっている

コンテキストマップ

  • 境界づけられたコンテキスト間の関係性を描いたもの
  • 注意点
    • 関係を省いたり単純化したりせず現状を表現する
    • 境界づけられたコンテキスト自体もユビキタス言語にすること
  • 境界つけられたコンテキスト間には下記のような関係がありうる
    • 共有カーネル
    • 顧客/共有者の開発チーム
      • 2つのチームに顧客と供給者の関係を確立する
    • 順応者
      • 相手のコンテキストに従う
    • 腐敗防止層
      • 隔離するためのレイヤを作成する
    • 別々の道
      • 2つのコンテキストのつながりを持たない
    • 公開ホストサービス
    • 公表された言語
    • 巨大な泥団子

感想

  • DDDについて今まで実装の話を少し読んでみたりしてピンとこなかったのがとても身近になったしDDDを本格的に学ぶモチベーションが上がった!
  • 前の職場では、エンジニアと非エンジニアが違う言葉使っているというのはあったな、と思った。それが原因で意思疎通に支障が生じることもあったと記憶している。ユビキタス言語を育てる、ということを取り入れるだけでもいいコミュニケーションにつながりそう
  • まだ先は長そうなので頑張ろうと思う
    • 次は下記資料の1番上のドメイン駆動設計の要約を読む

他のドメイン駆動設計のよさげな資料

プログラミングの基礎を読んだ

プログラミングの基礎を読んだのでまとめ

はじめに

関数型言語を仕事でも個人でも使ったことがなかったので何かしら触ってみたいと思っていたところ知人に進められたのがきっかけで読み始めた。
情報系出身ではなくコンピュータサイエンスの基礎にあやふやな部分が少なくない自分にはとても良かった。

学んだこととプラスアルファ

この本から学んだことのまとめ
自分が学びになったことのまとめなので網羅的な要約ではないことに注意

変数

  • Ocamlの変数は基本的には一度定義すると後から変更することができない
    • 他言語でいうところのconstのようなものだろうか
    • 参照透過性という性質(後述)

型推論と型チェック

  • 型を定義するとその型が何の型を受け取って何の型を返すのかを、関数内部の演算子などから推測することを型推論という
  • また定義された関数への引数の型が推論した型と合致しているかをチェックすることを型チェックという
# let add a b = a + b ;;
(* int2つを受け取り、int1つを受け取る関数であると推論している *)
val add : int -> int -> int = <fun>
# add "a" "b" ;;
(* int型が期待されているのにstring型が渡されたというエラーを発生させている *)
Error: This expression has type string but an expression was expected of type
         int

パターンマッチ

  • 組(複数の値をひとまとめにしたもの)やレコード(ハッシュのようなもの)、型といった構造データからデータの中身を取り出す方法
    • 入力が構造データの場合は行う
  • 下記が典型的な形で、matchwithに挟まれた式を実行し、その結果をパターンと比較する
    • パターンは復数列挙できる
matchwith
    パターン ->| パターン ->

レコード

  • 定義された型のフィールドを省略して、レコードを作ることはできない
    • パターンマッチでパターン変数に束縛する時は省略できる
  • 型同士でフィールドの重複は許可されない
    • 型推論と関係しているんじゃないかなと思う
      • パターンマッチの際にはフィールドを省略することができ、フィールドが1つでも成り立つが,複数の型で同一フィールドが使われていたら型推論できないはず。
  • 意味のあるかたまりに対して一つの型を定義するのが望ましいプログラミングスタイル
    • 他言語でも同様だと思う。

リスト定義

2つの定義によって帰納的に定められている。
自分自身を使って定義されたデータ型を再帰的なデータ型、あるいは自己参照をするデータ型と呼ぶ

  1. 空リスト[]はリストである
  2. firstが要素、restがリストならfirst::restもリストである(自分自身を定義に使っている)

上記の定義よりリストのパターンマッチは空かそれ以外のケースの2通りで表すことができるようだ

match リスト with
    [] ->| first :: rest ->

またリストは内部的にはLinked Listになっているらしい
なので、リストを再帰的にパターンマッチしていくことは連結リストをheadから次のnodeを見ていくことに等しい

自然数定義

リストと同じように2つの定義かつ帰納的に定めることができる

  1. 0は自然数である
  2. nが自然数ならn+1も自然数である

ダイクストラアルゴリズム

高階関数

  • Ocaml の関数はfirst-classの値 -> 他の値と同じように扱うことができる
    • 関数が引数にも返り値にもなることができる
  • 関数を引数として受け取る関数のことを高階関数と呼ぶ
    • OcamlのList.map、List.filterなどはそれにあたる

多相型と多相関数

  • 'a, 'b といった型は型変数と呼ばれ、どのような型でもよいことを表す
    • プログラム実行時に具体的な型に置き換わる
  • どのような型でもよいという性質のことを多相性(polymorphism)と呼ぶ
  • 型変数を含むような型のことを多相型、多相型を持つ関数のことを多相関数と呼ぶ <-> 単相関数

infix関数とprefix関数

  • prefix関数
    • 関数を引数の先頭に書き、引数を後に並べる関数。普通の関数
  • infix関数
    • +演算子など。関数を引数の間に置く
    • このinfix関数の中にはprefix関数に変換できるものがあり、それを使うと下記のようにプログラムを簡潔にできるかも
(* リスト lst の関数の和を求める関数 *)
(* sum : int list -> int *)
let sum lst = List.fold_right (+) lst 0

リストの性質

  • 似たような性質のデータの集めたもの
    • mapやfilter、fold_rightなどはそのような同種のデータを一括して扱うという意味で、リスト処理の本質を捉えていると言える
      • map : リストの全要素に対して同じ処理を施す
      • filter : リストの中から特定の要件を満たす要素を抽出する
      • fold_right : リストの各要素をまとめあげる
    • 再帰的にプログラムすることで関数名を使わずに実行できるが、リストの各要素をバラバラではなく1つのデータとして扱いたい
      • 例えば下記の場合、 (enumerate n) は 1~nまでのリスト、(divisor n) は nの約数 という同種のデータの集まりになる
lee rec enumerate n =
  if n = 0 then []
  else n :: enumerate (n - 1)
let divisor n = List.filter (fun x -> n mod x = 0) (enumerate n)

再帰関数

  • データ構造にしたがった再帰 [] | first :: rest で分けられるようなデータ構造
    • メリット
      • 簡単に関数を作ることができる
      • 停止性が明らか
  • 一般的な再帰(データ構造に従わない再帰)
    • 下記を考慮する必要がある
      • 自明に答えが出るのはどのようなケースか
      • より小さな部分問題はどのようにしたら作れるか
      • 再帰呼出しの結果から全体の結果がどのように得られるか

停止性の判定

一般の再帰は構造に従った再帰と違って、再帰呼出しの停止性が自明ではないので下記2点を確認する

  • 再帰呼び出しを行う際の部分問題が何らかの意味でもとの問題よりも簡単になっていること
  • それを繰り返すと有限回で自明なケースに帰着されること

一般的な再帰

  • 自明に答えが出るケースとそれ以外のケースについて考える
  • その上で下記4つのことを考える
    1. 自明に答えが出るケースはどのような場合か
    2. その場合の答えはなにか
    3. 部分問題はどのように作ることができるか(1つとは限らない)
    4. 部分問題の結果から全体の結果を得るにはどのようにすればよいか

情報の蓄積

  • 欠落している情報を補うための引数アキュムレータ
    • ex) 距離とそれまでの距離の合計を持つ型のリストを受け取り、距離の合計を埋めたリストを返す時、再帰的にリストを受け取ってもそれまでの距離という情報が欠落しているため解くことができない このような時に、アキュムレータを使う。ただし、元の関数はリストだけを受け取って、リストだけを返したいのでその場合局所変数定義など内部的に用いる

バリアント型

まずは下記の例を参照

# type color_t = Blue | Red | Black ;;
- : type color_t = Blue | Red | Black
# Black ;;
- : color_t = Black
  • Blue、 Red、Black を構成子として、color_t という型を宣言している
  • このcolor_tがバリアント型

木構造

  • 自己参照できるので下記のような木構造を作るのに使うことができる
    • ex) 各ノード、リーフが整数を持つ木構造
type tree_t =
    Empty
  | Leaf of int
  | Node of tree_t * int * tree_t

# 使う時はこんな感じ
# tree1 = Empty
# tree2 = Leaf (1)
# tree3 = Node (Empty, 2, Node (Emply, 5, Leaf(9)))
  • 多相型を使うとより汎用的な木構造の定義をできる
    • ex) 各リーフ、ノードが連想(ハッシュと思ってよい)を持つ木構造
type ('a * 'b) association_tree_t =
    Empty
  | Leaf of 'a * 'b
  | Node of ('a * 'b) association_tree_t * 'a * 'b * ('a * 'b) association_tree_t

# 使う時
# tree1 = Empty
# tree2 = Leaf ("nekootoko3", 100)
# tree3 = Node (Empty, ("nekootoko3", 100), Node (Leaf ("pokemon", 100), ("occhan", 1500), Empty))

全通りを尽くしていない場合の対処

  • 例外の場合にinfinityを返さなければならないケースなどが存在すると思う
    • 例えばそれが空のリストを渡された場合に発生するとしたら、そもそも引数を2つ受け取るようにすることで解決できる
      • 引数の1つ目はリストには本来リストに含まれる値を渡し、その値とリストとで処理を行う。空のリストの場合には1つ目の値をそのまま返す
  • プログラムの構造を見直すことで網羅性高くうまい具合に例外も処理できる場合がある

オプション型

  • Ocamlに下記のような定義で組み込まれている
  • 値が存在しない場合のNoneと何かしらが存在しているSome ('a)を区別することができる
    • これによって該当値が見つからない場合は0を返すなど、特殊な返り値を使う必要が明確に存在しないことを表すことができる
type 'a option =
    None
  | Some of 'a

例外と例外処理

  • 他言語でもよくあるような形だと思う
  • raise 定義されたエラー型で例外を発生させられる
  • exception 定義したいエラー型 (of int)でエラー型の定義もできる
  • 下記のように例外の補足・処理を記述できる
trywith
    エラー型 -> 処理
  | エラー型 -> 処理

モジュール

  • 通常のモジュールと変わらない
    • 意味的にまとまりのある定義を1つにまとめる
  • 外部に公開するインターフェースはシグネチャ(signature)と呼ばれる
  • シグネチャとモジュールの実体は例えば下記のように宣言できる
module モジュールの名前 : sig
  シグネチャの本体
  ex) type ('a, 'b) t
  ex) val empty : ('a, 'b) t
  ex) val search : ('a, 'b) t -> 'a -> 'b
end = struct
  モジュールの本体
  ex) ここでは上で外部宣言されている3つを実装する必要がある
    1. 連想を持つ`t`という型
    2. `t`という型の構成子からなるemptyという変数
    3. `t`という型、とtのキーとなっている型を受け取りtの値を返すsearchという関数
end
  • 他言語のインターフェースと同じようなものだと思う
    • モジュールがシグネチャを満たしていなければ、Error: Signature mismatch:というエラーが発生する
    • シグネチャ、モジュールは別ファイルに分割もできるので、シグネチャは同じで内部実装を変えることもできる

副作用と参照透過性

  • 値の受け渡し以外を行っている関数を副作用を持つ関数と呼ぶ
    • 例えば文字の表示や渡された変数に代入して値を書き換えることがなどがそれにあたる
  • 参照透過性とは一度定義された変数がその後変更されないという性質のことを指す
    • この性質によりプログラムの途中や関数内で変数が書き換えられないことが保証された上で処理を書くことができるのが嬉しい
    • Ocamlのint型やリストなどは参照透過性を持っている
    • 参照型(ref)や配列(array)は参照透過性(referrence tranceparrency)を持たない型であり、データが不変であることが保証されない

参照透過性の喪失が問題を引き起こすのはモジュールのシグネチャ(インターフェース)だけが公開されており、破壊的変更を行うように見えない時など
下記例は極端だが、関数の利用者は配列要素の合計値を知りたいだけだが、実際には配列の各要素にそれまでの合計値を入れてしまっている
この配列を関数に渡す前の状態と思って利用すると思わぬ結果を引き起こす

(* 何の型を受け取って何を返すかだけが公開されている)
val add : int array -> int

(* 実際の実装 *)
let add arr =
  let sum = ref 0 in
  for i = 0 to (Array.length arr) - 1 do
    sum := !sum + arr.(i);
    arr.(i) <- !sum
  done;
  !sum

(* 実際に配列を定義して渡してみると、、、)
# let arr = [|1; 2; 3; 4; 5|] ;;
val arr : int array = [|1; 2; 3; 4; 5|]
# add arr ;;
- : int 15
# arr ;;
- : int array = [|1; 3; 6; 10; 15|]
(* 中身が書き換わっている *)

感想

非常によい本だと思った。
プログラミング初学者向けかと言われると怪しいが、少しプログラミングに慣れてきてプログラミングの新たなパラダイムを学びたい、
基礎的な概念をちゃんと身につけたいといったニーズには応えられる本なのではないかと思う。
丁寧に解説されているし、練習問題も多く入っているので身につけられることは少なくない。
オプション型って素晴らしいなぁと思ったり、よいコードを書くために必要なことも学べたんじゃないかと思う。
ただ関数型言語について分かったかと言われるとどうだろう。
それに関しては実際に自分でより多くのプログララムを関数型言語で書くことでその良し悪しを実感したり、
同じく関数型言語の別言語を学んでみることで関数型言語というパラダイムを共通項をまとめてみたりする経験が必要な気がする。
というわけで関数型言語を巡る冒険はしばらく続けていきたい。
さしあたり次はoz/mozartやる

protocol buffers, gRPC周りの整理

protocol buffers, gRPC周りの整理

この記事に書くこと

書くこと

  • gRPCの簡単な説明
  • protocol buffersの簡単な説明
  • 上記のサンプルコード(公式ドキュメントとほとんど同じ)

最初にざっくりと

  • protocol buffersは読み書きしやすいIDL(XMLみたいなもの)
  • gRPCはクライアント-サーバ間でのメソッド呼び出しを分かりやすく高速に行う機構

gRPC概要

  • クライアントが別環境のサーバーのメソッドをローカルのメソッド呼び出しのように行える仕組み
    • サーバーサイドはインターフェースを実装し、クライアントからの呼び出しをハンドルするためサーバを起動する
    • クライアントサイドは、サーバーサイドと同じメソッドを提供するスタブを持たせる
  • protocol buffers で、型、メソッド、メソッドがどの型を受け取り、どの型を返すかを定義する
    • 型定義はjsonでも問題ないが通常はprotocol buffersを使うことを想定している
  • http/2.0 をベースとしている

protocol buffers 概要

  • 構造化されたデータを自動的に読み書きするための機構
  • XMLみたいなものだが、XMLより可読性高く各言語の構造体へのエンコード、デコードも早い。
  • protoc
    • .protoファイル用コンパイラ
    • protoファイルの定義したデータ構造から任意の言語でのデータアクセス用クラスを生成する

サンプルコード

  • 下記がprotocol buffersの例。
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayGoodBye (GoodByeRequest) returns (GoodByeReply) {}
}

message HelloRequest {
  string name = 1;
  int32 count = 2;
}

message HelloReply {
  string message = 1;
  int32 count = 2;
}

message GoodByeRequest {
  string name = 1;
}

message GoodByeReply {
  string message = 1;
}
  • 詳細はドキュメントを読んでもらうとして、簡単な概要。
    • service Greeterは、rpc SayHelloメソッドと rpc SayGoodByeメソッドを持っている
    • SayHelloメソッドは message HelloRequest型を受け取り、 message HelloReply型を返す
    • Helloメソッドは message HelloRequest型を受け取り、message HelloReply型を返す
    • HelloRequest型は、name(string), count(int32)を持つ型
    • HelloReply型は、message(string), count(int32)を持つ型
    • GoodByeRequest、GoodByeReplyに関しても同様。

この.protoファイルを眺めると、サービスがどんなメソッドを持っていて、何を受け取って何を返すかがすぐに分かる!!

  • protocでgolangのコードを生成すると、下記のようなコードが生成される(一部抜粋)
// -----------------------------------
// 構造体関連
// -----------------------------------
type GoodByeRequest struct {
    Name                 string   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    XXX_NoUnkeyedLiteral struct{} `json:"-"`
    XXX_unrecognized     []byte   `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}

func (m *GoodByeRequest) Reset()         { *m = GoodByeRequest{} }
func (m *GoodByeRequest) String() string { return proto.CompactTextString(m) }
func (*GoodByeRequest) ProtoMessage()    {}
func (*GoodByeRequest) Descriptor() ([]byte, []int) {
    return fileDescriptor_17b8c58d586b62f2, []int{2}
}

func (m *GoodByeRequest) XXX_Unmarshal(b []byte) error {
    return xxx_messageInfo_GoodByeRequest.Unmarshal(m, b)
}
func (m *GoodByeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
    return xxx_messageInfo_GoodByeRequest.Marshal(b, m, deterministic)
}
func (m *GoodByeRequest) XXX_Merge(src proto.Message) {
    xxx_messageInfo_GoodByeRequest.Merge(m, src)
}
func (m *GoodByeRequest) XXX_Size() int {
    return xxx_messageInfo_GoodByeRequest.Size(m)
}
func (m *GoodByeRequest) XXX_DiscardUnknown() {
    xxx_messageInfo_GoodByeRequest.DiscardUnknown(m)
}

var xxx_messageInfo_GoodByeRequest proto.InternalMessageInfo

func (m *GoodByeRequest) GetName() string {
    if m != nil {
        return m.Name
    }
    return ""
}

// -----------------------------------
// クライアント関連
// -----------------------------------

// GreeterClient is the client API for Greeter service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type GreeterClient interface {
    // Sends a greeting
    SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
    // Sends a greeting again
    SayGoodBye(ctx context.Context, in *GoodByeRequest, opts ...grpc.CallOption) (*GoodByeReply, error)
}

type greeterClient struct {
    cc *grpc.ClientConn
}

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
    out := new(HelloReply)
    err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}

func (c *greeterClient) SayGoodBye(ctx context.Context, in *GoodByeRequest, opts ...grpc.CallOption) (*GoodByeReply, error) {
    out := new(GoodByeReply)
    err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayGoodBye", in, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}

// -----------------------------------
// サーバー関連
// -----------------------------------

// GreeterServer is the server API for Greeter service.
type GreeterServer interface {
    // Sends a greeting
    SayHello(context.Context, *HelloRequest) (*HelloReply, error)
    // Sends a greeting again
    SayGoodBye(context.Context, *GoodByeRequest) (*GoodByeReply, error)
}

func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
    s.RegisterService(&_Greeter_serviceDesc, srv)
}

次に書きたいこと

procol buffers で型に値が入っていない場合とデフォルト値を区別する

  • protocol buffers では型のフィールドに値が入っていない場合、デフォルト値が使われてしまう
    • Stringだと""、int32だと0といった具合にデフォルト値が入る。
  • 値が無い場合とデフォルト値をクライアントが入れてきている場合を区別したい場合があるはずだ。
    • 例えば Person型に int32 age を定義した時、age が不明だからageフィールドを空にして送るときがあるはず。一方で age = 0 を送ることも想定されるが、unknownと0は明確に区別したい、など。
  • 上記のような時に、デフォルト型をラップしてnilを使えるようにする。

grpc-gateway

  • 何らかの理由でクライアント-サーバー間で直接gRPCを使えない時があるかもしれない。
  • だけどprotocol buffersはとても分かりやすいインターフェースなので使いたい。
  • grpc-gatewayを使うと、サーバーの前段にjson API を gRPC に変換してくれるリバースプロキシをprotoco buffersの定義から生成してくれるらしい。

参考

rails開発3週間で学んだことなど

rails開発を始めて早いものでもう3週間も経った。 最初の開発も一段落しつつあるので何かメモを残しておこうかなと。 rails以外でも学んだことは多いのでそれも含めてのメモ。

コードリーディング編

使っているライブラリやモジュールを全てgrepで調べることはできない

  • 例えばgolangでは
    • 使うモジュールやライブラリは最初に宣言するので、そこを見ると外部パッケージを使っているのかそれとも内部に実装されているものを使っているのかが分かる
import "github.com/gin-gonic/gin" // 外部パッケージ
import "nekootoko3/app/server/meshi" // 内部実装
  • railsではスクリプト内で明示的に使うパッケージが明示されていないことが多い
    • 親クラスでの実装、あるいはincludeしていればすぐに辿ることができる。
    • しかしgemとしてinstallしているパッケージに関しては突然何の前触れもなく現れてこいつはだれだ!?となる

rails console の登場

  • rails開発はデバッグも含め、このコンソール抜きでは進めることができないと言っても過言ではない
  • pryが入っていて色々めちゃめちゃ便利にできるようになっている。
    • dotenvなどを使って環境変数から変数の中身を取り込むようになっている変数の表示ができる
    • (コンソール上で) >> $ 突然出てきたモジュール名
      • 実装元のファイルとその実装内容を表示してくれる!
    • 実際にモデルやメソッドを動かして何がアウトプットされるのか確認できる
    • (コンソール上で) >> cd オブジェクト
      • オブジェクトに入ることができる!
      • 下記のlsを行うと使える
    • (コンソール上で) >> ls (突然出てきたモジュール名、何も指定しなくてもよい)
>> Delayed::Worker::VARIABLE
=> 3
>> $ Delayed::Worker::VARIABLE
いろいろ出てくる
  • rails のコードリーディングはこれを活用しよう

デバッグ

  • railsではメタプログラミングなるものがフル活用されており、動的にロジックが生成されるのでタイポがあっても普通にサーバーが立ち上がる(静的解析がされていない?)
  • なので、rails consoleでコンソールを立ち上げて、該当のパスへのリクエスト、メソッドの実行などを行って確かめる。

コーディングのTIPS

多値の受け取り

- 関数からメソッドが2つ以上返ってくる時、受け取る変数が1つの場合、暗黙的に配列として受け取る。スカラー値として受け取りたい、ただし、2つ目の値はいらないという場合は下記のように `_` を使い、いらないけどとりあえず受け取っとくみたいなことをやるとよい。
a = return_two_values_method # a = [var1, var2]
a, _ = return_two_values_method # a = var1, var2

返り値にハッシュではなく構造体を返す

  • ハッシュだと a["key"]みたいになって若干書きにくい
  • チェインメソッドを使えるよう構造体を使うとよい(rubyに構造体あるって知らなかった)
  • この記事を見るとよいかも。

return nil 明示的に書かなくてもよい

  • ruby は最後に評価された値が返される
  • 評価される値が何もない場合には、nilが返されるので return だけでも nil が返される。

参考

  • この記事も参考になりそう
    • 会社のrubyにはこの記事に入っているpryが入っている気がする

プロを目指す人のためのRuby入門を読んだ

10月から転職してRubyも書く人になったので、プロを目指す人のためのRuby入門を読んでみた。 この流れで勉強すればよかったな、っていう振り返りも最後に合わせて記載。

この本をやる前の状態

読んでどうだったか

  • ちょっとはプログラミング経験あって本気でRuby習得目指す人向けとあるように、初歩の初歩的な内容はほぼない。
  • 本全体に渡ってサンプルコードが多く、説明もわかりやすかった。
  • とはいえ、「第10章 yieldとProcを理解する」はわからなかった部分もあった。
    • よく分からなかった部分もなんとなくそういうものがあるのだな、とは分かったので業務の中で直面した時に戻ってくればよいかな。
  • Railsチュートリアルを読む前にやるべきだったな、と強く思う
    • 特に、シンボルやクラス、モジュールの辺りはざっとでも読んでから入ったら習得具合が違ったのではないかと思う。
    • Railsチュートリアル自体は非常によいチュートリアルだった。プログラミング初心者があの内容をしっかりできたら非常に力がつくのではないかと思う。というか、心がとても強いと思う。
  • 他の本を読んでいないので比較はできないが、自分には優しすぎず難しすぎずなちょうどいいレベル感だと感じた。
    • Railsやっていてよく分からないなぁ、というところも分かりやすく書いてあった印象。実務に戻ってみないと効果のほどは分からんが、、、

Rails学習のオススメな道筋

プログラミング経験1年以上を想定。 初心者目な人はProgateを先にガッツリやってみてから下記ステップへ。

  1. この本(プロを目指す人のためのRuby入門)をざっとやる。そんな時間かけなくてもいい。
  2. Railsチュートリアルをやる。ちゃんとやる。
    • Rubyに関して分からないところが出てきたら、この本に立ち戻る
    • Railsに関して分からないところが出てきたら、railsガイドを見よう。特にActiveRecordとかは重点的に見るといいと思う。

道筋と書いたけどこの2ステップになるかな〜

python3でプロコンのためのデータ構造とアルゴリズム

プログラミングコンテスト攻略のためのアルゴリズムとデータ構造の基礎編までを終えた。
アリ本が有名だが、こちらの方が評判がよくまとまっていると感じた。
新しい言語でやりたかったというのもあって、python3で書いている。
githubにコードと簡単なコメントあり。

なぜデータ構造とアルゴリズムの勉強を始めたのか

  • 自分は文系出身だが情報工学を専攻していたエンジニアがベースとして持っている知識は身につけようと考えたから。
  • インタプリタの実装をしてみたときや、広告配信サーバ周りの会話でちょっとしたアルゴリズムの話が出ることがあり、エンジニアのベースだと感じたから。

やってみて何かいいことあった?

  • コードやライブラリの計算量を少し意識できるようになった
  • 木の巡回やヒープ、動的計画法など、今までなんとなく見たようなものが体系だって学べた
  • この問題はこんな綺麗に解決できるのか!という感動があるw

コンピュータサイエンスおもしろい!

ハノイの塔を実装する

ハノイの塔についてこちら

前提

  • 3つの杭を3つの配列(orig, tmp, to)で表現する
  • 各配列をスタックとみなし行える操作はpushとpopのみ
  • 円盤1つを配列内のintとして表現。大きな円盤は大きな数字で
    • 例えば、3段のハノイの塔は[3,2,1]といった形で表現する

目標

  • n段のorigをtoに移し替えること

まずは

  • 1段のとき、2段のとき、3段のときを考えてその法則性を探る

1段

  1. orig (1)-> to
orig = []
tmp  = []
to   = [1]

2段

  1. orig (1)-> tmp
# 1段と同じ操作をorig -> tmp
orig = [2]
tmp  = [1]
to   = []
  1. orig (2)-> to
# 1段と同じ操作
orig = []
tmp  = [1]
to   = [2]
  1. tmp (1)-> to
# 1段と同じ操作をtmp -> to
orig = []
tmp  = []
to   = [2,1]

3段

  1. orig (1)-> to
  2. orig (2)-> tmp
  3. to (1)-> tmp
# 2段のときと同じ操作を orig -> tmp で実行
orig = [3]
tmp  = [2,1]
to   = []
  1. orig (3)-> to
# 1段のときと同じ操作
orig = []
tmp  = [2,1]
to   = [3]
  1. tmp (1)-> orig
  2. tmp (2)-> to
  3. orig (1)-> to
# 2段目のときと同じ操作を tmp -> to で実行
orig = []
tmp  = []
to   = [3,2,1]

ちょっと多いけど4段目

  1. orig (1)-> tmp
  2. orig (2)-> to
  3. tmp (1)-> to
  4. orig (3)-> tmp
  5. to (1)-> orig
  6. to (2)-> tmp
  7. orig (1)-> tmp
# 3段のときと同じ操作を orig -> tmp で実行
orig = [4]
tmp  = [3,2,1]
to   = []
  1. orig (4)-> to
# 1段のときの操作
orig = []
tmp  = [3,2,1]
to   = [4]
  1. tmp (1)-> to
  2. tmp (2)-> orig
  3. to (1)-> tmp
  4. tmp (3)-> to
  5. orig (1)-> tmp
  6. orig (2)-> to
  7. tmp (1)-> to
# 3段のときと同じ操作を tmp -> to で実行
orig = []
tmp  = []
to   = [4,3,2,1]

上記の試行から3つの手順に分けられる

N段のハノイの場合

  1. N-1段をorig->tmpに移動
  2. 1番下(N段目)をorig->toに移動
  3. N-1段をtmp->toに移動

手順1と3を再帰的に行うことで

コードにすると(python)

動かした回数を出したい場合は、実際に動かす操作を行なっている箇所、つまりto.appendの前後どちらかでカウントをインクリメントすればよい

def move(n, orig, to, tmp):
    if n == 0:
        return
    # n-1段をorigからtmpに移動する
    move(n-1, orig, tmp, to, cnt)
    # n段目をtoに移動する
    to.append(orig.pop())
    # n-1段をtmpからtoに移動する
    move(n-1, tmp, to, orig, cnt)


import sys

# 引数でハノイの塔の高さを指定する
if len(sys.argv) != 2:
    exit
N = int(sys.argv[1])

# N段の塔、空の塔を配列で擬似的に表す
orig = [i for i in range(N, 0, -1)]
tmp = []
to = []

# origの塔をtoに動かす
move(N, orig, to, tmp)

カウントありのpythonコード