مرحبا بالجميع!
أخيراً لدينا عقد لتحديث كتاب مارك سيمان "
Dependency Injection في .NET " - الشيء الرئيسي هو أنه أنهيه في أقرب وقت ممكن. لدينا أيضًا
كتاب في محرر Dinesh Rajput المحترم حول أنماط التصميم في Spring 5 ، حيث يتم تخصيص أحد الفصول أيضًا لتنفيذ التبعيات.
لطالما كنا نبحث عن مواد مثيرة للاهتمام ستذكر نقاط القوة في نموذج DI وتوضح اهتمامنا بها - والآن تم العثور عليها. صحيح ، فضل المؤلف أن يعطي أمثلة في Go. نأمل أن لا يمنعك هذا من متابعة أفكاره ويساعد على فهم المبادئ العامة للتحكم في الانقلاب والعمل مع الواجهات ، إذا كان هذا الموضوع قريبًا منك.
يكون اللون الأصلي العاطفي أكثر هدوءًا قليلاً ، ويتم تقليل عدد علامات التعجب في الترجمة. هل لديك قراءة لطيفة!
يعد استخدام
الواجهات تقنية مفهومة تسمح لك بإنشاء رمز يسهل اختباره وقابل للتوسيع بسهولة. لقد كنت مقتنعًا مرارًا وتكرارًا بأن هذه هي أقوى أداة لتصميم الهندسة المعمارية على الإطلاق.
الغرض من هذه المقالة هو شرح الواجهات وكيف يتم استخدامها وكيف توفر قابلية التوسعة واختبار الشفرة. أخيرًا ، يجب أن توضح المقالة كيف يمكن للواجهات أن تساعد في تحسين إدارة تسليم البرامج وتبسيط التخطيط!
واجهاتتصف الواجهة العقد. اعتمادًا على اللغة أو الإطار ، قد يتم إملاء استخدام الواجهات بشكل صريح أو ضمني. لذلك ، في لغة Go ،
يتم إملاء الواجهات بشكل صريح . إذا حاولت استخدام كيان كواجهة ، لكنه لن يكون متسقًا تمامًا مع قواعد هذه الواجهة ، فسيحدث خطأ في وقت الترجمة. على سبيل المثال ، عند تنفيذ المثال أعلاه ، نحصل على الخطأ التالي:
prog.go:22:85: cannot use BadPricer literal (type BadPricer) as type StockPricer in argument to isPricerHigherThan100: BadPricer does not implement StockPricer (missing CurrentPrice method) Program exited.
الواجهات هي أداة للمساعدة على فصل المتصل من المستدعي ، ويتم ذلك باستخدام العقد.
لنقم بتجسيد هذه المشكلة باستخدام مثال لبرنامج التداول الآلي للصرافة. سيتم استدعاء برنامج التاجر بسعر شراء محدد ورمز مؤشر. ثم يذهب البرنامج إلى التبادل لمعرفة الاقتباس الحالي لهذا المؤشر. علاوة على ذلك ، إذا لم يتجاوز سعر الشراء لهذا المؤشر السعر المحدد ، فسيقوم البرنامج بإجراء عملية شراء.

