مع كمية الموارد المجانية، والكتب، والدروس المجانية المتوفرة على الإنترنت ومعسكرات البرمجة التدريبية المتاحة الآن يمكن للجميع تعلم البرمجة. ومع ذلك، لا تزال هناك فجوة في الجودة بين البرمجة وهندسة البرمجيات. هل يجب أن تكون هناك فجوة؟
لقد كتبتُ أول "Hello world" منذ أكثر من عشرين عامًا - هذه هي الإجابة التي أعطيها إذا سألني أحدهم منذ متى وأنا أعمل مبرمجًا. على مدى السنوات العشر الماضية، كنت أستمتع بمهنة جعلتني ألمس الكود كل يوم تقريبًا - هذه هي الإجابة التي أعطيها إذا سُئلت منذ متى وأنا مبرمج محترف.
منذ متى وأنا مهندس برمجيات? أود أن أقول حوالي خمس سنوات. انتظر، لا يبدو أن هذه الأرقام تتوافق! إذن ما الذي تغير؟ من الذي أعتبره مهندس برمجيات ومن "مجرد" مبرمج؟
تعريف مهندس البرمجيات
البرمجة سهلة نسبياً. لم يعد الأمر كله عبارة عن استذكار تجميعي على أنظمة مقيدة بشكل سخيف بعد الآن. وإذا كنت تستخدم شيئًا معبّرًا وقويًا مثل روبي، فالأمر أسهل.
ما عليك سوى التقاط تذكرة والعثور على المكان الذي تحتاج إلى إدراج الشيفرة الخاصة بك، وتكتشف المنطق الذي تحتاج إلى وضعه هناك، ثم ينتهي الأمر. إن كنت أكثر تقدمًا قليلًا، تتأكد من أن شفرتك جميلة مقسمة منطقيًا إلى طرق. لديها مواصفات لائقة لا تختبر المسار السعيد فقط. هذا ما يفعله المبرمج الجيد.
لم يعد مهندس البرمجيات يفكر في الأساليب والفئات بعد الآن، على الأقل ليس في المقام الأول. من واقع خبرتي، يفكر مهندس البرمجيات في التدفقات. فهم يرون النهر الهادر الهادر من البيانات والتفاعل الهادر عبر النظام أولاً وقبل كل شيء. يفكرون فيما يحتاجون إلى القيام به من حيث تحويل أو تغيير هذا التدفق. وتأتي الكودات البرمجية الجميلة والأساليب المنطقية والمواصفات الرائعة كفكرة لاحقة.
إنها السلاحف على طول الطريق إلى الأسفل
يفكر الناس عمومًا بطريقة معينة حول معظم التفاعلات مع الواقع. ولعدم وجود مصطلح أفضل، دعنا نسميه المنظور "من أعلى إلى أسفل". إذا كان ما يعمل عليه عقلي هو إحضار كوب من الشاي، فسيكتشف أولاً الخطوات العامة: الذهاب إلى المطبخ، ووضع الغلاية على النار، وإعداد الفنجان، وصب الماء، والعودة إلى المكتب.
لن يفكر أولاً في الكوب الذي سأستخدمه أولاً، بينما أقف في حالة من التشتت على مكتبي؛ سيأتي هذا التشتت لاحقًا، بينما أقف أمام الخزانة. لن يضع في اعتباره أن الشاي قد يكون قد نفد (أو على الأقل، نفد من جيد الأشياء). إنها واسعة النطاق وتفاعلية وعرضة للخطأ. كل ما في الأمر - جداً بشري في الطبيعة.
عندما يفكر مهندس البرمجيات في إجراء تغييرات على تدفق البيانات المحير للعقل إلى حد ما، فمن الطبيعي أن يفعل ذلك بطريقة مماثلة. لننظر في هذا المثال لقصة المستخدم هذه:
يطلب عميل قطعة يدوية. عند تسعير الطلب، يجب مراعاة ما يلي:
- السعر الأساسي للقطعة في منطقة المستخدم المحلية
- شكل القطعة (معدل السعر)
- ما إذا كان طلبًا مستعجلاً (معدل السعر)
- ما إذا كان توصيل الطلب يتم في يوم عطلة في منطقة المستخدم المحلية (معدل السعر)
قد يبدو كل هذا مفتعلًا (ومن الواضح أنه كذلك)، لكنه ليس بعيدًا عن بعض قصص المستخدمين الفعلية التي سعدت بسحقها مؤخرًا.
والآن، دعنا نستعرض عملية التفكير التي قد يستخدمها مهندس البرمجيات لمعالجة هذا الأمر:
"حسنًا، نحتاج إلى الحصول على المستخدم وطلبه. ثم نبدأ بحساب الإجمالي. سنبدأ من الصفر. ثم سنطبق معدّل شكل القطعة. ثم رسوم الذروة. ثم نرى إن كان في يوم عطلة، ثم ننتهي قبل الغداء!"
آه، الاندفاع الذي يمكن أن تجلبه قصة مستخدم بسيطة. لكن مهندس البرمجيات هو مجرد إنسان، وليس آلة مثالية متعددة الخيوط، والوصفة أعلاه هي خطوط عريضة. يواصل المهندس التفكير بشكل أعمق إذن:
"مُعدِّل شكل القطعة هو... أوه، هذا يعتمد بشكل كبير على القطعة. وقد تكون مختلفة حسب اللغة المحلية، حتى وإن لم يكن الآن ففي المستقبل." يظنون أنهم احترقوا في السابق بسبب تغير متطلبات العمل, "وقد تكون رسوم الذروة كذلك. وأيام العطلات خاصة جدًا بالمناطق المحلية أيضًا، آه، وستكون المناطق الزمنية متضمنة! كان لدي مقال هنا حول التعامل مع الأوقات في المناطق الزمنية المختلفة في Rails هنا ... أوه، أتساءل عما إذا كان وقت الطلب مخزنًا مع المنطقة في قاعدة البيانات! من الأفضل التحقق من المخطط ".
حسناً، مهندس برمجيات توقف. من المفترض أن تقوم بإعداد كوب من الشاي، لكنك تجلس أمام الدولاب تفكر فيما إذا كان الكوب المزهر ينطبق حتى على مشكلة الشاي التي تواجهها.
أداة تخمير الكوب المثالي
لكن هذا ما يمكن أن يحدث بسهولة عندما تحاول القيام بشيء غير طبيعي بالنسبة للعقل البشري مثل التفكير في عدة تفاصيل عميقة في نفس الوقت.
بعد بحث قصير في مستودع الروابط الواسع الخاص بهم فيما يتعلق بالتعامل مع المنطقة الزمنية، يستجمع مهندسنا قواه ويبدأ في تحليل ذلك إلى كود فعلي. إذا جربوا النهج الساذج، فقد يبدو الأمر هكذا:
تعريف حساب_السعر(المستخدم، الطلب)
الطلب.price = 0
سعر_الطلب.price = WidgetPrices.find_by(widget_type: order.widget.type).price
سعر الطلب.price = WidgetShapes.find_by(widget_shape: order.widget.form).modifier
...
النهاية
وهكذا دواليك، بهذه الطريقة الإجرائية المبهجة، فقط ليتم إيقافها بشدة في أول مراجعة للرمز. لأنك إذا فكرت في الأمر، فمن الطبيعي تمامًا أن تفكر بهذه الطريقة: الخطوط العريضة أولًا، والتفاصيل لاحقًا. أنت لم تفكر حتى أنك لم تكن تظن أنك خرجت من الشاي الجيد في البداية، أليس كذلك؟
ومع ذلك، فإن مهندسنا مدرب جيدًا وليس غريبًا على كائن الخدمة، لذا إليك ما يبدأ في الحدوث بدلاً من ذلك:
صنف BaseOrderService
def self.call(user, order)
جديد(مستخدم، طلب).
نهاية
تعريف تهيئة(مستخدم، طلب)
مستخدم = مستخدم
@الطلب = الطلب
نهاية
تعريف الاستدعاء
يضع "[WARN] تنفيذ استدعاء غير افتراضي ل #{self.class.name}!"
المستخدم، الطلب
النهاية
النهاية
فئة WidgetPricePriceService < BaseOrderService؛ النهاية
فئة معدِّل سعر الشكل <خدمة الطلب الأساسي؛ النهاية
صنف RushPriceModifier < BaseOrderService؛ نهاية
صنف HolidayDelDelDelPriceModifier < BaseOrderService؛ نهاية
صنف OrderPriceCalcalculator < BaseOrderService
تعريف الاستدعاء
مستخدم، طلبية = WidgetPriceService.call(مستخدم، طلبية)
مستخدم، طلب = معدِّل سعر الشكل.call(مستخدم، طلب)
مستخدم، طلب = RushPriceModifier.call(مستخدم، طلب)
مستخدم، طلب = HolidayDelPeliveryPriceModifier.call(مستخدم، طلب)
المستخدم، الطلب
إنهاء
النهاية
```
جيد! يمكننا الآن استخدام بعض من TDD الجيد، وكتابة حالة اختبار له، وتوضيح الفصول حتى تتوافق جميع القطع في مكانها الصحيح. وسيكون جميلًا أيضًا.
كما أنه من المستحيل تماماً أن يكون منطقياً تماماً.
العدو هو الدولة
بالتأكيد، هذه كلها كائنات منفصلة بشكل جيد مع مسؤوليات واحدة. ولكن ها هي المشكلة: لا تزال كائنات. نمط كائن الخدمة مع "التظاهر بالقوة بأن هذا الكائن هو دالة" هو في الحقيقة عكاز. لا يوجد شيء يمنع أي شخص من استدعاء HolidayDeliveryPriceModifier.new(user, order).something_eliveryPriceModifier.new(user, order).something_elivery_entirely
. لا شيء يمنع الناس من إضافة حالة داخلية إلى هذه الكائنات.
ناهيك عن المستخدم
و الطلب
هي كائنات أيضًا، والعبث بها سهل مثل تسلل شخص ما في طلب الحفظ
في مكانٍ ما في هذه الكائنات الوظيفية "النقية" -الموضوعات- في مكانٍ ما، وتغيير المصدر الأساسي لحالة الحقيقة، أي قاعدة البيانات. في هذا المثال المفتعل ليس بالأمر الجلل، ولكن من المؤكد أنه يمكن أن يعود عليك بالضرر إذا نما هذا النظام في التعقيد وتوسع إلى أجزاء إضافية، غير متزامنة في كثير من الأحيان.
كان لدى المهندس الفكرة الصحيحة. واستخدم طريقة طبيعية جدًا للتعبير عن هذه الفكرة. ولكن معرفة كيفية التعبير عن هذه الفكرة - بطريقة جميلة وسهلة للتفكير - كان يمنعها نموذج OOP الأساسي تقريبًا. وإذا حاول شخص لم يقم بعد بالتعبير عن أفكاره كتحويلات لتدفق البيانات أن يغير الكود الأساسي بمهارة أقل، فستحدث أشياء سيئة.
أن تصبح نقيًا وظيفيًا
لو كان هناك فقط نموذج لا يكون فيه التعبير عن أفكارك من حيث تدفق البيانات أمرًا سهلاً فحسب، بل ضروريًا. لو كان بالإمكان جعل التفكير المنطقي بسيطًا، دون إمكانية إدخال آثار جانبية غير مرغوب فيها. لو كان بالإمكان أن تكون البيانات غير قابلة للتغيير، تمامًا مثل الكوب المنمق الذي تعد فيه الشاي.
نعم، أنا أمزح بالطبع. هذا النموذج موجود، ويسمى البرمجة الوظيفية.
لننظر كيف يمكن أن يبدو المثال أعلاه في إلكسير المفضل لديّ.
تعريف الوحدة WidgetPrices القيام بما يلي
تعريف طلب السعر([المستخدم، الطلب]) القيام بما يلي
[المستخدم، الطلب]
|> سعر القطعة
|> مُعدِّل الشكل
|> معدّل السعر السريع
|> معدّل سعر العطلة
النهاية
defp widgetprice([المستخدم، الطلب]) القيام بما يلي
%{القطعة: القطعة} = الطلب
السعر = WidgetRepo.getbase_price(القطعة)
[المستخدم، %{الطلب |السعر: السعر}]
نهاية
ديفب shapepricemodifier([المستخدم، الطلب]) القيام
%{القطعة: القطعة: القطعة ، السعر: السعر الحالي} = الطلب
المُعدِّل = WidgetRepo.getshapeprice(القطعة)
[المستخدم، %{الطلب |السعر: السعر الحالي * المعدل}]]
نهاية
ديفب rushp rushpricpricemodifier([المستخدم، الطلب]) القيام
%{الاندفاع: الاندفاع، السعر: السعر الحالي} = الطلب
إذا كان الاندفاع يفعل
[المستخدم، %{الطلب |السعر: السعر: السعر الحالي * 1.75}]
غير ذلك
[المستخدم، %{ الطلب | السعر: السعر الحالي} ]
النهاية
النهاية
ديفب holidayp holidaypricemodifier([المستخدم، الطلب]) القيام بما يلي
%{التاريخ: التاريخ، السعر: السعر الحالي} = الطلب
المُعدِّل = HolidayRepo.getholidaymodifier(المستخدم، التاريخ)
[المستخدم، %{الطلب |السعر: السعر الحالي * المعدل}]
نهاية
النهاية
```
قد تلاحظ أنه مثال كامل لكيفية تحقيق قصة المستخدم في الواقع. ذلك لأنّه أقل تعقيدًا مما هو عليه في روبي. نحن نستخدم بعض الميزات الرئيسية الفريدة من نوعها في إليكسير (ولكنها متوفرة بشكل عام في اللغات الوظيفية):
وظائف نقية. نحن لا نغير في الواقع الوارد الطلب
على الإطلاق، نحن فقط ننشئ نسخًا جديدة - تكرارات جديدة على الحالة الأولية. نحن لا نقفز إلى الجانب لتغيير أي شيء أيضًا. وحتى لو أردنا ذلك الطلب
هي مجرد خريطة "غبية"، لا يمكننا استدعاء طلب الحفظ
في أي نقطة هنا لأنه ببساطة لا يعرف ما هو ذلك.
مطابقة النمط. يشبه إلى حدٍ ما إعادة هيكلة ES6، وهذا يسمح لنا بانتزاع السعر
و القطعة
من الأمر وتمريره، بدلًا من إجبار رفاقنا على ويدجت ريبو
و هوليداي ريبو
لمعرفة كيفية التعامل مع الطلب
.
مشغل الأنابيب. شوهد في طلب_السعر
فهو يسمح لنا بتمرير البيانات عبر الدوال في نوع من "خط أنابيب" - وهو مفهوم مألوف على الفور لأي شخص قام بتشغيل PS aux | grep postgres
للتحقق مما إذا كان الشيء اللعين لا يزال يعمل أم لا.
هذه هي الطريقة التي تفكر بها
الآثار الجانبية ليست جزءًا أساسيًا من عملية تفكيرنا. بعد سكب الماء في كوبك لا تقلق عمومًا من أن خطأً ما في الغلاية قد يتسبب في ارتفاع درجة حرارتها وانفجارها - على الأقل ليس بما يكفي للذهاب إلى البحث في أجزائها الداخلية للتحقق مما إذا كان شخص ما لم يتركها عن غير قصد ينفجر_بعد_الصب
انقلبت عالياً
قد يستغرق الطريق من مبرمج إلى مهندس برمجيات - تجاوز القلق بشأن الكائنات والحالات إلى القلق بشأن تدفقات البيانات - في بعض الحالات سنوات. ومن المؤكد أنه استغرق سنوات بالنسبة لك الذي تربى على OOP. مع اللغات الوظيفية، يمكنك التفكير في التدفقات في ليلتك الأولى.
لقد صنعنا هندسة البرمجيات معقدة بالنسبة لنا ولكل وافد جديد في هذا المجال. لا يجب أن تكون البرمجة صعبة ومرهقة للعقل. يمكن أن تكون سهلة وطبيعية.
دعونا لا نجعل هذا الأمر معقداً ونبدأ العمل بالفعل. لأن هذه هي الطريقة التي نفكر بها.
اقرأ أيضًا: