NAKKA-Kの技術ブログ

技術に関する知見や考え方などを投稿します。

Reactのプロジェクトを平成最後の大リファクタリングした話

平成の技術的負債を令和に持ち越したくないからリファクタリングしよう!!と思ったのが事の始まりでした。

短期的に開発速度を求めLintを後回しにしてしまったプロジェクトにLintツールを導入し、Reactのコードを全て綺麗にした時のツールや方法を書きます。

Lintとは

ソースコードを静的にチェックし、書式やバグの原因になるような曖昧な記述について警告してくれるプログラムの事です。
Lintツールを使うと実行時エラーなどとは違ってコードを実行する前に警告を確認できるため、常に綺麗な状態を保つことができます。

やりたいこと

前提としてプロジェクトはReact+Reduxで、ECMAScriptで記述されています。

  • コード整形
  • 構文チェック
  • コミット時の自動Lint
  • プロジェクト全体の警告状況を可視化(Lintエラー数などがグラフで見れると嬉しい)

導入したツール

JavaScriptコードのLintが必要なのでどんなツールがいいのか色々調べました。 するとESLintPrettierという2つのツールがよく使われているようです。

ESLintはコード整形はもちろん他にも多くのチェック機能が付いています。そしてPrettierはコード整形に特化したツールです。 ESLintだけで事足りそうですがPrettierを併用する理由としては、Prettierの方がコード整形が上質で、しかも簡単に設定・実行できるからです。

という訳で、ESLintとPrettier、そしてそれらを併用するための周辺ライブラリを導入します。

  • prettier
    • 整形ツール
  • eslint
    • 整形ツールを含めたLintツール
  • eslint-config-prettier
    • ESLintとPrettierを繋ぐライブラリ
  • eslint-plugin-prettier
    • ESLintとPrettierを繋ぐライブラリ
  • eslint-plugin-react
    • ESLintでReactのコードを検査するためのライブラリ
  • eslint-nibble
    • Lintエラー数や種類をグラフで可視化できるツール(通常はなくても困らない)
$ npm install --save-dev prettier eslint eslint-config-prettier eslint-nibble eslint-plugin-prettier eslint-plugin-react

次にコミット時に自動Lintできるように追加のライブラリを導入します。

  • husky
    • Gitのcommitやpushに対して処理をhookするライブラリ
  • lint-staged
    • Gitにステージングされたファイルに対してLinterを実行するライブラリ
$ npm install --save-dev husky lint-staged

これでとりあえず必要なライブラリは全て導入しました。 あとはやるだけですね。

※ESLintやPrettierの設定を見たい方はこちらをご覧ください。

現状確認

まずは現状を把握するために入れた可視化ツールを使ってみましょう。

プロジェクトのルートでnpx eslint-nibble ./を実行します。

プロジェクトのLintエラーを可視化したグラフ
プロジェクトのLintエラーを可視化したグラフ

ぴったり400個ものエラーがありますね。おぞましい数です......。

さすがにこれを一気に修正するには規模が大きすぎて脳のリソース的にも、影響範囲的にも難しそうです。 少しずつ修正を加えていきましょう。

解決

基本的にはLintツールを走らせれば、整形だけで対応できるコードは自動的に修正してもらうことができます。

$ npx eslint file.js --fix

このコマンドを実行すれば指定したファイルにLintを実行し、発生したコード書式の問題などは自動で修正しファイルを上書きしてくれます。

しかし、コード書式だけならこれで問題ないのですが自動で修正できないLintエラーも存在します。

ReactのコンポーネントにはpropTypesという、引数のようなものに型を指定する方法が存在します。 この型指定はしてもしなくても実行に差し支えはありませんが、指定した方がコードのバグチェックなどにも役立つため書くことを推奨されています。 これらはLintツールが自動で修正することはできませんので、自分で該当箇所を見て型を追加する必要があります。

その他には、使っていない変数やimportされた物がある場合もエラーが発生しますが、自動修正はされません。

