575

Django でプロジェクトを書いていますが、コードの 80% がファイルにあることがわかりますmodels.py。このコードは紛らわしく、しばらくすると、実際に何が起こっているのか理解できなくなります。

これが私を悩ませているものです:

  1. 私のモデル レベル (データベースからのデータの処理のみを担当することになっていました) がメールを送信したり、他のサービスへの API を歩いたりするのは醜いと思います。
  2. また、ビューにビジネス ロジックを配置することは、制御が難しくなるため、受け入れられません。たとえば、私のアプリケーションでは、 の新しいインスタンスを作成する方法が少なくとも 3 つありますがUser、技術的にはそれらを均一に作成する必要があります。
  3. モデルのメソッドとプロパティが非決定論的になったり、副作用が発生したりする時期に、いつも気付くとは限りません。

簡単な例を次に示します。最初は、次のUserようなモデルでした。

class User(db.Models):

    def get_present_name(self):
        return self.name or 'Anonymous'

    def activate(self):
        self.status = 'activated'
        self.save()

時間の経過とともに、次のようになりました。

class User(db.Models):

    def get_present_name(self): 
        # property became non-deterministic in terms of database
        # data is taken from another service by api
        return remote_api.request_user_name(self.uid) or 'Anonymous' 

    def activate(self):
        # method now has a side effect (send message to user)
        self.status = 'activated'
        self.save()
        send_mail('Your account is activated!', '…', [self.email])

私が欲しいのは、コード内のエンティティを分離することです:

  1. データベースのエンティティ、持続性レベル: アプリケーションが保持するデータは?
  2. アプリケーションのエンティティ、ビジネス ロジック レベル: アプリケーションは何をするのか?

Django で適用できるこのようなアプローチを実装するための良い方法は何ですか?

4

10 に答える 10

740

データ モデルドメイン モデルの違いについて質問しているようです。後者はエンド ユーザーが認識するビジネス ロジックとエンティティを見つけることができる場所であり、前者は実際にデータを保存する場所です。

さらに、私はあなたの質問の 3 番目の部分を次のように解釈しました。

これらは 2 つの非常に異なる概念であり、それらを分離しておくことは常に困難です。ただし、この目的に使用できる一般的なパターンとツールがいくつかあります。

ドメイン モデルについて

最初に認識する必要があるのは、ドメイン モデルは実際にはデータに関するものではないということです。「このユーザーをアクティブにする」、「このユーザーを非アクティブにする」、「現在アクティブになっているユーザーは?」、「このユーザーの名前は?」などのアクション質問についてです。古典的な言葉で言えば、クエリコマンドについてです。

コマンドで考える

あなたの例のコマンドを見てみましょう:「このユーザーをアクティブにする」と「このユーザーを非アクティブにする」。コマンドの良いところは、与えられたときの小さなシナリオで簡単に表現できることです。

管理者がこのユーザーをアクティブ化し
たときに非アクティブなユーザーが与えられた場合、そのユーザーはアクティブになり、確認の電子メールがユーザーに送信れ、システム ログ (など)にエントリが追加されます。



このようなシナリオは、インフラストラクチャのさまざまな部分が単一のコマンドによってどのように影響を受けるかを確認するのに役立ちます。この場合、データベース (ある種の「アクティブ」フラグ)、メール サーバー、システム ログなどです。

このようなシナリオは、テスト駆動開発環境をセットアップする際にも非常に役立ちます。

そして最後に、コマンドで考えると、タスク指向のアプリケーションを作成するのに本当に役立ちます。あなたのユーザーはこれを高く評価するでしょう:-)

コマンドの表現

Django には、コマンドを表現する簡単な方法が 2 つあります。どちらも有効なオプションであり、2 つのアプローチを混在させることは珍しくありません。

サービス層

サービスモジュール@Hedde によってすでに説明されています。ここでは個別のモジュールを定義し、各コマンドは関数として表されます。

services.py

def activate_user(user_id):
    user = User.objects.get(pk=user_id)

    # set active flag
    user.active = True
    user.save()

    # mail user
    send_mail(...)

    # etc etc

