نظافة العمارة من خلال عيون مطور بايثون

مرحبا! اسمي يوجين ، أنا مطور بايثون. على مدار العام ونصف العام الماضي ، بدأ فريقنا في تطبيق مبادئ الهندسة المعمارية النظيفة بنشاط ، بعيدًا عن نموذج MVC الكلاسيكي. واليوم سأتحدث عن كيف توصلنا إلى هذا ، وما الذي يمنحنا إياه ، ولماذا لا يكون النقل المباشر للنُهُج من المحلفين الآخرين حلاً جيدًا دائمًا.



لقد كانت Python أداة تطوير أساسية منذ أكثر من سبع سنوات حتى الآن. عندما يسألوني أكثر ما يعجبني فيه ، أجيب بأن هذه هي قراءته الممتازة . بدأ التعارف الأول بقراءة كتاب "برمجة عقل جماعي" . كنت مهتمًا بالخوارزميات الموضحة فيه ، لكن كل الأمثلة كانت بلغة لم تكن مألوفة لي بعد ذلك. لم يكن هذا أمرًا معتادًا (لم تكن Python سائدة بعد في التعلم الآلي) ، وغالبًا ما تتم كتابة القوائم في كود زائف أو باستخدام الرسوم البيانية. ولكن بعد مقدمة سريعة للغة ، أقدر إيجازها: كل شيء كان سهلاً وواضحًا ، لا شيء غير ضروري ومشتت ، فقط جوهر العملية الموصوفة. الميزة الرئيسية لهذا هو التصميم المذهل للغة ، السكر النحوي البديهي للغاية. لقد كان هذا التعبير موضع تقدير دائمًا في المجتمع. ما هو " استيراد هذا " ، الموجود بالضرورة في الصفحات الأولى من أي كتاب مدرسي: يبدو وكأنه مشرف غير مرئي ، يقيم إجراءاتك باستمرار. في المنتديات ، كان من المفيد للمبتدئ استخدام CamelCase بطريقة أو بأخرى في اسم المتغير في القائمة ، لذلك على الفور تحولت زاوية المناقشة نحو لغة التعليمات البرمجية المقترحة مع إشارات إلى PEP8.
إن السعي وراء الأناقة بالإضافة إلى الديناميكية القوية للغة قد أوجد العديد من المكتبات بواجهة برمجة تطبيقات ممتعة حقًا. 

ومع ذلك ، فإن Python ، على الرغم من قوتها ، هي مجرد أداة تسمح لك بكتابة كود معبر وتوثيق ذاتي ، لكنها لا تضمن ذلك ، ولا تتوافق مع PEP8. عندما يبدأ متجرنا البسيط على الإنترنت في Django في جني الأموال ، ونتيجة لذلك ، يضخ الميزات ، في مرحلة ما ندرك أنه ليس بهذه البساطة ، وحتى إجراء التغييرات الأساسية يتطلب المزيد والمزيد من الجهد ، والأهم من ذلك ، هذا الاتجاه ينمو. ماذا حدث ومتى سارت الأمور بشكل خاطئ؟

كود تالف


إن الرمز السيء ليس هو الذي لا يتبع PEP8 أو لا يلبي متطلبات التعقيد السيكلومي. الكود السيء هو ، أولاً وقبل كل شيء ، تبعيات غير خاضعة للرقابة ، مما يؤدي إلى حقيقة أن التغيير في مكان واحد من البرنامج يؤدي إلى تغييرات غير متوقعة في أجزاء أخرى. نحن نفقد السيطرة على الكود ؛ توسيع الوظائف يتطلب دراسة تفصيلية للمشروع. يفقد مثل هذا الرمز مرونته ، كما هو ، يقاوم إجراء التغييرات ، بينما يصبح البرنامج نفسه "هشًا". 

العمارة النظيفة


يجب أن تتجنب البنية المختارة للتطبيق هذه المشكلة ، ولسنا أول من يواجه هذا: كان هناك نقاش في مجتمع Java حول إنشاء تصميم تطبيق مثالي لفترة طويلة.

