การแยกตรรกะทางธุรกิจและการเข้าถึงข้อมูลใน django


484

ฉันจะเขียนโครงการใน Django และผมเห็นว่า 80% models.pyของรหัสที่อยู่ในแฟ้ม รหัสนี้ทำให้เกิดความสับสนและหลังจากช่วงเวลาหนึ่งฉันหยุดที่จะเข้าใจสิ่งที่เกิดขึ้นจริง

นี่คือสิ่งที่รบกวนจิตใจฉัน:

  1. ฉันพบว่าน่าเกลียดที่ระดับโมเดลของฉัน (ซึ่งควรจะรับผิดชอบในการทำงานกับข้อมูลจากฐานข้อมูลเท่านั้น) ก็กำลังส่งอีเมลการเดินบน API ไปยังบริการอื่น ๆ เป็นต้น
  2. นอกจากนี้ฉันพบว่าไม่สามารถวางตรรกะทางธุรกิจในมุมมองได้เนื่องจากวิธีนี้เป็นการยากที่จะควบคุม ตัวอย่างเช่นในแอปพลิเคชันของฉันมีอย่างน้อยสามวิธีในการสร้างอินสแตนซ์ใหม่ของ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 ได้?


14
อ่านเกี่ยวกับสัญญาณ
Konstant

1
คุณลบแท็กได้ดี แต่คุณสามารถใช้ DCI เพื่อแยกความแตกต่างของระบบ (การทำงาน) และระบบ (รูปแบบข้อมูล / โดเมน)
Rune FS

2
คุณเสนอที่จะใช้ตรรกะทางธุรกิจทั้งหมดในการเรียกกลับสัญญาณ? น่าเสียดายที่แอปพลิเคชันของฉันไม่สามารถเชื่อมโยงกับเหตุการณ์ทั้งหมดในฐานข้อมูลได้
defuz

Rune FS ฉันพยายามใช้ DCI แต่ดูเหมือนว่าฉันไม่ต้องการอะไรมากสำหรับโครงการของฉัน: บริบทคำจำกัดความของบทบาทในฐานะ mixin กับวัตถุ ฯลฯ มีวิธีแยกง่ายกว่า "ไม่" และ " คือ"? คุณยกตัวอย่างเล็กน้อยได้ไหม
defuz

คำตอบ:


634

ดูเหมือนว่าคุณกำลังถามถึงความแตกต่างระหว่างตัวแบบข้อมูลและตัวแบบโดเมน - แบบหลังคือที่ที่คุณสามารถค้นหาตรรกะทางธุรกิจและเอนทิตีที่รับรู้โดยผู้ใช้ปลายทางของคุณอดีตคือที่ที่คุณเก็บข้อมูลของคุณ

นอกจากนี้ฉันได้ตีความส่วนที่ 3 ของคำถามของคุณเป็น: วิธีสังเกตความล้มเหลวในการแยกแบบจำลองเหล่านี้ออก

นี่เป็นแนวคิดที่แตกต่างกันสองอย่างและมันยากที่จะแยกพวกเขาออกจากกันเสมอ อย่างไรก็ตามมีรูปแบบและเครื่องมือทั่วไปบางอย่างที่สามารถใช้เพื่อจุดประสงค์นี้ได้

เกี่ยวกับรูปแบบโดเมน

สิ่งแรกที่คุณต้องจดจำคือโมเดลโดเมนของคุณไม่ได้เกี่ยวกับข้อมูลจริงๆ มันเกี่ยวกับการกระทำและคำถามเช่น "เปิดใช้งานผู้ใช้รายนี้", "ปิดใช้งานผู้ใช้รายนี้", "ผู้ใช้รายใดที่เปิดใช้งานอยู่ในขณะนี้" และ "ชื่อผู้ใช้รายนี้คืออะไร" ในแง่คลาสสิก: มันเป็นเรื่องของคำสั่งและคำสั่ง

คิดในคำสั่ง

เริ่มต้นด้วยการดูคำสั่งในตัวอย่างของคุณ: "เปิดใช้งานผู้ใช้รายนี้" และ "ปิดการใช้งานผู้ใช้รายนี้" สิ่งที่ดีเกี่ยวกับคำสั่งก็คือพวกเขาสามารถแสดงได้อย่างง่ายดายด้วยสถานการณ์สมมติขนาดเล็กเมื่อ - ในขณะนั้น:

ให้ผู้ใช้ที่ไม่ได้ใช้งาน
เมื่อผู้ดูแลระบบเปิดใช้งานผู้ใช้นี้
จากนั้นผู้ใช้จะเปิดใช้งาน
และอีเมลยืนยันถูกส่งไปยังผู้ใช้
และรายการจะถูกเพิ่มลงในบันทึกของระบบ
(ฯลฯ ฯลฯ )

