LaravelでSPAアプリケーションを構築する際に考えないといけないこと
はじめに
今回は、Laravel12でスターターキット周りの変更があったり、LivewireのバージョンアップがあったりとLaravelのフロント周辺の変更を踏まえつつ、2025年にLaravelで業務アプリを構築する際に考慮することをまとめてみたいと思います。
技術選定の基準
私が業務アプリを構築する際に技術選定を行う際に考慮することをまずはまとめます。
規模の把握
作成したいアプリの規模をまずは把握することが重要です。
規模、とひとことで言っても様々な観点があります。
- ユーザー数
- データ量
- 画面数
- リクエスト量
- 予算
- 期間
これらについて開発のキックオフ時には把握しておくことが重要です。
また、ビジネス的な観点も意識しておきましょう。例えば、アプリケーションを作成して納品することで価値を生む受託開発と、アプリケーションそのものの価値で収益を生むプロダクト開発では、技術選定の基準が異なるかもしれません。
そのアプリケーションの求められる性質がなんなのか考えてみましょう。
例えば、決済アプリであればセキュリティはもちろん、大量のデータを扱うため非同期処理のサポートが強いプログラミング言語だったりフレームワークを選定した方がいいかもしれません。
反対に、ユーザーが社内に限られる社内ツールの場合はそこまでパフォーマンスや非同期処理は不要であるものの、長期的に利用されたり、拡張の要望が今後多く発生するかもしれません。
バックエンド
今回選定するバックエンドのフレームワークはLaravelを選択します。
弊社で主流として利用していることもありますが、LaravelはPHPのフレームワークの中でも人気が高く、コミュニティも活発です。
一般的なWeb開発において欲しい機能のほとんどがフレームワークおよび拡張ライブラリとして提供されているため、フレームワークを利用する際の開発効率面でのメリットが大きいです。
また、LaravelはRuby on Railsと比べると制限がゆるく、フレームワークの用意した方法以外での実装がしやすいため、要件に合わせた柔軟な設計を行いやすいです。
生成AI時代におけるプログラミングでは、当たり前ですがOSSのようにソースコードが公開されているもの、利用者が多く存在するコード量が多いものほどLLMを利用する上でのメリットが大きくなります。
Laravelはその点でも有利であると言えます。
Laravelが適さないケースを考えてみよう
PHPフレームワークの中では圧倒的な人気を誇るLaravelですが、Laravelが適さないケースはあるのでしょうか。 この観点から考えてみることで自分の作成したいアプリケーションにLaravelが適しているかどうかを考えることができます。
リアルタイム性が求められるアプリケーション
例えば、大規模なチャットシステムや株価配信など、リアルタイム性が求められたり、秒間数万〜数十万のリクエストが発生するような高トラフィック/高スループットが要求されるようなアプリケーションの場合はLaravelはあまり適していないと言えます。
理由はLaravel、というよりPHPの特徴として同期的な処理を前提としています。ただし、近年はコンテナベースのインフラ技術が発展していることもあり、水平スケーリングで対処しやすい短いリクエストが大量に来るケースではPHPでも対応可能です。このようなケースの例としてはECサイトのような商品の閲覧やニュースサイトのような記事の閲覧による多アクセスのようなケースで、Redis等のキャッシュを利用したり、CDN、オートスケールの利用で十分運用できそうです。
とはいえ、PHP自体が同期処理を前提としていることもあり、1インスタンスあたりの処理効率が他の技術に比べて高いわけではないため、インフラコスト等の面で最適とは言えないケースもあります。
長時間の持続的接続が求められるアプリケーション
PHPでは、リクエスト処理が完結型の仕組み(プロセス or スレッド)で動作します。そのため、WebSocketなどを利用した常時接続を大量に保持するような負荷には向いていません。
無理やり実現する場合はインフラ構成等の工夫が必要になってきますが、そもそもイベント駆動や非同期I/Oを前提とした言語やフレームワークの方が適していると思います。
Laravelが適しているケース
逆に言えば、上記以外のケースのほとんどをLaravelはカバーできると言えます。
ただし、ほとんどのケースに対応できる汎用性はありながらも特定のケースに特化しているわけではないので、特定のケースに特化したフレームワークや言語に比べると、その分パフォーマンスや開発効率が劣ることもあります。
あくまで一般的なWebアプリケーションを作成する際にはLaravelで困ることは少ないと言えるでしょう。
フロントエンド
Laravelを基準にフロントエンド技術を選定する際の2025年時点での有力な選択肢について考えてみましょう。
なお、すべてSPA構築することを前提としています。
Vue.js
Laravelとの連携において採用される実績が多く、比較的組み合わせる場合の情報量は多いと言えます。
ただし、Vue2系からVue3系への移行に伴う破壊的変更や、日本では人気ですが世界的にはReactの方が人気があります。
個人的にはフレームワークが多くのことを処理してくれるため、Reactと比べると学習コストが低い印象で、初心者が書いても致命的なパフォーマンスの劣化を引き起こしにくい点ではReactよりも優れていると感じています。
React
世界で最も人気のあるフレームワークであり、日本でも人気が高いです。
そのコミュニティの大きさや企業での採用実績、React Nativeなどモバイルアプリケーションでも関連した技術があることから、Reactを選択するメリットは多いと言えます。
また、SPAだけでなくNext.jsなどのSSRをサポートするフレームワークもあり、SEO対策やパフォーマンスが求められる場合にも対応しやすいです。
レンダリングの際に考えることの多さではVue.jsと比べると多いですが、どちらにせよある程度作り込んでいくとどちらの技術を採用しても考えないといけないことなのであまり差はないかもしれません。
Laravel + Inertia.js
SPAの場合、通常フロントエンドはLaravelとは別のプロジェクトとして通信を行い、APIを介してデータのやり取りを行います。 しかし、Inertiaをもちいると、Laravelのルーティングやコントローラで受け取ったデータを、Bladeテンプレートの代わりにそのままVueやReactのコンポーネントに渡すことができます。
Bladeを利用しているアプリケーションから徐々にSPA化していく場合やバックエンドエンジニア中心のチームだと選択肢として有力です。
Laravel + Livewire
Laravelが提供しているLivewireという技術も選択肢に入ります。
これはLaravelアプリケーション内でVueやReactといったフロントエンドフレームワークを利用せずにBladeテンプレートを利用して動的なコンポーネントを実装することができます。
Livewireのコンポーネントはサーバーと非同期通信し、DOMを部分的に差し替えるため、SPAのようなUXを提供することができます。JQueryを利用しているアプリケーションから徐々にSPA化していく場合や、バックエンドエンジニア中心のチームだと選択肢として有力です。
ただ、Laravelとの連携がしやすい反面、深く統合されてしまうため、ReactやVue.jsのようなフロントエンドフレームワークを利用する場合に比べると、フロントエンドの技術としてのスキルが身につきにくく、技術としても主流ではないため、情報量の少なさや生成AI時代におけるプログラミングの観点からもあまりおすすめできません。
採用する技術の選定
視点としては企業で採用、運用していく、運用期間が長くなることを想定しているアプリケーションの場合、デファクトスタンダードや息の長い技術を選びつつ、いざという時に移行が致命的に難しくない技術を選ぶことが重要です。
記事執筆時点で上記に当てはまる技術としてはLaravel + Reactが安牌かなと思います。
LaravelでReactを採用する時の統合方法
ここからが本題なんですが、LaravelとReactを統合する方法について考えてみます。
LaravelとViteを利用してReactを統合する
Laravelを利用しつつフロントエンドフレームワークを利用する場合、現在はビルドにViteを利用することが推奨されています。
ReactやVue.jsを利用する場合はLaravelに用意されている統合方法を利用することができます。
laravel-react-app/ ├─ app/ ├─ public/ ├─ resources/ │ ├─ js/ │ │ ├─ components/ │ │ │ └─ Example.tsx │ │ └─ app.tsx │ ├─ views/ │ │ └─ index.blade.php ├─ vite.config.js ├─ package.json └─ ...
- resources/js/app.tsxにReactコンポーネントを置き、app.tsxをエントリポイントとして管理します。
- resources/views/index.blade.phpにapp.tsxを読み込むディレクティブを記述します。
vite.config.tsにはLaravel用の設定を記述します。
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
laravel({
input: ['resources/js/app.tsx'],
refresh: true,
}),
react(),
],
});
LaravelのBladeにはアセットの読み込みを記述して、Laravelからのルーティングはこのファイルに向けます。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Laravel + React</title> @viteReactRefresh @vite('resources/js/app.tsx') </head> <body> <div id="app"></div> </body> </html>
Reactコンポーネントのエントリポイントでは、コンポーネントをマウントします。
import React from 'react'; import ReactDOM from 'react-dom/client'; import Example from './components/Example'; ReactDOM.createRoot(document.getElementById('app')).render(<Example />);
この方法を取る場合はLaravelと同一のリポジトリで管理がしやすく、ビルド結果をpublic/build配下に生成される静的アセットを本番環境に配置します。
開発環境ではViteの開発サーバーを利用できるため、HMRを利用して開発を行うことができます。
なお、現状のViteでは開発サーバーと本番用ビルドに利用している技術が異なるため、開発環境と本番環境での挙動の違いに注意が必要です。
LaravelとReactを別リポジトリで管理する
フロントエンド (React) とバックエンド (Laravel) をそれぞれ別プロジェクトとして扱う、あるいは同一リポジトリでも完全にディレクトリを分離し、React 側で独自の index.html を持つ形式です。
Laravel は主に RESTful API(JSONレスポンス)を提供し、React アプリ側がそれを呼び出して画面を描画する“ヘッドレス”や完全 SPA に近い構成になります。
インフラも含め、フロントエンドとバックエンドを独立してスケール、デプロイできる点がメリットです。
my-project/
├─ backend-laravel/
│ ├─ app/
│ ├─ routes/
│ ├─ ...
│ └─ Dockerfile (optional)
└─ frontend-react/
├─ public/
│ └─ index.html
├─ src/
├─ package.json
├─ vite.config.ts
└─ Dockerfile (optional)
この方法を取る場合はインフラ構成においても幅広い選択肢を持つことができます。
バックエンドとフロントエンドを別々のホスティング先にデプロイする
フロントエンドをVerce、Netlify、Firebase Hostingなどの静的ホスティングサービスにデプロイし、バックエンドをHeroku、AWS、GCPなどのPaaSにデプロイする方法です。
この方法を選択する場合は通信のレイテンシを意識しつつ適切なCORS設定やドメインの設定が必要です。
同一サーバー上でNginxリバースプロキシ
Nginx の設定で /api/ 以下を Laravel (PHP-FPM など) に振り分け、 / (それ以外) をビルド済みの React の index.html に返すようにします。 React アプリはビルド結果が dist や build フォルダにまとめられ、それを Nginx で静的ファイルとして返す形にします。
この方法の場合、後からフロントエンドでSSRが必要になった場合でもLaravelに依存せず自由な構成を取ることができます。
また、リポジトリを明確に分割できるため、大規模な開発チームでバックエンドチームとフロントエンドチームを明確に分業しやすい点もメリットになります。
技術選定における観点
自分が技術選定を行う場合の観点についてまとめてみました。
- 規模の把握
- 技術がデファクトスタンダードに近いか
- いざという時の移行が容易か
- チームのスキルセット
まとめ
最後に、本記事で取り上げた内容をポイントごとに簡潔にまとめます。Laravel で SPA を構築する際の検討材料として参考にしてください。
規模や要件の把握が最優先
- 「ユーザー数」「データ量」「リクエスト数」「予算・期間」などを事前に整理し、要件に見合った技術選定を行う。
- 受託開発かプロダクト開発か、運用期間の長さなど、ビジネス的な観点も考慮する。
Laravel が得意な領域と不得意な領域を理解する
- Laravel は汎用的な Web アプリケーション構築に強く、コミュニティも活発。
- リアルタイム性や常時接続を大量に捌くアプリケーションには不向き(サーバー/言語特性上、非同期処理を得意としないなど)だが、水平スケールや他技術との組み合わせである程度はカバー可能。
フロントエンド技術選定の観点
- Vue.js:学習コストが低めで Laravel との連携事例が多い。
- React:世界的に採用事例が多く、コミュニティが大規模。特に大中規模開発や React Native との連携を考える場合に有力。
- Inertia.js / Livewire:Laravel との親和性が高く、従来の Blade テンプレートから段階的に SPA 化できる。ただし、フロントエンド主導のモダンな技術要素は若干抑えめ。
SPA 化の構成例
長期運用と移行の容易性
- デファクトスタンダードやコミュニティが活発な技術を選ぶと、情報が多く生成 AI 時代に合致しやすい。
- 「いざという時の移行」のしやすさも考慮し、Laravel+React のように広く使われている組み合わせを選ぶと安心感がある。
所感
今回はLaravelと組み合わせるフロントエンド技術について考えてみました。
今、LaravelでSPAアプリケーションを構築する場合、自分ならLaravelとReactを採用しつつ、完全に別リポジトリで管理する方法を選択するかなと思います。
これには、バックエンドとフロントエンドの関心の分離はもちろんのこと、生成AIを活用する上で保持するコンテキストや検索範囲をリポジトリ単位で制限しやすいことを考慮しています。
反対に、規模が小さいプロジェクトであれば、バックエンドにもTSを採用してしまって全部生成AIのコンテキスト対象としてしまうのが生産性が高いかもしれません。
Mockeryで学ぶMockとSpyの違いとつかいかた
はじめに
単体テストにおいてテストダブルを用いることは、コードの設計品質を保つうえで、抽象に依存させる大きな動機になるため、コード本体の品質を上げることに繋がります。
テストダブルのなかでも、外部リソースや他のクラスへの依存を切り離せる「モック(Mock)」と、呼び出しを記録して後から検証できる「スパイ(Spy)」はしばしば混同されがちな概念です。
PHPでモックを扱うライブラリとして有名なMockeryでも、モックとスパイはユニットテストを設計する際の要となります。
本記事では、Mockeryを例に挙げつつ、モックとスパイの違いや使い分け、具体的なコード例を交えて分かりやすく解説してみます。
ライブラリとしての使い方の多くは過去の記事が参考になると思います。
注意
記事執筆時点のMockery最新バージョンである1.6.12では、Spyオブジェクトに関するタイプヒントがうまく動作しないバグが報告されています。
Spyで生成したオブジェクトに対して呼び出し回数の検証を行うtimes等のメソッドがIDE等では認識されないケースがありますが、問題なく動作します。
詳細は下記のGitHub Issueを参照してください。
Mockeryとは
MockeryはPHPのモックオブジェクト生成ライブラリで、PHPUnitなどのテストフレームワークと組み合わせて使用されることが多いです。
Laravel等のフレームワークでも標準で採用されています。
Mocking - Laravel 11.x - The PHP Framework For Web Artisans
テスト対象の依存関係をモック化し、メソッド呼び出し回数や引数、戻り値などを詳細に設定・検証できます。
可読性の高いメソッドチェーンによる記述方式を採用しており、可読性が高く、直感的な操作が可能なため、PHPUnit標準のモックと比較しても学習コストが比較的低いのが特徴です。
Mockeryを使うと、外部APIやデータベースなどへの依存を極力排除しながらテストを実施できるようになります。
Mockとは?
モックは、テスト対象が依存する外部要素やオブジェクトを「模倣」し、あらかじめ設定した条件に沿ってメソッド呼び出しの可否や戻り値を制御するテストダブルの一種です。
最大の特徴は「期待する呼び出し回数や引数、戻り値などを事前に定義し、その通りに呼び出されるかを検証する」点にあります。
例えば、外部APIを呼び出すメソッドをテストしたい場合、実際にネットワーク通信を行う代わりにモックを使って、「どの引数で何回呼び出され、その結果として何を返すか」を決めておけば、外部リソースに依存しない安定したテストが可能になります。
Laravelではライブラリを使わずともサービスコンテナ等の仕組みでモックの実装に切り替えることもできますが、テストケースの中でモックを用意したいケースが多く、そういった場合にはMockeryを使うことで簡単にモックオブジェクトを作成できます。
モックを使うと、呼び出しの期待値が満たされない場合にテストが失敗するようになるため、依存するメソッドが「本当に期待された通りに使われているか」を厳密にチェックできます。
言い換えると、想定した引数が渡されているか、想定した回数だけ呼び出されているかといったことをテストすることができます。
一方で、事前に呼び出しの振る舞いを細かく設定するため、実装の変更に合わせてテストの修正頻度が増えることがあります。テスト対象の責務とモック化の範囲を見極めたり、テストコードを共通化するなどしてコストを下げる必要があります。
Spyとは?
スパイもモックと同様にテストダブルの一種ですが、「呼び出しを記録し、後から参照して検証できる」ことを重視しています。
モックが事前に設定した期待値に沿った振る舞いを強制するのに対し、スパイは実際にメソッドを(部分的にまたは完全に)実行し、最終的に「何回、どのような引数で呼び出されたか」をテスト終了時などにチェックします。
呼び出し回数や引数を事前に縛りすぎたくない場合や、実際の処理をできるだけ動かしつつ呼び出し履歴のみを確認したい場合に効果的です。
例えばログ出力処理をテストする際に、ログを「実際に吐き出すかどうか」を確認するだけで十分ならスパイが向いています。
ログのメソッドが何回呼ばれたかをテスト後に調べられればよいので、厳密に呼び出し順序や引数を規定する必要がなければ「あとで呼び出し記録をチェックする」アプローチで事足ります。
スパイはモックと比べて「実際の処理を動かす」ため、テスト対象の振る舞いが変わらないことを確認しやすいという利点があります。
似たようで違うオブジェクトですが、Laravelで利用する場合はどちらもサービスコンテナを利用して抽象に対してバインドして利用することができます。
スパイやモックは対象の抽象クラスやインターフェースの実装としてオブジェクトが生成されますが、クラスに対してもスパイやモックを生成することができます。ただし、その仕組み上finalクラスやprivateメソッドに対してはスパイやモックを生成することができません。
Mockeryでのモックの利用例
use Mockery;
use PHPUnit\Framework\TestCase;
class UserServiceTest extends TestCase
{
public function tearDown(): void
{
Mockery::close();
}
public function testGetUserById()
{
$mockRepository = Mockery::mock('UserRepository');
$mockRepository
->shouldReceive('findUserById')
->once()
->with(123)
->andReturn([
'id' => 123,
'name' => 'Taro'
]);
$userService = new UserService($mockRepository);
$result = $userService->getUserById(123);
$this->assertSame(123, $result['id']);
$this->assertSame('Taro', $result['name']);
}
}
上記のように、shouldReceive('findUserById') で呼び出しメソッドを指定し、once() で呼び出し回数、with(123) で引数、andReturn(...) で戻り値を定義します。期待した通りの呼び出しが行われない場合はテストが失敗し、モックとしての役割を果たします。
この例ではコンストラクタインジェクションを利用しています。直接引数に指定していますが、Laravelのサービスコンテナと合わせて利用したい場合は$this->app->instance()を利用してインスタンスを直接バインドすることができます。
MockeryのPartial Mock
Mockeryでは、スパイに近いふるまいを実現するためにPartial Mockという機能が用意されています。
makePartial()を使ってモック化したいメソッドだけを指定し、ほかは元の実装を呼び出すようにできます。
さらに、実際に呼び出しがあったかどうかを後で検証する shouldHaveReceived() などのAPIも備えています。
Spyとは別物ですが、オブジェクトの1部分だけをモック化するという場合はこちらを利用すると便利です。
use Mockery;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
public function tearDown(): void
{
Mockery::close();
}
public function testPartialMockAsSpy()
{
$partial = Mockery::mock('MyClass')->makePartial();
$partial->shouldReceive('calculate')->andReturn(50);
$result = $partial->process(10, 5);
$partial->shouldHaveReceived('calculate')->once();
$this->assertSame(50, $result);
}
}
この例では、calculate メソッドのみモック化し、他のメソッドは通常の実装どおりに動作させています。
そしてテスト終了時に shouldHaveReceived('calculate') を呼び出すことで、想定どおりにメソッドが呼び出されたかを確認しています。
呼び出し記録をあとでチェックする点がモックとは違います。
呼び出す場所が違うということは、実際にテスト中に検証に失敗した場合にエラーが発生するタイミングが異なるということです。
モックの場合は意図していない呼び出しの場合は即座にエラーが発生しますが、スパイの場合はテスト終了後、検証が呼び出されたタイミングでエラーが発生します。
MockeryでのSpyの利用例
Mockeryには、モック(Mock)のほかに「スパイ(Spy)」として機能するオブジェクトを簡単に生成するメソッドとして Mockery::spy() が用意されています。
スパイは、呼び出しの記録を後から検証するアプローチを得意とするテストダブルです。
モックと同じように依存関係を差し替えられる一方で、テストの中で「実際に呼び出されたか」をあとから確認したい場合に活用されます。
use Mockery;
use PHPUnit\Framework\TestCase;
class MyClass {
public function doSomething($param)
{
// 実装(テスト内では実際に動かさない場合も)
}
}
class SpyExampleTest extends TestCase
{
public function tearDown(): void
{
// テスト完了後にMockeryをクローズ
Mockery::close();
}
public function testSpyUsage()
{
// 1) Spyを生成する
$myClassSpy = Mockery::spy('MyClass');
// 2) テスト対象(例として直接呼び出し)でメソッドを呼ぶ
$myClassSpy->doSomething('Hello');
// 3) テスト終了後に呼び出し状況を検証する
$myClassSpy->shouldHaveReceived('doSomething')
->once()
->with('Hello');
}
}
スパイの生成 Mockery::spy('MyClass') で、MyClass のスパイを生成しています。このオブジェクトを使って実際のメソッドを呼び出すと、その呼び出し情報(回数や引数など)が記録されます。
メソッドの呼び出し テストの中で doSomething() を呼び出すと、テスト完了時点で「どのようなメソッドが何回呼ばれたか」がスパイ内部に保持されます。
呼び出しの検証 shouldHaveReceived('doSomething') を利用して、「doSomething メソッドが呼ばれたかどうか」「何度呼ばれたか」「どんな引数が渡されたか」を後から検証します。期待通りでなければテストが失敗し、呼び出し回数や引数が確認できます。
また、イベントの発火の監視も可能です。
$dispatcherSpy = Mockery::spy('EventDispatcher');
// テスト対象のメソッド内で $dispatcherSpy->dispatch('UserCreated') が呼ばれる想定
$userService->createUser($dispatcherSpy);
$dispatcherSpy->shouldHaveReceived('dispatch')
->once()
->with('UserCreated');
この例だと、ユーザーを作成したあとでdispatchメソッドがUserCreatedイベントを1度だけ発火することを検証しています。
モックと違い、呼び出し後の検証になるため、「必ず1回だけ」呼び出される、というよりは「本当に呼ばれたか」を検証したい場合に向いています。
同じ考え方で、ログを吐き出すメソッドをスパイで検証すれば、ログを実際に吐き出す必要はないが、吐き出す内容を検証する事ができます。
なお、私も勘違いしていたんですが、Spyにクラス名を指定して生成した場合、デフォルトで 実際のメソッドは呼ばれない ようになっています。
呼び出し履歴や回数こそ記録されますが、実際の処理は動作しません。
Spyの生成時に既存のインスタンスを引数に渡した場合は、実際のメソッドをspy化するため、実際の処理が動作します。
パーシャルモックとスパイの使い分け
Mockeryでは、クラスの一部のメソッドだけモック化し、他のメソッドは実際の実装を呼び出す「パーシャルモック」も提供しています。一見するとスパイに似ていますが、以下のような使い分けが考えられます。
- スパイ:
- Mockery::spy('ClassName') などで生成し、主に「呼び出し履歴を検証」することが目的。事前に戻り値や呼び出し回数を縛らず、後で shouldHaveReceived() を使ってチェックする。
- パーシャルモック:
- Mockery::mock('ClassName')->makePartial() のように生成し、「特定のメソッドだけモック化」したうえで、ほかのメソッドは実際に呼ばせることができる。
- 例えば、計算処理やDBアクセスをするメソッドはモック化しつつ、そのほかのユーティリティ的なメソッドは実際に動かして結果を検証する、といった使い方ができる。
- Mockery::mock('ClassName')->makePartial() のように生成し、「特定のメソッドだけモック化」したうえで、ほかのメソッドは実際に呼ばせることができる。
まとめ
Mockery では、モック的な「事前定義」とスパイ的な「事後検証」を同時に活用するケースが多いです。
shouldReceive() と shouldHaveReceived() を組み合わせることで、厳密な呼び出しチェックと呼び出し履歴の後追い確認の両面をカバーできます。
つまり、「事前の厳密性を優先するか、事後の柔軟な検証を優先するか」という観点で使い分けを考えるといいかなと思います。
良いソフトウェアを目指して設計への入門
はじめに
これまでもいくつも設計に関する記事を書いてきました。
今回は設計が大事なのはわかったけど何から始めればいいの?という方向けに、設計への入門としてのアプローチをまとめてみました。
初学者のときに知っておきたかったことを中心にまとめているので、設計に興味がある方はぜひ参考にしてみてください。
ここでの設計とは、ソフトウェアの設計、特にコードを書くうえでのソフトウェア構造の設計を指します。
良いソフトウェアとは
まずは設計を学ぶ上での目標を明確にしましょう。
設計の目標は良いソフトウェアを作ることですが、良いソフトウェアとはどのようなものでしょうか?
もう少し具体化してみましょう。
良いソフトウェアとは、以下のような特徴を持つものだと言われます。
- 可読性
- 保守性
- 効率性
- 信頼性
- 拡張性
一つ一つ見ていきましょう。
可読性
ソフトウェアの可読性は、コードがどれだけ理解しやすいかを示す指標です。
他の開発者が容易にコードを理解し、修正や改善がしやすい状態にあるコードは可読性が高いと言えます。
可読性が高いコードは、仕様の誤解によるバグの発生を抑制し、将来的な機能拡充のコストを低減することができます。
コンピュータ・プログラミングにおける可読性(readability)とは、プログラムのソースコードを人間が読んだときの、その目的や処理の流れの理解しやすさを指している。
引用元: 可読性 - Wikipedia
ソフトウェアに関するプラクティスのうち、可読性に関わるものは非常に多く存在します。一般にコーディング規約と呼ばれるものはほとんどが可読性向上を目的としています。
また、ソフトウェアエンジニアにとってのバイブルとも言えるリーダブルコード - O'Reilly Japanも可読性向上のための書籍です。
それくらいソフトウェアエンジニアにとって可読性は重要な要素です。
他にも意識するポイントがいくつもあるので、可読性を高めることが目的なんだなと理解しながら、内容を確認していきましょう。
命名の重要性
まずは変数やクラス、メソッドなどの名前に適切な名前をつけることから始めましょう。
ソフトウェアというのは基本的に変数の名前やメソッドの名前は短くても動作しますが、多くのエンジニアは適切に処理を表す名前をつけたほうが良いと考えており、プログラミングにおいて最も重要な要素の一つだと言えるでしょう。
命名においてのポイント
- 名前が表す内容が明確であること
- できるだけ省略せずに名前をつける
- 品詞を意識する
- 変数名やクラス名には名詞、メソッドには動詞を使うことでより理解しやすくなる
- 抽象的な単語や情報量のない単語を利用しない
- 一時的を表す変数名に
tmpを使うのは避ける 〇〇infoや〇〇dataなど、抽象度の高い、情報量のない単語を使わない(プログラミングに使う変数は全部infoやdataです。)
- 一時的を表す変数名に
- 対になる変数には名前に意味を持たせる
startとendなど、関連する変数には関連性を持たせる- ソフトウェアの世界には驚き最小の原則 - Wikipediaと呼ばれる原則がある
- できる限り読んだ人が存在を予測できるように作るべきだという原則。
美しさ
美しさとは、コードの見た目の美しさを指します。やや抽象的ですが、コードのインデントや改行、コメントの使い方などが美しさに関わります。
コーディング規約でもこれらのルールを中心に記述されていることが多いです。
美しさに関しては、人それぞれ感じ方が異なる部分もありますが、基本的に意識するべきポイントは下記のようなものがあります。
- コードの改行位置やインデントを統一する
- 関連する処理をまとめてブロックにする
美しさに関する内容はLinterやフォーマッターを導入することで自動化しやすいものなので、できるだけ早めにこれらを整備しておくと良いでしょう。
コメント
コメントはコードの可読性を向上させるための重要な要素です。
よくHowを説明するコードよりもWhyを説明するコメントを書くべきと言われます。
コメントの内容についてはよく議論のポイントになることもありますが、基本的には以下のような内容をコメントに書くことが多いです。
- コードを読めばわかることを書かない
- 自分の考えを書く
- 実装したときに他に考えた選択肢と現在のコードになった理由を書く
- コードの欠陥に対するメモとしてコメントを書く。
- たとえば、よく使われるコメントとして、
TODOやFIXME、HACKなどがある
- たとえば、よく使われるコメントとして、
- コードの背景を書く
- コードの背景になる情報を書く
- 読み手がコードを読んだときに驚く、気になる箇所を想像してコメントを書く
不変性
ここで、改めてコードが読めないという事象について考えてみましょう。
その読めないコードはどこから読めなくなるのでしょうか?
私自身の経験から言えば、コードが読めなくなる原因の一つに、脳内メモリの容量不足があります。
登場する変数の多さ、変数の中身の書き換えの多さ、関数呼び出しのネスト関係、複雑なif文の条件分岐など、コードの複雑さが増すと、脳内メモリの容量が足りなくなり、コードが読めなくなることがあります。
1行1行のコードは読めても、その内容を覚えながら処理を追うことが難しくなります。
この現象に関連するプラクティスとして、不変性があります。
不変性とは、文字通り変更されないことを指します。
変数の値をできるだけ変更せず、代入したい場合には適した名前をつけた別の変数を用意することで、変数の命名が中身の記憶を助けてくれます。
また、オブジェクトにおいてもfinal等の修飾子を使って変更を禁止することで、オブジェクトの状態が変わらないことを保証することができる言語が多くあります。
できる限り不変性を保つことで、コードを読むにあたって記憶することが減り、コードの見通しが良くなります。
保守性
保守性とは、プログラムが将来的にどれだけ簡単に変更や拡張ができるかを示す指標です。
言い換えれば、機能追加やバグ修正が容易に行えるかどうかを示します。
保守性が高いコードは、変更に対して柔軟に対応できるため、長期的な開発においてコストを抑えることができます。変更にかかる時間が短縮できるということは、市場や技術の変化にも迅速に対応できるようになり、これはソフトウェアとしての競争力に直結します。
保守性を高めるための方法の一つに、コードのモジュール化があります。
モジュール化とは、コードを機能ごとに分割し、それぞれの機能を独立したモジュールとして扱うことです。
モジュール間の依存関係をなくし、変更の影響範囲を限定することで、保守性を高めることができます。
また、モジュール化は再利用性を高め、テストが書きやすくなるというメリットもあります。
関連する原理、原則として最も有名なのは、SOLID原則でしょう。
SOLID原則は、オブジェクト指向プログラミングにおける5つの原則をまとめたものです。
- 単一責任の原則(Single Responsibility Principle)
- オープン/クローズドの原則(Open/Closed Principle)
- リスコフの置換原則(Liskov Substitution Principle)
- インターフェース分離の原則(Interface Segregation Principle)
- 依存性逆転の原則(Dependency Inversion Principle)
詳しい解説はWEB上に多くありますので、興味のある方は調べてみてください。
他にもモジュール化と関連のある概念として、高凝集・低結合があります。
これは、情報の隠蔽によってモジュール間の結合を弱くし、モジュール内の凝集度を高めることに繋がります。オブジェクト志向プログラミングにおけるカプセル化の考え方に近いです。
ほかにも、有名な原理原則は数多くあるので、ここで紹介しておきます。
- DRY(Don't Repeat Yourself)
- 同じコードを繰り返し書かない
- 繰り返し箇所が多いと、修正漏れや工数の増加につながる
- ただし、関心事が異なる場合にDRYを適用してしまうとかえって可読性が下がることもあるので注意
- KISS(Keep It Simple, Stupid)
- 可能な限りシンプルに保つ
- 過度に複雑な設計を避け、初学者でも理解できるレベルを目指すことで可読性・保守性を高める。
- YAGNI(You Aren't Gonna Need It)
- 未来の機能を先取りして実装しない
- エンジニアの予測が外れた場合のコストを考慮し、必要になった時に実装する
- ただし、将来の変更への拡張性を考慮して設計する必要はあるため、過度なYAGNIも問題
- Boy Scout Rule(ボーイスカウトのルール)
- 「来たときよりも美しく」
- コードに手を加える際には少しでもリファクタリングしてコードをきれいにする
- Law of Demeter(デメテルの法則)
- 「友達の友達に直接話しかけない」
- オブジェクトは、密接に関係するオブジェクトだけを知るようにし、深いチェーンを辿らないようにすることで結合度を下げる。
- SLAP(Single Level of Abstraction Principle)
- 「関数やメソッドの中で扱う抽象度を1つに統一する」
- 低レベルな処理と高レベルな処理を混在させず、階層的に構造を分けることで可読性を高める。
効率性
効率性は、ソフトウェアがパフォーマンスとリソースの効率的な利用を行っているかを示す指標です。
効率的なソフトウェアは、ユーザーにとって快適な利用環境を提供し、コストを抑えることができます。
効率性を高めるためには、アルゴリズムやデータ構造の選択が重要です。
また、リソースやパフォーマンスにおけるボトルネックを特定し、最適化を行うことも効率性向上につながります。
マルチスレッドや非同期処理を活用することで、並列処理を行い、処理時間を短縮することも効率性向上の手段の一つです。
信頼性
信頼性は、ソフトウェアが正確に動作し、安定しているかを示す指標です。
主にエラー処理の文脈で語られることが多いですが、信頼性を高めるためには、エラー処理だけでなく、テストやデバッグ、ログの出力なども重要です。
ソフトウェアは、予期しない障害に対処できるように設計される必要があります。
いいエラー処理とは、問題が発生した際にソフトウェアが安全に停止するか、または問題を報告して適切な対処を行うことを保証します。
また、これまで紹介してきた可読性や保守性、効率性などによってそもそもバグを減らすことも信頼性向上につながります。
モジュール化によるテストの充実や自動テストなどのテクニックも信頼性向上に役立ちます。
拡張性
拡張性は、ソフトウェアが将来的な変更や拡張に対応できるかを示す指標です。
要素としては次のようなものがあります。
抽象化
保守性の項目でも触れたモジュール化に関連する概念として、抽象化があります。
抽象化とは、具体的な実装の詳細を隠蔽し、抽象度の高いインターフェースのみを外部に公開することです。
抽象化されたシステムは、具体的にどのように実装されているかを知らなくても、インターフェースを通じて利用することができます。
例えば、DBへのアクセスや外部APIの利用など、具体的な実装が変わる可能性がある部分において、抽象化を行うことでDBやAPIの変更が発生した場合でもアプリケーションの他の部分には影響を与えないようにすることができます。
高凝集・低結合
モジュール化の項目でも触れた高凝集・低結合も拡張性向上につながります。
高凝集とは、モジュール内部の要素(関数や変数など)が一貫した目的や役割を持つ状態を指します。
もっと簡単に言うと、「1つのモジュールが関連性の高い機能だけをまとめて持っている」ことです。
高凝集なモジュールには以下のようなメリットがあります。
- 理解しやすい
- モジュールの役割の単位でまとまっているため、どこで何をやっているか把握しやすくなります。
- 保守・修正がしやすい
- 変更や追加が必要なとき、モジュール内部に収まっていることが多いので影響範囲を把握しやすいです。
- 再利用性が高い
- しっかり役割が定義されているモジュールは、他の箇所はもちろん、他のプロジェクトでも同じ役割で流用できる可能性が高まります。
反対に、低凝集の場合のデメリットとしては、何かを変更した場合に意図していない箇所に影響が及ぶ可能性が高まるという点が挙げられます。
低結合とは、モジュール間の依存度が低い状態を指します。
具体的には、「あるモジュールを修正しても他のモジュールに大きな変更を強制しない」ような状態が理想です。
低結合なモジュールには以下のようなメリットがあります。
- 変更の影響範囲が狭い
- 拡張しやすい
- テストしやすい
まとめ
今回は良いソフトウェアとはという観点から、設計において重要な要素を紹介しました。
ソフトウェアとしての良さを追求していくと、先人たちの残した原理原則やプラクティスが見えてきます。
流行しているアーキテクチャもこれらの原理原則に基づいて設計されていることが多く、まずは原理原則を理解することでアーキテクチャの理解、取捨選択ができるようになります。
可読性や保守性への理解が進むと、「動く」ソフトウェアと「良い」ソフトウェアの間には大きな差があることがわかってきます。
コードを書くことのコストと、将来的に読むコスト、変更するコストのバランスを考えながら、設計を進めていくことが大切です。
自社SaaSで働くということと「設計」の目指すところ
対象読者
はじめに
これから自社SaaS企業に入社する方が今後行う業務においては、単に動作するアプリケーションを作るだけでなく、そのアプリケーションを長期的に保守・運用し続けることになります。
リリースした後も頻繁に機能改善やバグ修正、ビジネス要件の追加などが求められるため、コードが長い期間にわたって生き続けることになります。
こうした背景から、近年注目されている設計手法にドメイン駆動設計(DDD)やクリーンアーキテクチャなどの考え方があります。
こうした設計手法が注目を集める理由は、単に「きれいなアーキテクチャを組みたいから」という美学的なものにとどまりません。
従来のWEB開発の主流だったMVC全盛の時代のDBテーブル中心の設計とは異なり、ビジネスロジックを中心にアプリケーションを構築することによって変更のスピードや柔軟性が手に入る、すなわちビジネス価値の提供速度を落とさずにアプリケーションを進化させられるという、企業にとっての大きなメリットがあります。
本記事では、アプリケーションレイヤのエンジニアが設計を意識することの重要さを、特に自社SaaSを提供する企業の長期運用の観点から考えてみます。
今回想定しているアプリケーションの形態
この記事でいう「アプリケーション」とは、日本企業でよく見られるような「受託開発」や「SES(システムエンジニアリングサービス)」「派遣」などで扱う、契約上“作成すればビジネスとして完了”、利益が生まれるソフトウェアとは異なります。
納品によって開発が一区切りつく性質のシステム開発では、多くの場合、完成時点でビジネスがゴールに達することになります。
しかし、ここで想定しているのは、ユーザーが継続的に利用し続けるアプリケーションです。SaaSとして提供されるプロダクトがその典型例ですが、他にも自社サービスとして運営され、リリース後も機能追加やUI/UX改善を繰り返しながら収益を得るタイプのアプリケーションをイメージしています。このようなアプリケーションは、
- リリース後も終わらない: ユーザーが使い続けてくれる限り、改善要望やバグ修正、新機能の開発が継続的に発生する
- 長寿命が基本: 大規模に集客が成功した場合や、ある程度のユーザー数が定着した場合、数年〜十数年という長期にわたってビジネス運用される
- 契約完了よりも継続利用が重要: 作って終わりではなく、いかにユーザーの満足度を高めつつ開発コストを抑えて運用するかが企業の収益に直接関わる
という特徴を持っています。こうしたアプリケーションでは、最初のリリース後こそが本当のスタートラインとも言えるでしょう。会社やチームが成長し続けるためにも、長期的な視点で設計、継続投資を行う必要があるのです。
この記事における「設計」とは
ここでは先ほどの「アプリケーション」の定義から、設計という言葉についても整理します。
ソフトウェア業界において、「設計」という単語は幅広い意味を持ちますが、本記事では特に以下の観点で設計を捉えます。
ます、「設計」という言葉からは、ER図やクラス図などを頭に思い浮かべる方も多いと思います。しかし、ここではもう少し広い意味での設計、つまりアプリケーションの構造を整理し、どこに責務を置き、どのように変更に耐えられるようにするかを考える行為を指します。とりわけ、ビジネスロジックが集中するアプリケーション層の設計を意識することが最も重要です。
その上で、本記事で取り扱う設計手法について概要を説明します。
- ドメイン駆動設計(DDD): ドメイン(ビジネス領域)をよく理解し、エンティティや値オブジェクト、ドメインサービスなどの概念を用いてコードを組み立てることで、ビジネス上の意味を失わないままモデル化を行う手法です。
- クリーンアーキテクチャ: アプリケーションを複数の層(レイヤ)に分割し、ドメイン層やユースケース層を中心に据えて、インフラ層(フレームワークや外部サービス)への依存を最小限に抑えることで、将来的にフレームワークや外部サービスを切り替えやすくする設計パターンです。
これらの設計手法がもたらす共通の恩恵は、“ビジネスの変化に合わせて柔軟にコードを進化させられるようになる”という点に集約されます。SaaSのように継続的な機能追加・変更が必須のサービス開発では、こうした柔軟性がビジネス上の利益にも直結する強みとなります。
DB設計ベースのアーキテクチャが抱える課題
DBに引きずられるドメインモデル
長らくMVCアーキテクチャやActiveRecordを用いたフレームワークが広く用いられてきたことから、データベースのテーブル構造とモデルクラスが1対1に結びついた設計が主流でした。これは一見すると実装がスムーズに進むように見えますが、以下のようなデメリットが顕在化しがちです。
- ビジネスロジックとの乖離
テーブルを正規化していたり、リレーションを貼っていたりすると、ビジネス上の概念としては一つの単位なのにコード上は複数のテーブルやモデルに分散してしまい、開発者同士の認識がズレやすくなります。 - 不要な結合度の上昇
DB主導の設計では、モデルの変更がそのままDBスキーマの変更に直結する場合が多く、アプリケーションレイヤの改修とDBのマイグレーションが分離できなくなることがあります。 - テストの難易度
DBのスキーマとモデルがベタに結合していると、ユニットテストのような小さな単位での検証が難しくなり、結果的にテストが書かれない、もしくは膨大なテスト用DBデータが必要になってしまいます。また、DBへのI/Oのコストも大きくなります。
つまり、作り上げるまでの0 => 1には強いが、1 => 10や10 => 100の段階ではコードの変更が難しくなります。
こういった技術は変更容易性を犠牲にして作り上げるまでのスピードを取得するためのもので、借金のようなものです。こういった選択が技術的負債と呼ばれます。
個人的には世の中で技術的負債と呼ばれるものは多くの場合技術の選択肢が少ない状況での選択であり、積もうと思った負債ではないので、技術的負債という言葉には違和感を感じます。トレードオフを理解した上であとでやる選択肢を選んだ結果が技術的負債であると思っています。
データの寿命とコードの寿命
ビジネスロジックを変更するとき、テーブル構造まで含めて大規模な変更が必要になるケースでは、運用環境に溜まっているデータの移行も伴います。一度蓄積されたデータは企業にとって大切な資産であり、業務フローと密接に結びついているため、容易に“壊して”作り直すわけにはいきません。
一方で、アプリケーションコードは要件に合わせて積極的にリファクタリング・リプレースが行える部分です。DB主導でコードを書いてしまうと、コード自体まで「変更が難しくなってしまう」という問題が生じます。
本来DBと比べると変更が容易であるコードの変更容易性をDBと同程度まで変更を難しくしてしまうことにつながります。
ドメイン駆動設計(DDD)やクリーンアーキテクチャがもたらすメリット
近年流行しているドメイン駆動設計(DDD)やクリーンアーキテクチャは、上記のような問題を解決するための設計手法として注目されています。
これらの思想はMVCで作られたアプリケーションの課題の反省から、ビジネスロジックを中心に据え、DBやフレームワークといったインフラ層との依存を最小限に抑えることで、アプリケーションの変更容易性を高めることを目指しています。
将来の変更に対する柔軟性
DDDやクリーンアーキテクチャの思想に沿って設計を行うと、アプリケーションを構成する各要素の責務がはっきり分離されます。具体例としては、ユースケース層(アプリケーション層)とドメイン層を明確に分割する、さらにインフラ層(DBや外部サービスへのアクセス)はあくまでドメイン層やユースケース層から切り離して依存関係を一方向にする、といったアプローチが挙げられます。
ビジネス上の概念(ドメインモデル)がコードに明確に反映されるため、要件の変更が起こった際もドメイン側の修正で済んだり、あるいはユースケース層だけを変更すれば良いなど、局所的な修正で対応できる可能性が高まります。DBスキーマ変更を要さない場合は、速やかに新しいロジックへ移行できるでしょう。
修正が局所的になるということは、テストも局所的に行うことができます。テストというのは対象のスコープが小さければ小さいほど観点の網羅性を高めることができます。
ビジネス上の価値創出への貢献
アプリケーションの設計が良いと、新機能のリリース速度にも大きく寄与します。細かい機能修正や追加実装を行う際に、全体構造を大きく壊す必要がなければ、エンジニアはコアなビジネスロジックの改善に集中できます。
競合他社より早く、ユーザーの要望に応えることができれば、ユーザーの満足度向上につながり、ビジネスの成長にも寄与します。
また、定期的なリファクタリングが可能になるのも大きなメリットです。ビジネスロジックとインフラへのアクセスが密結合していない状態であれば、新しい要件に合わせてコードをスリムにしていくことが比較的容易になります。結果として、アプリケーションそのものの品質が向上し、ユーザーにとっても快適なサービス運用が続けられます。
設計の目指すところ
本記事のタイトルにもある通り、私たちが設計を意識するのは「アプリケーションのリリースから、サービス終了するまでの期間で、リターンに対するコストを最小にする」ためです。SaaS企業であれば、リリース後すぐにユーザーのフィードバックを得て改善を回す必要があり、そのサイクルを素早く回すほど競合他社に対して優位性を得られます。
- 売れないアプリケーションに過度な設計投資をするリスク
何事にもタイミングがあり、リリース前の段階で大掛かりなアーキテクチャを組んでも、そのプロダクト自体が収益を生まないまま終わる可能性は否定できません。 - 設計スキルそのものは使い回しがきく
一度学んだドメイン駆動設計やクリーンアーキテクチャの知識は、他のプロジェクトや将来のサービス開発でも有効活用できます。自分自身の市場価値向上にも寄与するでしょう。
実際に一度習得した設計知識を使うこと自体のコストはあまり高くありません。 - 変更コストを下げ、変更スピードを上げる
長期運用が前提の場合、単なる仕様変更でも運用コストの差が積み重なり、企業収益に大きく影響を与えます。設計をしっかりしておくことで、その差を最低限に抑えられます。
技術面でのリスクヘッジ
フロントエンドとバックエンドの分離
ReactやVueなどのフロントエンドフレームワークが短い周期で進化を続けている中で、バックエンドが長期的にメンテナンスされるケースが増えています。
とはいえフロントエンドは流行り廃りが激しい、とはよく言いますが、Reactの流行はかなり長く、バックエンドと比べると短いものの、それなりの期間は続いています。
それでも、境界がしっかりしていれば、フロントエンドを別のフレームワークに乗り換えたり、モバイルアプリを追加して同じユースケース層やドメイン層を再利用できるなど、柔軟性が向上します。
SOLID原則を意識した責務分割
SOLID原則(単一責任の原則、オープン/クローズドの原則、リスコフの置換原則、インターフェイス分離の原則、依存関係逆転の原則)は、オブジェクト指向言語でコードを書く際の重要な指針です。これを実践することで、テスト容易性やコードの読みやすさが向上し、結果として「捨てる」範囲を最小限に絞ったリプレースが可能になります。
捨てやすさがもたらすアジリティ
長期運用の中では、どうしても不要になった機能や、時代遅れになった技術スタックが出てきます。
設計が悪いと、不要な機能を切り離すにも大量の依存関係を辿ってコードを修正しなければならず、その作業には大きなコストがかかります。一方、アーキテクチャの境界がはっきりしていれば、“不要な部分だけサクッと捨てる”という選択肢が生まれ、素早い意思決定を可能にします。
テストがもたらす安心感
設計の良いアプリケーションほど、テストが書きやすい構造になっています。これは、SOLID原則やクリーンアーキテクチャがドメインロジックとインフラの依存を分離しやすくしているためです。
- ユニットテスト: ビジネスロジック(ドメイン層)を小さな単位でテストしやすくなる。
- インテグレーションテスト: ユースケース層とインフラ層の統合をテストしやすくなる。
- リグレッションテスト: コード変更による既存機能の破壊(デグレ)を早期に検知できる。
SaaS開発では小さい単位の変更が頻発し、短いスプリントを積み重ねるアジャイル開発スタイルを採用することが多いです。テストの自動化と設計の分離が進んでいれば、あるモジュールの変更が他のモジュールに影響を与える可能性を早期に検知でき、リリースまでのスケジュールにも好影響を与えます。
会社員エンジニアとしての設計意識
個人開発や小規模のプロトタイプ開発では、スピード重視の設計が求められ、ドキュメントも最小限となりがちです。実際、サービスが世の中に出るかどうかが不透明な段階で、あまりにも綿密な設計を組むと、「せっかく作り込んだのに誰も使わない」というリスクが高まってしまいます。
しかし、企業でのサービス開発、特にすでにユーザーが存在するSaaSでは状況がまったく異なります。リリースした瞬間からユーザーの声が届き、障害や問題が発生すれば即座に対処を迫られるでしょう。
こうした現場に身を置くエンジニアにとって、コードの可読性・保守性・拡張性は単なる「自己満足」では終わりません。
- チーム全体で進める開発: 設計指針が共有されていなければ、開発者ごとに実装スタイルがバラバラになり、保守が困難になります。
- 複数年にわたる運用が前提: 適切なアーキテクチャが組まれていなければ、新人の配属やメンテナンス担当の交代時など、属人化が原因のトラブルが頻発する可能性が高いです。
- ビジネス要件の迅速な反映: 設計が整っていれば、仕様変更の影響範囲が限定的になり、実装に踏み切りやすくなります。
まとめ
本記事では、アプリケーションレイヤのエンジニアが設計を意識することで得られるメリットを、特に自社SaaSを販売・運用する企業にフォーカスして考察しました。
また、日本で一般的に見られる受託開発やSESなどの「作って終わり」型のシステム開発と違い、ユーザーが使い続けることが前提となるアプリケーションを想定している点を強調しました。
DBベースの設計からアプリケーション層中心の設計へシフトすることで、ビジネスロジックが明確になり、将来的な変更や拡張に強いアプリケーションを作り上げることができます。
- ドメイン駆動設計やクリーンアーキテクチャの流行背景
単なる流行ではなく、長期的な保守運用とビジネス価値を考えたときの必然的な手法。 - DB設計とアプリケーション設計の切り離し
DBはあくまでもデータの永続化手段。ビジネスロジックを表現するためには、アプリケーションレイヤでのモデル化が重要。 - 設計の本質的な目的
アプリケーション開発・運用期間全体で、リターンに対してコストを最小にするための投資であり、売れる・売れないを含めたビジネスリスクをコントロールする手段でもある。 - テストしやすさと変更しやすさ
良い設計はテストを書きやすくし、結果的に変更を恐れない文化を育む。SaaSに求められる高頻度な改善サイクルにマッチする。 - ユーザーの継続利用を前提とした考え方
作って終わりではなく、リリース後からが本番。継続的な保守と運用こそが、企業の収益とユーザー体験に直接繋がる。
これから自社SaaSで働く方やには、まずは小さなプロジェクトでも「どのようにアプリケーションを分割するか」「ドメインをどのように表現するか」など、設計に関する問いを常に意識してみてほしいと思います。 いきなり完璧な設計を目指す必要はありませんが、考え抜かれた構造のメリットを肌で感じ、必要に応じてドキュメントやコードを改善していく姿勢こそが、これからのエンジニアに求められる重要な資質だと考えます。
エンジニアの言語化能力と知識獲得のプロセスの考察
はじめに
OpenAIがChatGPTをリリースし、12月にはProプランがリリースされ、ますますエンジニアとしての価値が問われる時代になってきたなあと感じています。
今回の記事はポエム的な要素も含まれています。
そんな中で、職業エンジニアが価値を出し続けるためには、なんとなくの理解ではなく、根本を理解することやその理解から仕組みを作れることが求められるようになりました。
また、ChatGPTのようなLLMを使いこなして生産性を上げるためにも、自分の作りたいもの、知りたい知識に近付くための言語化能力が求められるようになってきています。
この記事では、エンジニアの言語化能力および知識獲得のプロセスについて考察してみます。
テキストにおけるインプットとアウトプットの両軸で考えてみます。
知識の獲得
知識獲得の手段
WEBエンジニアという職業はやや特殊で、専門性のほとんどの知識はWEB上に公開されています。
それにもかかわらず、エンジニアという職業の平均年収が全職種の平均年収よりも高かったり、エンジニアを目指す人のためのプログラミングスクールが増えたりしています。
このことから、公開されている情報の中から求めている情報に辿り着く能力、および情報を自分の知識として定着させる能力は知識そのものよりも価値あるものだと言えそうです。
WEB上に公開されているプログラミングに関する知識には、以下のようなものがあります。
- Google Scholarなどの学術論文
- RFC(Request for Comments)、MDNなどの技術の標準化のための文書
- 特定のライブラリ、および言語のソースコード
- 特定のライブラリ、および言語の公式ドキュメント
- 企業が公開している技術ブログ
- Qiita、Zennなどの技術系の知識共有サービス
- teratail、Stack Overflowなどの技術系のQ&Aサービス
- 個人ブログ
この中から信頼できる情報をどのレベルまで選択し、理解できるかが知識の獲得に大きく影響します。
特にChatGPTのようなLLMを利用する場合、その回答を精査することができない状態だと成果物の品質や生産性が大きく低下します。
今後改善されるかもしれませんが、2024年時点ではLLMの回答をそのまま信じることはできません。
「生成AIは能力の高いエンジニアの能力をさらに伸ばし、そうでない場合はさほど変わらないか、かえって混乱を招き、格差を広げる」という現象が起こるので、最低限信頼できる情報源にたどり着き、内容を理解する能力が求められるようになっています。
なぜエンジニアはテキストでの知識共有が重要なのか
WEBエンジニアリング、というよりプログラミングにおけるテキストでの知識が重要な理由はプログラミングそのものがテキストであり、かつ再現性が高いからだと思います。
例えば営業など毎回相手によって結果が異なるような内容の場合は、共通項を見つけていくことはできるものの、結果の再現性が保証されるものではありません。
プログラミングでは、個人のPC環境など違いはあるものの、基本的には同じコードを書けば同じ結果が得られるので、テキストでの知識が重要になります。
また、プログラミングにおいては内容が無限に複雑化していったり、技術の詳細はどんどん細分化していくので、もはや全てを理解することは不可能になっており、個人の経験や知識は貴重な資産となります。ドキュメンテーションツールやナレッジ共有に価値があるのもこのためです。
言語化し、個人の知見をドキュメントとして残し、チームやコミュニティ全体で共有することで新人のオンボーディングがスムーズになったり、エンジニアの離職や異動による知識の損失を抑えることができます。
エンジニアは比較的流動性が高い職業なので、知見の共有によるメリットは大きいです。テキストにおけるコミュニケーションの最大のメリットは、その場に人がいなくても、いつでも、どこでも、誰でも知識を共有できることです。
言語化能力
言語化による自己理解と問題解決
言語化は他者への説明だけでなく、自己理解を深めるための重要な手段でもあります。
エンジニアリングの世界では、「ラバーダッキング」という手法が知られるほど有名な方法です。ラバーダッキングとは、問題解決のためにラバーダックに向かって抱えている問題を説明することで、自分の抱えている問題を理解できる、というものです。
人に相談する際に、話しているうちに解決策が思い浮かぶことはないでしょうか?それこそがラバーダッキングの効果です。
エンジニアが悩む問題の多くは言語化せず脳内で解決しようとすることが原因であることが多く、例えばプログラムが難しくて読めない、というケースはコードの中の難しい部分が脳内のメモリを超えてしまっていることが大半です。1行であれば理解できるのに複数行のコードを理解できない、という場合はこのケースです。
効果的な教育
エンジニアリングチームの大半はチーム内に経験豊富なエンジニアが1人以上いることが多く、新人や後輩を指導するケースがほとんどです。
この場合も先述した知見の共有が非常に効果的で、かつ同じチームなので具体例や経験談、既存のコードを例に挙げることで、新人や後輩に理解を深めることができます。
非言語化部分を考える
言語化の有効性を考察してきたものの、言語化だけでは伝わらない部分もあります。
UI/UX
ユーザーインターフェースやユーザーエクスペリエンスは言語化だけでは伝わらない部分が多いです。例えば、ボタンの配置や色、アニメーションなどは言語化だけでは伝わりにくい部分です。
これらの感覚は、その人がこれまで触れてきたものなどの経験に基づく部分が大きいため、言語化だけでは伝わりにくい部分が少なからず存在します。
エンジニアの仕事はこれらの部分を考慮しながら、ユーザーに価値を提供することが求められるため、言語化だけではなく、非言語化部分も重要な要素となる、ということを尊重することが重要です。
経験則と身体知
例えばコードレビューの際、明確な言語化より先に指摘のコメントをしたことはないでしょうか?
これらは長年の経験から得られる経験則や普段触れているコードとの差分による違和感などが影響していると思います。
「このコードの書き方はメンテナンスがしやすい」「このアーキテクチャは拡張性が高い」といった内容をエンジニアは日頃から考えていますが、これらを常に意識しているわけではありません。
一度言語化し、自分の中で理解し、感覚として身につけることで、普段のコーディングに活かしていることが多いと思います。
なので、質問されるともちろん過去一度言語化しているので回答することはできるが、常に勉強してきた内容すべてを意識しているわけではない、ということが言えると思います。
考えるより慣れろ、の進化版とも言えるかもしれません。
言語化によって達成できる限界
個人的に好きな曲の一つにポルノグラフィティの「パレット」という曲があります。
歌詞の一節を紹介します。
月は決して泣いていないし 鳥は唄を忘れてはいない 変わらずそこにあるものを歪めて見るのは失礼だ だって知っている言葉はほんのちょっとで 感じれることは それよりも多くて 無理やり 窮屈な服 着せてるみたい
言語化に関する部分としては下記の二つです。
- 存在している事象に対する感じ方は人によって違うので、解釈と事実は異なる
- 「言葉」と実際の事象の間にはギャップがあり、すべてを表現することはできない
言語化の難しさ
テキストコミュニケーションにおける難しさの最大の要因は、コミュニケーションの相手を書き手が選べないことです。
テキストでの知識共有はその場にいない、1人以上の相手に向けて行われることが多く、相手の理解度や知識レベルに合わせた説明が難しく、理解が同期的に進むわけではないので、都度発生する疑問や誤解を解消するためのコミュニケーションを取ることができません。
また、同じ単語について同じ理解をしていることを前提にコミュニケーションが行われるため、誤解が生じる可能性が高くなります。
とは言え、同期的にコミュニケーションを取れば言語化は完璧かというとそうではないと思っています。次に、伝言ゲームの例を挙げて考察してみます。
伝言ゲームから学ぶ言語化の限界
伝言ゲームは、情報が伝達される過程で内容が変化していくゲームです。このゲームが成立する背景には、言語化の幅広さと多様な解釈が可能であることが関与しています。
WEBエンジニアリングにおいても、情報の伝達にはこのような言語化の幅広さが存在し、それがコミュニケーションの柔軟性と創造的な発想を促進する役割を果たします。
言葉には多様な解釈が存在し、同じ言葉でも人によって意味が異なることがあります。伝言ゲームにおいては、この言語化の幅広さが情報の歪みを引き起こす原因となります。 WEBエンジニアリングでも、仕様や要件を伝える際に、言葉の選び方や表現方法によって解釈が異なることがあり、誤解が生じやすくなります。
これを防ぐためには、言語化の際に明確さと具体性を重視し、必要に応じて補足説明や確認を行うことが重要です。また、曖昧な表現を避け、共通の理解を持つための努力が求められます。同じ言葉でも文脈によって意味の異なる単語がプログラミングにおいては多数存在します。(ex. Modelなど)
伝言ゲームの例からもわかる通り、コミュニケーションにおいては100のものを100のまま相手に伝えることは不可能です。
100のものをできるだけそのまま相手に伝えるためには、次のようなアプローチが必要だと思います。
コミュニケーションを円滑にするためのアプローチ
はじめに、基本は言語化で共有基盤を築くことが重要だと思います。
言語化の限界も認識しつつ、それでもテキストコミュニケーションのメリットは大きいため、大部分はテキストで共有していくことが効果的です。
次に、非言語部分をカバーすることを尊重することです。言語化ではカバーできない部分はペアプログラミングやハンズオンなどで補完することが重要で、そこに対する投資を行う必要があります。
また、非言語部分のカバーとして、言語化能力を活用できる例として、メタファーや例え話を使うというものがあります。
コミュニケーションの相手が特定される場合に限りますが、目の前の事象をそのまま理解できていない相手に対して、相手の知っている事象との類似性を説明することで理解を深めることができます。
まとめ
2024年末の今、Rustに入門してみる
はじめに
今回はRustへの入門記事です。
2024年の抱負として、技術書を読んだり、個人開発を習慣化したりといったことを目標にしていました。
実際、今年は技術書を読んだり、個人開発をしてはいるんですが、習慣化という点ではまだまだです。
これらをやりたい背景として、自分コンフォートゾーンをガンガン抜け出したいんだなと気づいたので、以前から気になっていたRustに挑戦してみることにしました。
個人的にRustに興味がある理由としては、以下の点が挙げられます。
- PHP、TypeScriptを中心に業務をしているので、CLIツールを作るのが少し面倒
- 純粋な静的型付け言語に興味がある
- 言語設計の良さを聞く機会が多いので、設計面での学びがあるかもしれない
- バイナリとして実行されるのでコンテナ技術との相性が良さそう
ほかにもGo言語も候補にあがっていましたが、現時点での評価としては簡単に学習できつつも設計の悪さを感じる記事を多く目にしました。簡単に学習できること自体はPHP等も同じだと思っているので、優先順位は設計などを学べそうだったり、最終的な安全性が高いRustが上でした。Go自体もあまり触ったことがないので、将来的には触ってみたいと思っています。
また、Rustは私がWEBエンジニアになった2022年から注目されている言語で、その後も注目度が高まっている印象があります。
ただ、当時は学習コストが高い印象があり、軽く触れてはいたものの挫折していました。
2024年時点で、JetBrainsがRustのIDEであるRustRoverをリリースしたり、Rust関連の書籍が増えたりと、学習環境が整ってきている印象があります。
また、ChatGPTなどプログラミング学習に使えるAI等周辺環境も良くなりつつあるので、再度挑戦してみることにしました。
業務で使いたいなと思うユースケースとしては、下記のようなものがあります。
- LambdaのカスタムランタイムでRustを使い、高速な処理を実現する
- コストに直結するので、高速な処理ができると嬉しい
- CLIツールを作成する
- PHPやTypeScriptで作成するのがやや大変
- 基盤周りなど、コード量が多くなくても、アクセス数が多かったり、コンテナ技術との相性が求められるもの
参考資料
書籍も購入しました
基礎知識
Rustとは
Rustは、Mozillaが開発したシステムプログラミング言語です。
現在はRust Foundationが開発をしているOSSです。
このRust Foundationは、AWS、Google、HUAWEI、Microsoft、Mozilla等によって設立された非営利団体です。
システムプログラミング言語が何かわからなかったんですが、他の言語との区別として、下記のようなポイントがあるようです。
- システムプログラミング言語はシステムプログラミングのための言語で、CやC++が有名
- システムプログラミングとは、OSやデバイスドライバ、組み込みシステムなどの低レベルなプログラミングを指す
- アプリケーション向けの言語との違いは、物理的なハードウェアへのより直接的なアクセス手段を提供していること
ざっくりよく知られている特徴をあげておきます。
- メモリ安全性を保証する
- Rustは所有権システム(Ownership)という仕組みがあり、メモリ管理のエラーをコンパイル時点で検出できる
- 高パフォーマンス
- C、C++に匹敵するパフォーマンスを持つ
- 並行性のサポート
- 安全なスレッド間の共有を保証する
チュートリアル
The Rust Bookにチュートリアルがあるので、ざっくり必要な部分だけを抜粋してみます。
インストール
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
Cargoを使ったプロジェクトの作成
Heloo, World!自体はもっとシンプルにできるんですが、Rustを使う場合は基本的に公式ビルドシステムであるCargoを使います。
cargo new [プロジェクト名] cd [プロジェクト名] cargo run
セットアップした時点でHello, World!プログラムが作成されています。
cargo runでコンパイルと実行がされるので、Hello, World!と表示されるはずです。
また、セットアップした時点でCargo.tomlというファイルが作成されており、これがnpmでいうpackage.jsonのように依存関係の管理やプロジェクトの設定を行うことができるものです。
全体的な雰囲気を掴みたい場合はThe Rust Bookの数当てゲームのプログラミングの章が良さそうです。
書き方でおさえておくところ
変数
変数はデフォルトでイミュータブルになります。
再代入ができないので、他の言語とはやや扱いが異なります。
Rustの文脈では代入ではなく「束縛」という言葉を使うことが多いようです。(bindの訳かな)
let x = 0; x = 1; // error[E0384]: cannot assign twice to immutable variable `x`
イミュータブルな変数を作成する場合は、明示的にmutキーワードを使います。
let mut x = 0; x = 1; // OK
データ型
Rustにおける値は全てデータ型を持っています。
まずはスカラー型。C言語と同じようにbit数によって分かれています。
スカラー型
論理値型
- true
- false
文字型
- char型
- シングルクォートで囲む
- Unicodeスカラー値を表す
- 文字列型ではなく、文字型
- String型は言語ではなく、標準ライブラリで提供される。
- 結構ややこしい概念なので、The Rust Bookを参照
- char型
複合型
関数
関数はfnキーワードで定義します。
fn another_function(x: i32) { println!("The value of x is: {}", x); // xの値は{}です }
仮引数には型を指定する必要があります。
また、関数はそれを呼び出したコードに値を返すことができます。
fn five() -> i32 { 5 }
関数本体ブロックの最後の式の値がその関数の戻り値になります。returnキーワードは早期リターンする場合に使います。
関数の最後の式が戻り値になるので、セミコロンをつけてしまうと文になり、エラーになります
さいごに
Rustの基礎をざっくりと抑えてみました。
今回の内容はまだ言語の基礎的な部分でRustらしい部分は出てきていませんが、学習を進めながらまた記事にしていきたいと思います。
データベースのマイグレーション時にインデックスを削除する場合に注意すべき外部キー制約とインデックスの依存関係
はじめに
今回は業務のなかで少しつまづいたポイントについて自分の備忘録としてまとめたいと思います。
MySQLにおける外部キー制約作成時の既存インデックスの利用についてです。
対象システム
- MySQL 8.0
- Laravel 11
TL;DR
- 特定のインデックスを削除する場合、特定の外部キー制約がインデックスを必要としている場合にエラーになる
- 外部キー制約の作成時には自動的にインデックスが作成される
- 外部キー制約の作成時に、既存のインデックスが存在する場合はインデックスの作成をせず、既存のインデックスを利用する。条件は以下の通り
- 外部キーとして利用するカラムが、既存のインデックスの1つ目のカラムと一致している場合
- これは複合インデックスの場合でも最初のカラムであれば利用される
- 外部キーとして利用するカラムが、既存のインデックスの1つ目のカラムと一致している場合
背景
担当しているプロジェクトにおいて、開発した機能をステージング環境にデプロイする際に、マイグレーションファイルを実行するとエラーが発生しました。
SQLSTATE[HY000]: General error: 1553 Cannot drop index 'unique_index_name': needed in a foreign key constraint (Connection: mysql, SQL: alter table `table_name` drop index `unique_index_name`)
このエラー自体は、特定のインデックスを削除しようとした際にそのインデックスが外部キー制約によって必要とされているために発生します。
つまり、ステージング環境には複合外部キー制約Aと複合外部キー制約B、ユニークインデックスBが存在していました。
この複合外部キー制約BとユニークインデックスBは同時に作られたものです。
複合外部キー制約Bの作成時には自動的にユニークインデックスBが作成されていました。
スキーマを確認し、複合外部キー制約Bを消してからユニークインデックスBを削除する必要があると判断しました。
しかし、実際には外部キー制約Bが存在しない状態で、外部キー制約Aのみが存在する状態でユニークインデックスBを削除しようとするとエラーが発生しました。
外部キー制約AもユニークインデックスBに依存していることが原因でした。
仕様
試行錯誤の結果たどり着いた仮説でしたが、MySQLにおいては外部キー制約の作成時に既存のインデックスを利用する仕様がありました。
MySQLのInnoDBストレージエンジンにおいて、外部キー制約を設定する際に、参照元テーブルのカラムに対してインデックスが必要です。
このインデックスは自動的に作成されますが、特定の条件下では既存のインデックスを利用することがあります。
今回のケースではこの仕様を正しく理解できておらず、一見無関係に見える外部キー制約Aを考慮していないことが原因でした。
特に、複合ユニークインデックスの場合に気付きにくい仕様です。
複合ユニークインデックスとは
複合ユニークインデックスとは、複数のカラムに対してユニーク制約を持たせるインデックスのことです。
複数のカラムに対してユニークな組み合わせを保証します。例えば、(A, B)というインデックスはカラムAとカラムBの組み合わせがユニークであることを保証します。
MySQLが既存の複合ユニークインデックスを外部キー制約に再利用するための条件は次のようになります。
- 外部キー列がインデックスの先頭列であること
- 外部キーとして指定する列が、既存のインデックスの先頭(最も左側)に位置している必要があります。つまり、外部キー列がインデックスの最初のカラムである場合、そのインデックスを外部キー制約に利用できます。
公式ドキュメント内の記述は以下の通りです。
MySQL では、外部キーチェックを高速に実行でき、かつテーブルスキャンが必要なくなるように、外部キーおよび参照されるキーに関するインデックスが必要です。 参照しているテーブルには、外部キーカラムが同じ順序で最初のカラムとしてリストされているインデックスが存在する必要があります。 このようなインデックスが存在しない場合は、参照しているテーブル上に自動的に作成されます。 外部キー制約の施行に使用できる別のインデックスを作成した場合、このインデックスは後で暗黙的に削除される可能性があります。index_name が指定されている場合は、前述のように使用されます。
例: 外部キー列が既存の複合インデックスの先頭にある場合
CREATE TABLE parent_table ( id BIGINT UNSIGNED PRIMARY KEY, name VARCHAR(255) ); CREATE TABLE child_table ( child_id BIGINT UNSIGNED PRIMARY KEY, parent_id BIGINT UNSIGNED, other_column VARCHAR(255), UNIQUE KEY unique_parent_other (parent_id, other_column), FOREIGN KEY (parent_id) REFERENCES parent_table(id) );
この場合、child_table の parent_id 列は既に (parent_id, other_column) という複合ユニークインデックスの先頭に位置しています。したがって、MySQLはこの既存のインデックスを外部キー制約のために再利用します。新たなインデックスを作成する必要はありません。
一見関係のないカラムの外部キー制約を削除しなければならない理由
今回の事象で発生した「一見関係のないカラムの外部キー制約を削除しなければならない」という問題は、インデックスの依存関係と外部キー制約の共有によるものです。具体的には、以下のような状況が考えられます:
複合インデックスの共有利用
複数の外部キー制約が同じ複合インデックスを共有している場合、インデックスを削除する前に、すべての外部キー制約を削除する必要があります。これは、インデックスが依然として外部キー制約によって使用されているためです。インデックスの再利用による依存関係
外部キー制約が複数の列に対して設定されている場合、これらの制約が同じインデックスを利用している可能性があります。特に、外部キー制約が複数の列を対象としている場合、それぞれの制約が同じ複合インデックスを使用することがあります。
まとめ
データベースマイグレーションにおいて、外部キー制約とインデックスの依存関係の理解は非常に重要です。 今回のように複合ユニークインデックスを使用する場合、外部キー制約がインデックスの先頭列に依存しているかどうかを確認する必要があります。
- 外部キー制約を設定する前に、外部キー列がインデックスの先頭に位置している場合は既存のインデックスに依存する
- インデックスを削除する際は、そのインデックスに依存するすべての外部キー制約を事前に削除する。