في عام 2000 ، جمع روبرت مارتن (المعروف أيضًا باسم العم بوب) في مقاله " مبادئ التصميم والتصميم " خمسة مبادئ لتصميم تطبيقات OOP تحت اختصار SOLID الذي لا يُنسى. تلقى المجتمع هذه المبادئ بشكل جيد وتجاوزت نظام جافا البيئي. ومع ذلك ، فهي مجردة للغاية في طبيعتها. في وقت لاحق كانت هناك محاولات عديدة لتطوير تصميم تطبيق عام يعتمد على مبادئ SOLID. وهي تشمل: "الهندسة المعمارية السداسية" و "المنافذ والمهايئات" و "الهندسة المعمارية المنتفخة" ولديها الكثير من تفاصيل التنفيذ المشتركة وإن كانت مختلفة. وفي عام 2012 ، تم نشر مقال من قبل روبرت مارتن نفسه ، حيث اقترح نسخته الخاصة بعنوان " العمارة النظيفة ".



وفقًا لـ Uncle Bob ، فإن العمارة هي في المقام الأول " حدود وعوائق " ، فمن الضروري فهم الاحتياجات بوضوح وتحديد واجهات البرامج حتى لا تفقد السيطرة على التطبيق. للقيام بذلك ، يتم تقسيم البرنامج إلى طبقات. بالانتقال من طبقة إلى أخرى ، يمكن فقط نقل البيانات ( يمكن أن تكون الهياكل البسيطة وكائنات DTO بمثابة بيانات ) - هذه هي قاعدة الحدود. العبارة الأخرى الأكثر استخدامًا والتي تشير إلى أن " التطبيق يجب أن يصرخ " تعني أن الشيء الرئيسي في التطبيق ليس الإطار المستخدم أو تقنية تخزين البيانات ، ولكن ما يفعله هذا التطبيق في الواقع ، والوظيفة التي يؤديها - منطق الأعمال للتطبيق . لذلك ، لا تحتوي الطبقات على بنية خطية ، ولكن لها تسلسل هرمي . ومن هنا قاعدتان أخريان:

  • قاعدة الأولوية للطبقة الداخلية - هي الطبقة الداخلية التي تحدد الواجهة التي ستتفاعل من خلالها مع العالم الخارجي ؛
  • قاعدة التبعية - يجب توجيه التبعيات من الطبقة الداخلية إلى الخارجية.

القاعدة الأخيرة غير نمطية في عالم بايثون. لتطبيق أي سيناريو منطقي تجاري معقد ، تحتاج دائمًا إلى الوصول إلى الخدمات الخارجية (على سبيل المثال ، قاعدة بيانات) ، ولكن لتجنب هذه التبعية ، يجب أن تعلن طبقة منطق الأعمال نفسها الواجهة التي ستتفاعل من خلالها مع العالم الخارجي. تسمى هذه التقنية " عكس التبعية " (الحرف D في SOLID) وتستخدم على نطاق واسع في اللغات ذات الكتابة الثابتة. وفقا لروبرت مارتن ، هذه هي الميزة الرئيسية التي جاءت مع OOP .

هذه القواعد الثلاثة هي جوهر العمارة النظيفة:

  • قاعدة عبور الحدود ؛
  • قاعدة التبعية ؛
  • قاعدة الأولوية للطبقة الداخلية.

تشمل مزايا هذا النهج ما يلي:

  • سهولة الاختبار - يتم عزل الطبقات ، على التوالي ، ويمكن اختبارها بدون ترقيع قرد ، يمكنك ضبط الطلاء على طبقات مختلفة ، اعتمادًا على درجة أهميتها ؛
  • سهولة تغيير قواعد العمل ، حيث يتم جمعها جميعًا في مكان واحد ، ولا يتم توزيعها على المشروع ولا يتم خلطها مع رمز المستوى المنخفض ؛
  • الاستقلالية عن الوكلاء الخارجيين : يسمح لك وجود تجريدات بين منطق الأعمال والعالم الخارجي في حالات معينة بتغيير المصادر الخارجية دون التأثير على الطبقات الداخلية. يعمل إذا لم تربط منطق الأعمال بالميزات المحددة للوكلاء الخارجيين ، على سبيل المثال ، معاملات قاعدة البيانات ؛
  • , , , .

, . . , . « Clean Architecture».

Python


هذه نظرية ، يمكن العثور على أمثلة على التطبيق العملي في المقالة والتقارير الأصلية وكتاب روبرت مارتن. يعتمدون على العديد من أنماط التصميم الشائعة من عالم Java: المحول ، البوابة ، Interactor ، Fasade ، Repository ، DTO ، إلخ.