フォームの使用

もう 1 つの方法は、コマンドごとに Django フォームを使用することです。密接に関連する複数の側面を組み合わせているため、私はこのアプローチを好みます。

  • コマンドの実行 (何をしますか?)
  • コマンド パラメータの検証 (これを行うことができますか?)
  • コマンドの表示 (どうすればよいですか?)

フォーム.py

class ActivateUserForm(forms.Form):

    user_id = IntegerField(widget = UsernameSelectWidget, verbose_name="Select a user to activate")
    # the username select widget is not a standard Django widget, I just made it up

    def clean_user_id(self):
        user_id = self.cleaned_data['user_id']
        if User.objects.get(pk=user_id).active:
            raise ValidationError("This user cannot be activated")
        # you can also check authorizations etc. 
        return user_id

    def execute(self):
        """
        This is not a standard method in the forms API; it is intended to replace the 
        'extract-data-from-form-in-view-and-do-stuff' pattern by a more testable pattern. 
        """
        user_id = self.cleaned_data['user_id']

        user = User.objects.get(pk=user_id)

        # set active flag
        user.active = True
        user.save()

        # mail user
        send_mail(...)

        # etc etc

クエリで考える

あなたの例にはクエリが含まれていなかったので、いくつかの便利なクエリを自由に作成しました。私は「質問」という用語を使用することを好みますが、クエリは古典的な用語です。興味深いクエリは、「このユーザーの名前は?」、「このユーザーはログインできますか?」、「非アクティブ化されたユーザーのリストを表示してください」、「非アクティブ化されたユーザーの地理的分布は?」です。

これらの質問への回答に着手する前に、常に次の質問を自問する必要があります。

  • 私のテンプレートのためだけのプレゼンテーションクエリ、および/または
  • コマンドの実行に関連付けられたビジネス ロジッククエリ、および/または
  • レポートクエリ。

プレゼンテーション クエリは、単にユーザー インターフェイスを改善するために作成されます。ビジネス ロジック クエリへの回答は、コマンドの実行に直接影響します。レポート クエリは単に分析を目的としており、時間の制約が緩くなっています。これらのカテゴリは相互に排他的ではありません。

もう 1 つの質問は、「回答を完全に制御できますか?」ということです。たとえば、(このコンテキストで) ユーザーの名前を照会する場合、外部 API に依存しているため、結果を制御することはできません。

クエリの作成

Django での最も基本的なクエリは、Manager オブジェクトの使用です。

User.objects.filter(active=True)

もちろん、これはデータが実際にデータ モデルで表現されている場合にのみ機能します。これは必ずしもそうではありません。そのような場合は、以下のオプションを検討できます。

カスタムタグとフィルター

最初の代替手段は、カスタム タグとテンプレート フィルターなど、単なるプレゼンテーション用のクエリに役立ちます。

template.html

<h1>Welcome, {{ user|friendly_name }}</h1>

template_tags.py

@register.filter
def friendly_name(user):
    return remote_api.get_cached_name(user.id)

クエリ方法

クエリが単なるプレゼンテーションではない場合は、services.pyにクエリを追加するか (それを使用している場合)、queries.pyモジュールを導入できます。

クエリ.py

def inactive_users():
    return User.objects.filter(active=False)


def users_called_publysher():
    for user in User.objects.all():
        if remote_api.get_cached_name(user.id) == "publysher":
            yield user 

プロキシ モデル

プロキシ モデルは、ビジネス ロジックとレポートのコンテキストで非常に役立ちます。基本的に、モデルの拡張サブセットを定義します。メソッドをオーバーライドすることで、Manager のベース QuerySet をオーバーライドできますManager.get_queryset()

models.py

class InactiveUserManager(models.Manager):
    def get_queryset(self):
        query_set = super(InactiveUserManager, self).get_queryset()
        return query_set.filter(active=False)

class InactiveUser(User):
    """
    >>> for user in InactiveUser.objects.all():
    …        assert user.active is False 
    """

    objects = InactiveUserManager()
    class Meta:
        proxy = True