であればコード整形の他に、忘れられた変数たちや型の定義を自分でしていく必要がある訳ですが、広範囲に渡る修正のためどこから手をつけていくか考える必要がありそうです。

そんな時はまずeslintコマンドに--fixオプションを付けずに実行してみましょう。 そうすれば、Lintエラーの一覧だけが表示され修正の上書きはされません。

$ npx eslint file.js

エラーの内容を見ていくと、書式系のエラーと自動修正できなさそうなエラーがある程度わかります。 まず影響範囲の少ないファイルやディレクトリから順番にチェックしながらエラーを修正していきます。

私の場合は小さなコンポーネントディレクトリから順番に攻めました。 ディレクトリ以下を一括でチェックする場合は以下のようにすれば良いです。

$ npx eslint AComponent/*/*

ここでいきなりfixを付けるよりかは、一旦エラーの数や度合いをチェックした方が良いです。 もし自動修正できないエラーの数が多いようであれば、チェックする領域を狭めながら一度に修正しやすい範囲に押さえましょう。 そうでないと1コミットの修正量が異常な数になったり、同時に直す箇所が多すぎて手が回らなくなったりする可能性があります。

一度に修正する範囲が決まったらその範囲だけを--fixオプションを付けたeslintコマンドで自動修正しましょう。 その後、残ったエラーを手動で解決していくと良いです。

$ npx eslint AComponent/*/* --fix

一度に修正すると決めた範囲の修正が終わるごとにコミットしましょう。 毎回コミットをせずにまとめてやってしまうと、コードレビューの時にレビューの手が回らなくなってしまいます。 修正する時に手が回らない範囲をコミットしてしまうと、コードレビューの手が回るわけがありませんから。

コミットと同時にプルリクエストの規模が大きくなりすぎるのも問題です。 流石に1コミットごとにプルリクエストを出すわけではありませんが、1~200行程度、多くとも1000行以下で一度プルリクエストを出して欲しいです。 こちらもコードレビューがひたすら辛くなってしまいますし、一度の影響範囲が大きくなりすぎて他の作業者と競合してしまう可能性が高くなってしまいます。

後はこれらのことを気をつけて地道に修正していけばその先に待つのはなんのLintエラーも無い、すっきりとした開放的な環境です。

修正完了

Lintエラーを全て解消した!
Lintエラーを全て解消した!

Lintエラーを全て解消しました!!

最後に、これからはLintエラーを含んだコードをリポジトリに混入させないよう、コミットにhookしてLintエラーを自動修正・エラー出力・問題がある場合にコミットを取り消しするように設定します。 package.jsonに設定を追記しましょう。

    "scripts": {
        "precommit": "lint-staged"
    },
    "lint-staged": {
        "*.{js,jsx}": [
            "eslint ./ --fix",
            "git add"
        ]
    },

これでLintエラーを含んだままコミットしようとしてもリポジトリが汚されることはありません!

ああ、なんと素晴らしいことでしょうか......。

まとめ

このプロジェクトの規模はjs・jsxファイルを合わせて70ファイルで5000行程度でした。 この程度のプロジェクトでも1.5人日(約12時間)程度かかりました。

おそらく業務コードや長期間運用するコードはこれどころではない規模になってしまうと思います。 Lintツールを導入するのにはそこまで時間がかかりませんし、最初から導入していれば後から面倒な修正作業も発生せず、常に綺麗なコードを保つことができます。

迷ったらとりあえずLintツールをいれましょう。

さもなくば後から発狂することになりますよ......。

参考

資格を取るべきなのか専門学校生としての経験をもとに考えてみた

最近Twitterでは基本情報やらITパスポートやら資格についての話がよく見受けられます。

資格をかなり重要視している専門学校の、生徒だった経験から資格をとった方が良いのか、取らなくても良いのか、どのような心構えで取るべきなのかを個人的な考えで書いていこうと思います。

ここ最近で話に上がっている資格

(補足)

各資格について少しだけ補足しておきます。

ITパスポート

