プログラミング | 株式会社Altus-Five / 株式会社Altus-Five は、技術力で勝負するシステム開発会社です。 Mon, 01 Sep 2025 00:37:48 +0000 ja hourly 1 https://wordpress.org/?v=6.8.2 /wp-content/uploads/2025/01/cropped-favicon-32x32.png プログラミング | 株式会社Altus-Five / 32 32 06 データパイプラインの開発・保守/運用 /works/2025/08/29/data-pipeline-development-maintenance-operation/ /works/2025/08/29/data-pipeline-development-maintenance-operation/#respond Fri, 29 Aug 2025 08:39:50 +0000 /?p=748 データパイプラインの基盤移行・統合と運用ルール整備を実施。可視化やリカバリ設計により安定したデータ運用環境を構築。最新技術を活用し、保守・運用効率化と品質向上を実現したプロジェクトです。

The post 06 データパイプラインの開発・保守/運用 first appeared on 株式会社Altus-Five.

]]>
プロジェクト概要

データパイプラインの基盤移行・統合と運用ルール整備を実施。可視化やリカバリ設計により、安定したデータ運用環境を構築しました。
最新のデータオーケストレーション技術を活用し、保守・運用の効率化と品質向上を実現しました。

技術要素

  • Python
  • dagster
  • dbt
  • Hadoop
  • Athena
  • Glue
  • Docker
  • PostgreSQL

プロジェクトの特徴

  • データパイプラインの可視化
    dagster基盤による運用状況の見える化
  • リカバリ設計
    途中からの復旧が可能な柔軟な設計
  • 保守・運用ルール整備
    安定運用のための手順・ルールを策定

成果

  • データ運用の安定化
  • 保守・運用効率の向上
  • 最新技術への対応
  • 運用品質の向上

Altus-Fiveの強み

最新技術への対応力と運用ルール策定力で、複雑なデータ基盤の移行・運用も安定してリード。顧客の業務効率化と品質向上に貢献できる点が弊社の強みです。

The post 06 データパイプラインの開発・保守/運用 first appeared on 株式会社Altus-Five.

]]>
/works/2025/08/29/data-pipeline-development-maintenance-operation/feed/ 0
05 電力会社様向け卒FITシステム開発 /works/2025/08/29/post-fit-system-development-for-power-companies/ /works/2025/08/29/post-fit-system-development-for-power-companies/#respond Fri, 29 Aug 2025 08:35:40 +0000 /?p=746 再生可能エネルギーの固定価格買取制度(FIT)満了後のスイッチング業務を支援するシステムを開発。シームレスな申請手順と外部連携機能で業務効率化とユーザー満足度向上を実現。リリースタイミングに合わせた柔軟な仕様調整力が強みです。

The post 05 電力会社様向け卒FITシステム開発 first appeared on 株式会社Altus-Five.

]]>
プロジェクト概要

国の再生可能エネルギー固定価格買取制度(FIT)満了後のスイッチングを支援するシステムを開発。シームレスな申請手順と外部連携機能で業務効率化に貢献しました。
リリースタイミングに合わせた仕様調整力で、顧客の要望に柔軟に対応しました。

技術要素

  • Java11
  • Spring Boot 2
  • Thymeleaf
  • JQuery
  • PostgreSQL

プロジェクトの特徴

  • シームレスな申請手順
    ブラウザの読み込みを最小限に抑えた快適な操作性
  • OCCTO連携機能
    外部機関との連携による業務自動化
  • 柔軟な仕様調整
    リリース時期に合わせた迅速な対応

成果

  • スイッチング業務の効率化
  • ユーザー満足度の向上
  • 外部連携による業務自動化
  • 期日厳守のリリース

Altus-Fiveの強み

リリースタイミングに合わせた仕様調整力と最新技術への対応力で、顧客の要望に柔軟かつ迅速に応えることができる点が弊社の強みです。

The post 05 電力会社様向け卒FITシステム開発 first appeared on 株式会社Altus-Five.

]]>
/works/2025/08/29/post-fit-system-development-for-power-companies/feed/ 0
TypeScriptで名前付き引数っぽい実装をするます。 /blog/2024/01/25/typescript-python-like-named-parameters/ Thu, 25 Jan 2024 10:40:00 +0000 http://43.207.2.176/?p=31 TypeScriptでは、Pythonのような関数呼び出し時に引数名を使って「名前=値」の形式で引数を指定することができません。 でも、 Options Objectパターンを使って、似たような実装をすることができます。 […]

The post TypeScriptで名前付き引数っぽい実装をするます。 first appeared on 株式会社Altus-Five.

]]>
TypeScriptでは、Pythonのような関数呼び出し時に引数名を使って「名前=値」の形式で引数を指定することができません。 でも、 Options Objectパターンを使って、似たような実装をすることができます。

https://typescriptbook.jp/reference/functions/keyword-arguments-and-options-object-pattern

そして、 js.langchain のコードを見ていたら、これが、たくさん、使われてました。 例えば、以下のようなコードです。

export interface BaseLangChainParams {
    verbose?: boolean;
    callbacks?: Callbacks;
    tags?: string[];
    metadata?: Record<string, unknown>;
}

export abstract class BaseLangChain<...> extends Runnable<...>
  implements BaseLangChainParams
{
  verbose: boolean;
  callbacks?: Callbacks;
  tags?: string[];
  metadata?: Record<string, unknown>;

  constructor(params: BaseLangChainParams) {
    super(params);
    this.verbose = params.verbose ?? getVerbosity();
    this.callbacks = params.callbacks;
    this.tags = params.tags ?? [];
    this.metadata = params.metadata ?? {};
  }
  ...

引数の型が同じものが連続していると、引数を指定する順番を間違って、バグになることがあります。 でも、上記の実装方法だと、それを防げます。

const chain = new BaseLangChain({
    verbose: false,
    tags: ['hoge'],
    metadata: { fuga: false, piyo: 'PIYO' },
});

Options Objectパターンというのを知らなかったので、最初見たときに、なんのために、Xxxx クラスと XxxxParams という interface を 実装するのか疑問だったのだけど、Options Objectパターンというプログラミングのベストプラクティスに則っているのだと、わかりました。 TypeScript や Java のように名前付き引数がない言語で、引数が多いときに、このパターンが、役に立ちそうです。 interfaceの実装が必要で、少し冗長になるので、引数が多いときに限定して使うと良いように思います。

ご参考まで。

The post TypeScriptで名前付き引数っぽい実装をするます。 first appeared on 株式会社Altus-Five.

]]>
コードの品質を測定する方法 /blog/2023/10/17/measuring-code-quality/ Tue, 17 Oct 2023 10:40:00 +0000 http://43.207.2.176/?p=28 コードの品質を測定する方法が記載されてました。 GitHub や Gitlab などの API のあるコード管理を使っているなら、 CI や、品質計測用の日次バッチで、自動計測できそうです。 変更のリードタイム: fir […]

The post コードの品質を測定する方法 first appeared on 株式会社Altus-Five.

]]>
コードの品質を測定する方法が記載されてました。

https://codezine.jp/article/detail/18337

GitHub や Gitlab などの API のあるコード管理を使っているなら、 CI や、品質計測用の日次バッチで、自動計測できそうです。

変更のリードタイム: first commit から、その commit が本番にリリースされるまでの時間

ブランチのログから取得できます。 ついでに API で Issue の着手から Close までの時間、 PR の作成から、マージまでの時間なんかも計測できます。

手戻り数: プロジェクト管理システムにおいて、プロセスの前段に戻ってきたタスクの数

例えば、あるタスクを「完了」に移動したが、実際にはバグがあった場合、タスクは前段に戻され「手戻り」となる。

と記載されてましたが、なんらかのルールを設けないと、計測に抜け漏れが出そうです。

私は、これまで、完了にしたタスクを戻すことはしてなくて、新しいタスクを作っていました。 タスク(Issueチケット)の動かし方を分析して、仕様変更で手戻りが発生したケース、バグによる手戻りのケースなどで分類分けして、 Issue チケットのワークフローをルールで決めておかないと、正しく計測できない気がします。

次のタスクに取り掛かって、前のタスクのバグが見つかったとしても、前のタスクの状態を戻さずに作業することも、よくあるので、厳密な管理は難しいかもしれません。 参考程度の指標として使うのがよいかもです。

ルールが決まったら、 API で回収できるので、自動化できます。

手戻り率: プロジェクト管理システムにおいて、プロセスの前段に戻ってきたタスクの割合

タスクが戻った回数をB、タスクが進んだ回数をFとすると、手戻り率は (B / (F + B)) * 100 で表せる。

