بيثون إدارة الاعتماد: مقارنة بين النهج

صورة

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

لسوء الحظ ، في مجتمع بيثون لا يوجد مفهوم عالمي عن "العمارة الجيدة" ، لا يوجد سوى مفهوم "بيثونية" ، لذلك علينا أن نتوصل إلى العمارة بأنفسنا. تحت القص - 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 # : client = APIClient() 

أو يمكن أن تكون مستقلة عن django:

 class APIClient: def __init__(self, api_key): self.api_key = api_key # : client = APIClient(api_key='abc') 

في الحالة الثانية ، يكون المُنشئ أكثر تعقيدًا ، ولكن يمكن إجراء أي تلاعب بهذه الفئة دون تحميل آلات 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 الخاصة بمختصر الخدمة:

 # shortener_client.py import requests class ShortenerClient: def __init__(self, api_key): self.api_key = api_key def shorten_link(self, url): response = requests.post( url='https://fstrk.cc/short', headers={'Authorization': self.api_key}, json={'url': url} ) return response.json()['url'] 

وهناك وحدة نمطية تقصر جميع الروابط في النص. للقيام بذلك ، يستخدم عميل API المقلل:

 # text_processor.py import re from shortener_client import ShortenerClient class TextProcessor: def __init__(self, text): self.text = text def process(self): changed_text = self.text links = re.findall( r'https?://[^\r\n\t") ]*', self.text, flags=re.MULTILINE ) api_client = ShortenerClient('abc') for link in links: shortened = api_client.shorten_link(link) changed_text = changed_text.replace(link, shortened) return changed_text 

يعيش منطق تنفيذ التعليمات البرمجية في ملف تحكم منفصل (دعنا نسميها وحدة تحكم):

 # controller.py from text_processor import TextProcessor processor = TextProcessor("""  1: https://ya.ru  2: https://google.com """) print(processor.process()) 

كل شيء يعمل. يوزع المعالج النص ، ويقصر الروابط باستخدام المقلل ، ويعيد النتيجة. التبعيات تبدو مثل هذا:

صورة

المشكلة


هذه هي المشكلة: تعتمد فئة 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 . سوف يساعد في التحقق من تواقيع الطرق الموروثة. للقيام بذلك ، يجب إضافة التعليقات التوضيحية إلى الواجهة:

 # shortener_client.py from abc import ABC, abstractmethod class AbstractClient(ABC): @abstractmethod def __init__(self, api_key: str) -> None: pass @abstractmethod def shorten_link(self, link: str) -> str: pass class ShortenerClient(AbstractClient): def __init__(self, api_key: str) -> None: self.api_key = api_key def shorten_link(self, link: str, callback_url: str) -> str: return 'xxx' 

إذا mypy الآن من هذه الشفرة باستخدام mypy ، mypy على خطأ بسبب وسيطة callback_url الإضافية:

 mypy shortener_client.py >>> error: Signature of "shorten_link" incompatible with supertype "AbstractClient" 

الآن لدينا طريقة موثوقة لارتكاب واجهة الفصل.

انعكاس التبعية


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

 # text_processor.py import re from abc import ABC, abstractmethod class AbstractClient(ABC): @abstractmethod def __init__(self, api_key: str) -> None: pass @abstractmethod def shorten_link(self, link: str) -> str: pass class TextProcessor: def __init__(self, text, shortener_client: AbstractClient) -> None: self.text = text self.shortener_client = shortener_client def process(self) -> str: changed_text = self.text links = re.findall( r'https?://[^\r\n\t") ]*', self.text, flags=re.MULTILINE ) for link in links: shortened = self.shortener_client.shorten_link(link) changed_text = changed_text.replace(link, shortened) return changed_text 

وهذا سوف يغير اتجاه الإدمان! الآن يمتلك TextProcessor واجهة التفاعل ، ونتيجة لذلك ، يعتمد ShortenerClient عليها ، وليس العكس.

صورة

بكلمات بسيطة ، يمكننا وصف جوهر تحولنا على النحو التالي:

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

مستهلكين متعددين


إذا استخدمت عدة وحدات روابط مختصرة ، فيجب وضع الواجهة ليس في واحدة منها ، ولكن في بعض الملفات المنفصلة ، الموجودة أعلى الملفات الأخرى ، أعلى في التسلسل الهرمي:

