Command Palette

Search for a command to run...

المدوّنة

كيف نبني

نُشر في
مدة القراءة
5 د قراءة
الأشكال
5 أشكال
Tabaga Team

عادات البنية وجودة الكود وراء كل منتج من Tabaga Team، بالأرقام من مستودعاتنا نفسها.

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

12
منتجًا بشكل المجلدات نفسه
14
نوع فشل في تسلسل مغلق واحد
468
اختبارًا في تطبيق واحد، كثير منها للاتجاه والتباين
65
ملاحظة في تدقيق كتبناه عن منتجنا نحن

هيكل واحد

lib/core/theme · router · di · services/abstractionsfeatures/orders/one folder per featuredata/repositories · dtos · mappersdomain/entities · use cases · failurespresentation/bloc · screens · widgetsauth/same three foldersbilling/same three foldersgallery/every widget, in debug buildsthe two rulesfeatures import corecore never imports a featurepresentation → domain → datanever the other waysame in twelve productsFLUTTER · BLoC OR CUBIT · GET_IT + INJECTABLE · GOROUTER
الشكل 1.شكل كل منتج: مجلد لكل ميزة بالغرف الثلاث نفسها في داخله، ونواة لا تصل إليها الميزات من الجانب أبدًا.

كل تطبيق نشحنه له الهيكل نفسه: مجلد core/ لما يتشاركه التطبيق كله، ومجلد لكل ميزة في داخله data وdomain وpresentation. العرض هو الودجات والحالة، والمجال هو الكيانات وحالات الاستعمال وأنواع الفشل، والبيانات هي المستودعات والمحوّلات. الحالة بـ BLoC أو Cubit في معظم المنتجات وبـ Riverpod في اثنين؛ وحقن الاعتماديات بـ get_it مع injectable؛ والتنقل بـ GoRouter مع حراسة حسب الدور.

قاعدتان تجعلان الهيكل يستحق. الميزات تستورد النواة، والنواة لا تستورد ميزة أبدًا. العرض يعتمد على المجال، والمجال على البيانات، وليس العكس أبدًا. المنتج الجديد يبدأ بنسخ الشكل، لا بتقرير شكل.

الخادم تفصيل

presentationwidgets · BLoC / Cubit · GoRouter guardsdomainentities · use cases · Failure typesdatarepositories · DTOs · mappersabstractionsAuthService · Db · Storage · Push · Smsarrows point inward only; nothing imports a Firebase type above the data layerFirebaseSupabaseAppwriteDrift / SQLiteswapped in get_it
الشكل 2.شكل واحد لكل منتج: العرض يعتمد على المجال، والمجال على التجريدات، والخادم محوّل يُختار في حاوية الحقن.

القاعدة الأهم هي اتجاه الأسهم. لا شيء فوق طبقة البيانات يستورد نوعًا من Firebase أو Supabase أو Appwrite. حين يحتاج منتج إلى خادم، فهو يحتاج إلى واجهة في core/services/abstractions، والمحوّل الفعلي يُختار مرة واحدة، في الحاوية.

تلك القاعدة هي سبب قدرة منصة قطع غيار السيارات على مغادرة Firebase إلى Appwrite بلمس نحو 78 ملف Dart و18 دالة سحابية بدل التطبيق كله، وسبب أن منتجًا للبناء يعمل على Appwrite Cloud، أو على VPS في الجزائر، أو على جهاز في مكتب العميل بالكود نفسه وعنوان مختلف.

جدران، لا أعراف

quickyes_coremodels · repositoriesquickyes_uitheme · widgets · l10nquickyes_adminstaff screens · blocscustomer appPlay · App Storestaff appinternal APK · Windowsno edgeenforced where?pubspec.yaml, not a code reviewsame idea elsewherethree flavours, one reposerver-authoritative writesrules in the databaseif it matters, make it a constraint
الشكل 3.حدّ لا تعبره بالخطأ: تطبيق العميل لا يعتمد على حزمة الإدارة، فكود الإدارة لا يُشحن أبدًا في ملف العميل.

الأعراف تتآكل؛ القيود لا. منصة طلب الطعام للمطاعم مستودع واحد بثلاث حزم داخلية: النواة والواجهة والإدارة. تطبيق العميل يعتمد على الأوليين لا الثالثة، فكود الإدارة لا يمكن أن ينتهي في ملف العميل حتى بالخطأ. الحد يسكن في pubspec.yaml، لا في مراجعة كود.

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

إن كانت القاعدة مهمة، فاجعلها قيدًا في مكان لا يصل إليه التطبيق.

أخطاء تستطيع الإمساك بها

repositorytalks to the adapterEither<Failure, T>NetworkFailure · AuthFailureValidationFailure · ConflictFailure… sealed, 14 kinds in one productRight(T)Left(Failure)LoadedError(f)UIno try/catch in widgetsno stack traces to usersevery failure has a sentence in ar · fr · en
الشكل 4.الأخطاء قيم: المستودع يعيد إما Failure منمّطًا أو نتيجة، والـ bloc يحوّل الاثنين إلى حالة، والشاشة لا ترى استثناءً أبدًا.

