اذهب إلى المحتوى

فهم مجال رؤية الحزم Visibility في لغة جو Go


هدى جبور

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

تُعَدّ الرؤية Visibility هي النطاق الذي يمكن الوصول للحزمة ضمنه، فإذا صرّحت مثلًا عن متغير داخل دالة ضمن حزمة A، فلن تتمكن من الوصول إلى هذا المتغير إلى ضمن الدالة التي صُرّح عنه فيها وضمن الحزمة A فقط، في حين إذا صرّحت عن المتغير نفسه ضمن الحزمة وليس بداخل دالة أو أيّ نطاق آخر، فيمكنك السماح للحزم الأخرى بالوصول إلى هذا المتغير أو لا.

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

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

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

المتطلبات

سيعتمد المقال على بنية المجلد التالية:

.
├── bin 
 
└── src
    └── github.com
        └── gopherguides

العناصر المصدرة وغير المصدرة

لا تمتلك لغة جو محددات وصول لتحديد الرؤية مثل عام public وخاص private ومحمي protected كما في باقي لغات البرمجة، إذ تحدِّد لغة جو إمكانية رؤية العناصر اعتمادًا على ما إذا كان مُصدّرًا أم لا وذلك وفقًا لكيفية التصريح عنه، فإذا كان مصدّرًا فهو مرئي من خارج الحزمة وإلا فهو مرئي فقط داخل الحزمة التي أُنشئ فيها.

لكي نجعل عنصرًا ما -مثل متغير أو دالة أو نوع بيانات جديد …إلخ- داخل الحزمة مُصدّرًا، سنكتب أول محرف منه كبيرًا، أي إذا كان اسم الدالة hsoub، فستكون مرئيةً فقط ضمن الحزمة نفسها وتكون غير مُصدّرة؛ أما إذا كتبناها Hsoub، فستكون مرئيةً من خارج الحزمة وتكون مُصدّرة.

لاحظ المثال التالي وانتبه لحالة أول محرف في كل التصريحات:

package greet

import "fmt"

var Greeting string

func Hello(name string) string {
    return fmt.Sprintf(Greeting, name)
}

من الواضح أنّ هذه الشيفرة موجودة ضمن الحزمة greet، ولاحظ أننا صرّحنا عن متغير اسمه Greeting وجعلنا أول محرف كبيرًا، لذا فهذا المتغير سيكون مُصدّرًا ويمكن رؤيته من خارج الحزمة، وينطبق الأمر نفسه على الدالة Hell، فقد كتبنا أول محرف منها كبيرًا.

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

تحديد رؤية الحزمة

سنُنشئ الحزمة logging مع الأخذ بالحسبان ما نريده مرئيًا وما نريده غير مرئي لكي يكون لدينا نظرة أوضح عن آلية عمل قابلية الرؤية في برامجنا.

ستكون هذه الحزمة مسؤولةً عن تسجيل أيّ رسالة من البرنامج إلى وحدة التحكم console، كما يُحدد المستوى الذي يحدث عنده التسجيل، إذ يصف المستوى نوع السجل وسيكون أحد الحالات الثلاث: معلومات info أو تحذير warning أو خطأ error، لذا أنشئ بدايةً مجلد الحزمة logging داخل المجلد src كما يلي:

$ mkdir logging

انتقل إلى مجلد الحزمة:

$ cd logging

أنشئ ملف logging.go باستخدام محرر شيفرات مثل نانو nano:

$ nano logging.go

ضع فيه الشيفرة التالية:

package logging

import (
    "fmt"
    "time"
)

var debug bool

func Debug(b bool) {
    debug = b
}

func Log(statement string) {
    if !debug {
        return
    }

    fmt.Printf("%s %s\n", time.Now().Format(time.RFC3339), statement)
}

تصف التعليمة الأولى في هذه الشيفرة أننا ضمن الحزمة logging، وتوجد لدينا ضمن هذه الحزمة دالتين مُصدّرتين هما Debug و Log، وبالتالي يمكن استدعاء هذه الدوال من أيّ حزمة أخرى تستورد الحزمة logging، ولدينا أيضًا متغير غير مُصدّر اسمه debug، وبالتالي لايمكن الوصول إليه إلى ضمن الحزمة.

على الرغم من أنه لدينا متغير ودالة بالاسم نفسه "debug"، إلا أنهما مختلفان، فالدالة بدأت بمحرف كبير؛ أما المتغير، فقد بدأ بمحرف صغير ولغة جو حساسة لحالة المحارف.

احفظ الملف بعد وضع الشيفرة فيه، ولاستخدام الحزمة في مكان آخر، يجب استيرادها، لذا سنُنشئ حزمةً جديدةً ونجرب عليها، وبالتالي انتقل إلى المجلد logging وأنشئ مجلدًا اسمه cmd وانتقل إليه:

$ cd ..
$ mkdir cmd
$ cd cmd

أنشئ الملف main.go بداخله:

$ nano main.go

أضف إليه الشيفرة التالية:

package main

import "github.com/gopherguides/logging"

func main() {
    logging.Debug(true)

    logging.Log("This is a debug statement...")
}

أصبح لدينا الآن كامل الشيفرة المطلوب، لكن نحتاج قبل تشغيلها إلى إنشاء ملفَي ضبط حتى تعمل التعليمات البرمجية بطريقة صحيحة.

تُستخدَم وحدات لغة جو Go Modules لضبط اعتماديات الحزمة لاستيراد الموارد، إذ تُعَدّ وحدات لغة جو ملفات ضبط موضوعة في مجلد الحزمة الخاص بك وتخبر المُصرّف بمكان استيراد الحزم منه، ولن نتحدث عن وحدات لغة جو في هذا المقال، لكن سنكتب السطرين التاليين لكي تعمل الشيفرة السابقة، لذا افتح ملف go.mod ضمن المجلد cmd:

$ nano go.mod

ثم ضع السطرين التاليين فيه:

module github.com/gopherguides/cmd
replace github.com/gopherguides/logging => ../logging

يُخبر السطر الأول المُصرّف أنّ الحزمة cmd لديها مسار الملف github.com/gopherguides/cmd؛ أما السطر الثاني، فيُخبر المُصرّف أنّ الحزمة github.com/gopherguides/logging يمكن العثور عليها محليًا على القرص في المجلد ‎../logging.

نحتاج إيضًا إلى وجود ملف go.mod بداخل الحزمة logging، لذا سننتقل إلى مجلدها ثم سننشئ هذا الملف بداخله:

$ cd ../logging
$ nano go.mod

أضف الشيفرة التالية إلى الملف:

module github.com/gopherguides/logging

يخبر هذا المترجم أنّ حزمة logging التي أنشأناها هي الحزمة github.com/gopherguides/logging، إذ سيسمح لنا ذلك باستيراد الحزمة من داخل الحزمة main كما يلي:

package main

import "github.com/gopherguides/logging"

func main() {
    logging.Debug(true)

    logging.Log("This is a debug statement...")
}

يجب أن يكون لديك الآن بنية المجلد التالية:

├── cmd
│   ├── go.mod
│   └── main.go
└── logging
    ├── go.mod
    └── logging.go

أصبح بإمكاننا الآن تشغيل البرنامج main من حزمة cmd بالأوامر التالية بعد أن أنشأنا جميع ملفات الضبط:

$ cd ../cmd
$ go run main.go

سنحصل على رسالة تطبع التوقيت وتقول أن هذه تعليمة تنقيح debug:

2019-08-28T11:36:09-05:00 This is a debug statement...

يطبع البرنامج الوقت الحالي بتنسيق RFC 3339 متبوعًا بالتعليمة التي أرسلناها إلى المُسجّل logger، وقد صُمِّم RFC 3339 لتمثيل الوقت على الإنترنت ومن الشائع استخدامه في ملفات التسجيل والتتبع log files.

بما أن الدالتين Debug و Log هما دوال مُصدّرة، لذا يمكن استخدامهما في الحزمة main، لكن لا يمكن استخدام المتغير debug في حزمة logging، وإذا حاولت الوصول إليه، فستحصل على خطأ في وقت التصريف compile-time error.