حسنًا ، ماذا عن Python؟ كما قلت ، فإن الاقتضاب يحظى بالتقدير في مجتمع بايثون ، فالذي ترسخ في الآخرين بعيد عن كونه سيتجذر معنا. في المرة الأولى التي انتقلت فيها إلى هذا الموضوع قبل ثلاث سنوات ، لم يكن هناك الكثير من المواد حول موضوع استخدام العمارة النظيفة في Python ، ولكن الرابط الأول في Google كان مشروع ليوناردو جيورداني: يصف المؤلف بالتفصيل عملية إنشاء واجهة برمجة تطبيقات لموقع بحث عن العقارات باستخدام طريقة TDD ، تطبيق العمارة النظيفة.
لسوء الحظ ، على الرغم من التفسير الدقيق واتباع جميع قوانين العم بوب ، فإن هذا المثال مخيف إلى حد ما

تتكون واجهة برمجة تطبيقات المشروع من طريقة واحدة - الحصول على قائمة بفلتر متاح. أعتقد أنه حتى بالنسبة للمطور المبتدئ ، فإن الرمز لمثل هذا المشروع لن يستغرق أكثر من 15 سطرًا. ولكن في هذه الحالة ، أخذ ست علب. يمكنك الرجوع إلى تخطيط غير ناجح تمامًا ، وهذا صحيح ، ولكن على أي حال ، من الصعب على شخص ما شرح فعالية هذا النهج ، في إشارة إلى هذا المشروع.

هناك مشكلة أكثر خطورة ، إذا كنت لا تقرأ المقالة وتبدأ على الفور في دراسة المشروع ، فمن الصعب جدًا فهمه. النظر في تنفيذ منطق الأعمال:

from rentomatic.response_objects import response_objects as res

class RoomListUseCase(object):
   def __init__(self, repo):
       self.repo = repo
   def execute(self, request_object):
       if not request_object:
           return res.ResponseFailure.build_from_invalid_request_object(
               request_object)
       try:
           rooms = self.repo.list(filters=request_object.filters)
           return res.ResponseSuccess(rooms)
       except Exception as exc:
           return res.ResponseFailure.build_system_error(
               "{}: {}".format(exc.__class__.__name__, "{}".format(exc)))

فئة RoomListUseCase التي تقوم بتطبيق منطق الأعمال (لا تشبه إلى حد كبير منطق الأعمال ، أليس كذلك؟) تتم تهيئة المشروع بواسطة كائن الريبو . ولكن ما هو الريبو ؟ بالطبع ، من السياق ، يمكننا أن نفهم أن الريبو يطبق قالب المستودع للوصول إلى البيانات ، إذا نظرنا إلى نص RoomListUseCase ، فإننا نفهم أنه يجب أن يكون لديك طريقة قائمة واحدة ، ومدخلها قائمة بالفلاتر ، وهي غير واضحة في الإخراج ، تحتاج إلى البحث في ResponseSuccess. وإذا كان السيناريو أكثر تعقيدًا ، مع وصول متعدد إلى مصدر البيانات؟ اتضح لفهم ما هو الريبو ، يمكنك فقط الرجوع إلى التنفيذ. لكن أين هي؟ يكمن في وحدة منفصلة ، والتي لا ترتبط بأي حال من الأحوال RoomListUseCase. وبالتالي ، لفهم ما يحدث ، تحتاج إلى الصعود إلى المستوى الأعلى (مستوى الإطار) ومعرفة ما يتم تغذيته لمدخلات الفصل عند إنشاء الكائن.

قد تعتقد أنني أسرد عيوب الكتابة الديناميكية ، لكن هذا ليس صحيحًا تمامًا. إنها كتابة ديناميكية تسمح لك بكتابة كود معبر ومضغوط . يتبادر إلى الذهن التشابه مع الخدمات الصغيرة ، عندما نقطع مجموعة متجانسة إلى خدمات صغيرة ، فإن التصميم يأخذ صلابة إضافية ، حيث يمكن القيام بأي شيء داخل الخدمة الصغيرة (PL ، الأطر ، الهندسة المعمارية) ، ولكن يجب أن يتوافق مع الواجهة المعلنة. لذا هنا: عندما قسمنا مشروعنا إلى طبقات ،يجب أن تكون العلاقات بين الطبقات متسقة مع العقد ، بينما داخل الطبقة ، يكون العقد اختياريًا. خلاف ذلك ، تحتاج إلى الاحتفاظ بسياق كبير إلى حد ما في رأسك. تذكر أن المشكلة في الشفرة السيئة هي التبعيات ، وبالتالي ، بدون واجهة صريحة ، نعود مرة أخرى إلى حيث أردنا الابتعاد - لعدم وجود علاقات صريحة بين السبب والنتيجة .

