Hero Image
システム設計-part3-

はじめに バックエンドエンジニアのロードマップに沿ってエンジニアとしての自己肯定感を養うシリーズです。 ※現場で役立つシステム設計の原則を元に記事を作成しています。 業務ロジック メソッドをロジックの置き場所にする 現場で役立つシステム設計の原則では、“従来"という表現をされていますが、手続き型と呼ばれている設計ではデータクラスと機能クラスに分けて表現します。 その名の通りデータクラスはデータを格納して、機能クラスはデータクラスのデータを判断、加工、計算するといった使い方です。 この手続き型の問題は、拡張するときの変更箇所の特定に時間がかかるということです。 なぜかというと、データクラスが参照できるクラスであれば、アーキテクチャのどのレイヤーにでもロジックが書けてしまうからです。 便利のようには見えますが、先に言った変更箇所の特定に時間がかかるこの方法は最善ではありません。 解決としては、Java 本来のクラスの使い方を踏襲することです。 データとロジックを 1 つのクラスに閉じてしまおうという考え方です。 class PersonName { private String firstName; private String lastName; String fullName() { return String.format("%s %s", firstName, lastName); } } データであるfirstNameとlastName、そしてロジック(メソッド)のfullName()が同じクラス内にあります。 こうするとクラス内でデータを扱うことができて変更もこのクラス内で閉じることができます。 また、メソッドはクラス内のインスタンス変数(firstNameやlastName)を使って何らかの処理を行う用途で作成します。 クラスが肥大化したら小さく分ける これもやってしまいがちですが、改修を繰り返していくうちに、クラスが大きくなっていきます。 大きくなったクラスは手続き型同様に変更箇所の特定に時間がかかります。 それを防ぐために、大きくなってしまったクラスを次のルールで細分化します。 インスタンス変数とメソッドを対応付ける メソッドが全てのインスタンス変数を使うようになる 細分化したクラスはそれぞれ独立性が高くなるので、別のクラスで使う時にも再利用ができるようになります。 こうした関連の強いデータとロジックをまとめたクラスを凝集度が高いと言います。 凝集度が高いクラスは、変更箇所もそのクラスに閉じることになるので、疎結合になり他への影響が少なくて済みます。 まとめ 時すでに遅しと言いますか、現場での反省点をつらつら振り返ってベストプラクティスを学んでいるという感じです。 次回に活かそうというモチベーションは上がるのでいい復習方法だと感じます。 備考 現場で役立つシステム設計の原則 表紙イラスト:Loose Drawing

Hero Image
スレッドと並行処理

