なぜ常識がパターンよりも重要であり、アクティブレコードはそれほど悪くないのですか?

開発者、特に若い開発者はパターンを愛し、どのパターンをここかそこに適用すべきかについて議論するのが好きなのはたまたまです。しわがれのポイントに議論するために:これはファサードまたはプロキシであり、おそらくシングルトンです。そして、あなたのアーキテクチャがきれいで六角形でない場合、何人かの開発者は聖なる審問の危機に瀕する準備ができています。



そうすることで、彼らはパターンが唯一の可能な解決策であることを忘れています。他の原則と同様に、パターンには適用の限界があり、それらを理解することが重要です。地獄への道は、権威ある言葉でさえ盲目的で宗教的な固執で舗装されています。



また、フレームワークに必要なパターンが存在するからといって、それらが正しく意識的に適用されることを保証するものではありません。







アクティブレコードのきらめきと貧困



アクティブレコードパターンをアンチパターンとして見てみましょう。これは、一部のプログラミング言語とフレームワークがあらゆる方法で回避しようとします。



Active Recordの本質は単純です。つまり、ビジネスロジックをエンティティストレージロジックとともに保存します。言い換えると、非常に簡単に言えば、データベース内の各テーブルは、動作とともにエンティティクラスに対応します。





1つのクラスでビジネスロジックとストレージロジックを組み合わせるのは非常に悪い、使用できないパターンであるというかなり強い意見があります。それは単独の責任の原則に違反します。そしてこの理由で、DjangoORMは設計上悪いです。



実際、ストレージロジックとドメインロジックを同じクラスに組み合わせるのはあまり良くないかもしれません。


たとえば、ユーザーモデルとプロファイルモデルを見てみましょう。これはかなり一般的なパターンです。メインプレートがあり、追加のプレートがあり、必ずしも必須ではありませんが、必要なデータを格納する場合があります。





「user」ドメインのエンティティが2つのテーブルに格納され、コードに2つのクラスがあることがわかりました。また、で直接修正を行うたびにuser.profile、これは別のモデルであり、変更を加えたことを覚えておく必要があります。そして、それを別々に保存します。



   def create(self, validated_data):
        # create user 
        user = User.objects.create(
            url = validated_data['url'],
            email = validated_data['email'],
            # etc ...
        )

        profile_data = validated_data.pop('profile')
        # create profile
        profile = Profile.objects.create(
            user = user
            first_name = profile_data['first_name'],
            last_name = profile_data['last_name'],
            # etc...
        )

        return user


ユーザーのリストを取得するにはprofile、結合のある2つの記号をすぐに選択SELECT N+1し、ループに入らないようにするために、これらのユーザーから属性を取得するかどうかを検討する必要があります。



user = User.objects.get(email='example@examplemail.com')
user.userprofile.company_name
user.userprofile.country


マイクロサービスアーキテクチャ内で、ユーザーデータの一部が別のサービス(たとえば、LDAPの役割と権限)に格納されている場合、事態はさらに悪化します。



同時に、もちろん、APIの外部ユーザーにどういうわけかこれを気にかけてほしくないのです。RESTリソースがありますが、/users/{user_id}データがどのように格納されているかを考えずに操作したいと思います。それらが異なるソースに保存されている場合、ユーザーを変更したり、データのリストを取得したりすることはより困難になります。



一般的に言えば、ORM!=ドメインモデル!





また、「データベース内の1つのテーブル-ドメインの1つのエンティティ」という仮定と現実の世界が異なるほど、アクティブレコードパターンの問題が多くなります。


ビジネスロジックを作成するたびに、ドメインの本質がどのように保存されているかを覚えておく必要があります。



ORMメソッドは、最も低いレベルの抽象化です。それらは主題領域の制限をサポートしていません。つまり、間違いを犯す機会を与えてくれます。また、データベースで実際に行われるクエリをユーザーから隠すため、クエリが非効率的で長くなります。クエリが結合やフィルターではなくループで行われる古典的な方法。



そして、クエリ構築(クエリを構築する機能)以外に、ORMは私たちに何を提供しますか?気にしないで。新しいデータベースに移動する機能はありますか?そして、彼らの正しい心としっかりした記憶の中で誰が新しいデータベースに移動し、ORMはこれで彼を助けましたか?ドメインモデル(!)をデータベースにマップする試みとしてではなく、便利な方法でデータベースにクエリを実行できる単純なライブラリとして認識している場合は、すべてが適切に機能します。



そして、それがクラスModelの名前やファイルの名前で使用されているという事実にもかかわらずmodels、それらはモデルにはなりません。自分をだましてはいけません。これはラベルの説明にすぎません。それらは何もカプセル化するのに役立ちません。



しかし、すべてがとても悪い場合は、どうすればよいですか?階層化されたアーキテクチャのパターンが役に立ちます。



階層化されたアーキテクチャが反撃します!



階層化アーキテクチャの背後にある考え方は単純です。ビジネスロジック、ストレージロジック、および使用ロジックを分離します。



ストレージを状態の変化から分離することは完全に論理的であるように思われます。それら。 「抽象」ストレージからデータを受信および保存できる別のレイヤーを作成します。