สถานการณ์ดังกล่าวมีประโยชน์ในการดูว่าส่วนต่าง ๆ ของโครงสร้างพื้นฐานของคุณสามารถได้รับผลกระทบจากคำสั่งเดียวได้อย่างไรในกรณีนี้ฐานข้อมูลของคุณ (การตั้งค่าสถานะ 'active' บางชนิด), เมลเซิร์ฟเวอร์ของคุณ, บันทึกระบบของคุณ ฯลฯ

สถานการณ์ดังกล่าวยังช่วยคุณในการตั้งค่าสภาพแวดล้อมการพัฒนาที่ขับเคลื่อนด้วยการทดสอบ

และสุดท้ายการคิดคำสั่งจะช่วยให้คุณสร้างแอปพลิเคชันที่เน้นงาน ผู้ใช้ของคุณจะประทับใจในสิ่งนี้ :-)

คำสั่งการแสดง

Django มีสองวิธีง่ายๆในการแสดงคำสั่ง; เป็นทั้งตัวเลือกที่ถูกต้องและไม่ผิดปกติที่จะผสมสองวิธีเข้าด้วยกัน

ชั้นบริการ

โมดูลบริการได้รับการอธิบายโดย @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

ใช้แบบฟอร์ม

อีกวิธีคือใช้แบบฟอร์ม Django สำหรับแต่ละคำสั่ง ฉันชอบวิธีนี้มากกว่าเพราะมันรวมแง่มุมต่าง ๆ ที่เกี่ยวข้องอย่างใกล้ชิด:

  • การดำเนินการของคำสั่ง (มันทำอะไร?)
  • การตรวจสอบความถูกต้องของพารามิเตอร์คำสั่ง (สามารถทำได้หรือไม่)
  • การนำเสนอคำสั่ง (ฉันจะทำสิ่งนี้ได้อย่างไร)

forms.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

กำลังคิดในการค้นหา

ตัวอย่างเช่นคุณไม่มีข้อความค้นหาใด ๆ ดังนั้นฉันจึงใช้เสรีภาพในการสร้างข้อความค้นหาที่มีประโยชน์สองสามข้อ ฉันชอบที่จะใช้คำว่า "คำถาม" แต่คำค้นหาคือคำศัพท์ดั้งเดิม ข้อความค้นหาที่น่าสนใจคือ: "ชื่อของผู้ใช้นี้คืออะไร", "ผู้ใช้นี้สามารถเข้าสู่ระบบได้หรือไม่", "แสดงรายการผู้ใช้ที่ไม่ได้ใช้งาน" ให้ฉันดูและ "การกระจายทางภูมิศาสตร์ของผู้ใช้ที่ปิดใช้งานคืออะไร"

ก่อนที่จะเริ่มตอบคำถามเหล่านี้คุณควรถามตัวเองด้วยคำถามสองข้อเสมอ: นี่คือแบบสอบถามแบบนำเสนอเฉพาะสำหรับแม่แบบของฉันและ / หรือแบบสอบถามตรรกะทางธุรกิจที่เชื่อมโยงกับการดำเนินการคำสั่งของฉันและ / หรือแบบสอบถามแบบรายงาน

คำสั่งที่นำเสนอเป็นเพียงการปรับปรุงส่วนติดต่อผู้ใช้ คำตอบของการค้นหาตรรกะทางธุรกิจส่งผลโดยตรงต่อการดำเนินการตามคำสั่งของคุณ การรายงานการสืบค้นนั้นมีวัตถุประสงค์เพื่อการวิเคราะห์และมีข้อ จำกัด ด้านเวลาเท่านั้น หมวดหมู่เหล่านี้ไม่ได้เกิดขึ้นพร้อมกัน

อีกคำถามคือ: "ฉันสามารถควบคุมคำตอบได้ทั้งหมดหรือไม่" ตัวอย่างเช่นเมื่อสอบถามชื่อผู้ใช้ (ในบริบทนี้) เราไม่สามารถควบคุมผลลัพธ์ได้เนื่องจากเราใช้ 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ของคุณ(หากคุณกำลังใช้งานอยู่) หรือแนะนำโมดูลquery.py :

queries.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 

รุ่นพร็อกซี

รุ่นพร็อกซีมีประโยชน์อย่างมากในบริบทของตรรกะทางธุรกิจและการรายงาน คุณกำหนดชุดย่อยที่ปรับปรุงแล้วของโมเดล คุณสามารถแทนที่ 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

แบบจำลองการสืบค้น

สำหรับแบบสอบถามที่มีความซับซ้อนโดยเนื้อแท้ แต่มีการดำเนินการค่อนข้างบ่อยมีความเป็นไปได้ของรูปแบบแบบสอบถาม รูปแบบแบบสอบถามเป็นรูปแบบของการทำให้เป็นปกติซึ่งข้อมูลที่เกี่ยวข้องสำหรับแบบสอบถามเดียวจะถูกเก็บไว้ในรูปแบบที่แยกต่างหาก เคล็ดลับของหลักสูตรคือการทำให้รูปแบบที่ไม่สอดคล้องกันให้สอดคล้องกับรูปแบบหลัก แบบจำลองการสืบค้นสามารถใช้ได้เมื่อการเปลี่ยนแปลงทั้งหมดอยู่ภายใต้การควบคุมของคุณ

models.py

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

ตัวเลือกแรกคือการอัพเดตโมเดลเหล่านี้ในคำสั่งของคุณ สิ่งนี้มีประโยชน์มากหากโมเดลเหล่านี้ถูกเปลี่ยนโดยคำสั่งหนึ่งหรือสองคำสั่งเท่านั้น

forms.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()

ตัวเลือกที่ดีกว่าคือการใช้สัญญาณที่กำหนดเอง สัญญาณเหล่านี้แน่นอนที่ปล่อยออกมาโดยคำสั่งของคุณ สัญญาณมีข้อได้เปรียบที่คุณสามารถเก็บแบบจำลองแบบสอบถามหลายแบบให้ตรงกับรุ่นเดิมของคุณ นอกจากนี้การประมวลผลสัญญาณสามารถ offloaded เพื่องานพื้นหลังโดยใช้ Celery หรือกรอบที่คล้ายกัน

signals.py

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

forms.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: สัญญาณ

สถาปัตยกรรม: การออกแบบโดเมนขับเคลื่อน


11
ยินดีที่ได้เห็นคำตอบที่ได้รวม DDD เข้ากับคำถามที่เกี่ยวข้องกับ django เพียงเพราะ Django ใช้ ActiveRecord สำหรับการคงอยู่ไม่ได้หมายความว่าการแยกข้อกังวลควรออกไปนอกหน้าต่าง คำตอบที่ดี
Scott Coates

6
ถ้าฉันต้องการตรวจสอบว่าผู้ใช้ looged เป็นเจ้าของวัตถุก่อนที่จะลบวัตถุนั้นฉันควรตรวจสอบว่าในมุมมองหรือในรูปแบบ / โมดูลบริการ?
อีวาน

6
@Ivan: ทั้งคู่ มันจะต้องอยู่ในรูปแบบโมดูล / บริการเพราะมันเป็นส่วนหนึ่งของข้อ จำกัด ของธุรกิจของคุณ มันควรจะยังอยู่ในมุมมองเพราะคุณควรกระทำในปัจจุบันที่ผู้ใช้สามารถปฏิบัติจริง
publysher

4
ผู้จัดการที่กำหนดเองวิธีการUser.objects.inactive_users()นี้เป็นวิธีที่ดีที่จะใช้การสอบถาม: แต่ตัวอย่างรูปแบบการพร็อกซี่ที่นี่ IMO จะนำไปสู่ความหมายไม่ถูกต้อง: และยังu = InactiveUser.objects.all()[0]; u.active = True; u.save() isinstance(u, InactiveUser) == Trueนอกจากนี้ฉันจะพูดถึงวิธีที่มีประสิทธิภาพในการรักษารูปแบบแบบสอบถามในหลายกรณีอยู่กับมุมมอง db
Aryeh Leib Taurog

1
@adnanmuttaleb สิ่งนี้ถูกต้อง โปรดทราบว่าคำตอบนั้นใช้คำว่า "Domain Model" เท่านั้น ฉันได้รวมลิงก์ไปยัง DDD ไม่ใช่เพราะคำตอบของฉันคือ DDD แต่เนื่องจากหนังสือเล่มนั้นทำงานได้ยอดเยี่ยมในการช่วยให้คุณนึกถึงโมเดลโดเมน
publysher

148

ฉันมักจะใช้ชั้นบริการในระหว่างมุมมองและรูปแบบ สิ่งนี้ทำหน้าที่เหมือนกับ 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()

โปรดทราบฉันมักจะนำแบบจำลองมุมมองและบริการไปยังระดับโมดูลและแยกออกไปยิ่งขึ้นอยู่กับขนาดของโครงการ


8
ผมชอบวิธีการทั่วไป แต่จากความเข้าใจของคุณตัวอย่างที่เฉพาะเจาะจงของฉันมักจะได้รับการดำเนินการเป็นผู้จัดการ
arie

9
@ arie อาจไม่จำเป็นต้องเป็นตัวอย่างที่ดีกว่าสำหรับบริการเว็บช็อปจะรวมถึงสิ่งต่าง ๆ เช่นการสร้างเซสชันรถเข็นงานแบบอะซิงโครนัสเช่นการคำนวณการจัดอันดับผลิตภัณฑ์การสร้างและส่งอีเมลเป็นต้น
Hedde van der Heide