上記のとおり、「手戻り」の識別方法を、どこまで厳密化できるかが課題ですが、参考指標にはできそうです。

コード行数(LOC:Line Of Code)

昔の開発では、プロジェクトの終了時に、開発したコードの行数を数えてたな・・・と、懐かしいです。 記事の中でも、「取り扱いが難しい指標」と記載されています。

本当に手が早い人で、コードの行数が多い=生産性が高いと評価できる人もいるかもしれないけど、コピペ君が書いたコードかもしれないです。 LOC を生産性の指標とするのは、やや問題がありそうです。

ただし、コード量が多いとメンテナンスにかかるコストが大きくなるのは、そのとおりなので、バグの数を LOC で割ったものを「バグ密度」と定義して、品質の指標には、使えると思います。
保守性を推し量る指標にもなるかもしれません。

行数のカウント方法には、LOC、NLOC、LLOC、CLOC など、やり方がいくつかあり、さらに「2つの命令が書かれた行は2行と数える」「括弧だけの行を除く」というルールを設ける場合もあるようです。そこまでやると、なかなか大変ですが、 AST を使ってコードをパースすれば、計測できなくはないです。

どこまでやるかは検討するとして、自動化できます。

循環的複雑度(Cyclomatic Complexity)

大雑把にいえばコード内のif/else、for、switch、whileなどの分岐やループの数を数え上げたもの

だそうです。

ツールがあるので、自動化可能です。

  • OCLint: C言語向けの静的解析ツール
  • GMetrics: Java向けのメトリクス分析ツール
  • Lizard: コマンドラインの循環的複雑度計測ツール(たくさんの言語がサポートされている)

循環的複雑度とそのコードが編集された回数には密接な関係があるそうで、 複雑なコードは修正回数が多くなるというのは、頷けるところです。

私としては、オブジェクト指向の複雑度も計測してほしいです。
オブジェクト指向が進むと、条件分岐じゃなくて、汎化と具象化で機能を入れ替えますが、 他人が書いた複雑なオブジェクト指向な実装は、コードの理解とデバッグ難易度が高いので、 ちょうどよい塩梅の指標が欲しいです。

コードの重複(Code Duplication, Clone Code)

DRY 原則としても有名です。コピペはやめましょう。

これも、ツールがあるので、自動化可能です。

  • Clone Detective
  • Simian
  • PMD
  • SonarQube

検出した重複コードを使ってコード品質を測定するなら、 例えば、プロジェクトのコード行数に対する重複コード行数の割合、 つまり「重複率」を測定することができます。 この「重複率」をKPIとして利用しているチームも多いでしょう。

との記載もありました。

また、以下のようにも。

(「重複率」は)低い方がより良いだろう、ということは自明だと思いますが、 先に紹介した循環的複雑度とは異なり、実は重複度に関しては明確な基準値はありません。

重複度に関しては、その絶対値よりも値の時系列変化に着目した方が良いでしょう。 ある程度の期間の中で何か著しい増加が観測されたら、 チームを集めてその原因や対処方法について検討してみて下さい。

グラフ表示できるとよさそうです。

その他の指標

以下は、健全なプロジェクトであれば、すでに取り組んでいるでしょう。

  • ユニットテストとカバレッジ
  • コードレビュー頻度
  • 性能プロファイリング

まとめ

このような計測データを、プロジェクト別に集計するだけではなくて、個人別に集計すると、これまで見えていなかった課題が、見える化されるかもしれません。みなさんも、品質指標の計測の自動化に取り組んでみては、いかがでしょうか?

The post コードの品質を測定する方法 first appeared on 株式会社Altus-Five.

]]>
デメテルの法則 /blog/2023/01/26/demeters-law/ Thu, 26 Jan 2023 10:40:00 +0000 http://43.207.2.176/?p=27 「直接の友達とだけ話すこと」というプログラミングのお約束です。 社員、部署、会社というオブジェクトがあったとします。 社員の会社名を取り出すときに、社員から”直接の友達”じゃない会社にアクセスしてはダメという話です。 こ […]

The post デメテルの法則 first appeared on 株式会社Altus-Five.

]]>
「直接の友達とだけ話すこと」というプログラミングのお約束です。

社員、部署、会社というオブジェクトがあったとします。

class Employee {
  String id;
  String name;
  Depertment dept;
}

class Depertment {
  String id;
  String name;
  Company company;
}

class Company {
  String id;
  String name;
}

社員の会社名を取り出すときに、社員から”直接の友達”じゃない会社にアクセスしてはダメという話です。 こういうのはダメです。

employee.dept.name

じゃ、Employee に getter を追加したらいいのか?

class Employee {
  String id;
  String name;
  Depertment dept;

  String getCompanyName() {
    return dept.name;
  }
}

これも正解ではない。というか、何も解決してない。 問題は、社員、部署、会社という3つのオブジェクトに関与してしまっていることです。 依存が大きくて、ほかのオブジェクトの影響を受けやすいです。

これのテストコードを書くことを想像してください。テストデータとして、3つのオブジェクトのテストデータを作らないといけないです。 ユニットテストのコードの大部分は、テストデータとモックなので、依存するオブジェクトが多いと、ユニットテストのコード量も肥大していきます。

ではどうするのか? 解決策は、専用のクラスを作って、責任範囲をそのクラスに限定することです。 “社員の会社名を出力する” とき、このオブジェクトのことだけ知ってればよくて、3つのオブジェクトを知る必要はない。それらのオブジェクトの事情に影響をうけることもない。

class CompanyEmployeeName {
  String employeeId;
  String employeeName;
  String companyName;
}

また、クラスは、多重継承よりも、コンポジットの方がよいのだけど、さらに、プロパティは、できるだけフラットに設計するのが、よいとされてます。 専用クラスを作って、データを移し替えることで、フラットにすることを検討しましょう。

これだけで、テスタビリティは、ものすごく改善します。テストデータは、1つだけだから、テストコードもスッキリします。 とりあえず、なんらかの処理を書くときは、ほかの処理で使っているオブジェクトを使いまわすんじゃなくて、 いったん、必要な項目だけの専用の入れ物を作って、移し替えて、結合の依存をできるだけ薄い状態にして、処理を書くようにする。 これを徹底することで、ユニットテストの作業負荷を小さくしていきましょう。

デメテルの法則、覚えておきましょう。

The post デメテルの法則 first appeared on 株式会社Altus-Five.

]]>
PySparkの分散される処理単位であるクロージャと共有変数の仕組み /blog/2020/06/15/pyspark/ /blog/2020/06/15/pyspark/#respond Mon, 15 Jun 2020 05:32:00 +0000 http://43.207.2.176/?p=270 Spark では、処理が分散されて、複数のノードやスレッドで実行されますが、 分散先でのデータの共有方法と、分散先で実行される処理単位のクロージャについて説明します。 共有変数 Spark には、データの共有とか、集約す […]

The post PySparkの分散される処理単位であるクロージャと共有変数の仕組み first appeared on 株式会社Altus-Five.

]]>
Spark では、処理が分散されて、複数のノードやスレッドで実行されますが、 分散先でのデータの共有方法と、分散先で実行される処理単位のクロージャについて説明します。

共有変数

Spark には、データの共有とか、集約するための仕組みが備わっています。
それが、ブロードキャスト(Broadcast)と集約器(Accumulator) です。
http://mogile.web.fc2.com/spark/spark220/rdd-programming-guide.html#shared-variables

ブロードキャスト
参照専用の共有メモリのようなもので、ドライバーで共有したい変数をブロードキャスト変数として登録すると、その変数のラッパーオブジェクトが作成されて、ラッパーオブジェクトが、各エグゼキューターに配信されます。
エグゼキューターからは、ラッパーを通じて、共有変数の実態にアクセスすることができます。 このあたりは、通信のコストを下げるために効果的なブロードキャストアルゴリズムで実装されていると、マニュアルに記載されています。

集約器(Accumulator)
accumulatorは、ドライバーで初期化されて、エグゼキューターからは、加算処理だけが可能で読み取りは出来きません。 読み取りはドライバーからのみ行うことができます。
何かの件数をカウントしたり合計値を求めるような処理に使われます。

クロージャ

PySpark では、ドライバがRDDのメソッドをクロージャとしてシリアライズして、エグゼキューターに配信して、エグゼキューターでデシリアライズされて、処理が実行されます。

Python は “全てがオブジェクト” ということで、関数もオブジェクトなので、関数自体が様々な属性を持っています。
例えば、func()という関数は、func.__code__でコード自体を参照可能だし、func.__globals__で関数内で参照している global な変数がリストされます。他にも、func.__defaults__func.__dict__などもあります。 https://docs.python.org/ja/3/reference/datamodel.html

