لماذا يعد التصميم أمرًا سيئًا للمبرمجين الأذكياء

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

مصممة للمبرمجين الضعفاء؟

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

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

فلماذا تم جعل الأمر بهذه البساطة؟ وهنا بضعة اقتباسات روب بايك (تقريبا.: أحد المبدعين المشاركين في لغة Go):

النقطة الأساسية هنا هي أن المبرمجين لدينا (تقريبا.: موظفي جوجل) ليسوا باحثين. إنهم، كقاعدة عامة، صغار جدًا، يأتون إلينا بعد الدراسة، ربما درسوا Java، أو C/C++، أو Python. إنهم لا يستطيعون فهم لغة رائعة، ولكن في نفس الوقت نريد منهم إنشاء برامج جيدة. ولهذا السبب يجب أن تكون لغتهم سهلة الفهم والتعلم.
 
يجب أن يكون مألوفًا، ويشبه تقريبًا C. يبدأ المبرمجون العاملون في Google حياتهم المهنية في وقت مبكر ويكونون في الغالب على دراية باللغات الإجرائية، ولا سيما عائلة C. إن متطلبات الإنتاجية السريعة في لغة برمجة جديدة تعني أن اللغة لا ينبغي أن تكون متطرفة للغاية.

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

التحف من البساطة المفرطة

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

ليست معبرة جدا

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

على سبيل المثال، أداة وحدة التحكم التي تقرأ stdin أو ملف من وسيطات سطر الأوامر ستبدو كما يلي:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "os"
)

func main() {

    flag.Parse()
    flags := flag.Args()

    var text string
    var scanner *bufio.Scanner
    var err error

    if len(flags) > 0 {

        file, err := os.Open(flags[0])

        if err != nil {
            log.Fatal(err)
        }

        scanner = bufio.NewScanner(file)

    } else {
        scanner = bufio.NewScanner(os.Stdin)
    }

    for scanner.Scan() {
        text += scanner.Text()
    }

    err = scanner.Err()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(text)
}

على الرغم من أن هذا الرمز يحاول أيضًا أن يكون عامًا قدر الإمكان، إلا أن الإسهاب القسري لـ Go يعيق الطريق، ونتيجة لذلك، يؤدي حل مشكلة بسيطة إلى كمية كبيرة من التعليمات البرمجية.

هنا، على سبيل المثال، حل لنفس المشكلة في D:

import std.stdio, std.array, std.conv;

void main(string[] args)
{
    try
    {
        auto source = args.length > 1 ? File(args[1], "r") : stdin;
        auto text   = source.byLine.join.to!(string);

        writeln(text);
    }
    catch (Exception ex)
    {
        writeln(ex.msg);
    }
}

ومن هو أكثر قابلية للقراءة الآن؟ سأعطي صوتي لـ D. الكود الخاص به أكثر قابلية للقراءة لأنه يصف الإجراءات بشكل أكثر وضوحًا. يستخدم D مفاهيم أكثر تعقيدًا (تقريبا.: استدعاء وظيفة بديلة и قوالب) مقارنة بمثال Go، ولكن لا يوجد شيء معقد حقًا في فهمها.

جحيم النسخ

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

package main

import "fmt"

func int64Sum(list []int64) (uint64) {
    var result int64 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int32Sum(list []int32) (uint64) {
    var result int32 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int16Sum(list []int16) (uint64) {
    var result int16 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int8Sum(list []int8) (uint64) {
    var result int8 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func main() {

    list8  := []int8 {1, 2, 3, 4, 5}
    list16 := []int16{1, 2, 3, 4, 5}
    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(int8Sum(list8))
    fmt.Println(int16Sum(list16))
    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}

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

نفس المثال على د:

import std.stdio;
import std.algorithm;

void main(string[] args)
{
    [1, 2, 3, 4, 5].reduce!((a, b) => a + b).writeln;
}

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

تجاوز نظام نوع بسيط

أتخيل أن مبرمجي Go الذين يقرأون هذا سيخرجون رغوة من أفواههم ويصرخون: "أنت تفعل ذلك بشكل خاطئ!" حسنًا، هناك طريقة أخرى لإنشاء دالة وأنواع عامة، ولكنها تكسر نظام الكتابة تمامًا!

ألقِ نظرة على هذا المثال لإصلاح اللغة الغبي للتغلب على المشكلة:

package main

import "fmt"
import "reflect"

func Reduce(in interface{}, memo interface{}, fn func(interface{}, interface{}) interface{}) interface{} {
    val := reflect.ValueOf(in)

    for i := 0; i < val.Len(); i++ {
        memo = fn(val.Index(i).Interface(), memo)
    }

    return memo
}

func main() {

    list := []int{1, 2, 3, 4, 5}

    result := Reduce(list, 0, func(val interface{}, memo interface{}) interface{} {
        return memo.(int) + val.(int)
    })

    fmt.Println(result)
}

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

هذه أداة قوية يجب استخدامها بحذر. ويجب تجنبه إلا في حالة الضرورة القصوى.

سأختار قوالب D بدلاً من هذا الهراء. كيف يمكن لأي شخص أن يقول ذلك interface{} أكثر قابلية للقراءة أو حتى كتابة آمنة؟

مشاكل إدارة التبعية

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

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

الأمتعة الثقافية من شي

في رأيي، تم تطوير Go بواسطة أشخاص استخدموا لغة C طوال حياتهم وأولئك الذين لم يرغبوا في تجربة شيء جديد. يمكن وصف اللغة بأنها C مع عجلات إضافية (أصل.: عجلات التدريب). لا يوجد فيها أفكار جديدة سوى دعم التوازي (وهو بالمناسبة رائع) وهذا عار. لديك توازي ممتاز في لغة عرجاء بالكاد يمكن استخدامها.

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

البساطة لمصلحتك الخاصة

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

إنه مطول للغاية وغير مثير للإعجاب وسيئ بالنسبة للمبرمجين الأذكياء.

شكرا ميرسينفالد للتعديلات

المصدر: www.habr.com

إضافة تعليق