パソコンを扱う人なら分かっていた方が良い資格です。

基本情報技術者

プログラマはもちろんですが、ITに関係する人であればこの資格の内容はわかっておいた方が良いです。

ITパスポートの単純上位互換に近く、PCに関する話からマネジメント、金銭の話まで幅広く出題されます。 とりあえずこの資格の内容を理解していれば最低限IT人材と言えるかと思います。

応用情報技術者

しっかりとしたIT人材としてやっていくには最低限ここまで理解しておくべきだと思います。 資格としてはそれなりに面倒ですが、しっかりと理解すればさほど難しい資格ではありません。

資格自体が必須になるタイミング

資格を持っているということのみでフィルタリングされる場合があります。

例えばSIer系(?)でプロジェクトに配備される人の条件として、基本情報を持っている人が一定数必要である場合などがあります。 国のプロジェクトではこのようなフィルタリングをされる可能性が高いです。

また就職活動をする際に基本情報などの資格を持っていることが応募条件である場合もあります。

よく聞くのはこのくらいです。

Twitterでよく見る意見

賛成派

  • 資格を持っていることで一定の知識を担保できる
  • 取っておいて損はない

否定派

  • 資格勉強しかできない頭でっかちが多い
  • 資格を取るくらいならプログラミングした方がいい

全体として

やはり賛否両論という感じです。

「知識ベースの資格には意味がないが、実用的なその他に分類される資格には意味がある。」といった資格によって賛否が違う内容もよく見かけます。

資格で本当に知識を担保できるのか?

資格を取るためには一定以上の勉強をしなければいけません。 となればもちろんその資格分の知識は勉強したのでしょう。

ですが、資格を取ると言った時に、どの様に勉強をしているのでしょうか?

資格を取るためだけに勉強して、実際に取った後はほとんど忘れてしまっているなんてこともよく聞きます。 しかも実技はからっきしで頭でっかちでなおタチが悪いなんて人をよく見ますね。

こんな人が多い状態で本当に知識を担保できるのかというと少し疑問があります。

資格は必要ない

個人的は資格自体が必須になる場合以外で、資格は一切必要ないと思っています。

なぜか?

わざと過激な言い方をします。

資格を取るためだけに勉強した知識などほとんど覚えている人はおらず、それで頭でっかちになってしまうのであれば取らない方がマシだと思うからです。 そしてそんな勉強は時間の無駄です。 それこそプログラミングをしていた方が時間の有効活用になります。

資格の有用性

それでも資格には一定の有用性があります。

資格を取るためだけに勉強して知識がほとんど抜けていたとしても、その資格を取るために努力した事実はなくならないのです。 その資格が難しいものであるほど、その人はそれだけの努力をしたのです。

それだけ努力ができる人だと認識できる一点においては非常に有用だと思います。

私が資格の学習をする理由

ここまで資格を否定しておいてなんですが、私は資格の学習をすることには賛成します。 私自身ITパスポート〜応用情報まで取得していますし、ネットワークスペシャリストも受けていました(後4点で落ちましたけど!)。

ここで一番言いたいことは、「ただ資格を取るために勉強する行為にはほぼ意味がないが、知識を習得するために学習しそのゴールとして資格取得を目指すことには大いなる意味がある」と言うことです。

専門学校なんてのは特にそうでしたが、資格を取らせることを大きな目標にしています。 なぜなら資格取得率が重要であることと、資格によるフィルタリングを通り抜け就活を有利にするためと、資格を取ることで少しでも知識を覚えさせたいからです。

しかし専門学校にいる人はもちろん、資格を取るためだけに勉強する人のなんと多いことか。 それでは資格のために勉強したことはほとんど血肉にならないじゃないか。

私はゲームでも称号を無駄に集めたがる性格なので、そう言う理由で資格が取りたいとも思っています。 なので資格を取りたいと思うことはなんら問題ありません。

その資格を取るときの学習姿勢が一番重要なのです。 その姿勢を間違えなければ学習した全ての内容が血肉となり、得た知識が技術の向上に直結します。