クロージャとは、関数と関数内で使用される変数などがセットになったものを指します。 ときどき、クロージャとlambda関数が同じものとして説明されている記事を見ることがありますが、違うものです。lambda関数は、無名関数がクロージャを構成するときの要素の1つというのが、正しいです。

繰り返しになりますが、PySparkは、クロージャをシリアライズして、各エグゼキューターに配信します。

Python のシリアライズモジュールには pickle, marshal, dill, cloudpickle などがあります。(※この記事もあわせて読んでください。https://qiita.com/kojisuganuma/items/e9b29e8e5ef5f5f289b2
PySpark のシリアライズモジュールは、Python標準の pickle を元にした、独自のものでした。 実際のソースコードは、こちらです。 https://github.com/apache/spark/blob/d48935400ca47275f677b527c636976af09332c8/python/pyspark/cloudpickle.py#L222
A func comprises: code, globals, defaults, closure, and dict.とコメントがあるとおり、 PySparkがエグゼキューターに配信するクロージャは、以下を要素としています。

  • コード
  • 関数内で使っているグローバル変数
  • 引数のデフォルト値
  • 引数以外の変数に対してのセル (cell) 群
  • 関数属性の名前空間

シリアライズされる変数(グローバル変数など)が持つ値は、”宣言時のスコープ”によって設定された値です。 間違いやすいのは、global変数が宣言されていて、その変数の設定を init() などで、別の関数で初期化してたりすると、たとえ、それが RDD を実行する前だとしても、配信先では、復元されないということです。あくまで、変数宣言時のスコープで行われる処理だけがシリアライズされます。

参考記事

https://qiita.com/Hiroki11x/items/4f5129094da4c91955bc#%E2%85%B3-rdd%E3%81%AE%E6%93%8D%E4%BD%9C
https://www.lifewithpython.com/2014/09/python-use-closures.html

stackoverflow にも興味深い記事がありました。
「PySparkがブロードキャストされなかった変数を参照できるのはなぜですか?」
https://stackoverflow.com/questions/33337446/why-is-pyspark-picking-up-a-variable-that-was-not-broadcast
以下、回答の訳です。

基本的には、クロージャはシリアライズされて各executorとtaskに送られ、 task実行中にアクセスできるようになるので、 RDDのスコープ外のすべての変数をブロードキャストする必要はありません。
ブロードキャスト変数は、ブロードキャストされるデータセットが大きい場合に便利です。

まとめ

ブロードキャストとクロージャの使い分け

グローバル変数の初期値としてセットして、それを RDD や UDF の関数内から、参照するように実装しておけば、クロージャとして、データも一緒にエグゼキューターに配信されるので、小さいデータは、クロージャとして実装して、大きいデータはブロードキャストの機能を使うという使い分けができます。

分散実行されるコードの境界

Javaと違って、Pythonのコードだと、分散先で実行されるコードの境界がわかりづらいのだけど、その答えは、クロージャが実行されるということでした。

ご参考までに。

The post PySparkの分散される処理単位であるクロージャと共有変数の仕組み first appeared on 株式会社Altus-Five.

]]>
/blog/2020/06/15/pyspark/feed/ 0
「オブジェクト指向エクササイズ」でクセの強いコードを矯正しよう /blog/2019/09/24/object-oriented-programming-exercise/ /blog/2019/09/24/object-oriented-programming-exercise/#respond Tue, 24 Sep 2019 09:22:00 +0000 http://43.207.2.176/?p=289 この記事は、10年前に書いた記事なんですが、今年の新人の人たちにも、やってみてほしくて、古いblog記事から掘り起こして転載しています。 linterを使ったコーディングスタイルのチェックだけでは、スパゲッティなコードを […]

The post 「オブジェクト指向エクササイズ」でクセの強いコードを矯正しよう first appeared on 株式会社Altus-Five.

]]>
この記事は、10年前に書いた記事なんですが、今年の新人の人たちにも、やってみてほしくて、古いblog記事から掘り起こして転載しています。

linterを使ったコーディングスタイルのチェックだけでは、スパゲッティなコードを直すことができません。 よくできたコードは、パッと見で、”なんか違う”と感じさせるところがあり、あぁ、このコードを書いた人はデキるって思わせるものです。
読みやすくて、きれいなコードを書くためには、どうしたらよいんだろうか?さらには、それを人に教えるには、どうしたら?
そのヒントが「オブジェクト指向エクササイズ」にあります。
この本を読んだときに、”これだ!”と感じたことを覚えています。

以下、10年前に書いた勉強会後記です。


今回の勉強会は「ThoughtWorksアンソロジー」の第5章、「オブジェクト指向エクササイズ」の読み合わせをやりました。

エクササイズは、1000行程度の小さなプロジェクトを準備して、次の9つのルールを厳密に適用させてみるという内容です。ルールを強制することで、いままでの自分流のコーディングとは違ったアプローチを発見することを目的としています。

  • ルール1: 1 つのメソッドにつきインデントは1段階までにすること
  • ルール2: else 句を使用しないこと
  • ルール3: すべてのプリミティブ型と文字列型をラップすること
  • ルール4: 1 行につきドットは1 つまでにすること
  • ルール5: 名前を省略しないこと
  • ルール6: すべてのエンティティを小さくすること
  • ルール7: 1 つのクラスにつきインスタンス変数は2 つまでにすること
  • ルール8: ファーストクラスコレクションを使用すること
  • ルール9: Getter 、Setter 、プロパティを使用しないこと

参加したメンバーには、事前に1000行程度のプログラムを準備するように伝えてあったんですが、読み合わせが終わった時点で時間がなくなってしまい、実際のプログラムのリファクタリングは宿題となりました。残念ながら、エクササイズのビフォーアフターを載せることはできないのですが、ルールの要約と、読み合わせ中に話題となったことを少し書こうと思います。

ルール1

1つのメソッドにつきインデントは1段階までにすること

インデントの深さを抑えることで、メソッドをコンパクトにすることが目的。
メソッドが小さくなると、そのメソッドの再利用性が高まるし、さらに小さなコード片ならバグを発見するのも簡単になる。
さて、読み合わせ中に、こんな話がありました。「これを実際に適用したら、プログラムを追うときに、あちこちに飛ばないといけないので、実際には読みにくいプログラムになるんじゃないか」と。
しかし、この問いは9つのルールを全部適用してみれば、なるほど解決するだろうと思います。小さくなったメソッドの役割はどんどん鮮明になっていき、クラス分割や再配置が行われ、全般的に洗練されると期待できるからです。
これは良いエクササイズになりそうです。

ルール2

else 句を使用しないこと

ネストした条件分岐は、重複したコードを作りやすいので、else 句を使わずにプログラムを作っていくとよい。
else 句の代替策としては、ガード節と早期 return を使うようにする。あるいはポリモフィズムを使う。
これも良いエクササイズになりそうです。

ルール3

すべてのプリミティブ型と文字列型をラップすること

例えば、int型を引数にしたメソッドがあったら、int型ではなくて、クラスでラップすること。
もし、間違った変数をメソッドの引数に指定したとしても、静的型付け言語なら、コンパイラがエラーであることを教えてくれる。
面倒なんですよね。こういうのは、でもやった方がいいです。

ルール4

1行につきドットは1つまでにすること

複数のドットを使っているコードが何行かあれば、責務の配置を間違っている箇所がたくさん見つかる。
責務の配置とは、どのオブジェクトに、どんな仕事をさせるかということで、例えば、開発部のAという人に仕事の指示をするのに、総務部からAに作業依頼している状態はおかしいと言っている。
この状態をプログラムコードにすると、総務部からAにアクセスするために、人事にアクセスしてAを取り出して、Aの抱えている仕事を取り出して、その仕事のステータスを着手に変えて、という風にドットが複数つながると、その兆候が出てくる。(ちょっと例題が無理過ぎてわかり辛くなったかもしれない)
これは、なるほどなぁと 思いました。こういう矯正方法があるんですね。

ルール5

名前を省略しないこと

省略は紛らわしくなるし、もっと重大な問題を隠してしまいがち。他にも名前を省略したくなる理由の1つとして、ここでも責務の配置ミスがあげられている。
正しく配置された責務は、order.ship() のように文脈にピッタリのはずだ。エクササイズでは、すべてのエンティティの名前には、1つか2つの単語だけを使って省略しないことを、より具体的なルールとしている。
そうそう、文脈に無理のないコードというのは、見やすくて良いコードだと思います。そのためにも、ネーミングが重要です。また別の機会に、ネーミングに関する勉強会をしようと思います。

ルール6

すべてのエンティティを小さくすること