はじめに バックエンドエンジニアのロードマップに沿ってエンジニアとしての自己肯定感を養うシリーズです。 スレッド プロセスが最低1つは持っている実行単位のことです。 こんな言い方をするのは、プロセスが複数のスレッドを管理できるからです。 実行単位という視点でプロセスとの違いは、「アドレス空間」を共有できるという点です。 尾を引くようにプロセス管理の話に繋がりますが、プロセスにはそれぞれ1つのアドレス空間が割り当てられます。 そして別のプロセスからアドレス空間へのアクセスは原則できません。(これを可能にするために共有メモリという方法を使います) それに対して、スレッドは1つのプロセスの実行単位を分けたものですから、同じアドレス空間を共有できるというわけです。 そういうわけで、スレッドとプロセスをそれぞれ複数起動する場合は、スレッドの方がアドレス空間を1つで済ませることができるため省コストになります。 では、複数のスレッドを起動してやることは?というと並行処理です。 並行処理 これもすでに出てきている話ではあります。プロセス管理の記事で出した複数アプリを同時に起動させるという部分です。 「同時に」というのは私たちユーザがそう解釈しているだけで、アプリはカーネルが割り当てた非常に短い処理時間ごとに切り替えているのでしたよね。これが並行処理です。 スレッドでも同じように短い処理時間ごとに切り替えて「同時に」処理させることができます。 並列処理との違い 私自身、再三調べては納得 → 忘れるを繰り返していましたが、プロセス管理(3 度目)をまとめることでやっと理解できたと思います。 並行処理では処理時間ごとに切り替えると言いましたが、並列処理では CPU 1つは言わず2つで処理してしまえばいいじゃないという考え方です。 図で見ると非常にわかりやすいのですが、並行処理だとパン食べてチーズ食べてハム食べてレタス食べて、、を繰り返して食べ切る作戦なのに対して、並列処理はミックスサンドとして食べ切るようなイメージです。 そんなの絶対ミックスサンドとして処理したら無限じゃんと思われますが、並列処理にも上限があるようです。 アムダールの法則といって複数のプロセッサ(CPU のことですね)を使って並列化による高速化を行う場合、そのプログラムの中で逐次的に実行される処理部分(並列)の時間によって、高速化が制限されるというものです。 出典:wikipedia「アムダールの法則」より引用 まあ、上限があるといっても高速するのに変わりはないわけです。 今回はその中でも比較的面白い実装を見つけたのでそれを紹介します。 ワーカープール スレッドプールとも呼ばれるものです。並行処理でたくさんのスレッドを起動して、、というのももちろん可能ですが、それには代償が伴います。 ワーカープールはそのようにいくつもスレッドを起動させるのではなく、すでに起動したスレッドを使い回そうの精神で実装される並行処理です。 以下のような実装です。 こちらを参考にさせていただきました。 (ほぼコメントつけただけですが) package main import ( "fmt" "time" ) // 使い回し用のワーカー func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Println("worker", id, "started job", j) time.Sleep(time.Second) // 1秒待ち(重い処理を想定) fmt.Println("worker", id, "finished job", j) results <- j * 2 } } func main() { // タスクの数 const numJobs = 5 // こなさなければいけないタスク jobs := make(chan int, numJobs) // タスクの成果物 results := make(chan int, numJobs) for w := 1; w <= 3; w++ { // 使い回し用のワーカーだけ生成しておく(この状態ではまだタスクをもらってないのでブロック) go worker(w, jobs, results) } // タスク数だけjobsに渡す for j := 1; j <= numJobs; j++ { // チャネルへの書き込みを契機にワーカー起動 jobs <- j } // タスク数だけ格納されたらチャネルを閉じる close(jobs) for a := 1; a <= numJobs; a++ { <-results } } // 結果 worker 3 started job 1 worker 1 started job 2 worker 2 started job 3 worker 3 finished job 1 worker 3 started job 4 worker 1 finished job 2 worker 1 started job 5 worker 2 finished job 3 worker 1 finished job 5 worker 3 finished job 4 実行するとわかりますが、順番がごっちゃになって処理されているのがわかります。

Hero Image
メモリ管理

はじめに バックエンドエンジニアのロードマップに沿ってエンジニアとしての自己肯定感を養うシリーズです。 仮想メモリ プロセス管理でもあったように、メモリはアドレス空間ごとにプロセスを管理します。 アドレス空間は 4KB/8KB 単位のページに分割して管理されています。 ページはそれぞれ論理アドレス、物理アドレスを対応づける単位でもあります。 論理アドレスと物理アドレスは常に紐づけられているわけではなく、そのページが必要になった時点で割り当てることも可能です。 そのため、論理アドレスを実際の物理アドレスの容量より大きく確保することができます。 (実際に使えるメモリの量よりも大きなメモリを想定できるということです。) 仮装メモリとして使う仕組みには次の3つが挙げられます。 ページング 仮想メモリといえばこれ、という風に教えられるものの筆頭かと思います。 ハードディスクを物理メモリの代わりに使うといったものです。 物理メモリが不足すると、OS のコアであるカーネルは使われていないページをハードディスクに移して論理アドレスを解放します。 そしてプロセスがハードディスクに移されたページにアクセスしようとすると、カーネルがプロセスを停止し、ハードディスクのページを再度物理メモリに読み込み、論理アドレスを対応づけます。 また、プロセス全体を単位にする場合はスワッピングと呼ばれます。 メモリマップトファイル ファイルをメモリとしてアクセスすることができるものです。 アクセスがあった瞬間に、カーネルがファイルをメモリに読み込みます。プロセスがメモリを使い終わると、論理アドレスと物理アドレスを解放して、メモリの内容をファイルに保存します。 共有メモリ 1つの物理アドレスを、複数のプロセスの論理アドレスに対応づけるものです。 アドレス空間をまたぐと危険では?!という見方もありますが、複数プロセスで処理できるため、巨大な画像データを編集するときには都合が良いみたいです。 ※Go では共有メモリを使わずに Message Passing を使っています。 メモリ管理 API malloc(3) メモリをヒープ領域に割り当てます。プログラム実行時に決まるサイズのメモリはヒープ領域から確保します。 ヒープは「何かを積み重ねた山」のことで、その名の通り、プログラムを実行してから決定する量だけメモリを確保しておく領域なので納得です。 malloc で確保したメモリはfreeで解放しなければいけません。 calloc(3) メモリをヒープ領域に割り当てます。malloc と異なる点は、割り当てたメモリをゼロクリアすることです。 こちらも malloc 同様、確保したメモリはfreeで解放しなければいけません。 realloc(3) malloc で割り当てたメモリのサイズを拡大、縮小します。こちらも確保したメモリはfreeで解放しなければいけません。 free 割り当てたメモリを開放します。いったん開放したアドレスにはアクセスしてはいけません。 メモリの開放漏れを防ぐために、malloc で確保したメモリは常に free で開放されるべきです。 brk(2) malloc や realloc が割り当てるためのメモリを探してくるものです。 物理アドレスが割り当てられていないページに物理アドレスを対応づけます。 余談 メモリはエラーでもかなりお世話になる部分なので、次回以降、実際のエラーやプログラミング言語(Go か Java)に絡めた記事を書きたいです。 備考 ふつうの Linux プログラミング 第 2 版 Linux の仕組みから学べる gcc プログラミングの王道