سنضيف السطر (fmt.Println(logging.debug إلى main.go:

package main

import "github.com/gopherguides/logging"

func main() {
    logging.Debug(true)

    logging.Log("This is a debug statement...")

    fmt.Println(logging.debug)
}

احفظ وشغل الملف، إذ ستحصل على الخطأ التالي (لا يمكن الإشارة إلى العنصر debug لأنه غير مُصدّر):

. . .
./main.go:10:14: cannot refer to unexported name logging.debug

سنتعرّف الآن على كيفية تصدير الحقول والتوابع من داخل السجلات structs بعد أن تعرّفنا على الملفات المُصدّرة وغير المُصدّرة.

نطاق الرؤية داخل السجلات structs

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

تخيل أن تضبط المتغير Debug على true ويأتي شخص آخر لا تعرفه يستخدم الحزمة نفسها ويضبطه على false، لذا سنحل هذه المشكلة من خلال استخدام مفهوم النسخ الذي تتبناه السجلات Structs في جو، وبالتالي كل شخص يستخدِم هذه الحزمة سيأخذ نسخةً مستقلةً منها، وبالتالي سنتجنب مثل هذه المشاكل.

سنعدّل الآن الحزمة logging كما يلي:

package logging

import (
    "fmt"
    "time"
)

type Logger struct {
    timeFormat string
    debug      bool
}

func New(timeFormat string, debug bool) *Logger {
    return &Logger{
        timeFormat: timeFormat,
        debug:      debug,
    }
}

func (l *Logger) Log(s string) {
    if !l.debug {
        return
    }
    fmt.Printf("%s %s\n", time.Now().Format(l.timeFormat), s)
}

أنشأنا هنا السجل Logger الذي سيضم العناصر غير المُصدّرة وتنسيق الوقت timeFormat المطلوب طباعته والمتغير debug وقيمته سواءً كانت true أو false.

تضبط الدالة New الحالة الأولية لإنشاء السجل logger داخليًا ضمن المتغيران timeFormat و debug غير المُصدَّران، كما أنشأنا التابع Log بداخل هذا السجل الذي يأخذ المعلومات المراد طباعتها، كما يوجد بداخل التابع Log مرجع reference يعود إلى متغير الدالة المحلية l الخاص به للحصول على سماحية الوصول مرةً أخرى إلى الحقول الداخلية مثل l.timeFormat و l.debug.

سيسمح لنا هذا النهج بإنشاء مسجل Logger في العديد من الحزم المختلفة واستخدامه باستقلال عن الحزم الأخرى له، ولاستخدامه في حزمة أخرى سنعدّل ملف cmd/main.go كما يلي:

package main

import (
    "time"

    "github.com/gopherguides/logging"
)

func main() {
    logger := logging.New(time.RFC3339, true)

    logger.Log("This is a debug statement...")
}

بتشغيل البرنامج سنحصل على الخرج التالي:

2019-08-28T11:56:49-05:00 This is a debug statement...

أنشأنا في هذا المثال نسخةً من المُسجّل logger من خلال استدعاء الدالة المُصدّرة New، كما خزّنا مرجعًا لهذه النسخة في متغير المُسجّل logger، ويمكننا الآن استدعاء logging.Log لطباعة المعلومات.

إذا حاولنا الإشارة إلى حقول غير مُصدّرة من Logger مثل الحقل timeFormat، فسنحصل على خطأ في وقت التصريف، لذا جرّب إضافة السطر (fmt.Println(logger.timeFormat إلى الملف cmd/main.go:

package main

import (
    "time"

    "github.com/gopherguides/logging"
)

func main() {
    logger := logging.New(time.RFC3339, true)

    logger.Log("This is a debug statement...")

    fmt.Println(logger.timeFormat)
}

ستحصل على الخطأ التالي (لا يمكن الإشارة إلى حقل أو دالة غير مصدرة):

. . .
cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)

سيُلاحظ المُصرّف أنّ logger.timeFormat غير مُصدّر، وبالتالي لا يمكن الوصول إليه من الحزمة logging.

نطاق الرؤية في التوابع

يمكننا تصدير أو عدم تصدير الدوال بطريقة الحقول ضمن السجلات Structs نفسها، ولتوضيح ذلك سنضيف مستويات التسجيل إلى المسجل logger الخاص بنا، إذ تُعَدّ مستويات التسجيل وسيلةً لتصنيف السجلات logs الخاصة بك، بحيث يمكنك البحث فيها عن أنواع معينة من الأحداث، وتكون المستويات التي سنضعها في المسجل logger كما يلي:

  • مستوى المعلومات info: تمثِّل الأحداث التي تُعلِم المستخدِم بإجراء ما مثل بدء البرنامج Program started أو إرسال البريد الإلكتروني Email sent، كما تساعدنا في تصحيح الأخطاء وتتبع أجزاء من برنامجنا لمعرفة ما إذا كان السلوك المتوقع قد حدث أم لا.
  • مستوى التحذير warning: تشير هذه الأحداث إلى حدوث أمر غير متوقع أو قد يُسبب مشاكل لاحقًا، لكنها ليست أخطاءً مثل فشل إرسال البريد الإلكتروني وإعادة محاولة الإرسالة Email failed to send, retrying، ويمكن القول أنها تساعدنا في رؤية أجزاء من برنامجنا لا تسير كما هو متوقع لها.
  • مستوى الأخطاء error: تشير إلى حدوث أخطاء أو مشاكل في البرنامج مثل الملف غير موجودFile not found، فهذه الأحداث هي أخطاء تؤدي إلى فشل البرنامج وبالتالي إيقافه.

قد ترغب في تفعيل أو إيقاف مستويات معينة من التسجيل، خاصةً إذا كان برنامجك لا يعمل كما هو متوقع له وترغب في تصحيح أخطاء البرنامج، لذا سنضيف وظيفة تعدِّل البرنامج بحيث عندما تكون debug مضبوطةً على true، فستُطبع كل رسائل المستويات؛ أما إذا كانت false، فستطبع رسائل الخطأ فقط.

سنضيف مستويات التسجيل إلى الملف logging/logging.go:

package logging

import (
    "fmt"
    "strings"
    "time"
)

type Logger struct {
    timeFormat string
    debug      bool
}

func New(timeFormat string, debug bool) *Logger {
    return &Logger{
        timeFormat: timeFormat,
        debug:      debug,
    }
}

func (l *Logger) Log(level string, s string) {
    level = strings.ToLower(level)
    switch level {
    case "info", "warning":
        if l.debug {
            l.write(level, s)
        }
    default:
        l.write(level, s)
    }
}

func (l *Logger) write(level string, s string) {
    fmt.Printf("[%s] %s %s\n", level, time.Now().Format(l.timeFormat), s)
}

لاحظ أننا صرّحنا عن وسيط جديد للتابع Log هو level بغية تحديد مستوى التسجيل، فإذا كان لدينا رسالة معلومات info أو تحذير warning وكان حقل debug مضبوطًا على true، فسيكتب الرسالة، وإلا فسيتجاهلها؛ أما إذا كانت الرسالة هي رسالة خطأ error، فسيطبعها دومًا.

يوجد تحديد ما إذا كانت الرسالة ستُطبع أم لا في التابع Log، كما عرّفنا أيضًا تابعًا غير مُصدّر يدعى write وهو مَن سيطبع الرسالة في نهاية المطاف.

يمكننا الآن استخدام مستويات التسجيل في الحزم الأخرى من خلال تعديل ملف cmd/main.go ليصبح كما يلي:

package main

import (
    "time"

    "github.com/gopherguides/logging"
)

func main() {
    logger := logging.New(time.RFC3339, true)

    logger.Log("info", "starting up service")
    logger.Log("warning", "no tasks found")
    logger.Log("error", "exiting: no work performed")

}

بتشغيل الملف ستحصل على الخرج التالي:

[info] 2019-09-23T20:53:38Z starting up service
[warning] 2019-09-23T20:53:38Z no tasks found
[error] 2019-09-23T20:53:38Z exiting: no work performed

نلاحظ نجاح استخدام التابع Log، كما يمكننا تمرير الوسيط level من خلال ضبط القيمة false إلى المتغير debug:

package main

import (
    "time"

    "github.com/gopherguides/logging"
)

func main() {
    logger := logging.New(time.RFC3339, false)

    logger.Log("info", "starting up service")
    logger.Log("warning", "no tasks found")
    logger.Log("error", "exiting: no work performed")

}

سنلاحظ أنه سيطبع فقط رسائل مستوى الخطأ (لم يُنفّذ أيّ عمل):

[error] 2019-08-28T13:58:52-05:00 exiting: no work performed

إذا حاولت استدعاء التابع write من خارج الحزمة logging، فستحصل على خطأ في وقت التصريف:

package main

import (
    "time"

    "github.com/gopherguides/logging"
)

func main() {
    logger := logging.New(time.RFC3339, true)

    logger.Log("info", "starting up service")
    logger.Log("warning", "no tasks found")
    logger.Log("error", "exiting: no work performed")

    logger.write("error", "log this message...")
}

يكون الخرج كما يلي:

cmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)

بمجرد أن بلاحظ المُصرّف أنك تحاول الوصول إلى شيء ما أول محرف منه صغير، فإنه يعلم أنه غير مصدّر وسيرمي خطأ تصريف.

يوضّح المسجل في هذا المقال كيف يمكننا كتابة التعليمات البرمجية التي تعرض فقط الأجزاء التي نريد أن تستخدِمها الحزم الأخرى، وبما أننا نتحكم في أجزاء الحزمة التي تظهر خارج الحزمة، فيمكننا لاحقًا إجراء تغييرات دون التأثير على أيّ شيفرة تعتمد على الحزمة الخاصة بنا، فإذا أردنا مثلًا إيقاف تشغيل رسائل مستوى المعلومات فقط عندما يكون debug مضبوطًا على false، فيمكنك إجراء هذا التغيير دون التأثير على أي جزء آخر من واجهة برمجة التطبيقات API الخاصة بك، كما يمكننا أيضًا إجراء تغييرات بأمان على رسالة السجل لتضمين المزيد من المعلومات مثل المجلد الذي كان البرنامج يعمل منه.

الخاتمة

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

ترجمة -وبتصرُّف- للمقال Understanding Package Visibility inGo لصاحبه Gopher Guides.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...