المستودعات تعيد Either<Failure, T>. الفشل نوع مغلق، أربعة عشر صنفًا في أكبر منتجاتنا: شبكة، مصادقة، تحقق، صلاحية، غير موجود، تعارض وهكذا، كلٌّ يحمل ما يكفي ليشرح نفسه بالعربية والفرنسية والإنجليزية. الـ bloc يحوّل Right إلى حالة محمّلة وLeft إلى حالة خطأ، والودجات لا ترى استثناءً أبدًا.

نتيجتان نحبهما. التزامن المتفائل يصير ConflictFailure تستطيع الواجهة التعامل معه برسالة حقيقية بدل انهيار. ومسارات الكود لحالة «الخادم قال لا» تُكتب وتُختبر من اليوم الأول، لأن نظام الأنواع لن يدعك تنساها.

قاعدة البيانات وثيقة تصميم

منتجات «بلا اتصال أولًا» تعامل القاعدة المحلية كالمخزن الأساسي: Drift على SQLite، وSQLCipher للتشفير، والترحيلات في مكان واحد مرقّم. أحد المخططات في الإصدار 51 بعد شهرين من الاستعمال اليومي. مدير الصيدلية يثبّت موقع تخزينه باختبار، لأن منتجًا أعيدت تسميته وجّه يومًا بناءً جديدًا إلى مجلد فارغ فبدت السجلات محذوفة. خادم التراخيص يملك اختبار انحراف مخطط يُسقط البناء حين تختلف القاعدة الحية والترحيلات، بعد أن تخطّى CREATE TABLE IF NOT EXISTS بصمت عمودًا جديدًا في الإنتاج بينما اجتاز كل اختبار وحدة ضد مستودع في الذاكرة.

الاختبارات ذاكرة

لا نطارد التغطية؛ نثبّت القرارات. منصة المعدات تملك 468 اختبارًا عبر نحو ثلاثين ملفًا، وحصة جيدة منها تفحص التدويل وتخطيط اليمين-إلى-اليسار وتباين الألوان بدل المنطق. منصة قطع غيار السيارات شغّلت نحو 398 اختبار وحدة مع اختبارات تكامل ضد خادم حقيقي قبل أن يثق أحد بهجرة. تطبيق الروضات يملك اختبارًا يفشل إن جُلب خط عند التشغيل بدل تضمينه، لأن التطبيق يُستعمل حيث لا Wi-Fi. مدير الصيدلية يملك اختبارًا لمعرّفات العلامة وآخر لتبنّي القاعدة القديمة.

كل واحد من تلك الاختبارات موجود لأن شيئًا ما ساء يومًا، أو كاد. هذا مقياس التغطية الوحيد الذي نتابعه.

من الـ commit إلى الطاولة

commitconventional messageanalyzerRTL rules · lintstestsunit · widget · integrationCIevery push and PRtag on mainrefused elsewheresigned buildEd25519 · SHA-256channelstable · beta per licencefix(saisie): vingt-trois champs…468 tests in one product, 398 in anotheran unsigned installer is not publishableplus a schema-drift test that fails the build when the live database and the migrations disagree
الشكل 5.من commit إلى جهاز العميل: كل خطوة بوابة، والثلاث الأخيرة هي ما يجعل إصدارات استوديو فردي جديرة بالثقة.

الـ commits تتبع الصيغة الاصطلاحية، بالإنجليزية أو الفرنسية حسب من سيقرأ سجل التغييرات. المحلّل يفرض قواعد الاتجاه: EdgeInsetsDirectional وAlignmentDirectional وstart وend، أبدًا left وright. CI تعمل عند كل push وpull request. وسوم الإصدار تُرفض ما لم تكن على main. مثبّتات Windows تُفحص بمجموع تحقق وتُوقَّع بمفتاح Ed25519 في CI، والمثبّت غير الموقّع غير قابل للنشر. قنوات الإصدار تُضبط لكل رخصة لا لكل بناء، فنعرف دائمًا أي بناء كان يشغّله العميل حين انكسر شيء.

لتطبيقات المتاجر، الأجزاء المملة مؤتمتة: تصريح أمان بيانات Play يولده سكربت، وخطوات الكونسول التي لا واجهة برمجية لها موثّقة خطوة بخطوة، وملاحظات الإصدار تُكتب بثلاث لغات في سجل التغييرات نفسه.

نظام التصميم يسكن داخل التطبيق

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

قول «غير جاهز» بصوت عالٍ

ندقّق عملنا ونكتب النتيجة حتى حين لا تكون مُرضية. منصة الأزياء مرّت ببرنامج تحصين من 92 مرحلة مع اثني عشر تدقيقًا حسب الدور قبل التسليم. منصة التبريد تملك AUDIT.md بـ 65 ملاحظة، ثلاث منها حرجة، وحكم «غير جاهز للإنتاج» في الفقرة الأولى. خرائط الطريق تستعمل جداول حالة بثلاثة ألوان، و«غير مبني» لون.

لا شيء هنا ذكي. هي المجموعة الصغيرة نفسها من العادات مطبّقة على كل منتج، حتى يكون كلٌّ أسهل من سابقه.

Command Palette

Search for a command to run...