
لقد كتبت في بيثون لمدة خمس سنوات ، منها السنوات الثلاث الماضية تم تطوير مشروعي الخاص. معظم هذه الطريقة يساعدني فريقي في ذلك. ومع كل إصدار ، مع كل ميزة جديدة ، نحاول بشكل متزايد التأكد من أن المشروع لا يتحول إلى فوضى من التعليمات البرمجية غير المدعومة ؛ نحن نكافح مع الواردات الدورية ، التبعيات المتبادلة ، وتخصيص وحدات قابلة لإعادة الاستخدام ، وإعادة بناء الهيكل.
لسوء الحظ ، في مجتمع بيثون لا يوجد مفهوم عالمي عن "العمارة الجيدة" ، لا يوجد سوى مفهوم "بيثونية" ، لذلك علينا أن نتوصل إلى العمارة بأنفسنا. تحت القص - Longrid مع انعكاسات على الهندسة المعمارية ، وقبل كل شيء - تنطبق إدارة التبعية على بيثون.
django.setup ()
سأبدأ بسؤال لل dzhangists. هل غالبا ما تكتب هذين الخطين؟
import django django.setup()
تحتاج إلى بدء تشغيل الملف من هذا إذا كنت ترغب في العمل مع كائنات django دون بدء تشغيل خادم الويب django نفسه. ينطبق هذا على الطرز وأدوات العمل مع الوقت (
django.utils.timezone
) ،
django.urls.reverse
(
django.urls.reverse
) ، وأكثر من ذلك بكثير. إذا لم يتم ذلك ، فستتلقى خطأ:
django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.
أنا أكتب باستمرار هذين الخطين. أنا معجب كبير برمز القذف ؛ أحب إنشاء ملف
.py
منفصل ،
.py
الأشياء به ، ثم اكتشفه - ثم قم بتضمينه في المشروع.
وهذا
django.setup()
المستمر يزعجني كثيرًا. أولاً ، سئمت من تكرارها في كل مكان ؛ وثانياً ، تستغرق تهيئة django عدة ثوان (لدينا مجموعة كبيرة) ، وعند إعادة تشغيل نفس الملف 10 و 20 و 100 مرة - يبطئ التطوير فقط.
كيف تتخلص من
django.setup()
؟ تحتاج إلى كتابة التعليمات البرمجية التي تعتمد الحد الأدنى على django.
على سبيل المثال ، إذا كتبنا عميلًا لواجهة برمجة تطبيقات خارجية ، فيمكننا جعله يعتمد على django:
from django.conf import settings class APIClient: def __init__(self): self.api_key = settings.SOME_API_KEY
أو يمكن أن تكون مستقلة عن django:
class APIClient: def __init__(self, api_key): self.api_key = api_key
في الحالة الثانية ، يكون المُنشئ أكثر تعقيدًا ، ولكن يمكن إجراء أي تلاعب بهذه الفئة دون تحميل آلات dzhangovskoy بأكملها.
الاختبارات تزداد سهولة أيضًا. كيفية اختبار مكون يعتمد على إعدادات
django.conf.settings
؟ مجرد قفل لهم مع ديكور
@override_settings
. وإذا كان المكون لا يعتمد على أي شيء ، فلن يكون هناك شيء لتبلله: لقد نقل المعلمات إلى المُنشئ - وقادها.
إدارة التبعية
تعد قصة التبعية في
django
هي المثال الأكثر وضوحا للمشكلة التي أواجهها كل يوم: مشكلات إدارة التبعية في بيثون - والهندسة الشاملة لتطبيقات بيثون.
العلاقة مع إدارة التبعية في مجتمع بيثون مختلطة. يمكن تمييز ثلاثة معسكرات رئيسية:
- بيثون هي لغة مرنة. نكتب كما نريد ، وهذا يتوقف على ما نريد. نحن لا نخجل من التبعيات الدورية ، واستبدال السمات للفئات في وقت التشغيل ، إلخ.
- بيثون هي لغة خاصة. هناك طرق اصطلاحية لبناء العمارة والتبعيات. يتم تنفيذ نقل البيانات لأعلى ولأسفل مكدس الاستدعاءات من قبل التكرارات ، coroutines ، ومديري السياق.
تقرير الفصل حول هذا الموضوع والمثالبراندون رودس ، دروببوإكس:
يرفعون IO الخاص بك .
مثال من التقرير:
def main(): """ """ with open("/etc/hosts") as file: for line in parse_hosts(file): print(line) def parse_hosts(lines): """ - """ for line in lines: if line.startswith("#"): continue yield line
- مرونة بيثون هي وسيلة إضافية لإطلاق النار على قدمك. تحتاج إلى مجموعة صارمة من القواعد لإدارة التبعيات. مثال جيد على ذلك هو الرجال الروس الثعبان الجاف . لا يزال هناك نهج أقل المتشددين - هيكل Django للحجم وطول العمر ، ولكن الفكرة هي نفسها.
هناك العديد من المقالات حول إدارة التبعية في python (
مثال 1 ،
مثال 2 ) ، لكنها جميعًا تنشر إعلانات أطر عمل Dependency Injection لشخص ما. هذه المقالة مقدمة جديدة في نفس الموضوع ، لكنها هذه المرة تجربة فكرية خالصة دون الإعلان. هذه محاولة لإيجاد توازن بين الطرق الثلاثة المذكورة أعلاه ، الاستغناء عن إطار إضافي وجعلها "ثورية".
لقد قرأت مؤخرًا
الهندسة النظيفة - ويبدو أنني أفهم ما هي قيمة حقن التبعية في الثعبان وكيف يمكن تنفيذه. رأيت هذا على مثال مشروعي الخاص. باختصار ، هذا
يحمي الشفرة من الانكسار عند تغيير شفرة أخرى .
مصدر البيانات
يوجد عميل API ينفذ طلبات HTTP الخاصة بمختصر الخدمة:
وهناك وحدة نمطية تقصر جميع الروابط في النص. للقيام بذلك ، يستخدم عميل API المقلل:
يعيش منطق تنفيذ التعليمات البرمجية في ملف تحكم منفصل (دعنا نسميها وحدة تحكم):
كل شيء يعمل. يوزع المعالج النص ، ويقصر الروابط باستخدام المقلل ، ويعيد النتيجة. التبعيات تبدو مثل هذا:

المشكلة
هذه هي المشكلة: تعتمد فئة
TextProcessor
على فئة
ShortenerClient
- وتتوقف
عند تغيير واجهة ShortenerClient
.
كيف يمكن أن يحدث هذا؟
لنفترض في مشروعنا أننا قررنا تتبع نسبة
shorten_link
وسيطة
callback_url
إلى طريقة
shorten_link
. تعني هذه الوسيطة العنوان الذي يجب أن تأتي إليه الإعلامات عند النقر فوق ارتباط.
بدأت طريقة
ShortenerClient.shorten_link
لتبدو كما يلي:
def shorten_link(self, url, callback_url): response = requests.post( url='https://fstrk.cc/short', headers={'Authorization': self.api_key}, json={'url': url, 'callback_on_click': callback_url} ) return response.json()['url']
وماذا يحدث؟ واتضح أننا عندما نحاول البدء ، حصلنا على خطأ:
TypeError: shorten_link() missing 1 required positional argument: 'callback_url'
وهذا هو ، قمنا بتغيير المقلل ، ولكن لم يكن هو الذي كسر ، ولكن موكله:

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

تجميد الواجهة
كيف يبدو إصلاح واجهة في الثعبان؟ هذه فئة مجردة:
from abc import ABC, abstractmethod class AbstractClient(ABC): @abstractmethod def __init__(self, api_key): pass @abstractmethod def shorten_link(self, link): pass
إذا نرث الآن من هذه الفئة وننسى أن ننفذ بعض الطرق ، فسوف نحصل على خطأ:
class ShortenerClient(AbstractClient): def __ini__(self, api_key): self.api_key = api_key client = ShortenerClient('123') >>> TypeError: Can't instantiate abstract class ShortenerClient with abstract methods __init__, shorten_link
لكن هذا لا يكفي. يلتقط الفصل التجريدي أسماء الطرق فقط ، ولكن ليس توقيعها.
تحتاج إلى أداة التحقق من التوقيع الثاني هذه الأداة الثانية هي
mypy
. سوف يساعد في التحقق من تواقيع الطرق الموروثة. للقيام بذلك ، يجب إضافة التعليقات التوضيحية إلى الواجهة:
إذا
mypy
الآن من هذه الشفرة باستخدام
mypy
،
mypy
على خطأ بسبب وسيطة
callback_url
الإضافية:
mypy shortener_client.py >>> error: Signature of "shorten_link" incompatible with supertype "AbstractClient"
الآن لدينا طريقة موثوقة لارتكاب واجهة الفصل.
انعكاس التبعية
بعد تصحيح الأخطاء في الواجهة ، يجب أن ننقلها إلى مكان آخر من أجل التخلص تمامًا من اعتماد المستهلك على ملف
shortener_client.py
. على سبيل المثال ، يمكنك سحب الواجهة مباشرة إلى المستهلك - إلى ملف باستخدام معالج
TextProcessor
:
وهذا سوف يغير اتجاه الإدمان! الآن يمتلك
TextProcessor
واجهة التفاعل ، ونتيجة لذلك ، يعتمد
ShortenerClient
عليها ، وليس العكس.

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