50行を超えるクラス、10ファイルを超えるパッケージは作らないというルール。
これは実際には難しいだろうなぁ。パッケージのデザインは決まっていることが多いし、50行以内で1クラスを作るのは難しそうです。
おそらくは、プログラムが洗練されていくと、コードはどんどん短くなっていくから、50行以内にまとめることも可能だということなのでしょう。

ルール7

1つのクラスにつきインスタンス変数は2 つまでにすること

インスタンス変数を2つ以上作りたくなったら、最も重要な1つと、それ以外のグループの2つに分類するとよい。重要な意味を持つインスタンス変数が10個も20個もあると、その変数が、すべての振る舞いの中でどのように変化するのかを、分析するのは大変である。
この章は、first、middle、lastを変数として持つNameクラスが例題に使われています。このNameクラスをリファクタリングして、familyとgivenの2つのインスタンス変数を持つクラスになっているんですが、日本人にはわかりにくい例題だったようです。
勉強会に参加したうちの2人が、この例題が示すことの有効性が感じられないと言っていました。
まぁ例題は置いといて、インスタンスを2つだけに制限したときに、どうなるか興味深いですね。

ルール8

ファーストクラスコレクションを使用すること

javaで言うなら、ListやSet、さらにはMapも。これらを操作する必要があるなら、それは1つのクラスとして独立させて、コレクションを操作する振る舞いに徹すること。
徹するために、このクラスにはコレクション以外の他のメンバー変数を持たせない方がよい。
これも実際にやろうとすると、面倒な場面の方が多いんですが、オブジェクトということを意識づけるための処方箋となりそうです。ついつい怠ってしまうので、ガイドラインで強制するといいですね。

ルール9

Getter 、Setter 、プロパティを使用しないこと

この内容は、ちょっとわかりづらかったです。参加した全員が1回読んだだけでは理解できなかったのですが、欄外の訳注で理解できました。
例えば、クラスAにインスタンス変数 a があり、クラスBに a を2倍にした値が必要だとする。このとき、getterなどによってクラスBで簡単に a の値が取得できるようになっていると、a を2倍にする処理はクラスBにかかれてしまう可能性がある。
やはり、a の値に関する振る舞いは、クラスAに書いた方がよいですね。

まとめ

このルールは、エクササイズのためのものであって、常にこのルールに沿ってプログラムを作成しなさいということではありません。
1度このルールでエクササイズしたら、「ルールを緩めてガイドラインとして使うとよい」と書いてあるので、しばらくは、少し緩まったルールを社内のガイドラインとして、常に意識して仕事することにしようと確認しました。

The post 「オブジェクト指向エクササイズ」でクセの強いコードを矯正しよう first appeared on 株式会社Altus-Five.

]]>
/blog/2019/09/24/object-oriented-programming-exercise/feed/ 0
全文検索を自社サイト・社内サーバーに構築したいクライアントのための留意点 /blog/2018/07/18/full_text_search/ /blog/2018/07/18/full_text_search/#respond Wed, 18 Jul 2018 14:59:00 +0000 http://43.207.2.176/?p=305 システム開発会社をお探しの企業さんへ。こんなお悩みありませんか? 「全文検索」という言葉をご存知である非エンジニアの方は、 おそらくシステム開発に携わるご担当者さんか、プログラミングや最新技術に興味関心のある方であろうと […]

The post 全文検索を自社サイト・社内サーバーに構築したいクライアントのための留意点 first appeared on 株式会社Altus-Five.

]]>
システム開発会社をお探しの企業さんへ。こんなお悩みありませんか?

「全文検索」という言葉をご存知である非エンジニアの方は、 おそらくシステム開発に携わるご担当者さんか、プログラミングや最新技術に興味関心のある方であろうと思います。

本記事は、自社サイトや社内サーバーで下記のような「全文検索機能」を実装したい方に向けて、 「Altus-Fiveの紹介」と「情報提供」を行う記事です。

  • 求人情報検索
  • 商品情報検索
  • 任意のクエリ(検索キーワード)に対する検索

特に、

  • 他社に見積もりを取ったところ、ニーズが特殊で要望に応えられない または 十分な性能が出せないと言われた
  • 特殊なデータに対する検索なので、既存のパッケージ等では開発できないと言われた
  • アクセスするための機器がPC・スマホ・タブレット等の市販品ではない(店頭で用いるディスプレイなど)

という方には、Altus-Fiveの得意とする 「フルスクラッチ型開発」 でお助けができるかもしれません。 もちろん、単に「全文検索ってどうやってるの?」という方にも興味深い内容のはずですので、 ぜひ、この記事を最後まで読んでみてください。

全文検索とは?

全文検索とは、 コンピュータ内の複数ファイルから、クエリ(検索キーワード)を含む行やファイルを全て見つけ出すこと を指します。 ファイル名などだけでなく、ファイルの中身も見て検索結果を取得するところがポイントです。

これだけではイメージが限定的になるので、具体的なシステムの例をあげてみましょう。

システム例全文検索の例
人材検索サービス「プログラマ」という文字列を(経歴などに)含む人材の一覧を取得する
レシピ検索サービス「ソース」や「スパゲッティ」を含むレシピの一覧を取得する
会計処理サーバー「株式会社フカミハマル」との取引データ一覧を取得する

こうして並べてみると、色々なサービスや、社内サーバーの機能開発において重要な技術であることが分かるかと思います。 そのため、様々な全文検索のためのパッケージが開発されており、 Altus-Fiveでも多数の全文検索開発実績があります。

さて、本記事は「全文検索」について、システムの発注前に知っておくと役立つ「豆知識」を紹介します。 少し長い内容になりますが、ぜひお付き合いください。

全文検索は、実装方式によってコストと性能が変わる

実は、全文検索にはいくつかの実装方式があります。 単に「全文検索機能」と言っても、 その中で動いているロジックには随分と大きな違いがある ということなのですね。

ロジックの違いが生む違いは 「実装コスト」(≒ 工期と見積額) 、 そして 「実行時間」(≒ 検索に対するユーザーの待ち時間) です。 どちらも依頼をされる側にとっては、重要なポイントになるのではないでしょうか。

もちろん、開発者がこの領域の専門家ですので、全面的にお任せいただくことも良いのですが、 発注をされる方が知っておいて損はない知識かと思います。

ここでは大きく 「grep型」 と 「索引型」 の2種類について、具体例を用いて分かりやすく解説してみたいと思います。

grep型の全文検索とは?

grepとは、データベースから必要な情報を検索する際にエンジニアがよく打ち込むコマンドであり、耳にしたことはあるという方もおられると思います。

例えば grep "毒リンゴ" shirayukihime.txtというコマンドを打ち込むと、 白雪姫の文章(shirayukihime.txt)の中で「毒リンゴ」というフレーズが含まれる行を 全て 表示してくれます。 shirayukihime.txtという指定を除けば、今みているフォルダ内にある全てのファイルを対象とした検索を行います(全文検索)。

grepは、本当は「global regular expression print」(ファイル全体から正規表現一致行を表示)の略なのですが、 「グレップ!」 という語感だけで 「検索したい対象を含む行を鷲掴みにして表示する」 という理解をされても、とりあえず差し支えないと思います(たぶん)。

ともあれ大切なことは、grep ≒ 検索であるということ。 ただし、検索にもいくつかの方式があり、grepでは後述する 索引型 検索とは異なる方法で必要なデータを見つけてきます。

全文検索を開発する際は、 grep型と索引型、どちらで実装するかの選択 が非常に重要になるということです。

grep型検索の実装イメージ

さて、grep型の全文検索はどのように「検索対象」を見つけ出してくるかをご説明しましょう。

ここでは 「人材検索サービス」 を例に解説してみたいと思います。 (某企業さんが提供しているような、求職者を検索出来るサービスを想像してみてください。)

システムを作る際は、「データベース」に必要データを入れておきます。

データベースとは「データを入れておく箱」のようなもので、 氏名・性別・現職・学歴・経歴など、システムによって異なる「必要なデータ」を入れておけるような構造をしています。

今回は人材検索が例ですので、 データベースは「アパート」だ と想像して頂くとわかり良いかもしれません(先ほどデータベースは箱だと書きましたが、アパートの部屋もよく『箱』と呼びますね)。

  1. データベースとは、サービスに登録されている人材に、仮想的に住んでもらうためのアパート である
  2. 検索とは、 「この条件に該当する人材はいないか?」と聞かれた時、当てはまる人材(の集まり)をアパート内から連れてくること である

と考えると、「データベース」と「検索」の関連性が掴みやすくなると思います。

検索例(grep型全文検索の応答)