4
ฉันชอบวิธีการนี้ด้วยเช่นกัน ฉันใหม่กับ python คุณจะทดสอบ views.py อย่างไร คุณจะล้อเลียนเลเยอร์บริการได้อย่างไร (ตัวอย่างเช่นหากบริการโทรผ่าน api ระยะไกล)
Teimuraz

71

แรกของทุกอย่าซ้ำตัวเอง

จากนั้นโปรดระวังอย่าทำ overengineer บางครั้งก็เสียเวลาและทำให้ใครบางคนเสียสมาธิกับสิ่งที่สำคัญ ตรวจสอบเซนของหลามเป็นครั้งคราว

ดูโครงการที่ใช้งานอยู่

  • มีคนอีกมาก = จำเป็นต้องจัดระเบียบอย่างเหมาะสม
  • ที่เก็บ djangoพวกเขามีโครงสร้างที่ตรงไปตรงมา
  • ที่เก็บ pip ที่มีโครงสร้างไดเร็กทอรี straigtforward
  • ที่เก็บผ้ายังเป็นหนึ่งที่ดีที่จะมอง

    • คุณสามารถวางโมเดลทั้งหมดของคุณไว้ใต้ yourapp/models/logicalgroup.py
  • เช่นUser, Groupและของที่เกี่ยวข้องรุ่นสามารถไปอยู่ภายใต้yourapp/models/users.py
  • เช่นPoll, Question, Answer... จะไปอยู่ภายใต้yourapp/models/polls.py
  • โหลดสิ่งที่คุณต้องการ__all__ภายในyourapp/models/__init__.py

เพิ่มเติมเกี่ยวกับ MVC

  • model เป็นข้อมูลของคุณ
    • ซึ่งรวมถึงข้อมูลจริงของคุณ
    • ซึ่งรวมถึงข้อมูลเซสชัน / คุกกี้ / แคช / fs / ดัชนีของคุณ
  • ผู้ใช้โต้ตอบกับคอนโทรลเลอร์เพื่อควบคุมโมเดล
    • นี่อาจเป็น API หรือมุมมองที่บันทึก / อัปเดตข้อมูลของคุณ
    • สามารถปรับได้ด้วยrequest.GET/ request.POST... ฯลฯ
    • คิดว่าเพจจิ้งหรือกรองเกินไป
  • ข้อมูลจะอัพเดตมุมมอง
    • เทมเพลตใช้ข้อมูลและจัดรูปแบบตามนั้น
    • APIs ที่ไม่มีเทมเพลตเป็นส่วนหนึ่งของมุมมอง เช่นtastypieหรือpiston
    • สิ่งนี้ควรคำนึงถึงมิดเดิลแวร์ด้วย

ใช้ประโยชน์จากมิดเดิลแวร์ / templatetags

  • หากคุณต้องการงานที่จะทำสำหรับแต่ละคำขอมิดเดิลแวร์เป็นวิธีหนึ่ง
    • เช่นการเพิ่มการประทับเวลา
    • เช่นอัปเดตการวัดเกี่ยวกับการเข้าชมหน้า
    • เช่นการเติมแคช
  • หากคุณมีตัวอย่างโค้ดที่ให้การจัดรูปแบบวัตถุอยู่เสมอ Templatetags นั้นดี
    • เช่นการใช้งานแท็บ / url breadcrumbs

ใช้ประโยชน์จากผู้จัดการแบบจำลอง

  • สร้างสามารถไปในUserUserManager(models.Manager)
  • models.Modelรายละเอียดเต็มไปด้วยเลือดสำหรับกรณีควรจะไปใน
  • รายละเอียดเต็มไปด้วยเลือดสำหรับจะไปในquerysetmodels.Manager
  • คุณอาจต้องการสร้าง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 มีแบ็กเอนด์ ... ฯลฯ
  • เพิ่ม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

เพิ่มเติมเกี่ยวกับรูปแบบการออกแบบ