まとめ

何度でも言いたいです。

資格を取ることでも資格の得点を取ることでもなく、己の研鑽のために学習をしてほしい

「ゼルダの伝説 ブレスオブザワイルド 」のUXについて考察しました

Nintendo Switch版「ゼルダの伝説 ブレスオブザワイルド 」(通称BoW)のUXについて考察してみました。

このゲームには素晴らしい仕組みや面白さなど多くの要素がありますが、今回話す内容はその中でもトップクラスに素晴らしい要素について書きたいと思います。

導入

BoWのもっともUXが高いと思う点が3つあります。

  • ほぼロードがない
  • スートーリーを意識しなくても良い
  • システム上の都合をユーザーに意識させていない

これらの要素は全て、ユーザーに対して開発者の問題やシステムの押し付けをしていないという共通点があります。

ほぼロードがない、とは

BoWの世界はオープンワールドになっており、エリア間の移動にロードがありマップが切り替わるという仕組みではなく、エリア同士がフラットで移動の際に遮るものはありません。

他のオープンワールドゲームにありがちな問題として、マップを高速で移動すると読み込みに時間がかかったりカクついたりすることがよくあります。 しかし、BoWでは殆どマップ読み込みのカクつきに出会ったことはありません。 (唯一例外として桃白白という特殊を実践した時、ごくごく稀にマップ読み込みにコンマ何秒か要する場合があります。)

スートーリーを意識しなくても良いこと、とは

オープンワールドではないゲームでは大体の場合、ストーリーを進めるごとにマップが解放されて先に進めるようになります。 そのためストーリーとしては自然に進行します。

オープンワールドのゲームでは自由にマップを歩けるため、ストーリー上先のマップにも行くことができます。 ですがストーリーの流れは限定されているため、ストーリーを進めるのが面倒になるということが多発しやすいです。

対してBoWではオープンワールドで自由にマップを歩ける上に、ストーリーすら自由に進められます。 そしてストーリーの進め方によってセリフやイベントが大きく変化します。 ストーリーの変化すら楽しんでプレイできます。

ちなみに私はプレイ開始直後、ラスボスに直行し頑張ってクリアしました。控えめに言って楽しいです。

システム上の都合をユーザーに意識させていない、とは

BoWはオープンワールドでマップ上で倒された敵やアイテムもそのままになります。 そのためシステム上ではその変化を保存するために多くの容量を使用します。 永遠に保存し続ければいつか容量が足りなくなってしまう場合もあります。
その上、ステージ上のアイテムが枯渇してユーザーが楽しめなくなるという場合も発生してしまいます。

それを解消するには多くの場合メタな表現(ゲームの都合で押し通すなど)をする必要があるわけですが、BoWでは「赤い月の夜」というゲーム上自然な内容によってそのシステムの容量を解放しています。

おそらく多くの一般人は裏でシステム容量の解放が行われていることにまったく気がつかないでしょう。 システム上の都合をユーザーに一切意識させることなく、ゲームシステム的にも自然に処理しています。 逆にその処理がユーザーへの面白さ提供にもなっているというのが非常に素晴らしい点です。

まとめ

開発やシステムの都合をユーザーに押し付けることほどユーザーの興を削ぐものはありません。 BoWはこれらの徹底が非常に高い水準で維持されています。 自分たちの作るゲームに大いなる愛を持って開発しているという点が伺えます。

ゲームに限らず、Webサービスなどの開発においても自分たちのサービスに対する愛を持って開発しましょう。

ロケール環境設定を良い感じに設定する方法

ロケール環境設定の詳細な意味は以下の私の記事を呼んでください。

qiita.com

結論

特にこだわりがないならとりあえずLC_ALLを設定しておけば大丈夫です。

export LC_ALL=ja_JP.UTF-8

ja_JP.UTF-8の部分は自由に変更してください。

プログラマーの学習方法は1つを極めるべきなのか、広く学ぶべきなのか?

