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.

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) {
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 dipilihLihatlah 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.
KesalahanApa 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:
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 fungsiModul 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 KetergantunganInilah 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?