クエリ モデル

本質的に複雑だが頻繁に実行されるクエリの場合、クエリ モデルの可能性があります。クエリ モデルは、1 つのクエリに関連するデータが別のモデルに格納される非正規化の形式です。もちろん秘訣は、非正規化モデルをプライマリ モデルと同期させておくことです。クエリ モデルは、変更が完全に管理されている場合にのみ使用できます。

models.py

class InactiveUserDistribution(models.Model):
    country = CharField(max_length=200)
    inactive_user_count = IntegerField(default=0)

最初のオプションは、コマンドでこれらのモデルを更新することです。これらのモデルが 1 つまたは 2 つのコマンドによってのみ変更される場合、これは非常に便利です。

フォーム.py

class ActivateUserForm(forms.Form):
    # see above
   
    def execute(self):
        # see above
        query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
        query_model.inactive_user_count -= 1
        query_model.save()

より良いオプションは、カスタム シグナルを使用することです。もちろん、これらのシグナルはコマンドによって発行されます。シグナルには、複数のクエリ モデルを元のモデルと同期させることができるという利点があります。さらに、Celery または同様のフレームワークを使用して、信号処理をバックグラウンド タスクにオフロードできます。

信号.py

user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])

フォーム.py

class ActivateUserForm(forms.Form):
    # see above
   
    def execute(self):
        # see above
        user_activated.send_robust(sender=self, user=user)

models.py

class InactiveUserDistribution(models.Model):
    # see above

@receiver(user_activated)
def on_user_activated(sender, **kwargs):
        user = kwargs['user']
        query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
        query_model.inactive_user_count -= 1
        query_model.save()
    

清潔に保つ

このアプローチを使用すると、コードがクリーンなままかどうかを判断するのが非常に簡単になります。次のガイドラインに従ってください。

  • 私のモデルには、データベースの状態を管理する以上のことを行うメソッドが含まれていますか? コマンドを抽出する必要があります。
  • モデルには、データベース フィールドにマップされないプロパティが含まれていますか? クエリを抽出する必要があります。
  • 私のモデルは私のデータベースではないインフラストラクチャ (メールなど) を参照していますか? コマンドを抽出する必要があります。

同じことがビューにも当てはまります (ビューはしばしば同じ問題に悩まされるため)。

  • ビューはデータベース モデルをアクティブに管理しますか? コマンドを抽出する必要があります。

参考文献

Django ドキュメント: プロキシ モデル

Django ドキュメント: シグナル

アーキテクチャ: ドメイン駆動設計

于 2012-10-12T10:59:06.263 に答える
176

私は通常、ビューとモデルの間にサービスレイヤーを実装します。これはプロジェクトのAPIのように機能し、何が起こっているのかをヘリコプターで確認できます。私はこの慣習を、Javaプロジェクト(JSF)でこの階層化手法を頻繁に使用する私の同僚から継承しました。例:

models.py

class Book:
   author = models.ForeignKey(User)
   title = models.CharField(max_length=125)

   class Meta:
       app_label = "library"

services.py

from library.models import Book

def get_books(limit=None, **filters):
    """ simple service function for retrieving books can be widely extended """
    return Book.objects.filter(**filters)[:limit]  # list[:None] will return the entire list

views.py

from library.services import get_books

class BookListView(ListView):
    """ simple view, e.g. implement a _build and _apply filters function """
    queryset = get_books()

念のために言っておきますが、私は通常、モデル、ビュー、およびサービスをモジュールレベルに移行し、プロジェクトのサイズに応じてさらに分離します。

于 2012-09-25T08:58:21.457 に答える
79

まず第一に、自分自身を繰り返さないでください

次に、オーバーエンジニアリングしないように注意してください。時にはそれは時間の無駄であり、誰かが重要なことに集中できなくなることもあります。時々pythonの禅を見直してください。

