04.モデル-stitcher.io

in Vlog

(jp) =

<!–

–>

前の章では、すべてのアプリケーションの 3 つのコア ビルディング ブロックのうちの 2 つ、つまり DTO とアクション、つまりデータと機能について説明しました。 この章では、私がこのコアの一部と考える最後の部分を見ていきます。それは、データ ストアに保持されているデータを公開することです。 言い換えれば、モデル。

さて、モデルはトリッキーなテーマです。 Laravel は Eloquent モデル クラスを介して多くの機能を提供します。つまり、Laravel はデータ ストア内のデータを表すだけでなく、クエリの作成、データの読み込みと保存、イベント システムの組み込みなども可能にします。

この章では、Laravel が提供するすべてのモデル機能を捨てるように言うつもりはありません — 確かに非常に便利です。 ただし、注意が必要ないくつかの落とし穴とその解決策を挙げます。 大規模なプロジェクトであっても、モデルがメンテナシップを困難にする原因にはなりません。

私の見解は、フレームワークと戦おうとするのではなく、フレームワークを受け入れるべきだということです。 ただし、大規模なプロジェクトが保守可能であるような方法でそれを受け入れる必要があります。 飛び込みましょう。

# モデル ≠ ビジネスロジック

多くの開発者が陥る最初の落とし穴は、モデルを次のように考えていることです。 ビジネスロジックに関しては、あるべき場所。 Laravel に組み込まれているモデルの責任をいくつかリストしましたが、これ以上追加しないように注意する必要があります。

次のようなことができるのは、最初は非常に魅力的に聞こえます。 $invoiceLine->price_including_vat また $invoice->total_price; そして確かにそうです。 実際、請求書と請求書明細は したほうがいい これらの方法があります。 ただし、重要な違いが 1 つあります。これらのメソッドは、何も計算してはなりません。 何を見てみましょう いいえ すること:

ここにある total_price 請求書モデルのアクセサで、すべての請求書行をループして合計金額を計算します。

class Invoice extends Model

    public function getTotalPriceAttribute(): int
    
        return $this->invoiceLines
            ->reduce(function (int $totalPrice, InvoiceLine $invoiceLine) 
                return $totalPrice + $invoiceLine->total_price;
            , 0);
    

そして、これが1行あたりの合計価格の計算方法です。

class InvoiceLine extends Model

    public function getTotalPriceAttribute(): int
    
        $vatCalculator = app(VatCalculator::class);
    
        $price = $this->item_amount * $this->item_price;

        if ($this->price_excluding_vat) 
            $price = $vatCalculator->totalPrice(
                $price, 
                $this->vat_percentage
            );
        
    
        return $price;
    

アクションに関する前の章を読んだので、代わりに私が何をするかを推測するかもしれません: 請求書の合計金額を計算することは、アクションによって表現されるべきユーザー ストーリーです。

InvoiceInvoiceLine モデルは単純なものを持つことができます total_priceprice_including_vat ただし、最初にアクションによって計算され、次にデータベースに格納されます。 使用時 $invoice->total_price、あなたは単に以前に計算されたデータを読んでいるだけです。

このアプローチにはいくつかの利点があります。 最初の明らかな 1 つは、パフォーマンスです。データが必要なときに毎回ではなく、1 回だけ計算を行っています。 次に、計算されたデータを直接クエリできます。 3 つ目は、副作用を心配する必要がないことです。

ここで、単一の責任がクラスを小さくし、保守しやすく、テストしやすくするのにどのように役立つかについて、純粋な議論を始めることができます。 また、依存性注入がサービスの場所よりも優れていること。 しかし、私は、同意しない2つの側面があることを知っている長い理論的議論をする代わりに、明白なことを述べます.

ですから、明らかなことは、あなたがやりたいと思うかもしれないとしても $invoice->send() また $invoice->toPdf()、モデル コードはますます成長しています。 これは時間の経過とともに発生するもので、最初は大したことではないようです。 $invoice->toPdf() 実際には 1 行または 2 行のコードだけかもしれません。

ただし、経験から、これらの 1 つまたは 2 つの行が加算されます。 1 行または 2 行は問題ではありませんが、1 行または 2 行の何百倍も問題があります。 現実には、モデル クラスは時間とともに成長し、実際には非常に大きくなる可能性があります。

単一の責任と依存性注入がもたらす利点について私に同意しないとしても、これについてはほとんど異論はありません。数百行のコードを含むモデル クラスは保守可能ではありません。

つまり、モデルとその目的は、データを提供することだけだと考えてください。データが適切に計算されることを確認することに他の何かが関係していると考えてください。

# モデルの縮小

私たちの目標が、モデル クラスを適度に小さく保つこと (ファイルを開くだけでモデル クラスを理解できるほど小さくすること) である場合は、さらにいくつかのものを移動する必要があります。 理想的には、ゲッターとセッター、単純なアクセサーとミューテーター、キャストとリレーションのみを保持したいと考えています。

他の責任は他のクラスに移動する必要があります。 1 つの例はクエリ スコープです。これらを専用のクエリ ビルダー クラスに簡単に移動できます。