في شكل مبسط ، يمكن تمثيل بنية هذا البرنامج على النحو التالي. من المثال أعلاه ، من الواضح أن عملية الحصول على السعر الحالي تعتمد بشكل مباشر على بروتوكول HTTP ، الذي يقوم البرنامج من خلاله بالاتصال بخدمة التبادل.
تعتمد حالة
Action
أيضًا بشكل مباشر على HTTP. وبالتالي ، يجب على كلتا الدولتين أن تفهم تمامًا كيفية استخدام HTTP لاستخراج بيانات التبادل و / أو إكمال المعاملات.
إليك ما قد يبدو عليه التنفيذ:
func analyze(ticker string, maxTradePrice float64) (bool, err) { resp, err := http.Get( "http://stock-service.com/currentprice/" + ticker ) if err != nil {
هنا ، يعتمد المتصل (
analyze
) بشكل مباشر على HTTP. إنها بحاجة إلى معرفة كيفية صياغة طلبات HTTP. كيف يتم تحليلها. كيفية معالجة عمليات إعادة المحاولة ، والمهلة ، والمصادقة ، وما إلى ذلك. لديها
قبضة وثيقة على
http
.
كلما نسمي التحليل ، يجب علينا أيضًا استدعاء مكتبة http
.
كيف تساعدنا الواجهة هنا؟ في العقد الذي توفره الواجهة ، يمكنك وصف
السلوك ، بدلاً من
التنفيذ المحدد.
type StockExchange interface { CurrentPrice(ticker string) float64 }
ما سبق يحدد مفهوم
StockExchange
. تقول هنا أن
StockExchange
يدعم استدعاء وظيفة
CurrentPrice
الوحيدة. تبدو لي هذه الخطوط الثلاثة أقوى تقنية معمارية على الإطلاق. فهي تساعدنا على التحكم في تبعيات التطبيق بثقة أكبر. تقديم الاختبار. توفير القابلية للتوسعة.
حقن التبعيةلفهم قيمة الواجهات تمامًا ، تحتاج إلى إتقان تقنية تسمى "حقن التبعية".
حقن التبعية يعني أن المتصل يوفر شيئًا يحتاجه المتصل. عادة ما يبدو هذا: يقوم المتصل بتكوين الكائن ، ثم يمررها إلى المستدعى. ثم يلخص الطرف المطلوب من التكوين والتنفيذ. في هذه الحالة ، هناك وساطة معروفة. ضع في اعتبارك طلبًا لخدمة HTTP Rest. لتنفيذ العميل ، نحتاج إلى استخدام مكتبة HTTP التي يمكنها صياغة طلبات HTTP وإرسالها واستلامها.
إذا وضعنا طلب HTTP خلف الواجهة ، فقد يتم فصل المتصل ، ولن تكون على علم بأن طلب HTTP حدث بالفعل.
يجب على المتصل إجراء مكالمة دالة عامة فقط. يمكن أن تكون هذه مكالمة محلية ، مكالمة عن بعد ، مكالمة HTTP ، مكالمة RPC ، إلخ. لا تدرك المتصل ما يحدث ، وعادة ما يناسبها تمامًا ، طالما أنها تحصل على النتائج المتوقعة. يوضح ما يلي شكل حقن التبعية في طريقة
analyze
بنا.
func analyze(se StockExchange, ticker string, maxTradePrice float64) (bool, error) { currentPrice := se.CurrentPrice(ticker) var hasTraded bool var err error if currentPrice <= maximumTradePrice { err = doTrade(ticker, currentPrice) if err == nil { hasTraded = true } } return hasTraded, err }
لا أتوقف أبدًا عن دهشتي مما يحدث هنا. قمنا بعكس شجرة التبعية تمامًا وبدأنا في التحكم بشكل أفضل في البرنامج بأكمله. علاوة على ذلك ، حتى من الناحية المرئية ، أصبح التنفيذ بأكمله أكثر نظافة وأكثر قابلية للفهم. نرى بوضوح أن طريقة التحليل يجب أن تختار السعر الحالي ، وتحقق مما إذا كان هذا السعر مناسبًا لنا ، وإذا كان الأمر كذلك ، فقم بإجراء صفقة.
الأهم من ذلك ، في هذه الحالة نقوم بفصل المتصل من المتصل. نظرًا لأنه يتم فصل المتصل والتطبيق بالكامل عن ما يسمى باستخدام الواجهة ، يمكنك توسيع الواجهة عن طريق إنشاء العديد من التطبيقات المختلفة لها. تسمح لك الواجهات بإنشاء العديد من عمليات التنفيذ المحددة المختلفة دون الحاجة إلى تغيير رمز الطرف المطلوب!

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

الإنتاج
تطبيق HTTP الأصلي موجود بالفعل في تنفيذ
analyze
الأساسي ؛ كل ما تبقى لنا هو استخراجه وتغليفه خلف تطبيق ملموس للواجهة.
type HTTPStockExchange struct {} func (se HTTPStockExchange) CurrentPrice(ticker string) float64 { resp, err := http.Get( "http://stock-service.com/currentprice/" + ticker ) if err != nil {
الشفرة التي
StockExchange
سابقًا بوظيفة التحليل هي الآن مستقلة تمامًا
StockExchange
بواجهة
StockExchange
، أي يمكننا الآن تمريرها
analyze
. كما تتذكر من الرسوم البيانية أعلاه ، لم يعد التحليل مرتبطًا بتبعية HTTP. باستخدام الواجهة ، لا "يتخيل"
analyze
ما يحدث خلف الكواليس. إنه يعرف فقط أنه سيتم ضمان إعطائه كائنًا يمكنه الاتصال به
CurrentPrice
.
أيضا هنا نستفيد من المزايا النموذجية للتغليف. في السابق ، عندما تم ربط طلبات http للتحليل ، كانت الطريقة الوحيدة للتواصل مع التبادل عبر http غير مباشرة - من خلال طريقة
analyze
. نعم ، يمكننا تغليف هذه المكالمات في وظائف وتنفيذ الوظيفة بشكل مستقل ، لكن الواجهات تجبرنا على فصل المتصل عن المتصل. الآن يمكننا اختبار
HTTPStockExchange
بغض النظر عن المتصل. يؤثر هذا بشكل أساسي على نطاق اختباراتنا وكيف نفهم ونستجيب لفشل الاختبار.
الاختبارفي الكود الحالي ، لدينا بنية
HTTPStockService
، والتي تسمح لنا بالتأكد بشكل منفصل من أنه يمكن التواصل مع خدمة التبادل وتحليل الردود المتلقاة منها. ولكن الآن دعونا نتأكد من أن التحليل يمكن أن يتعامل بشكل صحيح مع الاستجابة من واجهة
StockExchange
، علاوة على ذلك ، أن هذه العملية موثوقة وقابلة للتكرار.
currentPrice := se.CurrentPrice(ticker) if currentPrice <= maxTradePrice { err := doTrade(ticker, currentPrice) }
يمكننا استخدام التطبيق مع HTTP ، ولكن سيكون له العديد من العيوب. قد يكون إجراء مكالمات الشبكة في اختبار الوحدة بطيئًا ، خاصة بالنسبة للخدمات الخارجية. بسبب التأخيرات واتصال الشبكة غير المستقر ، قد تتحول الاختبارات إلى عدم موثوقية. بالإضافة إلى ذلك ، إذا احتجنا إلى اختبارات مع بيان أنه يمكننا إتمام المعاملة ، واختبارًا ببيان أنه يمكننا تصفية الحالات التي لا ينبغي إبرام المعاملة فيها ، فسيكون من الصعب العثور على بيانات إنتاج حقيقية تفي بموثوقية هذه الشروط. يمكن للمرء أن يختار
maxTradePrice
، يقلد بشكل مصطنع كل من الشروط بهذه الطريقة ، على سبيل المثال ، مع
maxTradePrice := -100
لا يجب إتمام المعاملة ، و
maxTradePrice := 10000000
يجب أن ينتهي مع المعاملة.
ولكن ماذا يحدث إذا تم تخصيص حصة معينة لنا في خدمة التبادل؟ أو إذا كان علينا أن ندفع؟ هل سندفع بالفعل (ويجب علينا) دفع أو إنفاق حصتنا عندما يتعلق الأمر باختبارات الوحدات؟ من الناحية المثالية ، يجب تشغيل الاختبارات بقدر الإمكان ، لذلك يجب أن تكون سريعة ورخيصة وموثوقة. أعتقد من هذه الفقرة أنه من الواضح لماذا استخدام إصدار مع HTTP خالص غير منطقي من حيث الاختبار!
هناك طريقة أفضل ، وتتضمن استخدام الواجهات!بوجود واجهة ، يمكنك تصنيع تطبيق
StockExchange
بعناية ، مما سيسمح لنا
analyze
بسرعة وأمان وموثوقية.
type StubExchange struct { Price float64 } func (se StubExchange) CurrentPrice(ticker string) float64 { return se.Price } func TestAnalyze_MakeTrade(t *testing.T) { se := StubExchange{Price: 10} maxTradePrice := 11 traded, err := analyze(se, "TSLA", maxTradePrice) if err != nil { t.Errorf("expected err == nil received: %s", err) } if !traded { t.Error("expected traded == true") } } func TestAnalyze_DontTrade(t *testing.T) { se := StubExchange{Price: 10} maxTradePrice := 9 traded, err := analyze(se, "TSLA", maxTradePrice)
يتم استخدام كعب خدمة التبادل أعلاه ، وبفضل ذلك تم إطلاق الفرع الذي يهمنا في
analyze
. بعد ذلك ، يتم إجراء عبارات في كل اختبار للتأكد من أن التحليل يفعل ما هو مطلوب. على الرغم من أن هذا برنامج اختبار ، فإن تجربتي تشير إلى أن المكونات / الهندسة المعمارية ، حيث يتم استخدام الواجهات بهذه الطريقة تقريبًا ، يتم اختبارها بهذه الطريقة من أجل المتانة في رمز المعركة أيضًا !!! بفضل الواجهات ، يمكننا استخدام
StockExchange
التحكم فيه في الذاكرة ، والذي يوفر اختبارات موثوقة وسهلة التكوين وسهلة الفهم
StockExchange
للتكرار وسريعة البرق !!!
Unpin - تكوين المتصلالآن بعد أن ناقشنا كيفية استخدام الواجهات لفصل المتصل من المستدعي ، وكيفية تنفيذ عمليات متعددة ، ما زلنا لم نتطرق إلى جانب بالغ الأهمية. كيفية تكوين وتقديم تنفيذ محدد في وقت محدد بدقة؟ يمكنك استدعاء وظيفة التحليل مباشرة ، ولكن ماذا تفعل في تكوين الإنتاج؟
هذا هو المكان الذي يأتي فيه تنفيذ التبعيات في متناول اليدين.
func main() { var ticker = flag.String("ticker", "", "stock ticker symbol to trade for") var maxTradePrice = flag.Float64("maxtradeprice", "", "max price to pay for a share of the ticker symbol." se := HTTPStockExchange{} analyze(se, *ticker, *maxTradePrice) }
تمامًا كما هو الحال في حالة الاختبار الخاصة بنا ، يتم تكوين التطبيق المحدد المحدد لـ StockExchange الذي سيتم استخدامه مع
analyze
قبل المتصل خارج التحليل. ثم يتم تمريره (حقنه)
analyze
. هذا يضمن أن تحليل NOTHING معروف عن كيفية تكوين
HTTPStockExchange
. ربما نود توفير نطاق http الذي سنستخدمه في شكل إشارة سطر الأوامر ، ومن ثم لن يتغير التحليل. أو ماذا نفعل إذا كنا بحاجة إلى تقديم نوع من المصادقة أو الرمز المميز للوصول إلى
HTTPStockExchange
، والذي سيتم استخراجه من البيئة؟ مرة أخرى ، لا يجب أن يتغير التحليل.
يتم التكوين على مستوى خارج
analyze
، وبالتالي تحرير التحليل تمامًا من الحاجة إلى تكوين تبعياته الخاصة. وبالتالي ، يتم تحقيق فصل صارم للواجبات.
قرارات رفوفربما تكون الأمثلة المذكورة أعلاه كافية تمامًا ، ولكن لا تزال هناك العديد من المزايا الأخرى للواجهات وحقن التبعية. تسمح الواجهات بتأجيل اتخاذ قرارات حول تطبيقات محددة. على الرغم من أن القرارات تتطلب منا تحديد السلوك الذي سندعمه ، إلا أنها لا تزال تسمح لنا باتخاذ قرارات بشأن تطبيقات محددة لاحقًا. لنفترض أننا علمنا أننا نريد إجراء معاملات آلية ، لكننا لم نكن متأكدين حتى الآن من مزود الخدمة الذي سنستخدمه. يتم التعامل مع فئة مماثلة من الحلول باستمرار عند العمل مع مستودعات البيانات. ما الذي يجب أن يستخدمه برنامجنا: mysql، postgres، redis، file system، cassandra؟ في النهاية ، كل هذا هو تفاصيل التنفيذ ، وتسمح لنا الواجهات بتأجيل القرارات النهائية بشأن هذه القضايا. إنها تسمح لنا بتطوير منطق الأعمال لبرامجنا ، والتحول إلى حلول تكنولوجية محددة في اللحظة الأخيرة!
على الرغم من حقيقة أن هذه التقنية وحدها تترك الكثير من الاحتمالات ، إلا أن شيئًا سحريًا يحدث على مستوى تخطيط المشروع. تخيل ما سيحدث إذا أضفنا تبعية أخرى إلى واجهة التبادل.

هنا سنقوم بإعادة تشكيل بنيتنا في شكل رسم بياني لا موجب موجه ، حتى بمجرد أن نتفق على تفاصيل واجهة التبادل ، يمكننا الاستمرار في العمل مع خط الأنابيب بشكل
HTTPStockExchange
باستخدام
HTTPStockExchange
. لقد أنشأنا موقفًا تساعدنا فيه إضافة شخص جديد إلى المشروع على التحرك بشكل أسرع. من خلال تعديل هيكلنا المعماري بهذه الطريقة ، فإننا نرى بشكل أفضل أين ومتى وإلى متى يمكننا إشراك المزيد من الأشخاص في المشروع من أجل الإسراع في تسليم المشروع بأكمله. بالإضافة إلى ذلك ، نظرًا لأن الاتصال بين واجهاتنا ضعيف ، فمن السهل عادةً المشاركة في العمل ، بدءًا من واجهات التنفيذ. يمكنك تطوير واختبار واختبار
HTTPStockExchange
بشكل مستقل تماما عن برنامجنا!
يمكن أن يؤدي تحليل التبعيات المعمارية والتخطيط وفقًا لهذه التبعيات إلى تسريع المشاريع بشكل كبير. باستخدام هذه التقنية المحددة ، تمكنت من إكمال المشاريع بسرعة كبيرة والتي تم تخصيصها لعدة أشهر.
إلى الأمامالآن يجب أن يكون أكثر وضوحًا كيف تضمن الواجهات وتنفيذ التبعيات متانة البرنامج المصمم. لنفترض أننا قمنا بتغيير موفر عروض الأسعار ، أو بدأنا في تدفق الحصص وحفظها في الوقت الفعلي ؛ هناك العديد من الاحتمالات الأخرى كما تريد. ستدعم طريقة التحليل في شكلها الحالي أي تطبيق مناسب للتكامل مع واجهة
StockExchange
.
se.CurrentPrice(ticker)
وبالتالي ، في كثير من الحالات ، يمكنك الاستغناء عن التغييرات. ليس في كل شيء ، ولكن في تلك الحالات التي يمكن التنبؤ بها والتي قد نواجهها. نحن لسنا محصنين فقط من الحاجة إلى تغيير رمز
analyze
والتحقق من وظائفه الرئيسية ، ولكن يمكننا بسهولة تقديم تطبيقات جديدة أو التبديل بين الموردين. يمكننا أيضًا بسلاسة توسيع أو تحديث عمليات التنفيذ المحددة التي لدينا بالفعل دون الحاجة إلى التغيير أو التحقق مرة أخرى من
analyze
!
آمل أن توضح الأمثلة أعلاه بشكل مقنع كيف أن إضعاف الاتصال بين الكيانات في البرنامج من خلال استخدام الواجهات يعيد تمامًا إعادة تبعية المتصل ويفصل المتصل عن المتصل. بفضل هذا الفصل ، لا يعتمد البرنامج على تنفيذ معين ، ولكنه يعتمد على
سلوك معين. يمكن توفير هذا السلوك من خلال مجموعة متنوعة من التطبيقات. يسمى مبدأ التصميم الحرج هذا أيضًا
بكتابة البطة .
إن مفهوم الواجهات والاعتماد على السلوك ، وليس على التنفيذ ، قوي جدًا لدرجة أنني أعتبر الواجهات لغة بدائية - نعم ، هذا أمر جذري تمامًا. آمل أن تكون الأمثلة التي تمت مناقشتها أعلاه مقنعة تمامًا ، وسوف توافق على ضرورة استخدام الواجهات وحقن التبعية منذ بداية المشروع. في جميع المشاريع التي عملت عليها تقريبًا ، لم يكن مطلوبًا تنفيذ واحد ، ولكن على الأقل تطبيقين: للإنتاج والاختبار.