
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、先日レッスンをさせていただいている生徒さんから「重複するコードを共通化する」テクニックが勉強したいという貴重な意見をいただきました。
確かに、ドキュメントではコードの書き方自体は説明していますが、「より効率的な(スリムな)」書き方ととなると、それほど多くはない印象です。
かく言う私も、(まだまだ未熟ですが)思い起こせばいろんな天才プログラマーさんたちのコードからテクニックをパクってきた歴史があるので、この意見は確かに的を射ているなと感じました。
※このあたりは、問題が出たときに自分でフレームワークの中身までチェックしたり、英語版ですが、やはりstack overflow
は勉強になりますよ
そこで
今回は、私がメインで使っている「Laravel(PHP)」「Vue」の中から、「こういう共通化ができますよ」というサンプルをまとめてみたいと思います。
ぜひ学習者さんたちのお役に立てると嬉しいです
「家では冬の寒さに耐えながら
クラフトビールを飲みます」
目次 [非表示]
共通化するメリット
そもそも「コードを共通化するとどんなメリットがあるの」という話ですが、個人的には以下の3つが大きいと考えています。
- 一度書いたコードは使い回しができる(ラクできる)
- 変更が必要になっても共通化したコードだけを変更すれば全てに適用される
- 機能ごとに本流のコードと分割するため、頭の整理がしやすい
ご注意
とは言え、共通化はやり過ぎないように気をつけています。なぜなら「えっ、これだけのために分割したの・・・」ってなると逆に混乱してしまうからです。後でコードを読む人のことを考えながらプログラムするのがヒューマンですよね。
では、ひとつずつ見ていきましょう
Laravel:ViewComposerをつかう
ViewComposer
とは、簡単に言うと「効率よくパラメータをビューに送る」機能です。
例えば、ログインユーザーのデータをビューに送る基本的なコードを見てみましょう。おそらく以下のようになると思います。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class HomeController extends Controller
{
// 省略
public function user_to_view_1(Request $request) {
$user = $request->user();
return view('user_to_view_1')->with([ //
ここでビューに送る
'user' => $user
]);
}
}
もちろんこれでもいいのですが、例えば別のメソッドでも同じことをする場合どうなるでしょうか。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class HomeController extends Controller
{
// 省略
public function user_to_view_2(Request $request) {
$user = $request->user();
return view('user_to_view_2')->with([ //
ここでも同じく送る
'user' => $user
]);
}
}
やはり、ここでも$user
をビューに送らないといけません。
もちろん、数が少ない場合はこれでも問題ありませんが、サイトの規模が大きくなってくるといちいち$user
を送るのはめんどうです。
そこで登場するのがViewComposer
です。
ViewComposer
を使えば先ほどのコードはひとつだけでOKになります。
では、実際に作ってみましょう
まず、ファイルをつくり中身を以下のようにします。
app/Providers/ViewServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
class ViewServiceProvider extends ServiceProvider
{
public function boot()
{
View::composer('*', function($view){
if(auth()->check()) {
$user = auth()->user();
$view->with('user', $user); //
ビューに $user を送る
}
});
}
}
※ コードの意味としては「もしログインしていたら、そのユーザーデータを全てのビューに送る」ということになります。
では、このViewServiceProvider
をLaravel
に登録して有効にしましょう。
config/app.php
<?php
return [
// 省略
'providers' => [
// 省略
App\Providers\ViewServiceProvider::class, //
ここを追加しました
],
これで準備は完了です。
こうすることで、いちいち$user
をビューに送るコードを省略してもビューの中で$user
にアクセスできるようになります。
public function user_to_view_1() {
return view('user_to_view_1'); //
この中で $userが 使える!
}
なお、ViewComposer
は特定のビューだけ有効にすることもできます。
例えば、「resources/views/auth」フォルダにあるビューだけ有効にするには、以下のようにします。
View::composer('auth/*', function($view){
// 省略
});
また、ひとつのビューだけで有効にする場合はこうなります。
以下は、「resources/views/user_to_view_1.blade.php」だけで有効になる例です。
View::composer('user_to_view_1', function($view){
// 省略
});
Laravel:スコープを使う
スコープとは、モデルに自分の好きな条件(Where
やOrderBy
など)を設定することができる機能です。
例えば、users
テーブルで以下のようなWHERE
句を考えてみましょう。
- 成人している(age >= 20)
- 東京都に住んでいる(prefecture_id = 13
都道府県コード)
- 今日登録した人(created_at = 今日)
これをコードで書くと以下のようになります。
$users = \App\User::where('age', '>=', 20)
->where('prefecture_id', 13)
->whereDate('created_at', today())
->get();
もちろんこのコードを1ヶ所だけで使うなら、このままで問題ありません。
しかし、もし何度もこの条件が必要になり、繰り返しコピペして使ってしまうとどうなるでしょうか。
人間のすることですから、どこかが抜けてしまうこともあるでしょうし、もし一部コード変更する必要がでたら、全てのコードを変更しないといけません。
そのため、この部分を共通化して「ラクできるコード」にしてみましょう。
スコープはモデル内にセットします。
app/User.php
class User extends Authenticatable
{
// 省略
// スコープ
public function scopeWhereNewTokyoAdults($query) {
$query->where('age', '>=', 20)
->where('prefecture_id', 13)
->whereDate('created_at', today());
}
}
すると、以下のようにたった1行でこの条件を呼び出せるので、繰り返し使う場合とてもラクですし、さらに保守管理にも有効です。
$users = \App\User::whereNewTokyoAdults()->get(); //
これだけでOK
スコープの詳しい内容については、以下のページをご覧ください。
【Laravel】スコープを使えば「うっかり」が減って「ラク」できる
Laravel:SQLを共通化する
スコープほど大掛かりではありませんが、条件分岐を使ったDBデータの取得でも「ちょっとした共通化」ができます。
例えば、今日と昨日で分岐したデータを取得する例を見てみましょう!
$when = 'today';
if($when === 'today') { //
今日登録した人
$today = today();
$users = \App\User::where('id', '<', 10)
->where('age', '>=', 20)
->where('prefecture_id', 13)
->whereDate('created_at', $today)
->get();
} else if($when === 'yesterday') { //
昨日登録した人
$yesterday = today()->subDay();
$users = \App\User::where('id', '<', 10)
->where('age', '>=', 20)
->where('prefecture_id', 13)
->whereDate('created_at', $yesterday)
->get();
}
このコードはif
で分岐をしてDBデータを取得していますが、where()
のほとんどは重複しています。
もちろんデータは正しく取得できますが、もし将来的に「prefecture_id
の部分は削除するように」と仕様変更が出たとすると、2ヵ所修正しないといけなくなります。つまり(1ヵ所だけなら心配はありませんが)このコードには「削除を忘れてしまう」リスクがあるわけです。
そこで、このコードも共通化してみましょう。
$query = \App\User::where('id', '<', 10)
->where('age', '>=', 20)
->where('prefecture_id', 13); //
まだ get() を実行しない
$when = 'today';
if($when === 'today') {
$today = today();
$query->whereDate('created_at', $today);
} else if($when === 'yesterday') {
$yesterday = today()->subDay();
$query->whereDate('created_at', $yesterday);
}
$users = $query->get(); //
ここで get() を実行する
このコードでは先ほどの重複部分をひとつにまとめていますが、重要なのは、一番最初の部分でget()
を呼び出していないことです。
そして、if文
の中でさらに条件を追加し、最終的にget()
しています。
また、when()
を使うとよりシンプルなコードでデータ取得できます。
$when = 'today';
$users = \App\User::where('id', '<', 10)
->where('age', '>=', 20)
->where('prefecture_id', 13)
->when($when, function($q, $when) {
if($when === 'today') {
$today = today();
$q->whereDate('created_at', $today);
} else if($when === 'yesterday') {
$yesterday = today()->subDay();
$q->whereDate('created_at', $yesterday);
}
})->get();
when()
は第一引数がtrue
(空白でない)なら中身が実行されるようになっています。
※ なお、コード中に出てくるパラメータが固定なら.env
やconfig
ファイルの中で一言管理しておく方がいいでしょう。
PHP:Traitをつかう
Trait
とは英語で「特徴」という意味の単語で、簡単に言うと「特定の機能だけ分割しておくもの」と考えてください。
では、Laravel
のモデルを例にしてTrait
を作ってみましょう
例えば、「本」と「ゲーム」のDBテーブルがあり、どちらもprice
(値段)のデータを持っているとします。
そして、例えばこの値段を「3桁カンマ + 円」(例:1,000円)という形式にする場合、以下のようなAccessor
を使うことが多いと思います。
app/Book.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
// Accessor
public function getPriceTextAttribute() { //
重複する部分(その1)
return number_format($this->price) .'円';
}
}
app/Game.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Game extends Model
{
// Accessor
public function getPriceTextAttribute() { //
重複する部分(その2)
return number_format($this->price) .'円';
}
}
しかし、コードを見ていただくとわかるとおりこのgetPriceTextAttribute()というメソッドは全く同じコードが重複しています。
ということで、この部分をTrait
で共通化してみましょう。
まずはPriceTrait.php
というファイルを作成します。
app/Traits/PriceTrait.php
<?php
namespace App\Traits;
trait PriceTrait {
// Accessor
public function getPriceTextAttribute() { //
共通化するメソッド
return number_format($this->price) .'円';
}
}
すると、このTrait
を以下のように呼び出すだけでBook
モデル、Game
モデルどちらからでもgetPriceTextAttribute()
を呼び出すことができるようになります。
app/Book.php
<?php
namespace App;
use App\Traits\PriceTrait; //
ここを追加しました
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
use PriceTrait; //
ここを追加しました
}
app/Game.php
<?php
namespace App;
use App\Traits\PriceTrait; //
ここを追加しました
use Illuminate\Database\Eloquent\Model;
class Game extends Model
{
use PriceTrait; //
ここを追加しました
}
つまり、本体にはAccessor
を定義していませんが、このまま以下のようにして呼び出すことができるというわけですね。
ちなみに、Trait
はPHPについている機能なのでAccessor
だけではなく、どんなものにも使うことができます。(つまり、Laravel
ならコントローラーでも使えます)
$book = \App\Book::find(1);
dump($book->price_text); //
TraitからAccessorを呼び出してます
$game = \App\Game::find(1);
dump($game->price_text); //
これも同じです
※ なお、Trait
の名前には「Notifiable」や「Billable」のように「****able(〜できる)」という形式が多く使われるようですが、今回はわかりやすくPriceTrait
にしています。
※ Trait
は、PHP 5.4
以降で使える機能です。
Vue:ミックスインをつかう
Vue
には、コードを共通化する「ミックスイン」という機能が備わっています。
では、ここでも値段を「3桁カンマ + 円」で表示するメソッドで見てみましょう
book_price.html
<html>
<body>
<div id="app">
<h1>本の値段を表示するサンプル</h1>
<span v-text="priceFormat(bookPrice)"></span> です。
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script>
new Vue({
el: '#app',
data: {
bookPrice: 1000
},
methods: {
priceFormat(price) { //
重複する部分(その1)
return price.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + // 3桁カンマにします
'円';
}
}
});
</script>
</body>
</html>
game_price.html
<html>
<body>
<div id="app">
<h1>ゲームの値段を表示するサンプル</h1>
<span v-text="priceFormat(gamePrice)"></span> です。
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script>
new Vue({
el: '#app',
data: {
gamePrice: 5000
},
methods: {
priceFormat(price) { //
重複する部分(その2)
return price.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + // 3桁カンマにします
'円';
}
}
});
</script>
</body>
</html>
こちらも見ていただくとわかるとおり、priceFormat()
というメソッドが全く同じコードで重複しています。
ということで、この部分を「ミックスイン」で共通化してスリムにしてみましょう。
まず、ミックスインのファイルを作成します。
js/vue/mixins.js
const priceMixin = {
methods: {
priceFormat(price) {
return price.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + // 3桁カンマにします
'円';
}
}
};
すると、以下のようにするだけでどこからでもpriceFormat()
にアクセスすることができるようになります。
<html>
<body>
<div id="app">
<h1>本の値段を表示するサンプル</h1>
<span v-text="priceFormat(bookPrice)"></span> です。
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<!--
ミックスインを読み込み -->
<script src="/js/vue/mixins.js"></script>
<script>
new Vue({
el: '#app',
mixins: [ priceMixin ], //
ここでセットします
data: {
bookPrice: 1000
}
});
</script>
</body>
</html>
さらに言うと、ミックスインはメソッドだけじゃなくdata
やcomputed
、mounted
など通常のVue
で使うものにも対応しています。
以下の例を見てください。
const priceMixin = {
data() {
return {
currency: 'JPY' // 通貨
}
},
methods: {
priceFormat(price) { // 3桁カンマ + 円
return price.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + // 3桁カンマにします
'円';
},
tax(price) { // 消費税がいくらか計算
return this.priceFormat(
Math.floor(price * 0.1)
);
}
},
computed: {
currencyText() { // 現在の通貨・テキスト
return `現在の通貨は${this.currency}です。`;
}
},
mounted() {
this.currency = 'USD'; // 米ドル
}
};
ミックスインの中にいろいろと追加していますが、これらは全て共通化コードとしてどこからでもアクセスができます。
そのため、Vue
本体にメソッドなどを全く書かなくても次のような使い方ができます。
<html>
<body>
<div id="app">
<h1>本の値段を表示するサンプル</h1>
<span v-text="priceFormat(bookPrice)"></span> (税:<span v-text="tax(bookPrice)"></span>)です。
<span v-text="currencyText"></span>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script src="/js/vue/mixins.js"></script>
<script>
new Vue({
el: '#app',
mixins: [ priceMixin ],
data: {
bookPrice: 1000
}
});
</script>
</body>
</html>
これを実行するとこうなります。(赤枠の部分がミックスインの中から来ています)
ご注意
なお、ミックスインのdata
は関数でreturn
することになっていますのでご注意ください。さらに、Vue 2
まではdata
をオブジェクトとしてセットできていましたが、Vue 3
からは標準でもreturn
する形になっているので、このあたりは頭の中を整理しておいた方がいいかもしれませんね
おわりに
ということで、今回はいつもとは少し違った視点の記事をお届けしてみました。
なお、いくつか思いつくままにまとめましたが、コードの共通化はこれ以外にも大小たくさんのものがあります。
例えば、JavaScript
の外部ファイル化もそうですし、コンポーネントもコード共通化のひとつです。
また、コードの共通化は「工夫」がものをいう分野だと思いますので、ぜひみなさんも「こうやったらもっと可読性があがるんじゃ」というようにパズルを解くような気持ちでやってみてはいかがでしょうか。
※ 個人的には、人気パズルゲーム「ぷよぷよ」の連鎖をよく授業中に考えてたんですが、その感覚と似てます
ではでは〜
「ぷよぷよでは、
スキヤポデスが好きでした」