アクティブなプロジェクトを見てみましょう

  • 人数が増える = 適切に組織化する必要性が高まる
  • django リポジトリは単純な構造です。
  • pip リポジトリには、単純なディレクトリ構造があります。
  • ファブリック リポジトリも参照するのに適しています。

    • すべてのモデルを下に配置できますyourapp/models/logicalgroup.py
  • たとえばUserGroup関連するモデルは下に移動できますyourapp/models/users.py
  • 例: Poll, ... 下に行くことができQuestionますAnsweryourapp/models/polls.py
  • __all__の中に必要なものをロードしますyourapp/models/__init__.py

MVC の詳細

  • モデルはあなたのデータです
    • これには実際のデータが含まれます
    • これには、セッション / Cookie / キャッシュ / fs / インデックス データも含まれます
  • ユーザーはコントローラーを操作してモデルを操作します
    • これは、API、またはデータを保存/更新するビューである可能性があります
    • request.GETこれは/ request.POST...etcで調整できます
    • ページングフィルタリングも考えてください。
  • データはビューを更新します
    • テンプレートはデータを取得し、それに応じてフォーマットします
    • テンプレートのない API もビューの一部です。たとえばtastypie、またはpiston
    • これは、ミドルウェアも考慮する必要があります。

ミドルウェア/テンプレートタグを活用する

  • リクエストごとに何らかの作業を行う必要がある場合は、ミドルウェアが 1 つの方法です。
    • 例: タイムスタンプの追加
    • 例: ページ ヒットに関する指標の更新
    • 例: キャッシュの作成
  • オブジェクトをフォーマットするために常に繰り返されるコードのスニペットがある場合は、テンプレートタグが適しています。
    • 例: アクティブなタブ / URL パンくずリスト

モデル マネージャーを活用する

  • 作成Userは に入れることができますUserManager(models.Manager)
  • インスタンスの詳細については、models.Model.
  • の悲惨な詳細はquerysetmodels.Manager.
  • 一度に 1 つずつ作成したい場合がUserあるため、モデル自体に存在する必要があると考えるかもしれませんが、オブジェクトを作成するときは、おそらくすべての詳細を持っているわけではありません。

例:

class UserManager(models.Manager):
   def create_user(self, username, ...):
      # plain create
   def create_superuser(self, username, ...):
      # may set is_superuser field.
   def activate(self, username):
      # may use save() and send_mail()
   def activate_in_bulk(self, queryset):
      # may use queryset.update() instead of save()
      # may use send_mass_mail() instead of send_mail()

可能な限りフォームを利用する

モデルにマップするフォームがあれば、多くのボイラープレート コードを削除できます。はModelForm documentationかなり良いです。カスタマイズが多い場合は、モデル コードからフォームのコードを分離することをお勧めします (または、より高度な用途では循環インポート エラーを回避する場合もあります)。

可能な場合は管理コマンドを使用する

  • 例えばyourapp/management/commands/createsuperuser.py
  • 例えばyourapp/management/commands/activateinbulk.py

ビジネスロジックがある場合は、それを分離できます

  • django.contrib.auth db にバックエンドがあるように、 backends を使用します...など。
  • settingビジネス ロジックに を追加します (例: AUTHENTICATION_BACKENDS)
  • あなたが使用できるdjango.contrib.auth.backends.RemoteUserBackend
  • あなたが使用できるyourapp.backends.remote_api.RemoteUserBackend
  • あなたが使用できるyourapp.backends.memcached.RemoteUserBackend
  • 難しいビジネス ロジックをバックエンドに委任する
  • 入力/出力で期待を正しく設定してください。
  • ビジネスロジックの変更は、設定を変更するのと同じくらい簡単です:)

バックエンドの例:

class User(db.Models):
    def get_present_name(self): 
        # property became not deterministic in terms of database
        # data is taken from another service by api
        return remote_api.request_user_name(self.uid) or 'Anonymous' 

次のようになります。

class User(db.Models):
   def get_present_name(self):
      for backend in get_backends():
         try:
            return backend.get_present_name(self)
         except: # make pylint happy.
            pass
      return None

設計パターンの詳細

  • 設計パターンについてはすでに良い質問があります
  • 実用的なデザイン パターンに関する非常に優れたビデオ
  • django のバックエンドは、明らかに委譲デザイン パターンを使用しています。