repo RoomListUseCase, execute — . - , , . - . , , , repo .

بشكل عام ، في ذلك الوقت تخليت عن الهندسة المعمارية النظيفة في مشروع جديد ، وأطبق مرة أخرى MVC الكلاسيكي. ولكن بعد أن ملأ الدفعة التالية من المخاريط ، عاد إلى هذه الفكرة بعد عام ، عندما بدأنا أخيرًا في إطلاق الخدمات في Python 3.5+. كما تعلمون، أحضر نوع شروح و دروس البيانات: أداتان قويتان لوصف الواجهة. بناءً على ذلك ، قمت برسم نموذج أولي للخدمة ، وكانت النتيجة بالفعل أفضل بكثير: توقفت الطبقات عن التشتت ، على الرغم من حقيقة أنه لا يزال هناك الكثير من التعليمات البرمجية ، خاصة عند الدمج مع الإطار. لكن هذا كان كافيًا لبدء تطبيق هذا النهج في المشاريع الصغيرة. بدأت الأطر تدريجيًا في الظهور والتي ركزت على الاستخدام الأقصى للتعليقات التوضيحية من النوع: apistar (الآن starlette) ، والإطارات المنصهرة. أصبحت حزمة pydantic / FastAPI شائعة الآن ، وأصبح التكامل مع هذه الأطر أسهل بكثير. هذا هو الشكل الذي سيبدو عليه المثال أعلاه من restomatic / services.py:

from typing import Optional, List
from pydantic import BaseModel

class Room(BaseModel):
   code: str
   size: int
   price: int
   latitude: float
   longitude: float

class RoomFilter(BaseModel):
   code: Optional[str] = None
   price_min: Optional[int] = None
   price_max: Optional[int] = None

class RoomStorage:
   def get_rooms(self, filters: RoomFilter) -> List[Room]: ...

class RoomListUseCase:
   def __init__(self, repo: RoomStorage):
       self.repo = repo
   def show_rooms(self, filters: RoomFilter) -> List[Room]:
       rooms = self.repo.get_rooms(filters=filters)
       return rooms

RoomListUseCase - فئة تنفذ منطق الأعمال للمشروع. يجب ألا تنتبه إلى حقيقة أن كل ما تفعله طريقة show_rooms هو استدعاء RoomStorage (لم أتوصل إلى هذا المثال). في الواقع ، يمكن أن يكون هناك أيضًا حساب خصم ، وترتيب قائمة استنادًا إلى الإعلانات ، وما إلى ذلك. ومع ذلك ، فإن الوحدة مكتفية ذاتيا. إذا أردنا استخدام هذا السيناريو في مشروع آخر ، فسيتعين علينا تنفيذ RoomStorage. وما هو مطلوب لذلك واضحًا بوضوح من الوحدة. على عكس المثال السابق ، فإن هذه الطبقة مكتفية ذاتيا ، وعند تغييرها ليس من الضروري مراعاة السياق بأكمله. من التبعيات غير النظامية فقط pydantic ، لماذا ، سيتضح في المكون الإضافي للإطار. لا تبعيات، طريقة أخرى لتحسين قراءة التعليمات البرمجية ، وليس سياق إضافي ، حتى مطور مبتدئ سيكون قادرًا على فهم ما تفعله هذه الوحدة.

لا يجب أن يكون سيناريو منطق الأعمال فئة ؛ فيما يلي مثال لسيناريو مماثل في شكل دالة:

def rool_list_use_case(filters: RoomFilter, repo: RoomStorage) -> List[Room]:
   rooms = repo.get_rooms(filters=filters)
   return rooms


وهنا العلاقة بالإطار:

from typing import List
from fastapi import FastAPI, Depends
from rentomatic import services, adapters
app = FastAPI()

def get_use_case() -> services.RoomListUseCase:
   return services.RoomListUseCase(adapters.MemoryStorage())

@app.post("/rooms", response_model=List[services.Room])
def rooms(filters: services.RoomFilter, use_case=Depends(get_use_case)):
   return use_case.show_rooms(filters)