عنصر التحكم
إذا لم يستورد المستهلكون
ShortenerClient
، فمن الذي سيقوم باستيراده وإنشاء كائن فئة؟ يجب أن يكون عنصر تحكم - في حالتنا هو
controller.py
.
الطريقة الأبسط هي حقن التبعية المباشرة ، حقن التبعية "في الجبهة". نقوم بإنشاء كائنات في رمز الاتصال ، ونقل كائن إلى آخر. الربح.
بيثون النهج
ويعتقد أن أكثر النهج "الثعبان" ليكون Dependency Injection من خلال الميراث.
يتحدث ريموند هيتنجر عن هذا بتفصيل كبير في تقريره Super Super. لتكييف التعليمات البرمجية مع هذا النمط ، تحتاج إلى تغيير
TextProcessor
قليلاً ، مما يجعله
TextProcessor
للتوريث:
ثم ، في رمز الاتصال ، ورثها:
المثال الثاني موجود في كل مكان في الأطر الشعبية:
- في جانغو ، نحن ورثنا باستمرار. نقوم بإعادة تعريف طرق العرض والنماذج والأشكال القائمة على الفصل ؛ بمعنى آخر ، حقن تبعياتنا في العمل الذي تم تصحيحه بالفعل للإطار.
- في DRF ، نفس الشيء. نحن بصدد توسيع وجهات النظر ، والمتسللين ، والأذونات.
- و هكذا. هناك الكثير من الأمثلة.
المثال الثاني يبدو أجمل وأكثر دراية ، أليس كذلك؟ دعونا تطويره ومعرفة ما إذا كان هذا الجمال هو الحفاظ عليها.
بيثون التنمية
في منطق الأعمال ، يوجد عادة أكثر من مكونين. افترض أن
TextProcessor
بنا ليست فئة مستقلة ، ولكن عنصر واحد فقط من عناصر
TextPipeline
يعالج النص ويرسله إلى البريد:
class TextPipeline: def __init__(self, text, email): self.text_processor = TextProcessor(text) self.mailer = Mailer(email) def process_and_mail(self) -> None: processed_text = self.text_processor.process() self.mailer.send_text(text=processed_text)
إذا أردنا عزل
TextPipeline
عن الفئات المستخدمة ، يجب أن نتبع نفس الإجراء كما كان من قبل:
- فئة
TextPipeline
ستعلن واجهات للمكونات المستخدمة ؛ - سيتم إجبار المكونات المستخدمة على التوافق مع هذه الواجهات ؛
- بعض الرموز الخارجية ستضع كل شيء معًا وتعمل.
سيبدو مخطط التبعية كما يلي:

