نشرنا مؤخرًا
مادة عن منهجية SOLID. نلفت انتباهكم اليوم إلى ترجمة مقالة تتناول تطبيق مبادئ SOLID في تطوير التطبيقات باستخدام مكتبة React الشعبية.

يقول كاتب المقال أنه هنا ، من أجل الإيجاز ، لا يظهر التنفيذ الكامل لبعض المكونات.
مبدأ المسؤولية الفردية (S)
يخبرنا مبدأ المسؤولية الفردية أن الوحدة يجب أن يكون لها سبب واحد فقط للتغيير.
تخيل أننا نقوم بتطوير تطبيق يعرض قائمة المستخدمين في الجدول. إليك رمز مكون
App
:
class App extends Component { state = { users: [{name: 'Jim', surname: 'Smith', age: 33}] }; componentDidMount() { this.fetchUsers(); } async fetchUsers() { const response = await fetch('http://totallyhardcodedurl.com/users'); const users = await response.json(); this.setState({users}); } render() { return ( <div className="App"> <header className="App-header"> // </header> <table> <thead> <tr> <th>First name</th> <th>Last name</th> <th>Age</th> </tr> </thead> <tbody> {this.state.users.map((user, index) => ( <tr key={index}> <td><input value={user.name} onChange={/* update name in the state */}/></td> <td><input value={user.surname} onChange={/* update surname in the state*/}/></td> <td><input value={user.age} onChange={/* update age in the state */}/></td> </tr> ))} </tbody> </table> <button onClick={() => this.saveUsersOnTheBackend()}>Save</button> </div> ); } saveUsersOnTheBackend(row) { fetch('http://totallyhardcodedurl.com/users', { method: "POST", body: JSON.stringify(this.state.users), }) } }
لدينا مكون في الحالة التي يتم تخزين قائمة المستخدمين بها. نقوم بتنزيل هذه القائمة عبر HTTP من خادم معين ؛ والقائمة قابلة للتعديل. ينتهك مكوننا مبدأ المسؤولية وحدها ، حيث أن لديه أكثر من سبب واحد للتغيير.
على وجه الخصوص ، يمكنني أن أرى أربعة أسباب لتغيير مكون. وبالتحديد ، يتغير المكون في الحالات التالية:
- في كل مرة تحتاج إلى تغيير عنوان التطبيق.
- في كل مرة تحتاج إلى إضافة مكون جديد إلى التطبيق (تذييل الصفحة ، على سبيل المثال).
- في كل مرة تحتاج إلى تغيير آلية تحميل بيانات المستخدم ، على سبيل المثال ، عنوان الخادم أو البروتوكول.
- في كل مرة تحتاج إلى تغيير الجدول (على سبيل المثال ، قم بتغيير تنسيق الأعمدة أو تنفيذ بعض الإجراءات الأخرى مثل هذا).
كيف تحل هذه المشاكل؟ من الضروري ، بعد تحديد أسباب تغيير المكون ، محاولة التخلص منها ، للاستنتاج من المكون الأصلي ، وإنشاء تجريدات مناسبة (مكونات أو وظائف) لكل سبب من هذا القبيل.
سنحل مشاكل مكون
App
خلال إعادة هيكلته. سيبدو رمزها ، بعد تقسيمها إلى عدة مكونات ، كما يلي:
class App extends Component { render() { return ( <div className="App"> <Header/> <UserList/> </div> ); } }
الآن ، إذا كنت بحاجة إلى تغيير العنوان ، فإننا نغير مكون
Header
، وإذا كنت بحاجة إلى إضافة مكون جديد إلى التطبيق ، فإننا نغير مكون
App
. هنا قمنا بحل المشاكل رقم 1 (تغيير رأس التطبيق) والمشكلة رقم 2 (إضافة مكون جديد إلى التطبيق). يتم ذلك عن طريق نقل المنطق المقابل من مكون
App
إلى المكونات الجديدة.
سنحل الآن المشاكل رقم 3 ورقم 4 من خلال إنشاء فئة
UserList
. هنا هو رمزه:
class UserList extends Component { static propTypes = { fetchUsers: PropTypes.func.isRequired, saveUsers: PropTypes.func.isRequired }; state = { users: [{name: 'Jim', surname: 'Smith', age: 33}] }; componentDidMount() { const users = this.props.fetchUsers(); this.setState({users}); } render() { return ( <div> <UserTable users={this.state.users} onUserChange={(user) => this.updateUser(user)}/> <button onClick={() => this.saveUsers()}>Save</button> </div> ); } updateUser(user) {
UserList
هو مكون الحاوية الجديد الخاص بنا. بفضله ، حللنا المشكلة رقم 3 (تغيير آلية تحميل المستخدم) من خلال إنشاء
saveUser
خاصية
saveUser
و
saveUser
. ونتيجة لذلك ، الآن بعد أن كنا بحاجة إلى تغيير الرابط المستخدم لتحميل قائمة المستخدمين ، ننتقل إلى الوظيفة المقابلة ونجري تغييرات عليها.
تم حل المشكلة الأخيرة التي لدينا في الرقم 4 (تغيير الجدول الذي يعرض قائمة المستخدمين) من خلال إدخال مكون عرض
UserTable
في المشروع ، والذي يشتمل على تكوين كود HTML وتصميم الجدول مع المستخدمين.
مبدأ الانفتاح - الإغلاق (O)
ينص المبدأ المفتوح المفتوح على أن كيانات البرنامج (الفئات والوحدات والوظائف) يجب أن تكون مفتوحة للتوسيع ، ولكن ليس للتعديل.
إذا نظرت إلى مكون
UserList
الموضح أعلاه ، فستلاحظ أنه إذا كنت بحاجة إلى عرض قائمة المستخدمين بتنسيق مختلف ، فسنضطر إلى تعديل طريقة
render
لهذا المكون. هذا انتهاك لمبدأ القرب من الانفتاح.
يمكنك جعل البرنامج يتماشى مع هذا المبدأ باستخدام
تكوين المكونات .
ألق نظرة على كود مكون
UserList
الذي تمت إعادة بنائه:
export class UserList extends Component { static propTypes = { fetchUsers: PropTypes.func.isRequired, saveUsers: PropTypes.func.isRequired }; state = { users: [{id: 1, name: 'Jim', surname: 'Smith', age: 33}] }; componentDidMount() { const users = this.props.fetchUsers(); this.setState({users}); } render() { return ( <div> {this.props.children({ users: this.state.users, saveUsers: this.saveUsers, onUserChange: this.onUserChange })} </div> ); } saveUsers = () => { this.props.saveUsers(this.state.users); }; onUserChange = (user) => {
تحول عنصر
UserList
، نتيجة للتعديل ، إلى أن يكون مفتوحًا للتمديد ، لأنه يعرض المكونات الفرعية ، مما يسهل تغيير سلوكه. هذا المكون مغلق للتعديل ، حيث يتم تنفيذ جميع التغييرات في مكونات منفصلة. يمكننا حتى نشر هذه المكونات بشكل مستقل.
لنلقِ الآن نظرة على كيفية عرض قائمة المستخدمين باستخدام المكون الجديد.
export class PopulatedUserList extends Component { render() { return ( <div> <UserList>{ ({users}) => { return <ul> {users.map((user, index) => <li key={index}>{user.id}: {user.name} {user.surname}</li>)} </ul> } } </UserList> </div> ); } }
نقوم هنا بتوسيع سلوك مكون
UserList
عن طريق إنشاء مكون جديد يعرف كيفية سرد المستخدمين. يمكننا أيضًا تنزيل معلومات أكثر تفصيلاً حول كل مستخدم في هذا المكون الجديد ، دون لمس مكوّن
UserList
، وكان هذا على وجه التحديد الغرض من إعادة هيكلة هذا المكون.
مبدأ استبدال Barbara Lisk (L)
يشير مبدأ الاستبدال لـ Barbara Liskov (مبدأ استبدال Liskov) إلى أنه يجب استبدال الكائنات في البرامج بمثيلات لأنواعها الفرعية دون انتهاك العملية الصحيحة للبرنامج.
إذا كان هذا التعريف يبدو لك مصاغًا بحرية - فهنا نسخة أكثر صرامة منه.
مبدأ استبدال Barbara Liskov: إذا كان هناك شيء يشبه البطة والدجال مثل البطة ، ولكنه يحتاج إلى بطاريات - ربما يتم اختيار التجريد الخاطئنلقي نظرة على المثال التالي:
class User { constructor(roles) { this.roles = roles; } getRoles() { return this.roles; } } class AdminUser extends User {} const ordinaryUser = new User(['moderator']); const adminUser = new AdminUser({role: 'moderator'},{role: 'admin'}); function showUserRoles(user) { const roles = user.getRoles(); roles.forEach((role) => console.log(role)); } showUserRoles(ordinaryUser); showUserRoles(adminUser);
لدينا فئة مستخدم يقبل مُنشئها أدوار المستخدم. بناءً على هذه الفئة ، نقوم بإنشاء فئة
AdminUser
. بعد ذلك ، أنشأنا وظيفة
showUserRoles
البسيطة التي تأخذ كائنًا من نوع
User
كمعلمة وتعرض جميع الأدوار المعينة للمستخدم في وحدة التحكم.
نسمي هذه الوظيفة بتمرير كائنات
ordinaryUser
adminUser
إليها ، وبعد ذلك نواجه خطأ.
خطأماذا حدث يشبه كائن فئة
AdminUser
كائن فئة
User
. من المؤكد أنها "الدجال" مثل
User
، لأنه يحتوي على نفس الأساليب مثل
User
. المشكلة هي "البطاريات". والحقيقة هي أنه عند إنشاء كائن
adminUser
، قمنا بتمرير كائنين إليه ، وليس مصفوفة.
هنا ينتهك مبدأ الاستبدال ، حيث يجب أن تعمل وظيفة
showUserRoles
بشكل صحيح مع كائنات فئة
User
ومع الكائنات التي تم إنشاؤها بناءً على الفئات المنحدرة من هذه الفئة.
ليس من الصعب
AdminUser
هذه المشكلة - فقط قم بتمرير صفيف إلى مُنشئ
AdminUser
بدلاً من الكائنات:
const ordinaryUser = new User(['moderator']); const adminUser = new AdminUser(['moderator','admin']);
مبدأ فصل الواجهة (I)
يشير مبدأ الفصل بين الواجهات إلى أن البرامج لا يجب أن تعتمد على ما لا تحتاج إليه.
يرتبط هذا المبدأ بشكل خاص باللغات ذات الكتابة الثابتة ، حيث يتم تعريف التبعيات بشكل صريح بواسطة الواجهات.
فكر في مثال:
class UserTable extends Component { ... render() { const user = {id: 1, name: 'Thomas', surname: 'Foobar', age: 33}; return ( <div> ... <UserRow user={user}/> ... </div> ); } ... } class UserRow extends Component { static propTypes = { user: PropTypes.object.isRequired, }; render() { return ( <tr> <td>Id: {this.props.user.id}</td> <td>Name: {this.props.user.name}</td> </tr> ) } }
UserRow
مكون
UserTable
مكون
UserRow
،
UserRow
، في الخصائص ، إلى كائن بمعلومات المستخدم الكاملة. إذا قمنا بتحليل رمز مكون
UserRow
، فقد اتضح أنه يعتمد على الكائن الذي يحتوي على جميع المعلومات حول المستخدم ، ولكنه يحتاج فقط إلى خصائص
id
name
.
إذا كتبت اختبارًا لهذا المكون واستخدمت TypeScript أو Flow ، فسيتعين عليك إنشاء تقليد لكائن
user
بكل خصائصه ، وإلا فسيترجم المترجم خطأ.
للوهلة الأولى ، لا يبدو أن هذه مشكلة إذا كنت تستخدم جافا سكريبت خالصًا ، ولكن إذا استقرت TypeScript في التعليمات البرمجية الخاصة بك ، فسيؤدي ذلك فجأة إلى إخفاق في الاختبار بسبب الحاجة إلى تعيين جميع خصائص الواجهات ، حتى إذا تم استخدام بعضها فقط.
ومع ذلك ، فإن البرنامج الذي يفي بمبدأ فصل الواجهة أكثر قابلية للفهم.
class UserTable extends Component { ... render() { const user = {id: 1, name: 'Thomas', surname: 'Foobar', age: 33}; return ( <div> ... <UserRow id={user.id} name={user.name}/> ... </div> ); } ... } class UserRow extends Component { static propTypes = { id: PropTypes.number.isRequired, name: PropTypes.string.isRequired, }; render() { return ( <tr> <td>Id: {this.props.id}</td> <td>Name: {this.props.name}</td> </tr> ) } }
تذكر أن هذا المبدأ لا ينطبق فقط على أنواع الخصائص التي تنتقل إلى المكونات.
مبدأ عكس التبعية (D)
يخبرنا مبدأ انعكاس التبعية أن هدف التبعية يجب أن يكون تجريدًا ، وليس شيئًا محددًا.
خذ بعين الاعتبار المثال التالي:
class App extends Component { ... async fetchUsers() { const users = await fetch('http:
إذا قمنا بتحليل هذا الرمز ، يصبح من الواضح أن مكون
App
يعتمد على وظيفة
fetch
العالمية. إذا كنت تصف علاقة هذه الكيانات في UML ، فستحصل على الرسم البياني التالي.
العلاقة بين المكون والوظيفةلا ينبغي أن تعتمد الوحدة النمطية عالية المستوى على تطبيقات ملموسة منخفضة المستوى لشيء ما. يجب أن تعتمد على التجريد.
لا يحتاج مكون
App
إلى معرفة كيفية تنزيل معلومات المستخدم. لحل هذه المشكلة ، نحتاج إلى عكس التبعيات بين مكون
App
ووظيفة
fetch
. يوجد أدناه مخطط UML يوضح ذلك.
انعكاس التبعيةهنا تنفيذ هذه الآلية.
class App extends Component { static propTypes = { fetchUsers: PropTypes.func.isRequired, saveUsers: PropTypes.func.isRequired }; ... componentDidMount() { const users = this.props.fetchUsers(); this.setState({users}); } ... }
يمكننا الآن أن نقول أن المكون غير متصل للغاية ، لأنه لا يحتوي على معلومات حول البروتوكول الذي نستخدمه - HTTP أو SOAP أو أي بروتوكول آخر. المكون لا يهتم على الإطلاق.
يعمل الامتثال لمبدأ انعكاس التبعية على توسيع إمكانياتنا للعمل مع التعليمات البرمجية ، حيث يمكننا بسهولة تغيير آلية تحميل البيانات ، ولن يتغير مكون
App
على الإطلاق.
بالإضافة إلى ذلك ، يبسط هذا الاختبار ، لأنه من السهل إنشاء وظيفة تحاكي وظيفة تحميل البيانات.
الملخص
باستثمار الوقت في كتابة كود عالي الجودة ، ستكسب امتنان زملائك ونفسك عندما يكون عليك في المستقبل مواجهة هذا الرمز مرة أخرى. يعد دمج مبادئ SOLID في تطوير تطبيقات React استثمارًا جديرًا بالاهتمام.
أعزائي القراء! هل تستخدم مبادئ SOLID عند تطوير تطبيقات React؟