เพิ่มเติมเกี่ยวกับขอบเขตของอินเตอร์เฟส

  • รหัสที่คุณต้องการใช้เป็นส่วนหนึ่งของรุ่นจริง ๆ หรือไม่ ->yourapp.models
  • รหัสเป็นส่วนหนึ่งของตรรกะทางธุรกิจหรือไม่ ->yourapp.vendor
  • รหัสเป็นส่วนหนึ่งของเครื่องมือ / libs ทั่วไปหรือไม่ ->yourapp.libs
  • รหัสเป็นส่วนหนึ่งของ libs ตรรกะทางธุรกิจหรือไม่? -> yourapp.libs.vendorหรือyourapp.vendor.libs
  • นี่เป็นข้อดี: คุณสามารถทดสอบโค้ดของคุณได้อย่างอิสระหรือไม่?
    • ใช่ดี :)
    • ไม่คุณอาจประสบปัญหาส่วนต่อประสาน
    • เมื่อมีการแยกชัดเจน unittest ควรเป็นเรื่องง่ายด้วยการใช้การเยาะเย้ย
  • การแยกเป็นตรรกะหรือไม่
    • ใช่ดี :)
    • ไม่คุณอาจมีปัญหาในการทดสอบแนวคิดเชิงตรรกะเหล่านั้นแยกกัน
  • คุณคิดว่าคุณจะต้อง refactor เมื่อคุณได้รับรหัสอีก 10 เท่าหรือไม่
    • ใช่ไม่ดีไม่มีบูเอโน่ refactor อาจทำงานได้มากมาย
    • ไม่มันยอดเยี่ยมมาก!

ในระยะสั้นคุณสามารถมี

  • 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

หรืออะไรก็ได้ที่ช่วยคุณ ค้นหาอินเทอร์เฟซที่คุณต้องการและขอบเขตจะช่วยคุณ


27

Django ใช้ MVC ชนิดดัดแปลงเล็กน้อย ไม่มีแนวคิดของ "คอนโทรลเลอร์" ใน Django พร็อกซีที่ใกล้เคียงที่สุดคือ "มุมมอง" ซึ่งมีแนวโน้มที่จะทำให้เกิดความสับสนกับการแปลง MVC เพราะใน MVC มุมมองจะเป็นเหมือน "เทมเพลต" ของ Django มากขึ้น

ใน Django "model" ไม่ใช่เพียงแค่ฐานข้อมูลนามธรรม ในบางแง่ก็มีหน้าที่ร่วมกับ "มุมมอง" ของ Django ในฐานะผู้ควบคุม MVC มันถือพฤติกรรมทั้งหมดที่เกี่ยวข้องกับอินสแตนซ์ หากอินสแตนซ์นั้นต้องโต้ตอบกับ API ภายนอกเนื่องจากเป็นส่วนหนึ่งของพฤติกรรมแสดงว่าเป็นรหัสรุ่น อันที่จริงแล้วโมเดลไม่จำเป็นต้องมีปฏิสัมพันธ์กับฐานข้อมูลเลยดังนั้นคุณสามารถนึกได้ว่ามีโมเดลที่มีอยู่ทั้งหมดเป็นเลเยอร์โต้ตอบกับ API ภายนอก มันเป็นแนวคิดที่อิสระมากขึ้นของ "โมเดล"


7

ใน Django โครงสร้าง MVC นั้น Chris Pratt กล่าวว่าแตกต่างจากโมเดล MVC แบบคลาสสิกที่ใช้ในเฟรมเวิร์กอื่นฉันคิดว่าเหตุผลหลักในการทำเช่นนี้คือการหลีกเลี่ยงโครงสร้างแอปพลิเคชันที่เข้มงวดเกินไป

ใน Django MVC ถูกนำไปใช้ในวิธีต่อไปนี้:

เลเยอร์การดูแบ่งออกเป็นสองส่วน มุมมองควรใช้เพื่อจัดการคำขอ HTTP เท่านั้นซึ่งถูกเรียกและตอบกลับ Views สื่อสารกับส่วนที่เหลือของแอปพลิเคชันของคุณ (แบบฟอร์ม modelforms คลาสที่กำหนดเองในกรณีง่าย ๆ โดยตรงกับรุ่น) เพื่อสร้างส่วนต่อประสานเราใช้เทมเพลต เทมเพลตเป็นเหมือนสตริงของ Django โดยจะจับคู่บริบทกับเนื้อหาเหล่านั้นและบริบทนี้ได้รับการสื่อสารไปยังแอปพลิเคชั่นมุมมองนี้

ชั้นเลเยอร์ให้การห่อหุ้มสิ่งที่เป็นนามธรรมการตรวจสอบความเฉลียวฉลาดและทำให้ข้อมูลของคุณเชิงวัตถุ (พวกเขาบอกว่าสักวัน DBMS จะยัง) นี่ไม่ได้หมายความว่าคุณควรสร้างไฟล์ model.py ขนาดใหญ่ (อันที่จริงแล้วคำแนะนำที่ดีมากคือการแยกแบบจำลองของคุณเป็นไฟล์ต่าง ๆ วางไว้ในโฟลเดอร์ที่เรียกว่า 'แบบจำลอง' สร้างไฟล์ '__init__.py' ลงในนี้ โฟลเดอร์ที่คุณนำเข้าทุกรุ่นของคุณและในที่สุดก็ใช้แอตทริบิวต์ 'app_label' ของ models.Model class) แบบจำลองควรทำให้คุณไม่สามารถทำงานกับข้อมูลได้ซึ่งจะทำให้แอปพลิเคชันของคุณง่ายขึ้น คุณควรสร้างคลาสภายนอกเช่น "เครื่องมือ" สำหรับแบบจำลองของคุณนอกจากนี้คุณยังสามารถใช้มรดกในแบบจำลองตั้งค่าแอตทริบิวต์ 'นามธรรม' ของคลาส Meta ของแบบจำลองของคุณเป็น 'จริง'