صورة

عنصر التحكم


إذا لم يستورد المستهلكون ShortenerClient ، فمن الذي سيقوم باستيراده وإنشاء كائن فئة؟ يجب أن يكون عنصر تحكم - في حالتنا هو controller.py .

الطريقة الأبسط هي حقن التبعية المباشرة ، حقن التبعية "في الجبهة". نقوم بإنشاء كائنات في رمز الاتصال ، ونقل كائن إلى آخر. الربح.

 # controller.py import TextProcessor import ShortenerClient processor = TextProcessor( text=' 1: https://ya.ru  2: https://google.com', shortener_client=ShortenerClient(api_key='123') ) print(processor.process()) 

بيثون النهج


ويعتقد أن أكثر النهج "الثعبان" ليكون Dependency Injection من خلال الميراث.

يتحدث ريموند هيتنجر عن هذا بتفصيل كبير في تقريره Super Super.

لتكييف التعليمات البرمجية مع هذا النمط ، تحتاج إلى تغيير TextProcessor قليلاً ، مما يجعله TextProcessor للتوريث:

 # text_processor.py class TextProcessor: def __init__(self, text: str) -> None: self.text = text self.shortener_client: AbstractClient = self.get_shortener_client() def get_shortener_client(self) -> AbstractClient: """      """ raise NotImplementedError 

ثم ، في رمز الاتصال ، ورثها:

 # controller.py import TextProcessor import ShortenerClient class ProcessorWithClient(TextProcessor): """   ,    """ def get_shortener_client(self) -> ShortenerClient: return ShortenerClient(api_key='abc') processor = ProcessorWithClient( text=' 1: https://ya.ru  2: https://google.com' ) print(processor.process()) 

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

  • في جانغو ، نحن ورثنا باستمرار. نقوم بإعادة تعريف طرق العرض والنماذج والأشكال القائمة على الفصل ؛ بمعنى آخر ، حقن تبعياتنا في العمل الذي تم تصحيحه بالفعل للإطار.
  • في 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 :

 # text_processor.py import INSTANCE_DICT class TextProcessor(AbstractTextProcessor): def __init__(self, text) -> None: self.text = text def process(self) -> str: shortener_client: AbstractClient = INSTANCE_DICT['Shortener'] # ...   

 # text_pipeline.py import INSTANCE_DICT class TextPipeline: def __init__(self) -> None: self.text_processor: AbstractTextProcessor = INSTANCE_DICT[ 'TextProcessor'] self.mailer: AbstractMailer = INSTANCE_DICT['Mailer'] def process_and_mail(self) -> None: processed_text = self.text_processor.process() self.mailer.send_text(text=processed_text) 

الحيلة هي وضع كائناتنا في هذا القاموس قبل الوصول إليها . هذا هو ما سنفعله في controller.py :

 # controller.py import INSTANCE_DICT import TextProcessor import ShortenerClient import Mailer import TextPipeline INSTANCE_DICT['Shortener'] = ShortenerClient('123') INSTANCE_DICT['Mailer'] = Mailer('abc@def.com') INSTANCE_DICT['TextProcessor'] = TextProcessor(text=' : https://ya.ru') pipeline = TextPipeline() pipeline.process_and_mail() 

مزايا العمل من خلال قاموس عالمي:

  • لا يوجد غطاء محرك سحري وأطر DI إضافية ؛
  • قائمة ثابتة من التبعيات التي لا تحتاج إلى إدارة التعشيش ؛
  • جميع مكافآت DI: اختبار بسيط ، استقلال ، حماية المكونات من الأعطال عند تغيير المكونات الأخرى.

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

ربما لن يكون ذلك كافياً بالنسبة لي في وقت ما ، وما زلت أختر نوعًا ما من الإطار.

وربما ، كل هذا غير ضروري ، ومن الأسهل الاستغناء عنه: كتابة واردات مباشرة وعدم إنشاء واجهات تجريدية غير ضرورية.

ما هي تجربتك مع إدارة التبعية في بيثون؟ بشكل عام - هل هو ضروري ، أم هل أنا اخترع مشكلة من الجو؟

Source: https://habr.com/ru/post/ar461511/


All Articles