プログラマーの学習方法はどうするのが一番良いのでしょうか?
ある人は1つに決めてひたすら習熟し、その後に別の技術に手を出すべきだ。と言います。
はたまたある人は1つだけを学んでいても意味がないから、幅広く学ぶべきだ。と言います。

どっちだよ!!って思いますよね。

この記事を書こうと思った最初の理由はこのツイートでした。

うんうん、その通りだと思います!

ですが、1つを極めた場合とあれこれ手を出した場合は何がどう違うのでしょうか?

2つの学習方法の違い

ここでは便宜上、「1つを極めた場合」を「特化型」、「広く学んだ場合」を「汎用型」と呼称することにします。

では早速、具体的にどのようなスキルマップになるのかグラフの例を見てみましょう。 各スキルの合計値はどちらも同じにしてあります。

特化型のスキルマップ
特化型のスキルマップ

汎用型のスキルマップ
汎用型のスキルマップ

これらのグラフを見ると、やはり特化型の方が1つのステータスに全振りできるので圧倒的に即戦力になりそうですね。 汎用型は全体が平均化してしまうので特化型の分野においては絶対に勝てそうにないです。

やはり特化型の一強に見えます。

筆者の学習方法

この流れで言うと「お前バカかよ」って言われそうですが、実のところ筆者はガチガチの汎用型です。

やりたいことがありすぎて気がついたら汎用型になってしまった典型パターンです。 ですが、おそらく自分には汎用型の方が向いていたのだと思います。

特化型より汎用型の方が向いていた話も出てきたところで両方のメリット・デメリットを考えてみましょう。

特化型

メリット

名前の通り特化できるので極めた分野においては無類の強さを発揮します。

  • この問題を得意な言語で解いてください、と言われたら特化分野を武器に戦えます。
  • 仕事でも特化分野の強さを生かしてリードできるかもしれません。
  • 分野が決まっているので仕事が選びやすいです。
  • 一つ極めたことで、似たような他の分野も学びやすくなります。
  • あれもこれもと多くの分野を学習する必要がありません。

デメリット

名前の通り特化しているので他の分野が弱いです。

  • この技術でやってください、と言われてもやったことないものが多いので最初から学ぶ必要があります。
  • 他の分野のことが全然分からなかったりするので、仕事でのコミュニケーションが大変です。
  • 特化分野以外の仕事は選び辛いです。
  • 他の技術をやると考え方の違いが大きすぎて、取り掛かりが大変だったりします。
  • 初心者の3年間は一本特化で行くぞ!と決めてしまうことによって、視野が狭くなって特定分野しかできなくなる場合があります。

汎用型

メリット

特化型とは違い幅広い分野を学んでいるため視野の広さで強さを発揮します。

  • いろんな分野を知っているので話せる技術の話が多いです。
  • 分野にこだわらず仕事を探せます。
  • 仕事をするときに自分の知っている分野が多いため、他分野の人(エンジニア以外も含め)とも簡単にコミュニケーションが取れます。
  • 多くの分野に触れてきたので、新しいことでも弊害なく受け入れることができます。
  • その時に応じた非常に柔軟な行動ができます

デメリット

分野ごとの力量が平均化しているので、自分の代名詞と呼べるこれぞという分野が生まれづらいです。

  • 特定分野のめちゃくちゃニッチな話にはついていけません。(EB3E90が~、なんて言われてもOSの細かい話にゃついていけないよ!となるわけです。)
  • この技術の経験が何年以上の人を募集する。と言われると辛い。
  • 自分が何の分野の人間だったか忘れそうになります。
  • 新しいことでも弊害なく受け入れすぎた結果、やりたいことが増え続けます。
  • 柔軟性が高すぎて、柔軟性の低い人が何故そんなに柔軟性がないのか分からなくなったりします。

汎用型の真の力

汎用型にはまだ見ぬ真の力があります。変身を残しているのです。

汎用型には特化型に絶対負けない数があります。
これらを抽象化することが重要なのです。