たとえば、すべてのストレージロジックをストレージクラスに残しますRepository。また、コントローラー(またはサービスレイヤー)は、エンティティの取得と保存にのみ使用します。そうすれば、保存と受信のロジックを好きなように変更できます。これが1つの場所になります。また、クライアントコードを作成するときは、保存する必要のある場所や取得する必要のある場所をもう1つ忘れていないことを確認できます。また、同じコードを何度も繰り返すこともありません。





エンティティが異なるテーブルまたはマイクロサービスのレコードで構成されているかどうかは関係ありません。または、タイプによって動作が異なるエンティティが1つのテーブルに格納されている場合。



しかし、この責任の分担は自由ではありません「悪い」コード変更を防ぐために、追加の抽象化レイヤーが作成されることを理解する必要があります。明らかに、RepositoryオブジェクトがSQLデータベースに格納されているという事実を隠しているため、SQLismが範囲外にならないようにする必要がありますRepositoryそして、すべてのリクエストは、最も単純で明白なものであっても、ストレージレイヤーを介してドラッグする必要があります。



たとえば、名前と部門でオフィスを取得する必要が生じた場合は、次のように記述する必要があります。



#     
interface OfficeRepository: CrudRepository<OfficeEntity, Long> {
    @Query("select o from OfficeEntity o " +
            "where o.number = :office and o.branch.number = :branch")
    fun getOffice(@Param("branch") branch: String,
                  @Param("office") office: String): OfficeEntity?
 ...


そして、アクティブレコードの場合、すべてがはるかに簡単です。



Office.objects.get(name=’Name’, branch=’Branch’)


事業体が実際に重要な方法で(複数のテーブル、異なるサービスなどに)格納されている場合でも、それはそれほど単純ではありません。このパターンが作成されたこの適切な(そして正しく)実装するには、ほとんどの場合、集計、作業単位、データマッパーなどのパターンを使用する必要があります。



集計を正しく選択し、それに課せられたすべての制限を正しく遵守し、データマッピングを正しく行うことは困難です。そして、非常に優れた開発者だけがこのタスクに対処できます。 Active Recordの場合、すべてを「正しく」実行できるもの。



通常の開発者はどうなりますか?すべてのパターンを知っていて、階層化されたアーキテクチャを使用すると、Active Recordとは異なり、コードが自動的に保守可能で優れたものになると確信している人。また、テーブルごとにCRUDリポジトリを作成します。そして、それらは



1つのプレート-1つのリポジトリ-1つのエンティティの概念で機能します。



ない:



1つのリポジトリ-1つのドメインオブジェクト。





彼らはまた、単語がクラスEntity使用されている場合、それはドメインモデルを反映していると盲目的に信じています。Modelアクティブレコードの単語のように



その結果、アクティブレコードとリポジトリ/データマッパーの両方のすべてのネガティブなプロパティを持つ、より複雑で柔軟性の低いストレージレイヤーになります。


しかし、階層化されたアーキテクチャはそれだけではありません。通常、サービス層も区別されます。



このようなサービスレイヤーを正しく実装することも難しい作業です。そして、たとえば、経験の浅い開発者は、サービスであるサービス層を作成します。これは、リポジトリまたはORM(DAO)のプロキシです。それら。サービスは、実際にビジネスロジックをカプセル化しないように作成されています。



#      
@Service
class AccountServiceImpl(val accountDaoService: AccountDaoService) : AccountService {
    override fun saveAccount(account: Account) =
            accountDaoService.saveAccount(convertClass(account, AccountEntity::class.java))

    override fun deleteAccount(id: Long) =
            accountDaoService.deleteAccount(id)


また、アクティブレコード層とサービス層の両方の欠点が組み合わされています。



その結果、若くて経験の浅いパターン愛好家によって書かれた階層化されたJavaフレームワークとコードでは、ビジネスロジックの単位あたりの抽象化の数がすべての合理的な制限を超え始めます。





レイヤーはありますが、それらはすべて些細なものであり、次のレイヤーを呼び出すためのレイヤーにすぎません。



フレームワークにOOPパターンが存在するからといって、それらが正しく適切に適用されるとは限りません。



特効薬はありません



特効薬がないことは明らかです。複雑な解決策は複雑な問題のためのものであり、単純な解決策は単純な問題のためのものです。



そして、悪いパターンと良いパターンはありません。ある状況では、Active Recordが優れており、他の状況では、階層化されたアーキテクチャが優れています。そして、はい、中小規模のアプリケーションの大部分では、ActiveRecordかなりうまく機能します。また、中小規模のアプリケーションの大部分では、階層化されたアーキテクチャ(Spring)のパフォーマンスが低下します。そして、ロジックが豊富な複雑なアプリケーションやWebサービスの場合は正反対です。



アプリケーションまたはサービスが単純であるほど、必要な抽象化の層は少なくなります。



ビジネスロジックがあまりないマイクロサービス内では、階層化されたアーキテクチャを使用しても意味がないことがよくあります。通常のトランザクションスクリプト(コントローラー内のスクリプト)は、目前のタスクに完全に適している場合があります。



実際、良い開発者は悪い開発者とは異なり、パターンを知っているだけでなく、いつ適用するかも理解しています。



All Articles