ここでデータベースから 「システム開発経験者」 だけを取り出したいというサービス側からの要求(クエリ)があったとしましょう。 grep型検索の手続きは下記のようなイメージです。

  1. アパートの入り口で「このアパートにシステム開発経験者は住んでいますか?」と聞かれたら、
  2. アパートの全ての部屋のドア を1つ1つ開けて、
  3. 『あなたはシステム開発経験者ですか?』と問いかけ、
  4. YESならばついてきてもらい、
  5. アパートの全室で質問を終えたとき、ついてきてくれた人材の集まりを「検索結果」として提供する

grep型は後述する索引型とは異なり、検索そのものには 特別な準備が不要 。 アパートを巡回する「使いっ走り」(彼の仕事は、専門的にも 『走査』 と呼びます)を用意すれば良いだけなので、開発上の工数は少なくて済む傾向にあります。

注意

ここでは仮に「アパート」としましたが、実際には 道中に猛獣あり、断崖絶壁ありのジャングル から花を摘んでくるような厳しいケースもあり、その場合は実装難度が跳ね上がることになります。(そんな案件もぜひ一度、ご相談ください。)

索引型の全文検索とは?

さて、先ほどの方法とは異なる 「索引型」 の全文検索とはどんなものでしょうか。

索引型の全文検索のポイントは、 「あらかじめ、聞かれそうな質問ごとに、該当する人材のリストを作っておく」 ということです。例えば、

  • 「システム開発経験者」は、アパートの0104号室、0103号室、0302号室、0305号室、…に住んでいる
  • 「現年収500万円台の人材」は、アパートの0102号室、0103号室、0207号室、0301号室、…に住んでいる
  • etc.

といった情報を書き留めておいた「メモ書き」をあらかじめ作っておくようなイメージです。 実はこのメモ書きが 「索引」(index)と呼ばれる もので、索引を作ることは 検索の所要時間を劇的に短くする 効果があります(ただし、全体のデータサイズやデータの性質による)。 索引型の全文検索では、検索のプロセスは下記のように変わり、

  1. アパートの入り口で「このアパートにシステム開発経験者は住んでいますか?」と聞かれたら、
  2. 手元のメモ書きを確認し、
  3. メモ書きに書かれた全ての部屋 のドアを開け、中にいる人材についてきてもらい、
  4. ついてきてくれた人材の集まりを「検索結果」として提供する

という4ステップで済みます。

ここでポイントは、先ほどの「grep型」と比べてステップが減っていることではなく、 「システム開発経験者の住んでいる部屋のドア」しか開けなくて良い ということです。

grep型検索では全ての部屋のドアを開けるため、「システム開発経験者の数」ではなく、 「アパート全体の大きさ」に比例した分だけ「使いっ走り」が汗をかかなければなりませんでした。

索引型検索ではあらかじめ「システム開発経験者の住んでいる部屋」をメモしてありますから、 「使いっ走り」は効率的に部屋を巡ることができます。 随分と「賢い」方法になっていることが分かるかと思います。

二つの方式は「ユーザーの待ち時間」が異なる

さて、システム開発者は「使いっ走り」に優しくしたい訳ではなく、 重要なのはその間待ちぼうけを食らう 「ユーザーの待ち時間」 です。

grep型と索引型の検索では、ユーザーの待ち時間が大きく異なります。 さらに 「全体のデータサイズ(登録人材数)が大きいほど、待ち時間の差は大きくなる傾向にある」 ということが分かるかと思います。

索引型全文検索のポイント – どのように索引を作るか

では常に索引型全文検索が優れているのか?というと、そうとは言い切れません。

索引型全文検索には、 データベース作成の段階で、索引をどう作るかを決める という工程があります。

現実的にシステム開発として考えた場合、「どのように索引を作るか?」という考案部分の工程が入ってきますので、工期も費用も大きくなることは否めません。

ただ、 待ち時間を考えると、grep型の検索が使い物にならないケース は多くあります。 その場合は当然、索引型全文検索などの高速な手法を選ばざるを得ないことになります。

さて、この時「索引」をどのように作るか、ということは、いくつかのパターンがあります。

  • データベース全文を走査し、実際に「よく出てきた単語」ごとの覚え書きを作っておく場合
  • 本物の辞書(シソーラスなど)を参照する場合

今回は一例として「人材検索サービス」を考えましたが、ケースバイケースで「良い索引の作り方」は異なってきます。より詳しい内容については、今後のAltus-Fiveブログで紹介できればと考えています。

いずれにせよ重要なことは、 grep型と索引型、発注者側も二つの方式があることを理解した上で、システムの要件にあった方式を選ぶことが大切 ということです。 以上、長い「留意点」についてお付き合いをいただき、ありがとうございました。

Altus-Fiveの強み

ここまで、システム開発を依頼したいクライアントさんを念頭におきながら、全文検索システムの方式について解説してみました。 最後に、システム開発会社としてのAltus-Fiveの強みを一点だけ、紹介させて頂きたいと思います。

フルスクラッチの開発を最も得意とする。

「フルスクラッチ開発」は、既存パッケージ等に頼らず、技術を組み合わせて0からシステムを作る開発方式。 システムの発注経験の少ない企業さんだと、「システムは0から作るのが当然では?」と思われるかもしれませんが、 実際には「パッケージ」と呼ばれる、プログラムの部品を繋げて作ることが主流です。

全文検索には全文検索のためのパッケージが、もちろん存在しています。 ただし、それぞれのパッケージには導入のための要件があり、冒頭で述べたような事情がある場合には 使えないケースも多い のです。

そんな「既存パッケージが使えない開発のご依頼」を頂いた時が、Altus-Fiveの本領発揮です。 フルスクラッチ案件が得意なので、貴社の要件 あるいは 既存システムを精査し、最適な技術の組を自社で考案・提案することが可能です。

もちろんシステム開発には「餅は餅屋」な側面もあることは確かですので、適宜外部の知見やパッケージを頼ることは検討します。

しかし、ただ「パッケージを繋げるだけ」の開発であれば、自社も他社も「出来ること」は同じになってしまう。 それでは開発会社としての魅力は出せないなぁ…ということで、あえてフルスクラッチにこだわり、1から10まで自社で作るシステムに、技術者魂を燃やして取り組んできました。

パッケージを繋げることが得意な会社と、1から10まで自社で作ることが得意な会社の違い。 その部分を「見える化」することが、実はAltus-Five最大の課題です。 弊社ブログでも長く取り組んできましたが、なかなかエンド企業様にご理解頂くことが難しい領域です。

もし今お困りのことがあるのであれば、まずは開発のご相談を頂くことが、 いちばんわかりやすい形で弊社の強みを感じて頂ける方法かもしれません。

「他社の見積もりでは時間面・費用面から依頼が出来なかった」という方でも、ぜひ一度ご来社頂きたいと思います。

「一度会って、話を聞いてみたい」というお打ち合わせも、大歓迎です。

お問い合わせはこちら。

The post 全文検索を自社サイト・社内サーバーに構築したいクライアントのための留意点 first appeared on 株式会社Altus-Five.

]]>
/blog/2018/07/18/full_text_search/feed/ 0
Rubyの配列(Array)を魔改造して、連想配列として使ってみた /blog/2018/05/07/associative-array/ /blog/2018/05/07/associative-array/#respond Sun, 06 May 2018 15:15:00 +0000 http://43.207.2.176/?p=313 可読性が高く、高速な「連想配列」 エンジニアであれば、「連想配列」のお世話になる機会は多いと思います。言語によりますが、任意のデータをkeyとして指定できる連想配列は、 コードの可読性を高く保ったまま、ニーズに合わせた柔 […]

The post Rubyの配列(Array)を魔改造して、連想配列として使ってみた first appeared on 株式会社Altus-Five.

]]>
可読性が高く、高速な「連想配列」

エンジニアであれば、「連想配列」のお世話になる機会は多いと思います。言語によりますが、任意のデータをkeyとして指定できる連想配列は、 コードの可読性を高く保ったまま、ニーズに合わせた柔軟なシステム実装をサポートしてくれる強力なツールです。