باستخدام وظيفة get_use_case ، يقوم FastAPI بتنفيذ نمط حقن التبعية . لا داعي للقلق بشأن تسلسل البيانات ، يتم تنفيذ كل العمل بواسطة FastAPI بالاشتراك مع pydantic. لسوء الحظ ، لا تكون البيانات دائمًا تنسيق منطق عمل مناسب للبث المباشر في المطعم <وعلى العكس من ذلك ، لا يعرف منطق العمل من أين أتت البيانات - مع خطوط مائلة ونص طلب وملفات تعريف ارتباط وما إلى ذلك . في هذه الحالة ، سيحتوي جسم وظيفة الغرفة على تحويل معين لبيانات الإدخال والإخراج ، ولكن في معظم الحالات ، إذا عملنا مع واجهة برمجة التطبيقات ، فإن وظيفة الوكيل السهل هذه كافية. 

, , , RoomStorage. , 15 , , , .

لم أقم عن قصد بفصل طبقة منطق الأعمال ، كما يوحي نموذج الهندسة المعمارية النظيفة المتعارف عليه. كان من المفترض أن تكون فئة الغرفة في طبقة منطقة مجال الكيان تمثل منطقة المجال ، ولكن في هذا المثال لا توجد حاجة لذلك. من الجمع بين طبقات Entity و UseCase ، لا يتوقف المشروع عن تنفيذ بنية نظيفة. قال روبرت مارتن نفسه مرارًا وتكرارًا أن عدد الطبقات يمكن أن يتغير صعودًا ونزولًا. في الوقت نفسه ، يلبي المشروع المعايير الرئيسية للهندسة المعمارية النظيفة:

  • قاعدة عبور الحدود: نماذج pydantic ، والتي هي في الأساس DTOs ، تعبر الحدود ؛
  • قاعدة التبعية : طبقة منطق الأعمال مستقلة عن الطبقات الأخرى ؛
  • قاعدة الأولوية للطبقة الداخلية : إنها طبقة منطق الأعمال التي تحدد الواجهة (RoomStorage) ، التي يتفاعل من خلالها منطق الأعمال مع العالم الخارجي.

اليوم ، تعمل العديد من مشاريع فريقنا ، التي تم تنفيذها باستخدام النهج الموصوف ، على حث. أحاول تنظيم حتى أصغر الخدمات بهذه الطريقة. إنها تدرب جيدًا - أسئلة لم أفكر بها قبل طرحها. على سبيل المثال ، ما هو منطق الأعمال هنا؟ هذا بعيد عن الوضوح دائمًا ، على سبيل المثال ، إذا كنت تكتب نوعًا من البروكسي. نقطة أخرى مهمة هي تعلم التفكير بشكل مختلف.. عندما نحصل على مهمة ، نبدأ عادةً بالتفكير في الأطر ، والخدمات المستخدمة ، حول ما إذا كانت هناك حاجة لقائمة انتظار حيث من الأفضل تخزين هذه البيانات ، والتي يمكن تخزينها مؤقتًا. في النهج الذي يملي العمارة النظيفة ، يجب علينا أولاً تنفيذ منطق الأعمال ثم الانتقال فقط إلى تنفيذ التفاعل مع البنية التحتية ، لأنه ، وفقًا لروبرت مارتن ، فإن المهمة الرئيسية للهندسة المعمارية هي تأخير اللحظة التي يكون فيها الاتصال مع أي ستكون طبقة البنية التحتية جزءًا لا يتجزأ من تطبيقك.

بشكل عام ، أرى احتمالًا إيجابيًا لاستخدام العمارة النظيفة في Python. لكن النموذج ، على الأرجح ، سيكون مختلفًا بشكل كبير عن كيفية قبوله في PLS الأخرى. على مدى السنوات القليلة الماضية ، رأيت زيادة كبيرة في الاهتمام بموضوع الهندسة المعمارية في المجتمع. لذلك ، في PyCon الأخير كان هناك العديد من التقارير حول استخدام DDD ، ويجب ملاحظة الرجال من المختبرات الجافة بشكل منفصل . في شركتنا ، تقوم العديد من الفرق بالفعل بتنفيذ النهج الموصوف بدرجة أو بأخرى. كلنا نفعل الشيء نفسه ، لقد كبرنا ، نمت مشاريعنا ، يجب على مجتمع بايثون أن يعمل مع هذا ، ويحدد النمط واللغة المشتركة ، التي أصبحت ، على سبيل المثال ، لجميع جانغو.

All Articles