インターフェイス境界の詳細

  • 使用したいコードは本当にモデルの一部ですか? ->yourapp.models
  • コードはビジネス ロジックの一部ですか? ->yourapp.vendor
  • コードは汎用ツール/ライブラリの一部ですか? ->yourapp.libs
  • コードはビジネス ロジック ライブラリの一部ですか? ->yourapp.libs.vendorまたはyourapp.vendor.libs
  • ここに良い例があります: コードを個別にテストできますか?
  • 分離は論理的ですか?
    • いいね :)
    • いいえ、これらの論理的な概念を個別にテストするのは難しいかもしれません。
  • 10 倍のコードを取得したら、リファクタリングが必要になると思いますか?
    • はい、だめです、いいえ、ブエノ、リファクタリングは大変な作業になる可能性があります
    • いいえ、それはただ素晴らしいです!

要するに、あなたは持つことができます

  • yourapp/core/backends.py
  • yourapp/core/models/__init__.py
  • yourapp/core/models/users.py
  • yourapp/core/models/questions.py
  • yourapp/core/backends.py
  • yourapp/core/forms.py
  • yourapp/core/handlers.py
  • yourapp/core/management/commands/__init__.py
  • yourapp/core/management/commands/closepolls.py
  • yourapp/core/management/commands/removeduplicates.py
  • yourapp/core/middleware.py
  • yourapp/core/signals.py
  • yourapp/core/templatetags/__init__.py
  • yourapp/core/templatetags/polls_extras.py
  • yourapp/core/views/__init__.py
  • yourapp/core/views/users.py
  • yourapp/core/views/questions.py
  • yourapp/core/signals.py
  • yourapp/lib/utils.py
  • yourapp/lib/textanalysis.py
  • yourapp/lib/ratings.py
  • yourapp/vendor/backends.py
  • yourapp/vendor/morebusinesslogic.py
  • yourapp/vendor/handlers.py
  • yourapp/vendor/middleware.py
  • yourapp/vendor/signals.py
  • yourapp/tests/test_polls.py
  • yourapp/tests/test_questions.py
  • yourapp/tests/test_duplicates.py
  • yourapp/tests/test_ratings.py

またはあなたを助ける他のもの; 必要なインターフェイス境界を見つけることが役立ちます。

于 2012-10-12T07:16:31.913 に答える
30

Django は、わずかに変更された種類の MVC を採用しています。Django には「コントローラー」という概念はありません。最も近いプロキシは「ビュー」です。これは、MVC ではビューが Django の「テンプレート」に似ているため、MVC 変換で混乱を招く傾向があります。

Django では、「モデル」は単なるデータベースの抽象化ではありません。いくつかの点で、MVC のコントローラーとして、Django の「ビュー」と役割を分担しています。インスタンスに関連付けられた動作全体を保持します。そのインスタンスが動作の一部として外部 API と対話する必要がある場合、それは依然としてモデル コードです。実際、モデルはデータベースと対話する必要はまったくないため、外部 API に対する対話型レイヤーとして完全に存在するモデルを持つことが考えられます。それは「モデル」のより自由な概念です。

于 2012-09-25T14:40:12.050 に答える
10

Django では、MVC 構造は Chris Pratt が言ったように、他のフレームワークで使用される古典的な MVC モデルとは異なります。これを行う主な理由は、CakePHP などの他の MVC フレームワークで発生するように、厳密すぎるアプリケーション構造を避けるためだと思います。

Django では、MVC は次のように実装されていました。

ビュー レイヤーが 2 つに分割されます。ビューは、HTTP リクエストを管理するためだけに使用する必要があります。ビューは呼び出されて応答します。ビューは、アプリケーションの残りの部分 (フォーム、モデルフォーム、カスタム クラス、単純なケースではモデルと直接) と通信します。インターフェイスを作成するには、テンプレートを使用します。テンプレートは Django にとって文字列のようなもので、コンテキストをテンプレートにマップし、このコンテキストはアプリケーションによってビューに伝えられました (ビューが要求したとき)。