ولكن كيف سيبدو رمز التجميع لهذه التبعيات الآن؟
import TextProcessor import ShortenerClient import Mailer import TextPipeline class ProcessorWithClient(TextProcessor): def get_shortener_client(self) -> ShortenerClient: return ShortenerClient(api_key='123') class PipelineWithDependencies(TextPipeline): def get_text_processor(self, text: str) -> ProcessorWithClient: return ProcessorWithClient(text) def get_mailer(self, email: str) -> Mailer: return Mailer(email) pipeline = PipelineWithDependencies( email='abc@def.com', text=' 1: https://ya.ru 2: https://google.com' ) pipeline.process_and_mail()
هل لاحظت؟ نرث أولاً فئة
TextProcessor
لإدراج
ShortenerClient
فيه ، ثم نرث
TextPipeline
لإدراج
TextProcessor
تعريفه (وكذلك
Mailer
) فيه. لدينا عدة مستويات من إعادة التعريف المتسلسل. معقدة بالفعل.
لماذا يتم تنظيم جميع الأطر بهذه الطريقة؟
نعم ، لأنها مناسبة فقط للأطر.- جميع مستويات الإطار محددة بوضوح ، وعددها محدود. على سبيل المثال ، في Django ، يمكنك تجاوز
FormField
لإدراجه في تجاوز Form
، لإدراج نموذج في تخطي View
. هذا كل شيء. ثلاثة مستويات. - يخدم كل إطار غرض واحد. هذه المهمة محددة بوضوح.
- يحتوي كل إطار على وثائق مفصلة تصف كيف وماذا سيرث ؛ ماذا ومع ما الجمع.
يمكنك بوضوح وبشكل لا لبس فيه تحديد وتوثيق منطق عملك؟ وخاصة بنية المستويات التي تعمل فيها؟ انا لا. لسوء الحظ ، فإن نهج ريموند هيتنجر لا يتناسب مع منطق الأعمال.
العودة إلى نهج الجبهة
في عدة مستويات من الصعوبة ، يفوز نهج بسيط. يبدو أكثر بساطة - وأسهل للتغيير عندما يتغير المنطق.
import TextProcessor import ShortenerClient import Mailer import TextPipeline pipeline = TextPipeline( text_processor=TextProcessor( text=' 1: https://ya.ru 2: https://google.com', shortener_client=ShortenerClient(api_key='abc') ), mailer=Mailer('abc@def.com') ) pipeline.process_and_mail()
لكن عندما يزداد عدد مستويات المنطق ، يصبح مثل هذا النهج غير مريح. علينا أن نبدأ مجموعة من الفصول بشكل حتمي ، وننقلهم إلى بعضهم البعض. أريد أن تجنب العديد من مستويات التعشيش.
دعونا نجرب مكالمة أخرى.
تخزين مثيل عالمي
دعونا نحاول إنشاء قاموس عالمي تكمن فيه مثيلات المكونات التي نحتاج إليها. ودع هذه المكونات تحصل على بعضها البعض من خلال الوصول إلى هذا القاموس.
دعنا نسميها
INSTANCE_DICT
:
الحيلة هي
وضع كائناتنا في هذا القاموس قبل الوصول إليها . هذا هو ما سنفعله في
controller.py
:
مزايا العمل من خلال قاموس عالمي:
- لا يوجد غطاء محرك سحري وأطر DI إضافية ؛
- قائمة ثابتة من التبعيات التي لا تحتاج إلى إدارة التعشيش ؛
- جميع مكافآت DI: اختبار بسيط ، استقلال ، حماية المكونات من الأعطال عند تغيير المكونات الأخرى.
بالطبع ، بدلاً من إنشاء
INSTANCE_DICT
، يمكنك استخدام نوع من إطار عمل DI ؛ لكن جوهر هذا لن يتغير. سيوفر الإطار إدارة أكثر مرونة للحالات ؛ سيسمح لك بإنشائها في شكل أحرف مفردة أو حزم ، مثل المصنع ؛ لكن الفكرة ستبقى كما هي.
ربما لن يكون ذلك كافياً بالنسبة لي في وقت ما ، وما زلت أختر نوعًا ما من الإطار.
وربما ، كل هذا غير ضروري ، ومن الأسهل الاستغناء عنه: كتابة واردات مباشرة وعدم إنشاء واجهات تجريدية غير ضرورية.
ما هي تجربتك مع إدارة التبعية في بيثون؟ بشكل عام - هل هو ضروري ، أم هل أنا اخترع مشكلة من الجو؟