ที่เหลืออยู่ที่ไหน โดยทั่วไปเว็บแอปพลิเคชั่นขนาดเล็กนั้นเป็นส่วนหนึ่งของอินเทอร์เฟซกับข้อมูลในบางกรณีของโปรแกรมขนาดเล็กที่ใช้มุมมองเพื่อสืบค้น กรณีทั่วไปที่มากกว่านั้นจะใช้ Forms หรือ ModelForms ซึ่งจริงๆแล้วคือ "controllers" นี่ไม่ใช่วิธีแก้ปัญหาที่ใช้งานได้จริงสำหรับปัญหาทั่วไปและเป็นวิธีที่รวดเร็วมาก มันเป็นสิ่งที่เว็บไซต์ใช้ทำ

ถ้าแบบฟอร์มไม่ใช่ enogh สำหรับคุณคุณควรสร้างคลาสของคุณเองเพื่อทำเวทย์มนตร์ตัวอย่างที่ดีของแอพพลิเคชั่นนี้คือผู้ดูแลระบบ: คุณสามารถอ่านรหัส ModelAmin ได้ซึ่งจะเป็นตัวควบคุม ไม่มีโครงสร้างมาตรฐานฉันแนะนำให้คุณตรวจสอบแอพ Django ที่มีอยู่มันขึ้นอยู่กับแต่ละกรณี นี่คือสิ่งที่นักพัฒนา Django ตั้งใจคุณสามารถเพิ่มคลาส xml parser คลาสตัวเชื่อมต่อ API เพิ่ม Celery สำหรับการปฏิบัติงานบิดสำหรับแอปพลิเคชันที่ใช้เครื่องปฏิกรณ์ใช้เฉพาะ ORM สร้างบริการเว็บปรับเปลี่ยนแอปพลิเคชันผู้ดูแลระบบและอื่น ๆ .. มันเป็นความรับผิดชอบของคุณในการสร้างรหัสคุณภาพที่ดีเคารพปรัชญา MVC หรือไม่ทำให้เป็นโมดูลที่ใช้และสร้างเลเยอร์สิ่งที่เป็นนามธรรมของคุณเอง มันยืดหยุ่นมาก

คำแนะนำของฉัน: อ่านรหัสให้มากที่สุดเท่าที่จะทำได้มีแอปพลิเคชั่น django อยู่มากมาย แต่อย่าจริงจังกับมันมากนัก แต่ละกรณีมีความแตกต่างรูปแบบและทฤษฎีช่วย แต่ไม่เสมอไปนี่เป็นความจริงที่ไม่ถูกต้อง django เพียงแค่ให้เครื่องมือที่ดีที่คุณสามารถใช้เพื่อบรรเทาความเจ็บปวดบางอย่าง (เช่นส่วนต่อประสานผู้ดูแลระบบการตรวจสอบรูปแบบเว็บ i18n ที่กล่าวถึงก่อนหน้านี้และอื่น ๆ ) แต่การออกแบบที่ดีมาจากนักออกแบบที่มีประสบการณ์

PS .: ใช้คลาส 'ผู้ใช้' จากแอปพลิเคชันรับรองความถูกต้อง (จาก django มาตรฐาน) คุณสามารถสร้างโปรไฟล์ผู้ใช้หรืออย่างน้อยก็อ่านรหัสมันจะเป็นประโยชน์สำหรับกรณีของคุณ


1

เป็นคำถามเก่า แต่ฉันต้องการเสนอทางออกให้ฉัน ก็ขึ้นอยู่กับการยอมรับว่ารูปแบบวัตถุมากเกินไปต้องมีฟังก์ชันการทำงานเพิ่มเติมในขณะที่มันอึดอัดใจที่จะวางไว้ภายในmodels.py ตรรกะทางธุรกิจขนาดใหญ่อาจถูกเขียนแยกกันขึ้นอยู่กับรสนิยมส่วนตัว แต่อย่างน้อยฉันก็ชอบแบบจำลองที่จะทำทุกอย่างที่เกี่ยวข้องกับตัวเอง โซลูชันนี้ยังสนับสนุนผู้ที่ต้องการวางตรรกะทั้งหมดไว้ในแบบจำลองด้วยตนเอง