Hero Image
Semantic Versioning

はじめに バックエンドエンジニアのロードマップに沿ってエンジニアとしての自己肯定感を養うシリーズです。 セマンティックバージョニング? アプリに振るバージョン番号をSemVerというルールに従って付与しましょうというものです。 確かにバージョン番号に意味を持たせることで、ユーザからもアプリのバージョン番号が上がればバグ修正なのか機能追加なのかわかりますし、プログラムからも互換性を考慮して処理を分けることができるのでよいですね。 これだけ覚えておけば OK バージョン番号の形式は、メジャー.マイナー.パッチです。(例:1.0.0) メジャー 後方互換性がない変更があった時にはこの番号を上げなければいけません(MUST) この番号を上げた際には、マイナー/パッチの番号は 0 にリセットしなければいけません(MUST) この番号が「0」の場合は初期開発用として扱います。リリースの段階でこの番号を「1」に上げます。 マイナー 後方互換性を保ちつつ、機能追加のある時にはこの番号を上げなければいけません(MUST) この番号を上げた際には、パッチの番号は 0 にリセットしなければいけません(MUST) パッチ 後方互換性を保ちつつ、バグ修正のある時にはこの番号を上げなければいけません(MUST) ※バグ修正とは間違った振る舞いを修正する内部の変更のことをいいます。 ちょっと踏み込むと プレリリースバージョンには、パッチ番号の後ろにハイフンで区切って識別子をつけることができます。 (例:1.1.0-alpha / 1.1.0-beta / 1.1.0-rc) ※ちなみに識別子のrcは「release candidate」の略でベータ版よりもさらに製品版に近い品質のバージョンにつけるそうです。(略を初めて知りました。) あと npm の packagge.json でもモジュールをセマンティックバージョンで管理してます。(~や^が付与されているのをよく見ると思います。) これについては上、真ん中、下で覚えるバージョニング範囲指定がわかりやすかったので共有しておきます。 余談 たかがバージョニング、されどバージョニングといった感じでした。知ってて損はないですよね。 備考 表紙イラスト:Loose Drawing

Hero Image
球面三角法による2点間の距離計算をGoで実装してみた

