Menerapkan Prinsip SOLID untuk Bereaksi Pengembangan Aplikasi

Kami baru-baru ini menerbitkan materi tentang metodologi SOLID. Hari ini kami membawa kepada Anda terjemahan dari artikel yang membahas penerapan prinsip-prinsip SOLID dalam pengembangan aplikasi menggunakan perpustakaan Bereaksi populer.

gambar

Penulis artikel mengatakan bahwa di sini, demi singkatnya, ia tidak menunjukkan implementasi penuh dari beberapa komponen.

Prinsip Tanggung Jawab Tunggal


Prinsip Tanggung Jawab Tunggal memberi tahu kita bahwa sebuah modul harus memiliki satu dan hanya satu alasan untuk perubahan.

Bayangkan kita sedang mengembangkan aplikasi yang menampilkan daftar pengguna dalam sebuah tabel. Berikut adalah kode untuk komponen 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),       })   } } 

Kami memiliki komponen dalam kondisi penyimpanan daftar pengguna. Kami mengunduh daftar ini melalui HTTP dari server tertentu, daftar ini dapat diedit. Komponen kami melanggar prinsip tanggung jawab tunggal, karena memiliki lebih dari satu alasan untuk perubahan.

Secara khusus, saya dapat melihat empat alasan untuk mengubah komponen. Yaitu, komponen berubah dalam kasus berikut:

  • Setiap kali Anda perlu mengubah judul aplikasi.
  • Setiap kali Anda perlu menambahkan komponen baru ke aplikasi (footer halaman, misalnya).
  • Setiap kali Anda perlu mengubah mekanisme untuk memuat data pengguna, misalnya, alamat server atau protokol.
  • Setiap kali Anda perlu mengubah tabel (misalnya, mengubah format kolom atau melakukan beberapa tindakan lain seperti ini).

Bagaimana cara mengatasi masalah ini? Penting, setelah mengidentifikasi alasan untuk mengubah komponen, untuk mencoba menghilangkannya, untuk menyimpulkan dari komponen asli, menciptakan abstraksi yang sesuai (komponen atau fungsi) untuk setiap alasan tersebut.

Kami akan menyelesaikan masalah komponen App kami dengan refactoring. Kodenya, setelah memecahnya menjadi beberapa komponen, akan terlihat seperti ini:

 class App extends Component {   render() {       return (           <div className="App">               <Header/>               <UserList/>           </div>       );   } } 

Sekarang, jika Anda perlu mengubah judul, kami mengubah komponen Header , dan jika Anda perlu menambahkan komponen baru ke aplikasi, kami mengubah komponen App . Di sini kami memecahkan masalah No. 1 (mengubah tajuk aplikasi) dan masalah No. 2 (menambahkan komponen baru ke aplikasi). Ini dilakukan dengan memindahkan logika yang sesuai dari komponen App ke komponen baru.

Kami sekarang akan memecahkan masalah No. 3 dan No. 4 dengan membuat kelas UserList . Ini kodenya:

 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) {     //         }   saveUsers(row) {       this.props.saveUsers(this.state.users);   } } 

UserList adalah komponen wadah baru kami. Berkat dia, kami memecahkan masalah No. 3 (mengubah mekanisme pemuatan pengguna) dengan membuat saveUser properti saveUser dan saveUser . Akibatnya, sekarang kita perlu mengubah tautan yang digunakan untuk memuat daftar pengguna, kita beralih ke fungsi yang sesuai dan melakukan perubahan.

Masalah terakhir yang kita miliki di nomor 4 (mengubah tabel yang menampilkan daftar pengguna) telah diselesaikan dengan memasukkan komponen presentasi UserTable ke dalam proyek, yang merangkum pembentukan kode HTML dan menata tabel dengan pengguna.

Prinsip keterbukaan-penutupan (O)


Prinsip Terbuka Tertutup menyatakan bahwa entitas program (kelas, modul, fungsi) harus terbuka untuk ekspansi, tetapi tidak untuk modifikasi.

Jika Anda melihat komponen UserList dijelaskan di atas, Anda akan melihat bahwa jika Anda perlu menampilkan daftar pengguna dalam format yang berbeda, kami harus memodifikasi metode render komponen ini. Ini merupakan pelanggaran prinsip keterbukaan-kedekatan.

Anda dapat menyesuaikan program dengan prinsip ini menggunakan komposisi komponen .

Lihatlah kode komponen UserList yang telah di-refactored:

 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) => {       //         }; } 

Komponen UserList , sebagai hasil dari modifikasi, ternyata terbuka untuk ekstensi, karena menampilkan komponen anak, yang memfasilitasi perubahan dalam perilakunya. Komponen ini ditutup untuk modifikasi, karena semua perubahan dilakukan dalam komponen yang terpisah. Kami bahkan dapat menggunakan komponen ini secara mandiri.

Sekarang mari kita lihat caranya, menggunakan komponen baru, daftar pengguna ditampilkan.

 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>       );   } } 

Di sini kami memperluas perilaku komponen UserList dengan membuat komponen baru yang tahu cara membuat daftar pengguna. Kami bahkan dapat mengunduh informasi yang lebih terperinci tentang masing-masing pengguna dalam komponen baru ini tanpa menyentuh komponen UserList , dan inilah tepatnya tujuan refactoring komponen ini.

Prinsip Substitusi Barbara Lisk (L)


Prinsip penggantian Barbara Liskov (Prinsip Pergantian Liskov) menunjukkan bahwa objek dalam program harus diganti dengan instance subtipe mereka tanpa melanggar operasi program yang benar.

Jika definisi ini menurut Anda terlalu bebas dirumuskan - ini adalah versi yang lebih ketat.