モデル層は、カプセル化、抽象化、検証、インテリジェンスを提供し、データをオブジェクト指向にします (いつか DBMS もそうなると彼らは言います)。これは、巨大な models.py ファイルを作成する必要があるという意味ではありません (実際、非常に良いアドバイスは、モデルを別のファイルに分割し、それらを「models」というフォルダーに入れ、「__init__.py」ファイルをこのフォルダーに作成することです)。すべてのモデルをインポートし、最後に models.Model クラスの属性「app_label」を使用するフォルダ)。モデルは、データの操作を抽象化する必要があります。これにより、アプリケーションがよりシンプルになります。また、必要に応じて、モデルの「ツール」などの外部クラスを作成する必要があります。モデルの Meta クラスの「abstract」属性を「True」に設定して、モデルで継承を使用することもできます。

残りはどこですか?一般に、小さな Web アプリケーションはデータへのインターフェイスのようなものです。いくつかの小さなプログラムの場合、ビューを使用してデータをクエリまたは挿入するだけで十分です。より一般的なケースでは、実際には「コントローラー」である Forms または ModelForms を使用します。これは、一般的な問題に対する実用的な解決策であり、非常に迅速な解決策です。それは、ウェブサイトが行うために使用するものです。

フォームが十分でない場合は、独自のクラスを作成して魔法を実行する必要があります。これの非常に良い例は管理アプリケーションです。ModelAmin コードを読み取ることができます。これは実際にはコントローラーとして機能します。標準的な構造はありません。既存の Django アプリを調べることをお勧めします。それぞれのケースに依存します。これは Django 開発者が意図したことです。xml パーサー クラス、API コネクタ クラスを追加し、タスクを実行するために Celery を追加し、リアクター ベースのアプリケーション用にひねり、ORM のみを使用し、Web サービスを作成し、管理アプリケーションを変更できます。 .. 高品質のコードを作成し、MVC の哲学を尊重するかどうか、モジュール ベースにし、独自の抽象化レイヤーを作成することは、ユーザーの責任です。とても柔軟です。

私のアドバイス: できるだけ多くのコードを読んでください。Django アプリケーションはたくさんありますが、あまり真剣に考えないでください。それぞれのケースは異なります。パターンと理論が役立ちますが、常にそうとは限りません。これは不正確な科学です。django は、いくつかの問題 (管理インターフェイス、Web フォームの検証、i18n、オブザーバー パターンの実装など) を軽減するために使用できる優れたツールを提供するだけです。しかし、良いデザインは経験豊富なデザイナーから生まれます。

PS .: auth アプリケーション (標準の django から) の「User」クラスを使用すると、たとえばユーザー プロファイルを作成したり、少なくともそのコードを読み取ったりすることができます。

于 2012-10-16T16:02:22.947 に答える
0

選択した回答 ( https://stackoverflow.com/a/12857584/871392 ) にほぼ同意しますが、クエリの作成セクションにオプションを追加したいと考えています。

make filter クエリなどのモデルの QuerySet クラスを定義できます。その後、組み込みの Manager および QuerySet クラスと同様に、このクエリセット クラスをモデルのマネージャーにプロキシできます。

ただし、1 つのドメイン モデルを取得するために複数のデータ モデルにクエリを実行する必要がある場合は、以前に提案したように、これを別のモジュールに配置する方が合理的だと思われます。

于 2014-03-16T21:07:10.133 に答える
-8

Django は、Web ページを配信するために簡単に使用できるように設計されています。これに慣れていない場合は、おそらく別のソリューションを使用する必要があります。

モデルのルートまたは一般的な操作 (同じインターフェイスを持つため) と、モデルのコントローラーのその他の操作を書いています。他のモデルからの操作が必要な場合は、そのコントローラーをインポートします。

このアプローチは、私と私のアプリケーションの複雑さには十分です。

Hedde の回答は、django と python 自体の柔軟性を示す例です。

とにかく非常に興味深い質問です!

于 2012-09-25T09:40:37.920 に答える