はじめに バックエンドエンジニアのロードマップに沿ってエンジニアとしての自己肯定感を養うシリーズです。 地球上の2点間の距離計算ってアプリだと Google Map API を使えば完了!だと思いますが、どう計算してるかって気になりますよね? 今回は球面三角法を利用した地球上の2点間の距離計算を Go で実装します。(調べたらフツーにあるんですが) 球面三角法とは その名の通り、三角関数を利用して球面上の辺や角の大きさを導出するものです。平面と球面とでの違いは辺の大きさが 球面では中心角によって表されることにあります。 よって、球面三角法を使用して算出した弧の長さ(中心角)と赤道の半径を乗算すると距離が求まります。 球面三角法の証明については、球面三角形の定理を参考にしました! (“高校生に向けて"とある通り、非常にわかりやすかったです) 球面三角法の余弦定理を利用して実際に距離を算出する方法は球面三角法の余弦定理がわかりやすいです。 実装 実装したソースコードは Github でも確認できます。 球面三角法を利用した2点間の距離計算 package main import "math" // Coordinate 緯度経度 type Coordinate struct { Longitude float64 Latitude float64 } // EarthRadius 赤道半径 const EarthRadius = 6378140 // DistanceOnTheEarth 地球上の 2 点間の距離を出す(球面三角法) func DistanceOnTheEarth(from, to Coordinate) float64 { fromLadLon := from.Longitude * math.Pi / 180 fromLadLat := from.Latitude * math.Pi / 180 toLadLon := to.

Hero Image
ソートアルゴリズムをGoで実装してみた

はじめに バックエンドエンジニアのロードマップに沿ってエンジニアとしての自己肯定感を養うシリーズです。 マージソート マージソートは、ソートのアルゴリズムで、既に整列してある複数個の列を 1 個の列にマージする際に、小さいものから先に新しい列に並べれば、新しい列も整列されている、というボトムアップの分割統治法による。大きい列を多数の列に分割し、そのそれぞれをマージする作業は並列化できる。 出典:wikipedia「マージソート」より引用 最悪の計算量が O(n log n) であるから少なくとも O(n^2)よりは速いんだろうなという印象(雑すぎるか) 以下「ソートを極める! 〜 なぜソートを学ぶのか 〜」を元に実装してみた(なるべくソースを見ないで実装を試みたがマージする箇所は折れた、、) package main import ( "fmt" "time" "github.com/uh-zz/traning/algorithm/shuffle" ) func main() { // ランダムな要素 n 個のスライス取得 input := shuffle.RandomIntList(n) inputLength := len(input) // マージソート MergeSort(&input, 0, inputLength) } // MergeSort マージソート func MergeSort(input \*[]int, left, right int) { // 要素数1つの場合は抜ける if right-left == 1 { return } // 配列を2つに分けるインデックス middle := left + (right-left)/2 // 配列左側 MergeSort(input, left, middle) // 配列右側 MergeSort(input, middle, right) var buffer []int // 左側と右側をバッファにためる(右側反転) for index := left; index < middle; index++ { buffer = append(buffer, (*input)[index]) } for index := right - 1; index >= middle; index-- { buffer = append(buffer, (*input)[index]) } // マージする scopeLeft := 0 scopeRight := len(buffer) - 1 for index := left; index < right; index++ { if buffer[scopeLeft] <= buffer[scopeRight] { // 左側採用 (*input)[index] = buffer[scopeLeft] scopeLeft++ } else { // 右側採用 (*input)[index] = buffer[scopeRight] scopeRight-- } } } これ考えたのぶっ飛んでるなあと思って Wikipedia 見てたら、考案者がフォン・ノイマンでやっぱりぶっ飛んでた(凄すぎ)

Hero Image
プロセス管理

はじめに バックエンドエンジニアのロードマップに沿ってエンジニアとしての自己肯定感を養うシリーズです。 プロセスとは プロセスという概念は Linux において、ファイルシステム、ストリームに並んで重要な構成要素の1つです。 プログラマが作成したソースコードはファイルに保存されます。そしてファイルの保存先はハードディスクです。 プログラムの実行時、プログラムはハードディスクからメモリへと読み込まれます。 CPU はメモリに読み込まれたプログラムを順次処理していきます。このとき、メモリに読み込まれて CPU に処理されているプログラムをプロセスといいます。 1つのプロセスを処理できるのは1つの CPU のみです。 そのため、同じプロセスしか一度に実行できなくなるといったことを避けるために、CPU はプロセスごとに処理時間を決めて次々に切り替えます。 普段使っている PC やスマホは Youtube や Line や Twitter など、複数アプリを同時に起動して使用しています。 あれは CPU が処理時間を決めて順に処理しているために実現されています。 OS のコアであるカーネルはプロセスの優先順位を考慮して、各プロセスに処理時間を割り当てます。 (この機能をスケジューラ、またはディスパッチャといいます。) アドレス空間 プロセス1つに対して、CPU とメモリがそれぞれ1つ必要です。CPU は前述の通り、処理時間を割り当てるのに対し、メモリはプロセスごとにアドレス空間を割り当てます。 メモリにプログラムを書き込む際にはアドレスが必要です。 しかしプロセスには 0 番地から始まるメモリが必要なため、1つのプロセスしか使えなくなってしまいます。 そこでプロセスから見えるアドレス(論理アドレス)と実際のアドレス(物理アドレス)を分けてしまいます。 こうすることで、カーネルと CPU によって論理アドレス → 物理アドレスと変換された実際のアドレスに対して書き込むことができます。 1つのプロセスの論理アドレス、物理アドレスを全体としてアドレス空間といいます。 アドレス空間はプロセスごとに割り当てられるので他のプロセスにアクセスできなくなります。 プロセス API fork(2) 自分のプロセスを複製して新しいプロセスを作ります。 Github でも fork がありますが、意味合いは同じです。既存のリポジトリを複製します。複製したリポジトリは自由に更新できますが、fork した元のリポジトリに対しては更新はできません。 プロセスの fork は元からあるプロセスを親プロセス、複製されたプロセスを子プロセスと呼びます。 子プロセスの fork 実行時の戻り値は 0 です。 (戻り値 0 は正常終了のステータスコード)そして親プロセスの fork 実行時の戻り値は子プロセスのプロセス ID です。

Hero Image
アジャイル開発

はじめに バックエンドエンジニアのロードマップに沿ってエンジニアとしての自己肯定感を養うシリーズです。 アジャイル開発 「アジャイル開発」っていうとなんかカッコいいしモダンっぽいというイメージをおそらく持っている人もいるでしょう。(私を含めて) 逆に「ウォーターフォール開発」はなんか古臭いし、どこぞの金融系ぷ r、、おっと誰か来たみたいなのでこの辺で。 とまあ、もてはやされたアジャイル開発ですが、フタを開けてみれば「要件定義 → 設計 → 実装 → テスト」の全工程を1つの単位として反復するという手法なのです。 反復する期間はチームやプロジェクトによってまちまちですが、1 週間〜4 週間ほどです。 ってことはですよ、V 字モデルのウォーターフォールを短いスパンで回してるだけ?、、それウォーターフォールじゃねぇか!! 、、というヤジも分からなくはありませんが、ちゃんとメリットがあります。 メリット 1. スピーディー(早い) だってそうですよね。ウォーターフォールでは全工程を段階的に進めていくのでリリースまでに時間がかかってしまいます。 対してアジャイルでは前工程を1つのサイクルとして反復するのでリリースまでの期間が短く済みます。 2. やすい(安い ×) しかもアジャイルは、開発サイクルが短い分、仕様変更や追加機能の対応がしやすいというのもあります。 ウォーターフォールだと、段階的に進めるので、1つの仕様変更があった場合、工程を戻すことになり、、おぉ、、考えただけでも恐ろしいですね。 3. ユーザーファースト(うまい?) これも納得ですね。 リリースが早い分、クライアント(ユーザー)に効率よく素早く提供できる → クライアント喜ぶ → 褒められる → 嬉しい=うまい? (これは数合わせです) アジャイル開発の手法 手法は以下の3つです。 スクラム エクストリームプログラミング ユーザ機能駆動開発 この中で私が経験したのは、スクラムのみです。(2020/07 時点) どのサイトでも言われている通り、この開発手法ではメンバーとのコミュニケーションが非常に重要です。 そのイテレーション(スプリント)でリリースする機能も複数人が関わっていたり、メンバー間での連携が必要な機能だったり。。 極めつけは1つのアプリの全機能を全メンバーが把握しているのがヨシとされるので、知らない機能は教えたり教わったりしないといけないからです。(これは私のチームだけなのかは知りませんが) まとめ 、、とすごく大変そうに見えますが(実際に大変ですが)、スクラムならではの団体戦みのある開発でまあうまく回せるんではないでしょうかというのが感想です。 備考 表紙イラスト:Loose Drawing