ดังนั้นฉันจึงคิดค้นแฮ็คที่ช่วยให้ฉันสามารถแยกตรรกะออกจากคำจำกัดความของแบบจำลองและยังคงได้รับคำแนะนำทั้งหมดจาก IDE ของฉัน

ข้อดีควรชัดเจน แต่นี่เป็นรายการบางอย่างที่ฉันสังเกตเห็น:

  • นิยามฐานข้อมูลยังคงอยู่ - ไม่มีตรรกะ "ขยะ" แนบอยู่
  • ตรรกะที่เกี่ยวข้องกับรูปแบบทั้งหมดนี้วางไว้อย่างเรียบร้อยในที่เดียว
  • บริการทั้งหมด (แบบฟอร์ม, REST, มุมมอง) มีจุดเชื่อมต่อเดียวกับตรรกะ
  • ดีที่สุดของทั้งหมด:ฉันไม่ต้องเขียนรหัสใด ๆ อีกต่อไปเมื่อฉันรู้ว่าmodels.pyของฉันรกเกินไปและต้องแยกตรรกะออกจากกัน การแยกนั้นราบรื่นและทำซ้ำได้: ฉันสามารถทำฟังก์ชั่นในแต่ละครั้งหรือทั้งคลาสหรือทั้ง models.py

ฉันใช้สิ่งนี้กับ Python 3.4 และสูงกว่าและ Django 1.8 และสูงกว่า

app / models.py

....
from app.logic.user import UserLogic

class User(models.Model, UserLogic):
    field1 = models.AnyField(....)
    ... field definitions ...

app / ตรรกะ / user.py

if False:
    # This allows the IDE to know about the User model and its member fields
    from main.models import User

class UserLogic(object):
    def logic_function(self: 'User'):
        ... code with hinting working normally ...

สิ่งเดียวที่ฉันไม่สามารถหาได้คือทำอย่างไรให้ IDE ของฉัน (PyCharm ในกรณีนี้) รู้ว่า UserLogic นั้นเป็นรุ่นของผู้ใช้จริง ๆ แต่เนื่องจากเป็นแฮ็คอย่างชัดเจนฉันยินดีที่จะยอมรับความรำคาญเล็กน้อยของการระบุประเภทselfพารามิเตอร์


จริงๆแล้วฉันเห็นว่ามันเป็นวิธีการที่ใช้งานง่าย แต่ฉันจะย้ายรุ่นสุดท้ายไปยังไฟล์อื่นและไม่สืบทอดใน models.py มันจะเหมือนกับ service.py เป็น clash userlogic + model
Maks

1

ฉันจะต้องเห็นด้วยกับคุณ มีจำนวนมากของความเป็นไปได้ใน Django แต่สถานที่ที่ดีที่สุดที่จะเริ่มมีการตรวจสอบเป็นปรัชญาการออกแบบของ Django

  1. การเรียก API จากคุณสมบัติของโมเดลจะไม่เหมาะดูเหมือนว่าจะเหมาะสมกว่าที่จะทำสิ่งนี้ในมุมมองและอาจสร้างเลเยอร์บริการเพื่อทำให้สิ่งต่าง ๆ แห้ง หากการเรียกใช้ API ไม่ถูกบล็อกและการโทรนั้นมีราคาแพงการส่งการร้องขอไปยังผู้ปฏิบัติงานบริการ (ผู้ปฏิบัติงานที่ใช้งานจากคิว) อาจสมเหตุสมผล

  2. ตามแบบปรัชญาการออกแบบของ Django นั้นสรุปทุกแง่มุมของ "วัตถุ" ดังนั้นตรรกะทางธุรกิจทั้งหมดที่เกี่ยวข้องกับวัตถุนั้นควรอยู่ที่นั่น:

รวมตรรกะโดเมนที่เกี่ยวข้องทั้งหมด