ユーザーとして連想配列を使う分には、普段あまり意識しませんが、 各言語に標準実装されている連想配列の利便性は、下記の2点を満たすことで支えられています。

  1. keyを引数とすることでvalueを取り出す(もしくは代入する)操作が簡便であること
  2. 上記の操作が、連想配列に格納済みのデータサイズによらず高速であること(重要!

今回は、普段お世話になっている「連想配列」について、あらためてその成り立ちや性質を見直してみたいと思います。 本記事(および 次回)は、

  • 「連想配列はどんな技術で実現されている?」疑問に思った方
  • 「自分で連想配列を実装してみたい」という物好きな方
  • 「平均検索速度が定数時間O(1)ってどういう意味なんだ」と疑問に思い夜も眠れない方

に捧げます。

連想配列は、配列ではない!

(みなさんの興味を惹くために、ちょっとラジカルな見出しをつけてみました。)

本記事では、配列と連想配列を 似たようなものとみなすことをやめ、その成り立ちを解剖する ことで、 連想配列(特にハッシュによるもの = ハッシュテーブル)がいかにありがたいツールであるかを明らかにしてみたいと思います。 なお、ここまでで御察しの通り、 筆者は連想配列、ハッシュテーブルが大好き です。 まだエンジニアリング経験の浅い学生時代、C++を中心にコーディングをしていたのですが、 C++11に unordered_map クラスとして連想配列が存在することを知らず、自前のハッシュテーブルを作ったりしていました。 要素へのアクセスが目に見えて高速化され、とても感動した覚えがあります。

連想配列と配列の対比

さて、連想配列と配列の違いについて詳しく見ていきましょう。 両者の共通点といえば、

  • データを格納するために、メモリを使うこと(あたりまえ)
  • key(もしくは添字)の指定によってデータにアクセスすること

であり、逆にいえば それくらいしか共通点はない 、と筆者は認識しております。 そのわずかな接点についても、下記のような違いがあり、むしろこの相違点が「配列と連想配列の違いである」と理解されている方も多いかと思います。

point配列連想配列
確保するメモリ領域の広さ任意、もしくは言語ごとの実装により動的確保言語ごとの実装による
指定するkey(添字)確保するメモリ領域の先頭からのオフセット(ゆえに整数に限る)任意(整数や文字列など)

ですが、実は上記のちがい以上に、 key(添字)・valueの組による値の参照/代入の過程 が、大きく異なっている点が両者の違いの本質です。 以下では、簡単な実験的実装をテーマに、両者のロジックの中身がどう異なるかを明るみにしてみたいと思います。

Rubyの配列を(無理やり)連想配列にしてみよう

なお、Rubyの連想配列クラスはHashクラスですが、これはハッシュ関数を用いて実装されたハッシュテーブルであることを意味します。 今回は、連想配列の定義を、

  • keyに対するvalueを記憶させることができる (array["altus"] = 5)
  • keyを渡すと、記憶されているvalueを取得できる (p(array["altus"]) #=> 5)
  • keyに対するvalueを上書きすることが出来る (array["altus"] = 50 #"altus"に対応するvalueを50に上書き)

であるとし、Hashクラスとは別の 「粗悪な」 連想配列を作ってみたいと思います。

「粗悪な」連想配列を実装してみる

まずは、下記ソースコードをご覧ください。

# 配列を連想配列化する(強引な)実装

class MyAssociativeArray < Array
  def []= (k, v)
    found = false
    self.each {|item|
      if item[0] == k then
        item[1] = v
        found = true
        break
      end
    }
    if not found then
      self.push([k, v])
    end
  end

  def [] (query)
    found = false
    value = nil
    self.each {|item| #線形走査
      if item[0] == query then
        found = true
        value = item[1]
        break
      end
    }
    value
  end
end
view rawassociative_array.rb hosted with ❤ by GitHub

上記コードは、RubyのArrayクラス(配列クラス)を継承し、MyAssociativeArrayクラスを生成しています。 要素取り出し []と代入演算子[]=をオーバーロードすることで、 arr["altus"] = 5 や p(arr["altus"]) といったアクセスが実現。 MyAssociativeArrayクラスを 連想配列として用いる ことに成功していることがわかりますね。

ほか、値の上書きや、存在しないkeyに対してnilを返すといった実装にも成功しており、 Arrayクラスをベースに、とりあえず連想配列として使える実装ができたことがわかります。

どうやっているのか、以下ソースコードの解説をしていきます。

RubyのArrayクラスを継承・メソッドをオーバーロードして連想配列にするまで

代入メソッド([]=)の実装

まず、Arrayクラスを継承し、MyAssociativeArrayクラスを定義します。

class MyAssociativeArray < Array
view rawassociative_array.rb hosted with ❤ by GitHub

そのため、MyAssociativeArrayクラスは、Arrayクラスの性質を受け継いでいます。 Rubyでは組み込みクラスのメソッドを上書きできるので(シンタックスシュガーの一種)、 「手軽かつ、パッと見わかりやすく、性能が悪いので反面教師にもなる」オリジナル連想配列が作れるのではないか、 というのが本記事のモチベーションでした(余談)。

さて、肝心のメソッド上書きの内容です。 はじめに「値の代入」に取り掛かりました。そのためにオーバーロードする必要のあるクラスメソッドは []=です。 このメソッドは第一引数に添字(key)、第二引数に値(value)を取ります。

今回の実装は、配列の要素としてkey, valueの組(2つの要素からなる配列)をもたせてしまおう という原始的なもので、 第一要素を 線形走査 することで、お目当ての要素にアクセスしたり、値を上書きすることができます(この点が 粗悪 たる所以です。詳しくは後述)。

代入すべきkey, valueが与えられた場合、まずは過去に格納したデータの中に、同一keyの値が存在しないかどうかを調べます。

    self.each {|item|
      if item[0] == k then
        item[1] = v
        found = true
        break
      end
    }
view rawassociative_array.rb hosted with ❤ by GitHub

もし存在すれば、該当する要素のvalueを上書きし(上記L9)、 存在しなければ、key, valueの組を新しく配列に格納します(下記コード)。

    if not found then
      self.push([k, v])
    end
view rawassociative_array.rb hosted with ❤ by GitHub

蛇足

第一要素がkey, 第二要素がvalueとみなす実装は、暗黙的な悪い実装なので、 連想配列の中身を連想配列で定義したくなりますが、もちろん今回は Hashクラスの利用は禁じ手です。

さて、これで値の代入ができるようになりました。

参照メソッドの実装

次は値の参照です。こちらも線形走査をする点は変わらないので、一気に紹介してしまいます。

  def [] (query)
    found = false
    value = nil
    self.each {|item| #線形走査
      if item[0] == query then
        found = true
        value = item[1]
        break
      end
    }
    value
  end
view rawassociative_array.rb hosted with ❤ by GitHub

たとえば p(arr["altus"])とすれば、arr において ”altus” keyに対応する要素を返すので、 格納されているvalue、例えば 5が出力されるといった具合です。 もし該当するkeyの要素が存在しない場合は nilを返します。

代入・参照に線形時間かかる連想配列 – 上記実装は何が悪いのか?

さて、ここまで度々(しつこく?)触れてきた、上記連想配列の「粗悪さ」について解説してみたいと思います。

ここまでの実装をイメージ化すると、下図のようになります。

(代入・参照とも仕組みは同じなので、一つの図で済ませてしまいました。)

たとえばarr["altus"]のように、keyに対応するvalueを参照する処理や、 arr["altus"] = 5のような代入処理の際に、上図で表されるような処理、 すなわち「既存のkey一つひとつに対する、与えられたkey(query)との等値判定」を行って、 等値なものが見つかったタイミングでvalueを返したり、代入したりといった処理を行います。

これがいわゆる 線形走査 であり、において、データサイズnに対して比例する時間、すなわちO(n)の時間がかかります(最悪値評価)。

ちなみに、参照・代入における最悪ケースは何かと言うと、「まだ連想配列内に存在しないkeyをqueryとして投げた場合」 です。 非常によくあるケースが最悪値なので、この性能の悪さは実用上の障害になり、 ある程度データサイズが大きくなると、上記の連想配列はほとんど役に立ちません。

では、どうすれば性能の良い連想配列を得られるのでしょう… という問いへの答え(の一つ)が、 次回の記事でご紹介する「ハッシュテーブル」です。 ご期待ください!

参考資料

The post Rubyの配列(Array)を魔改造して、連想配列として使ってみた first appeared on 株式会社Altus-Five.

]]>
/blog/2018/05/07/associative-array/feed/ 0
「ハッシュ」完全理解のための覚書 ハッシュテーブルをRubyで実装してみる /blog/2018/05/07/hashtable/ /blog/2018/05/07/hashtable/#respond Sun, 06 May 2018 15:04:00 +0000 http://43.207.2.176/?p=309 「ハッシュ」を完全理解するための道とは 前回、エンジニアが常々お世話になる「連想配列」についてのあれこれを述べました。Rubyのシンタックスシュガーを使って、 配列を連想配列に魔改造することで、どんな性能の連想 […]

The post 「ハッシュ」完全理解のための覚書 ハッシュテーブルをRubyで実装してみる first appeared on 株式会社Altus-Five.

]]>
「ハッシュ」を完全理解するための道とは