Prinsip substitusi Barbara Liskov: jika sesuatu terlihat seperti bebek dan dukun seperti bebek, tetapi perlu baterai - mungkin abstraksi yang salah dipilih

Lihatlah contoh berikut:

 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); 

Kami memiliki kelas User yang konstruktornya menerima peran pengguna. Berdasarkan kelas ini, kami membuat kelas AdminUser . Setelah itu, kami membuat fungsi showUserRoles sederhana yang mengambil objek bertipe User sebagai parameter dan menampilkan semua peran yang ditetapkan untuk pengguna di konsol.

Kami memanggil fungsi ini dengan mengirimkan objek ordinaryUser dan adminUser ke sana, setelah itu kami menemukan kesalahan.


Kesalahan

Apa yang terjadi Objek kelas AdminUser mirip dengan objek kelas User . Ini pasti "dukun" sebagai User , karena memiliki metode yang sama seperti User . Masalahnya adalah "baterai". Faktanya adalah bahwa ketika membuat objek adminUser , kami melewati beberapa objek untuk itu, bukan array.

Di sini prinsip substitusi dilanggar, karena fungsi showUserRoles harus bekerja dengan benar dengan objek kelas User dan dengan objek yang dibuat berdasarkan kelas turunan dari kelas ini.

Tidak sulit untuk AdminUser masalah ini - cukup serahkan array ke konstruktor AdminUser alih-alih objek:

 const ordinaryUser = new User(['moderator']); const adminUser = new AdminUser(['moderator','admin']); 

Prinsip pemisahan antarmuka (I)


Prinsip Segregasi Antarmuka menunjukkan bahwa program tidak boleh bergantung pada apa yang tidak mereka butuhkan.

Prinsip ini sangat relevan dalam bahasa dengan pengetikan statis, di mana dependensi didefinisikan secara eksplisit oleh antarmuka.

Pertimbangkan sebuah contoh:

 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>       )   } } 

Komponen UserTable UserRow komponen UserRow , meneruskannya, di properti, objek dengan informasi pengguna lengkap. Jika kita menganalisis kode komponen UserRow , ternyata itu tergantung pada objek yang berisi semua informasi tentang pengguna, tetapi ia hanya membutuhkan properti id dan name .

Jika Anda menulis tes untuk komponen ini dan menggunakan TypeScript atau Flow, Anda harus membuat tiruan untuk objek user dengan semua propertinya, jika tidak, kompiler akan membuat kesalahan.

Pada pandangan pertama, ini tampaknya tidak menjadi masalah jika Anda menggunakan JavaScript murni, tetapi jika TypeScript pernah mengendap dalam kode Anda, ini akan tiba-tiba menyebabkan kegagalan pengujian karena kebutuhan untuk menetapkan semua properti dari antarmuka, bahkan jika hanya beberapa dari mereka yang digunakan.

Namun, program yang memenuhi prinsip pemisahan antarmuka lebih mudah dipahami.

 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>       )   } } 

Ingatlah bahwa prinsip ini berlaku tidak hanya untuk tipe properti yang diteruskan ke komponen.

Prinsip Ketergantungan Inversi (D)


Prinsip Pembalikan Ketergantungan memberi tahu kita bahwa objek ketergantungan harus berupa abstraksi, bukan sesuatu yang spesifik.

Perhatikan contoh berikut:

 class App extends Component { ... async fetchUsers() {   const users = await fetch('http://totallyhardcodedurl.com/stupid');   this.setState({users}); } ... } 

Jika kami menganalisis kode ini, menjadi jelas bahwa komponen App bergantung pada fungsi fetch global. Jika Anda menggambarkan hubungan entitas ini di UML, Anda mendapatkan diagram berikut.


Hubungan antara komponen dan fungsi

Modul tingkat tinggi seharusnya tidak bergantung pada implementasi konkret tingkat rendah dari sesuatu. Itu harus tergantung pada abstraksi.

Komponen App tidak perlu tahu cara mengunduh informasi pengguna. Untuk mengatasi masalah ini, kita perlu membalikkan dependensi antara komponen App dan fungsi fetch . Di bawah ini adalah diagram UML yang menggambarkan hal ini.


Pembalikan Ketergantungan

Inilah implementasi mekanisme ini.

 class App extends Component {   static propTypes = {       fetchUsers: PropTypes.func.isRequired,       saveUsers: PropTypes.func.isRequired   };   ...     componentDidMount() {       const users = this.props.fetchUsers();       this.setState({users});   }   ... } 

Sekarang kita dapat mengatakan bahwa komponen tersebut tidak terlalu terhubung, karena tidak memiliki informasi tentang protokol yang kita gunakan - HTTP, SOAP, atau lainnya. Komponen tidak peduli sama sekali.

Kepatuhan dengan prinsip inversi dependensi memperluas kemungkinan kami untuk bekerja dengan kode, karena kami dapat dengan mudah mengubah mekanisme pemuatan data, dan komponen App tidak akan berubah sama sekali.

Selain itu, ini menyederhanakan pengujian, karena mudah untuk membuat fungsi yang mensimulasikan fungsi memuat data.

Ringkasan


Investasikan waktu dalam menulis kode berkualitas tinggi, Anda akan mendapatkan rasa terima kasih dari kolega Anda dan diri Anda sendiri ketika, di masa depan, Anda harus menghadapi kode ini lagi. Mengintegrasikan prinsip-prinsip SOLID ke dalam pengembangan aplikasi React adalah investasi yang berharga.

Pembaca yang budiman! Apakah Anda menggunakan prinsip SOLID ketika mengembangkan aplikasi Bereaksi?

Source: https://habr.com/ru/post/id428079/


All Articles