แบบจำลองควรห่อหุ้มทุกแง่มุมของ“ วัตถุ” ตามรูปแบบการออกแบบ Active Record ของ Martin Fowler

  1. ผลข้างเคียงที่คุณอธิบายชัดเจนตรรกะที่นี่อาจแบ่งย่อยเป็น Querysets และผู้จัดการได้ดีขึ้น นี่คือตัวอย่าง:

    models.py

    import datetime
    
    from djongo import models
    from django.db.models.query import QuerySet
    from django.contrib import admin
    from django.db import transaction
    
    
    class MyUser(models.Model):
    
        present_name = models.TextField(null=False, blank=True)
        status = models.TextField(null=False, blank=True)
        last_active = models.DateTimeField(auto_now=True, editable=False)
    
        # As mentioned you could put this in a template tag to pull it
        # from cache there. Depending on how it is used, it could be
        # retrieved from within the admin view or from a custom view
        # if that is the only place you will use it.
        #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'
    
        # Moved to admin as an action
        # def activate(self):
        #     # method now has a side effect (send message to user)
        #     self.status = 'activated'
        #     self.save()
        #     # send email via email service
        #     #send_mail('Your account is activated!', '…', [self.email])
    
        class Meta:
            ordering = ['-id']  # Needed for DRF pagination
    
        def __unicode__(self):
            return '{}'.format(self.pk)
    
    
    class MyUserRegistrationQuerySet(QuerySet):
    
        def for_inactive_users(self):
            new_date = datetime.datetime.now() - datetime.timedelta(days=3*365)  # 3 Years ago
            return self.filter(last_active__lte=new_date.year)
    
        def by_user_id(self, user_ids):
            return self.filter(id__in=user_ids)
    
    
    class MyUserRegistrationManager(models.Manager):
    
        def get_query_set(self):
            return MyUserRegistrationQuerySet(self.model, using=self._db)
    
        def with_no_activity(self):
            return self.get_query_set().for_inactive_users()

    admin.py

    # Then in model admin
    
    class MyUserRegistrationAdmin(admin.ModelAdmin):
        actions = (
            'send_welcome_emails',
        )
    
        def send_activate_emails(self, request, queryset):
            rows_affected = 0
            for obj in queryset:
                with transaction.commit_on_success():
                    # send_email('welcome_email', request, obj) # send email via email service
                    obj.status = 'activated'
                    obj.save()
                    rows_affected += 1
    
            self.message_user(request, 'sent %d' % rows_affected)
    
    admin.site.register(MyUser, MyUserRegistrationAdmin)

0

ฉันเห็นด้วยกับคำตอบที่เลือกเป็นส่วนใหญ่ ( https://stackoverflow.com/a/12857584/871392 ) แต่ต้องการเพิ่มตัวเลือกในการทำแบบสอบถาม

หนึ่งสามารถกำหนดคลาส QuerySet สำหรับแบบจำลองสำหรับทำแบบสอบถามตัวกรองและอื่น ๆ หลังจากนั้นคุณสามารถพร็อกซีคลาสชุดคิวรีนี้สำหรับผู้จัดการของโมเดลเช่นคลาสบิวด์อิน Manager และ QuerySet

แม้ว่าหากคุณต้องสอบถามตัวแบบข้อมูลหลายตัวเพื่อให้ได้แบบจำลองโดเมนหนึ่งดูเหมือนว่าฉันจะใส่โมดูลนี้ในโมดูลแยกต่างหากตามที่แนะนำไว้ก่อนหน้านี้


0

บทความที่ครอบคลุมมากที่สุดเกี่ยวกับตัวเลือกต่าง ๆ ด้วยข้อดีข้อเสีย:

  1. แนวคิด # 1: แบบจำลองไขมัน
  2. แนวคิด # 2: การวางตรรกะทางธุรกิจในมุมมอง / ฟอร์ม
  3. แนวคิด # 3: บริการ
  4. แนวคิด # 4: QuerySets / ผู้จัดการ
  5. ข้อสรุป

ที่มา: https://sunscrapers.com/blog/where-to-put-business-logic-django/


คุณควรเพิ่มคำอธิบาย
m02ph3u5

-6

Django ถูกออกแบบมาให้ใช้งานง่ายในการส่งหน้าเว็บ หากคุณไม่สะดวกใจกับเรื่องนี้คุณควรใช้วิธีแก้ไขปัญหาอื่น

ฉันกำลังเขียนรูทหรือการดำเนินการทั่วไปในโมเดล (เพื่อให้มีอินเทอร์เฟซเดียวกัน) และอื่น ๆ บนคอนโทรลเลอร์ของโมเดล หากฉันต้องการการดำเนินการจากรุ่นอื่นฉันจะนำเข้าคอนโทรลเลอร์

วิธีนี้เพียงพอสำหรับฉันและความซับซ้อนของแอปพลิเคชันของฉัน

คำตอบของ Hedde เป็นตัวอย่างที่แสดงความยืดหยุ่นของ django และ python

คำถามที่น่าสนใจมากอยู่แล้ว!


9
การบอกว่ามันดีพอสำหรับคุณที่จะช่วยให้ฉันเข้าใจคำถามเหอ
Chris Wesseling

1
Django มีข้อเสนอเพิ่มเติมมากมายนอกเหนือจาก django.db.models แต่ระบบนิเวศส่วนใหญ่ขึ้นอยู่กับรุ่นของคุณอย่างมากโดยใช้รุ่น django
andho

1
รูปแบบการออกแบบที่ใช้สำหรับการพัฒนาซอฟต์แวร์ และ django ได้รับการออกแบบให้ใช้งานง่ายในการส่งมอบซอฟต์แวร์ในระดับกลางหรือใหญ่ไม่เพียง แต่หน้าเว็บเท่านั้น!
โมฮัมหมัด Torkashvand
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.