前回、エンジニアが常々お世話になる「連想配列」についてのあれこれを述べました。Rubyのシンタックスシュガーを使って、 配列を連想配列に魔改造することで、どんな性能の連想配列になるかを確かめる ことが前回の内容でした。

今回は、連想配列実装の正式な(?)方法、ハッシュテーブルを実際に作ってみよう という試みです。粗悪な性能の連想配列だけでなく、実用性の高い連想配列も、さほど長くないコードで実現できるんです。

Rubyでの実装を通じて「ハッシュ」を(なんとなくではなく)理解しよう、というのが本記事のゴールになります。

注釈 ハッシュの完全理解とは言いつつも、今回 暗号化の話には触れない ので、その旨ご注意頂ければ幸いです。 ただし、暗号化についても今回の話と基本は同一ですので、理解の助けになるはずです。

実装が理解への近道だ

筆者は学生時代にハッシュテーブルを自作して、その性能の良さ(要素へのアクセスの機敏さ)に感動した人間であることを 前回 述べました。

ハッシュテーブルについて書かれた(まともな)文献については、「ハッシュ」と名のつく用語が複数出てきます。ハッシュ関数、ハッシュ値、などなど。

その関連性については色々な喩えがあるものの、 keyの成績や身長などが引き合いに出され、 それ内部ロジックどーなってんのよ? と言いたくなるような例が用いられたりしており、 「とりあえず」の理解の助けにはなるものの、エンジニアにとっては少し物足りない記述になっているように思えます。

本記事の内容は、下記の2つの項目について理解するにあたっては、なかなか良い内容なのではないかと自負しています。

  1. ハッシュ関数は、文字列など仕様によって規定されるいかなるkeyも入力できる形式でなければならない
    • その役割はハッシュ値を発行し、メモリ上の要素の格納場所を決めることである
  2. 異なるkeyに対するハッシュ値は、可能な限り重複がないことが望ましい
    • そのような性質を満たす「よい」ハッシュ関数のうち、極めてシンプルな実装のものも存在する(後述)

また、本記事では 下記3つの用語について、明確に区別すること をゴールとして、ハッシュテーブルの仕組みについてまとめていきます。

  1. ハッシュ関数
  2. ハッシュ値
  3. ハッシュテーブル(ハッシュ)

最後に、ハッシュテーブルの性能について書かれた文献を読むと、たいてい 「定数時間O(1)で要素にアクセスできる(ゆえに早い、最高だ!)」 と書かれています。 一体これがどういうことなのかも、分かりやすい解説をあまり見かけたことがありません。この点にも、チャレンジしてみたいと思います。

ハッシュ関数の設計

ハッシュテーブルの実装に必要な 最重要のメソッド がハッシュ関数です。

ここでは、文字列をkeyとするハッシュテーブルをユースケースとして想定し、 ハッシュ関数をRubyのメソッドとして、一から実装してみたいと思います。

ハッシュ関数の役割 – メモリ上のどこに要素を格納するか決める

早速前回の内容を引くのですが、 配列を強引に連想配列化した MyAssociativeArrayの実装では、 「メモリ上のどこにkey, valueの組を格納するか?」という問題については触れてきませんでした。 ここで、前回実装した連想配列クラス、 MyAssociativeArrayにおける要素代入のメソッドを見直してみましょう。


  def []= (k, v)
    found = false
    self.each {|item|
      if item[0] == k then
        item[1] = v
        found = true
        break
      end
    }
    if not found then
      self.push([k, v])
    end
  end
view rawassociative_array.rb hosted with ❤ by GitHub

ポイントは、MyAssociativeArrayクラスの継承元であるArrayクラスのpush関数を使ってkey, valueの組を格納している点です。 push関数は配列の末尾に要素を追加するメソッドなので、MyAssociativeArrayでは、新しく到着するkey, valueの組を既存要素の末尾に付け足していくという、ナイーブな方法でメモリ内に格納しています。

ハッシュテーブル実装の第一のポイントは、 メモリ内のどこに要素を格納するか? について、もう少し計画的なやり方を用いることです。

その際に登場するのが、先述した ハッシュ関数 です。

ハッシュを噛ませた場合の実装イメージ(要素格納の方法)を上図に表してみました。

データ代入の手続き

  1. あらかじめメモリ上の決まった広さの領域を確保しておく
    • 本記事ではテーブルサイズと呼ぶこととします
  2. key, valueを受け取ったら、ハッシュ関数 にkeyを入力
  3. ハッシュ値 を得て、1.で確保した領域のサイズTABLESIZEで割った値 offset を得ることで、メモリ上のどこに格納するかを決める

それぞれ、実際にハッシュテーブルを実装したコード(hashtable.rb)を参照しつつ、解説していきたいと思います。

今回、Arrayオブジェクトをインスタンス変数として持つMyHashTableクラスを実装することで、ハッシュテーブルの実装をしてみたいと思います。 まず、1.で述べた「決まった広さの領域を確保する」ことからです。今回は下記のように行います:

  def initialize()
    @arr = Array(TABLESIZE)
  end
view rawhashtable.rb hosted with ❤ by GitHub

あらかじめ定めた定数 TABLESIZE の要素数をもつ配列(Arrayオブジェクト)を生成し、インスタンス変数 @arr として保持しているだけの単純なコードです。 これで、メモリ上にkey, valueの組を保持するための領域が確保されます。 ( TABLESIZEの適切な決め方については、後ほど触れたいと思います。)

次に、代入処理を見てみましょう。(上述した流れの2, 3にあたる部分です。) 代入処理ではまず、確保した配列領域のどの位置にkey, valueの組を格納するかを決定します。配列の先頭から要素いくつ分後ろに格納するかを表すoffset変数を用意し(0オリジン)、ここに数値を代入します。 (もちろん、 0 <= offset < TABLESIZEを満たすような値を与える必要があります。)

さて、肝心のoffsetの数値は下記コードで決定されています。

  def []= (k, v)
    offset = hash_function(k) % TABLESIZE
view rawhashtable.rb hosted with ❤ by GitHub

右辺のMyHashFunctionクラスのone_at_a_timeメソッドを呼び出し、引数としてk(key)を渡しています。それをTABLESIZEで割った値をoffsetとして採用しています。

ここで、このone_at_a_timeこそが ハッシュ関数 と呼ばれるものです。 One at a timeハッシュは、質の良いハッシュ関数について論じたJenkinsの文献(英語)に記載されている中で、もっとも古典的なハッシュ関数です。 シンプルなアルゴリズムで動作の理解が易しいこと、デモとしては十分な性能があることから、今回の目的にピッタリと判断しました。動作を理解するのに必要な知識は ビット演算のみ ですので、不慣れな方は、ビット演算に関する文献を片手に読めば十分理解できるのではないかと思います。

one_at_a_timeハッシュ関数の中身をみてみましょう。


  def one_at_a_time(key) #keyは文字列を想定
    hash = 0
    key.each_char {|ch| #文字を取り出す
      hash += ch.ord
      hash += (hash << 10)
      hash ^= (hash >> 6)
    }
    hash += (hash << 3)
    hash ^= (hash >> 11)
    hash += (hash << 15)
    hash %= (2**30)
    # 0 <= hash < 2**30 の範囲で扱えるような数値に補正
    # FixNum クラスの数値範囲による
  end
view rawhash_function.rb hosted with ❤ by GitHub

ここで、上記コードは下記4つのステップで説明できます。

  1. key文字列の先頭から1文字取り出し、文字コード化し、二進数化する(L7)
  2. 1.で得られた値をhash変数に加える(L7)
  3. hash変数の値をビット演算でいい感じにかき混ぜる(L8-L9)
  4. 以下、文字列の末尾文字まで繰り返し
  5. 得られた値を最後にもう一度かき混ぜて出力(L11-L14)

例えば、このハッシュ関数に”altus”というkeyを入力してみると、0x25746a3bという出力値が返されます。この数値を ハッシュ値 と呼びます。

ハッシュ値をTABLESIZEで割った値をoffsetとして採用することで、配列内のどの位置に要素を格納するかを決定します。

このようにしてoffsetを確定したら、その位置にkey, valueの組を格納します。 ただし、最初の実装と同様、「既にそのkeyをもつ要素が格納されていれば、値を上書きする」よう実装する必要がありますので、ロジックは若干長くなります。 下記コードのL21が、肝心の代入を行なっている箇所です。

    if @arr[offset].nil? then
      @arr[offset] = Array.new().push([k, v])
    else
      found = false
      @arr[offset].each {|e|
        if(e[0] == k)
          found = true
          e[1] = v
          break
        end
      }
      if not found
        @arr[offset].push([k, v])
      end
    end
view rawhashtable.rb hosted with ❤ by GitHub