信じられないかもしれませんが、クエリ ビルダー クラスは、実際には Eloquent を使用する通常の方法です。 スコープは、それらの上にある単なる構文糖衣です。 クエリ ビルダー クラスは次のようになります。

namespace Domain\Invoices\QueryBuilders;

use Domain\Invoices\States\Paid;
use Illuminate\Database\Eloquent\Builder;

class InvoiceQueryBuilder extends Builder

    public function wherePaid(): self
    
        return $this->whereState('status', Paid::class);
    

次に、 newEloquentBuilder メソッドをモデルに追加し、カスタム クラスを返します。 Laravel はこれから使用します。

namespace Domain\Invoices\Models;

use Domain\Invoices\QueryBuilders\InvoiceQueryBuilder;

class Invoice extends Model 

    public function newEloquentBuilder($query): InvoiceQueryBuilder
    
        return new InvoiceQueryBuilder($query);
    

これが私がフレームワークを受け入れることで意味したことです: リポジトリ自体のような新しいパターンを導入する必要はなく、Laravel が提供するものの上に構築することができます。 少し考えてみると、フレームワークによって提供される商品を使用することと、コードが特定の場所で大きくなりすぎないようにすることとの間で、完璧なバランスをとっています。

この考え方を使用して、リレーション用のカスタム コレクション クラスを提供することもできます。 Laravel は優れたコレクション サポートを備えていますが、モデルまたはアプリケーション層のいずれかでコレクション関数の長いチェーンが発生することがよくあります。 これもまた理想的ではありません。幸運なことに、Laravel は、コレクション ロジックを専用クラスにバンドルするために必要なフックを提供してくれます。

カスタム コレクション クラスの例を次に示します。他の場所での長い関数チェーンを回避して、いくつかのメソッドを新しいメソッドに結合することは完全に可能であることに注意してください。

namespace Domain\Invoices\Collections;

use Domain\Invoices\Models\InvoiceLines;
use Illuminate\Database\Eloquent\Collection;

class InvoiceLineCollection extends Collection

    public function creditLines(): self
    
        return $this->filter(function (InvoiceLine $invoiceLine) 
            return $invoiceLine->isCreditLine();
        );
    

これは、コレクション クラスをモデルにリンクする方法です。 InvoiceLine、 この場合:

namespace Domain\Invoices\Models;

use Domain\Invoices\Collection\InvoiceLineCollection;

class InvoiceLine extends Model 

    public function newCollection(array $models = []): InvoiceLineCollection
    
        return new InvoiceLineCollection($models);
    

    public function isCreditLine(): bool
    
        return $this->price < 0.0;
    

を持っているすべてのモデル HasMany との関係 InvoiceLine、代わりにコレクション クラスを使用します。

$invoice
    ->invoiceLines
    ->creditLines()
    ->map(function (InvoiceLine $invoiceLine) 
        
    );

モデルにビジネス ロジックを提供させるのではなく、モデルをクリーンでデータ指向に保つようにしてください。 それを処理するためのより良い場所があります。

# 無の空袋

Taylor Otwell にも、このブログ シリーズに注目していただきありがとうございます。 先週 彼は尋ねた アンチパターンの Martin Fowler が書いたように、オブジェクトが空のデータの袋以上のものにならないようにする方法。

テイラーは時間をかけて Twitter でそれについて私に尋ねたので、すべての人がそれについて読むことができるこの章に私の回答を含めたほうがよいと考えました.

答え — 私の答え — は 2 つあります。 まず第一に、私はモデルを単純な古いデータが入った空のバッグとは考えていません。 アクセサー、ミューテーター、キャストを使用して、データベース内のプレーン データと開発者が使用したいデータの間に豊富なレイヤーを提供します。 この章では、他のいくつかの責任を別のクラスに移動することを主張しましたが、それは事実ですが、Laravel が提供するすべての機能のおかげで、「トリミングされた」バージョンのモデルは、単純なデータの袋よりもはるかに多くの価値を提供すると信じています。

次に、このトピックに関する Alan Kay のビジョンに言及する価値があると思います (彼は OOP という用語を思いついた人物です)。 彼自身、この講演で、パラダイムを「プロセス指向」ではなく「オブジェクト指向」と呼んだことを後悔していると述べました。 Alan は、実際にはプロセスとデータの分割の支持者であると主張しています。

その観点に同意するかどうかはあなた次第です。 私は Alan の洞察のいくつかに影響を受けたことを認めます。このブログ シリーズ全体を通して、そのことに気付くかもしれません。 前に言ったように、このシリーズをソフトウェア設計の聖杯と考えないでください。 私の目標は、現在のコードの書き方に挑戦し、問題を解決するためのより最適な方法があるかどうかを考えさせることです。

ですから、議論を続けましょう。それについてメールするか、話し合うことができます。 Twitter上で.

//platform.twitter.com/widgets.js

関連記事

前の投稿
米国で毎年発生する最大のハリケーン
次の投稿
シロアリのように見える5つの一般的なバグ(および違いを見つける方法)