浅く広くを上手くやるには、それぞれの技術を抽象化して繋げる必要があります。 上手く抽象化して技術の掛け算を発生させると、広く浅くやってるのに気が付いたら深いところまで理解していたりします。

一つの技術を突き詰めてやって行くと何度も成長の壁(学習曲線の横ばい部分)に当たり、この壁に当たっている間は辛く苦しい期間が続きます。 複数の分野を適切に並行して学習していると、成長の壁を別分野から迂回してすんなり通過することが多々あります。 これを上手く使えば成長の壁を意識することなく永遠に成長し続けることができます。

学習曲線グラフ
学習曲線グラフ

この抽象化、迂回が上手くできれば触った分野が多くなるほど情報が集積され、既存分野の底上げがされます。

グラフにするとこうなります。

汎用型の真の力を発揮した後のスキルマップ
汎用型の真の力を発揮した後のスキルマップ

ですが、この道を進むには生半可な気持ちでは難しいです。

汎用型を突き詰める場合にやること

汎用型を突き詰める場合にやること・大事にすることは大きく分けて3つあります。

1つ目に、多くの分野を学ぶということは、それだけやった内容を忘れていく可能性が高いと言うことです。 前提条件として学んだ内容を可能な限り自分の血肉として蓄える必要があります。
すでにやった内容を忘れないようにSNSやブログ、メモにアウトプットをすることは非常に重要です。 そしてやった内容をただアウトプットするのではなく、この段階で他の技術と抽象的に結びつけた自分の見解を加えることで抽象化の能力を高めることにもつながります。

2つ目に、ある分野を特化しないので、意図的に様々な分野を学び続ける必要があります。 もちろん特化型も学び続ける必要がありますが、それ以上に汎用型は学びを止めた時点で価値が下がります。
特化して鍛えた技術は古くなっても一定の需要がありますが、汎用的に鍛えた技術は古くなるとあまり価値が高くありません。

3つ目に、偏見や固定概念を持たずに新たな概念や技術をどんどん取り入れることが大事です。 自分の知らない考えを柔軟に取り入れ、ダメな考えは反面教師にし、自分を高め続けましょう。

柔軟な考えとは時に一般人が考えもしないようなところすら学びに転換できる能力も含まれていると思っています。 以下の記事は独自の発想転換で無理やり学びに繋げたエピソードを記事にしたものです。

nakka-k.hatenablog.com

まとめ

汎用型を突き詰めるには相当の努力と覚悟が必要だと思います。 そして何でも楽しんで学べる人でないと向いていません。

多くの人には特化型をおすすめします。 そして特化型は単純に強いです。

私自身、特化型に憧れもありますのでどちらが良い、悪いということもありません。 自分にあった道を選んでも良いんだということと、道を選ぶ際の基準を知っていただければ嬉しいです。

「HeadFirstデザインパターン」のObserverパターンをGo言語で実装してみた

「HeadFirstデザインパターン」ではJavaを用いてデザインパターンの実装が解説されています。 これらのデザインパターンをGo言語で実験的に設計し直した実装を紹介します。

前回はStrategyパターンを実装しました。

nakka-k.hatenablog.com

Observerパターンとは

簡単な概要だけを説明すると、

親となるインターフェースと子となるインターフェースがあり、子は親のグループに入ったり出たりできます。 そして、親はグループ内の子に対して任意のタイミングでメッセージを送ることができるパターンです。

本に出てくるObserverパターンのイメージ
本に出てくるObserverパターンのイメージ

実装

今回のコードは全てGitHubのリポジトリにありますので、全体のソースコードはそちらからご覧ください。

まずはObserverの親となるインターフェースと子となるインターフェースを定義します。

package main

// 親となるインターフェース
type Subject interface {
    RegisterObserver(o Observer)
    RemoveObserver(o Observer)
    NotifyObservers()
}

// 子となるインターフェース
type Observer interface {
    Update(temperature, humidity, pressure float64)
}

では次にSubjectの実装を見ていきましょう。