ハッシュ関数に求められる2つの性質

  1. 上記手続きには一切ランダム性がなく、同一のkeyを与えた時は必ず同じ出力値を返すこと(入力に対する出力の一意性)
  2. 異なるkeyを与えたとき、同じ出力値を返す可能性が極めて低いこと

ハッシュテーブルにおける値の参照 – keyのハッシュ値で格納場所がわかる

代入の次は参照です。最初の実装では線形走査でkeyにヒットする要素を見つけていましたが、今回は代入の方法が変わったため、参照の方法が劇的に効率的になります。 ポイントは明快で、 参照にもkeyのハッシュ値を使えば良い のです。

今回、代入時の要素格納を、keyのハッシュ値に基づいて行うようロジックを変更しました。そのため、要素参照時にもハッシュ値を発行し、ハッシュ値の指す位置を調べることで、与えられたkeyをもつ要素が、既に連想配列内にあるかどうかを高速に調べることができるのです。 実装方針としては、下記の通りです。

  1. keyに対応するハッシュ値からoffsetを求める
  2. offsetの指す位置に、与えられたkeyを持つ要素が格納されているかどうかを調べる

「keyに対応するハッシュ値からoffsetを求める」コードは下記の通りで、代入の時と全く同一です。

    hash_value = hash_function(k)
    offset = hash_value % TABLESIZE
view rawhashtable.rb hosted with ❤ by GitHub

「offsetの指す位置に、与えられたkeyを持つ要素が格納されているかどうかを調べる」コードは下記の通りです。

    value = nil
    if @arr[offset].nil?
      puts("not found")
    else
      found = false
      @arr[offset].each {|e|
        if e[0] == k
          found = true
          print("found: ", e[1], "\n")
          value = e[1]
        end
      }
      if not found
        puts("not found")
      end
    end
    puts()
view rawhashtable.rb hosted with ❤ by GitHub

まとめると、ハッシュテーブルの実装上の工夫、およびその結果としての性能の良さ(高速さ)は、下記の文章で言い表すことができます。

  • ハッシュテーブルでは、値の代入と参照を同じ手段(ハッシュ関数)を介して行うため、 1回 の等値判定で指定のkeyを持つ要素が見つかる
  • 同様の理由から、指定のkeyをもつ要素が連想配列内に存在しない場合は、 1回 の等値判定でそのことがわかる
    • ただし、いずれも例外あり(後述する『衝突』が起きた場合)

このことが、 ハッシュテーブルは定数時間O(1)で値の参照/代入が可能である という文言の意味するところなのですね。

衝突(collision)とは? – ハッシュテーブルの性能が劣化するケース

さて、上記ソースコードを載せつつさらりと流しましたが、性能に関して見過ごせない点があったことにお気づきだったでしょうか。 「ハッシュテーブルは1回の等値判定で参照/代入ができる!」などと言いつつ、 実は今回も、 参照/代入の実装において線形走査(each)を使っていた のですね。

代入

      @arr[offset].each {|e|
        if(e[0] == k)
          found = true
          e[1] = v
          break
        end
      }
view rawhashtable.rb hosted with ❤ by GitHub

参照


      @arr[offset].each {|e|
        if e[0] == k
          found = true
          print("found: ", e[1], "\n")
          value = e[1]
        end
      }
view rawhashtable.rb hosted with ❤ by GitHub

さて、こちらは 衝突(collision)に対する処理 と呼ばれるもので、ハッシュテーブルに実装について書かれた文献であれば、必ず言及されている重要な処理です。

衝突とは、 相異なる複数の入力キーに対して、同じハッシュ値が発行されること、もしくは配列内の同じ位置(offset)が格納場所として指定されてしまうこと を指します。

One-at-a-timeハッシュは、古典的ながらもよく考慮されたハッシュ関数ですので、相異なる複数の入力キーに対して同じハッシュ値を発行してしまうことは稀ですが、 その出力値は 0以上2^31未満と幅が大きいことから、今回は実用上TABLESIZEを指定し、One-at-a-timeの出力値をTABLESIZEで割った値を用いることで、値を0以上TABLESIZE未満に補正して用います。

今回の実装では、TABLESIZE52291を指定していましたので、ある程度大きな要素数になると、かなりの高確率で衝突が生じることになります。

ハッシュテーブルのサイズに関する余談

この時TABLESIZE365と指定してみると、 有名な「誕生日のパラドックス」と同じ問題設定になります。 「何人集まれば、その中に誕生日が同一の2人(以上)がいる確率が、50%を超えるか?」という問題に対する答えを問うもので、 その答えは 「23人」 であるという、意外な結論が有名です。 この興味深い結果をハッシュ値の文脈に載せてみると、

前提:TABLESIZE365に指定し、理想的なハッシュ関数を用いたとする

主張:keyについてランダムに要素を追加する場合、 要素数が「23」に達するまでに1回以上の衝突が起きる確率は50%以上 である

と読み換えることができ、誕生日のパラドックスは上記の主張が「正しい」ことを示す、技術的にもおもしろい(厄介な)帰結だということが分かります。

さて、話が逸れましたが、TABLESIZEをどんなに大きくした場合でも、衝突の起きる確率は0にはなりませんので、プログラム上はその際の処理を書いておく必要が生じます。 それが、先ほど挙げた参照/代入のロジックにおけるeach文の役割だったのですね。

以下、その内容を再掲・解説しておきますので、ご興味のある方はコードを解読してみてください。

代入における衝突対策ロジック

  def []= (k, v)
    offset = hash_function(k) % TABLESIZE

    if @arr[offset].nil? then
      @arr[offset] = Array.new().push([k, v])
    else
      found = false
      @arr[offset].each {|e|
        if(e[0] == k)
          found = true
          e[1] = v
          break
        end
      }
      if not found
        @arr[offset].push([k, v])
      end
    end
  end
view rawhashtable.rb hosted with ❤ by GitHub
  1. 新規のkeyに対するoffsetの位置に既存の要素が存在するかどうかを確かめる(L14)
  2. 存在しない場合は単純に要素を格納すればOK
    • ただし、今後衝突が起きた時のため配列を作っておき、いま代入したいkey, valueはその先頭要素として格納しておく(L15)
  3. 存在する場合は、既存keyの再入力(keyに対する値の上書き)か、異なるkeyに対するoffsetの重複(衝突)かを判定する(L18-L19)
  4. keyに対する値の上書きの場合は、上書き処理を行う(L21)
  5. 衝突の場合は、以前に入力されたkey, valueにおいて、上記2.の手続きで配列を準備してあるので、その末尾にいま入力したいkey, valueをpushする(L26)

参照における衝突対策ロジック

    value = nil
    if @arr[offset].nil?
      puts("not found")
    else
      found = false
      @arr[offset].each {|e|
        if e[0] == k
          found = true
          print("found: ", e[1], "\n")
          value = e[1]
        end
      }
      if not found
        puts("not found")
      end
    end
    puts()
    value
  end
view rawhashtable.rb hosted with ❤ by GitHub
  1. クエリとして投げられたkeyに対するoffsetの位置に要素が存在するかどうかを判定する(L39)
  2. 存在しない場合、nilを返す
  3. 存在する場合、@arr[offset]の要素に対して「クエリ」と「格納されているkey」との等値判定を行い、求める要素があるかどうかを調べる
    • ここで@arr[offset]内に複数要素が存在するのは、過去に衝突が起きた場合である。多くの場合、@arr[offset]の要素数は1であるため、eachループは1回しか回らない

まとめ

今回、コードを読みやすく、保守・拡張しやすくしてくれる「連想配列」について、その原理に迫ってみました。 定数時間O(1)で要素へのアクセスを可能にする実装の一つに「ハッシュテーブル」があり、その構成要素として「ハッシュ関数」が重要な役割を果たしていることを、Rubyで実際に連想配列を実装してみることでおさらいしてみました。 前回の冒頭で述べた「連想配列は配列ではない」という(物騒な)主張は、本記事でまとめた内容をゼミで学んで以来、実装と実験によって体感できた実体験に基づいています。 インタフェースが一見同じでも、内部の仕組みは随分違うし、そのことに助けられることがあるんだなぁ…と感動したことを覚えています。

普段ツールとして見ている/使っている実装について、 内部の仕組みがわかるといちいち感動できる 、そんな経験が広がっていけば、プログラミングはますます深く、楽しくなるのではないでしょうか。 (綺麗にまとめてみました!)

参考資料

The post 「ハッシュ」完全理解のための覚書 ハッシュテーブルをRubyで実装してみる first appeared on 株式会社Altus-Five.

]]>
/blog/2018/05/07/hashtable/feed/ 0