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?