package main

// Subjectインターフェースの実装
// グループに入った子を保存するようのObserver配列を持つ必要があります。
type WeatherData struct {
    observers   []Observer
    temperature float64
    humidity    float64
    pressure    float64
}

// コンストラクタ
func NewWeatherData() *WeatherData {
    wd := &WeatherData{}

    wd.observers = []Observer{}
    return wd
}

func (wd *WeatherData) RegisterObserver(o Observer) {
    wd.observers = append(wd.observers, o)
}

// (Go言語にはスライスの要素削除関数はないので自前で実装する必要があります)
// (実戦で書くときはもう少しちゃんと書きましょう)
func (wd *WeatherData) RemoveObserver(o Observer) {
    observers := make([]Observer, len(wd.observers)-1, len(wd.observers)-1)
    for _, obs := range wd.observers {
        if obs != o {
            observers = append(observers, obs)
        }
    }
    wd.observers = observers
}

// ここでグループに入っている子に対してメッセージを一斉送信します
func (wd *WeatherData) NotifyObservers() {
    for _, obs := range wd.observers {
        obs.Update(wd.temperature, wd.humidity, wd.pressure)
    }
}

func (wd *WeatherData) MeasurementsChanged() {
    wd.NotifyObservers()
}

func (wd *WeatherData) SetMeasurements(temperature, humidity, pressure float64) {
    wd.temperature = temperature
    wd.humidity = humidity
    wd.pressure = pressure
    wd.MeasurementsChanged()
}

func (wd *WeatherData) GetTemperature() float64 {
    return wd.temperature
}

func (wd *WeatherData) GetHumidity() float64 {
    return wd.humidity
}

func (wd *WeatherData) GetPressure() float64 {
    return wd.pressure
}

ここで重要なのが、構造体にObserver配列を持っている点です。 その配列に登録されたObserverたちを保存しておく必要があります。 この配列を管理することがSubjectの大きな仕事になります。

ついでにGo言語のインターフェースの仕様について補足しておきます。 Observer等のインターフェースは値渡しになっているように見えますが、内部にポインタを持っているので構造体とは違って参照渡しになっています。

次はObserverインターフェースの実装を見ていきましょう。

package main

import "fmt"

// Observerインターフェースの実装
// ここではWeatherData(Subjectインターフェース)の参照を持っていますが、実際は絶対に必要というわけではありません。
// しかし子の方から自由にグループ離脱ができて便利なため、親の参照を持つ場合が多いです。
type ForecastDisplay struct {
    currentPressure float64
    lastPressure    float64
    weatherData     *WeatherData
}

// コンストラクタ
// メンバ変数の初期値はコンストラクタ内でしています。
// そして自分自身をSubjectのグループに登録しています。
func NewForecastDisplay(weatherData *WeatherData) *ForecastDisplay {
    fd := &ForecastDisplay{}
    fd.currentPressure = 29.92

    fd.weatherData = weatherData
    weatherData.RegisterObserver(fd)
    return fd
}

func (fd *ForecastDisplay) Update(temperature, humidity, pressure float64) {
    fd.lastPressure = fd.currentPressure
    fd.currentPressure = pressure

    fd.Display()
}

func (fd *ForecastDisplay) Display() {
    fmt.Print("Forecast: ")
    if fd.currentPressure > fd.lastPressure {
        fmt.Println("Improving weather on the way!")
    } else if fd.currentPressure == fd.lastPressure {
        fmt.Println("More of the same")
    } else if fd.currentPressure < fd.lastPressure {
        fmt.Println("Watch out for cooler, rainy weather")
    }
}

ここで気をつけることはほとんどありませんが、自分が登録したグループの親であるweatherDataの参照を保存している点は要注意です。 ここは求められる仕様によって変えていっても大丈夫だとは思いますが、多くの場合は持っていた方が良いと思います。 なぜならコードが単純になりやすいからです。

コンストラクタについて補足すると、Javaと違ってオブジェクトが全て参照渡しではないので明示的にWeatherDataのポインタを渡すようにしています。 そして自分自身をSubjectのグループに追加する処理も走らせています。

まとめ

Observerパターンの一番の重要点は、情報が一方向にしか流れず、双方向に同期を取る必要がない点です。 Subjectが情報を一元管理し、変更した時点で子に対して通知するだけで後はよしなに、という簡素な関係です。

もしこれが同じ階層のオブジェクト同士でいろんな情報を相互にやりとりしていれば、情報の扱いが非常に面倒なことになってしまいます。

そしてもちろん、SubjectインターフェースとObserverインターフェースに対して実装することでコードを綺麗に保つことも重要です。 HeadFirst本でも書かれていますが、JavaにはObserverパターンを実装するためのパッケージが存在します。 これはインターフェースではなく、継承という形で実装されます。 そうなると拡張性や再利用生が失われかねませんので、やはりインターフェースで実装できるときはインターフェースで実装するようにした方が多くの場合良いでしょう。

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

この本のデザインパターンを順次試していく予定なので、次回もどうぞご覧ください。

「HeadFirstデザインパターン」のStrategyパターンをGo言語で実装してみた

「HeadFirstデザインパターン」ではJavaを用いてデザインパターンの実装が解説されています。 これらのデザインパターンをGo言語で実験的に設計し直した実装を紹介します。

Strategyパターンとは

簡単な概要だけを説明すると、

複数の振る舞いをインターフェース化し、特定のクラスにメンバ変数として持たせます。 それにより特定のクラスの内容を変更することなく、動的に振る舞いを変えることができるパターンです。

想定

複数種類の鴨を実装します。 それぞれの鴨が、飛んだり飛ばなかったり、鳴いたり鳴かなかったりします。

これをもし鴨という親クラスと鴨A・鴨Bと継承して、それぞれに飛ぶ処理や飛ばない処理を実装するのが単純な方法かもしれません。 オブジェクト指向を始めたばかりに多い実装だと思います。

ですがそれでは同じ飛ぶ処理を持つ鴨が出てきて、実装が重複する可能性が高くなります。 また、動的に変更することもできません。

これをStrategyパターンを使って実装するという想定です。 詳細を知りたい場合は書籍を読んでください。

Go言語での実装

今回のコードは全てGitHubのリポジトリにありますので、全体のソースコードはそちらからご覧ください。

飛ぶインターフェースを実装した振る舞い構造体を用意します。

同じく鳴くインターフェースを実装した振る舞い構造体を用意します。

鴨の実装は少し複雑です。

duck構造体は、全ての鴨が持つべき共通処理と振る舞い構造体(*Behavior)を持っています。 この構造体は抽象クラスのように使うつもりなのでprivateで定義しています。 具体的な鴨はDuckインターフェースを実装し、型の統一をします。

具体的な鴨の実装であるMallardDuck構造体に、duck構造体を委譲します。 これによりクラスの継承に似た状況を作り出します。

鴨たちが持つ飛行・鳴き声の振る舞い構造体は実行時に生成し、それぞれの鴨に渡しています。 そして、Duckインターフェースを実装しています。

これが鴨Strategyパターンの動作イメージです。 Duckインターフェースを実装した鴨たちのコンストラクタ関数を呼び出して配列を初期化します。 それをループで回して正常に動くことを確認するコードです。

まとめ

飛んだり鳴いたりする振る舞いを抽出し、鴨の共通構造体に持たせる。 具体的な鴨に共通構造体を委譲し、インターフェースを実装する。

これにより振る舞いは柔軟に変更でき、実装の重複が少なくなります。 そしてインターフェースへプログラミングすることによって自由な取り回しが可能です。

この実装が実戦で使えるかと言われるとわかりませんが、設計とはカスタマイズするためにあるのでいろいろ試してましょう。

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

この本のデザインパターンを順次試していく予定なので、次回もどうぞご覧ください。

nakka-k.hatenablog.com

参考

Strategy パターン - Wikipedia