Menulis Kode yang Dapat Dibaca untuk VBA – Pola Coba*
Akhir-akhir ini, saya menemukan diri saya menggunakan Try
pola semakin banyak. Saya sangat menyukai pola ini karena membuat kode lebih mudah dibaca. Ini sangat penting ketika memprogram dalam bahasa pemrograman yang matang seperti VBA di mana penanganan kesalahan terkait dengan aliran kontrol. Secara umum, saya menemukan prosedur apa pun yang mengandalkan penanganan kesalahan sebagai aliran kontrol lebih sulit untuk diikuti.
Skenario
Mari kita mulai dengan sebuah contoh. Model objek DAO adalah kandidat yang sempurna karena cara kerjanya. Lihat, semua objek DAO memiliki Properties
koleksi, yang berisi Property
objek. Namun, siapa pun dapat menambahkan properti khusus. Bahkan, Access akan menambahkan beberapa properti ke berbagai objek DAO. Oleh karena itu, kami mungkin memiliki properti yang mungkin tidak ada dan harus menangani kasus perubahan nilai properti yang ada dan kasus penambahan properti baru.
Mari gunakan Subdatasheet
properti sebagai contoh. Secara default, semua tabel yang dibuat melalui Access UI akan memiliki properti yang disetel ke Auto
, tapi kita mungkin tidak menginginkan itu. Tetapi jika kita memiliki tabel yang dibuat dalam kode atau cara lain, itu mungkin tidak memiliki properti. Jadi kita bisa mulai dengan versi awal kode untuk memperbarui properti semua tabel dan menangani kedua kasus.
Sub Publik EditTableSubdatasheetProperty( _ Opsional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetName" On Error GoTo ErrHandler Tetapkan db =CurrentDb Untuk Setiap tdf Dalam db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Kemudian If Len(tdf.Connect) =0 Dan (Not tdf.Name Like "~*") Kemudian 'Tidak terpasang, atau temp . Set prp =tdf.Properties(SubDatasheetPropertyName) If prp.Value <> NewValue Kemudian prp.Value =NewValue End If End If End IfContinue:NextExitProc:Exit SubErrHandler:If Err.Number =3270 Kemudian Set prp =tdf.CreatePropertyName(SubDatasheetProperty(SubData dbText, NewValue) tdf.Properties.Append prp Resume Continue End If MsgBox Err.Number &":" &Err.Description Resume ExitProc End Sub
Kode mungkin akan berfungsi. Namun, untuk memahaminya, kita mungkin harus membuat diagram beberapa diagram alir. Baris Set prp = tdf.Properties(SubDatasheetPropertyName)
berpotensi menimbulkan kesalahan 3270. Dalam kasus ini, kontrol melompat ke bagian penanganan kesalahan. Kami kemudian membuat properti dan melanjutkan pada titik loop yang berbeda menggunakan label Continue
. Ada beberapa pertanyaan…
- Bagaimana jika 3270 dinaikkan pada baris lain?
- Misalkan baris
Set prp =...
tidak melempar kesalahan 3270 tetapi sebenarnya ada kesalahan lain? - Bagaimana jika saat kita berada di dalam pengendali kesalahan, kesalahan lain terjadi saat menjalankan
Append
atauCreateProperty
? - Haruskah fungsi ini menampilkan
Msgbox
? Pikirkan tentang fungsi yang seharusnya bekerja pada sesuatu atas nama formulir atau tombol. Jika fungsi menampilkan kotak pesan, lalu keluar secara normal, kode panggilan tidak mengetahui ada yang tidak beres dan mungkin terus melakukan hal yang seharusnya tidak dilakukan. - Dapatkah Anda melihat sekilas kodenya dan langsung memahami fungsinya? saya tidak bisa. Saya harus menyipitkannya, lalu berpikir tentang apa yang harus terjadi jika terjadi kesalahan dan secara mental membuat sketsa jalan. Itu tidak mudah dibaca.
Tambahkan HasProperty
prosedur
Bisakah kita melakukan yang lebih baik? Ya! Beberapa programmer sudah mengenali masalah dengan menggunakan penanganan kesalahan seperti yang saya ilustrasikan dan dengan bijak mengabstraksikannya ke dalam fungsinya sendiri. Ini versi yang lebih baik:
Sub Publik EditTableSubdatasheetProperty( _ Opsional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetName" Set db =Untuk Setiap tdf Dalam db.TableDefs Jika (tdf.Attributes Dan dbSystemObject) =0 Kemudian Jika Len(tdf.Connect) =0 Dan (Tidak tdf.Name Seperti "~*") Kemudian 'Tidak terpasang, atau temp. Jika Bukan HasProperty(tdf, SubDatasheetPropertyName) Kemudian Tetapkan prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Tambahkan prp Lain Jika tdf.Properties(SubDatasheetPropertyName) <> NewValuePropertyNameSub) If.PropertyNameSubData Baru (SubDatasheetPropertyValue tdf) End If End If End If NextEnd SubPublic Function HasProperty(TargetObject As Object, PropertyName As String) As Boolean Dim Diabaikan Sebagai Varian Pada Error Lanjutkan Next Ignored =TargetObject.Properties(PropertyName) HasProperty =(Err.Number =0)End FunctionAlih-alih mencampuradukkan alur eksekusi dengan penanganan kesalahan, kami sekarang memiliki fungsi
HasFunction
yang dengan rapi mengabstraksi pemeriksaan rawan kesalahan untuk properti yang mungkin tidak ada. Akibatnya, kita tidak memerlukan penanganan error/alur eksekusi yang rumit seperti yang kita lihat pada contoh pertama. Ini adalah peningkatan besar dan membuat kode yang agak mudah dibaca. Tapi…
- Kami memiliki satu cabang yang menggunakan variabel
prp
dan kami memiliki cabang lain yang menggunakantdf.Properties(SubDatasheetPropertyName)
yang sebenarnya mengacu pada properti yang sama. Mengapa kita mengulangi diri kita sendiri dengan dua cara berbeda untuk merujuk properti yang sama? - Kami menangani properti cukup banyak.
HasProperty
harus menangani properti untuk mengetahui apakah itu ada maka cukup kembalikanBoolean
hasilnya, serahkan pada kode panggilan untuk mencoba lagi dan mendapatkan properti yang sama lagi untuk mengubah nilainya. - Demikian pula, kami menangani
NewValue
lebih dari yang diperlukan. Kami meneruskannya diCreateProperty
atau aturValue
properti properti. HasProperty
fungsi secara implisit mengasumsikan bahwa objek memilikiProperties
member dan menyebutnya late-bound, yang berarti error runtime jika jenis objek yang salah diberikan padanya.
Gunakan TryGetProperty
sebagai gantinya
Bisakah kita melakukan yang lebih baik? Ya! Di situlah kita perlu melihat pola Try. Jika Anda pernah memprogram dengan .NET, Anda mungkin pernah melihat metode seperti TryParse
di mana alih-alih meningkatkan kesalahan pada kegagalan, kita dapat mengatur kondisi untuk melakukan sesuatu untuk sukses dan sesuatu yang lain untuk kegagalan. Tetapi yang lebih penting, kami memiliki hasil yang tersedia untuk sukses. Jadi, bagaimana kami meningkatkan HasProperty
fungsi? Untuk satu hal, kita harus mengembalikan Property
obyek. Mari kita coba kode ini:
Public Function TryGetProperty( _ ByVal SourceProperties As DAO.Properties, _ ByVal PropertyName As String, _ ByRef OutProperty As DAO.Property _) As Boolean On Error Resume Next Set OutProperty =SourceProperties(PropertyName) Jika Err.Number Kemudian Tetapkan OutProperty =Tidak Ada Yang Berakhir Jika Pada Kesalahan GoTo 0 TryGetProperty =(Tidak KeluarProperty Bukan Apa-apa) Fungsi Akhir
Dengan sedikit perubahan, kami telah mencetak beberapa kemenangan besar:
- Akses ke
Property
tidak lagi terikat terlambat. Kita tidak perlu berharap bahwa suatu objek memiliki properti bernamaProperties
dan itu dariDAO.Properties
. Ini dapat diverifikasi pada waktu kompilasi. - Alih-alih hanya
Boolean
hasilnya, kita juga bisa mendapatkanProperty
. yang diambil objek, tetapi hanya pada keberhasilan. Jika kita gagal,OutProperty
parameternya adalahNothing
. Kami masih akan menggunakanBoolean
result untuk membantu menyetel alur naik seperti yang akan segera Anda lihat. - Dengan menamai fungsi baru kita dengan
Try
awalan, kami menunjukkan bahwa ini dijamin tidak menimbulkan kesalahan dalam kondisi operasi normal. Jelas, kami tidak dapat mencegah kesalahan memori atau sesuatu seperti itu, tetapi pada saat itu, kami memiliki masalah yang jauh lebih besar. Namun di bawah kondisi operasi normal, kami menghindari kekusutan penanganan kesalahan kami dengan alur eksekusi. Kode sekarang dapat dibaca dari atas ke bawah tanpa melompat maju atau mundur.
Perhatikan bahwa menurut konvensi, saya mengawali properti "keluar" dengan Out
. Itu membantu memperjelas bahwa kita seharusnya meneruskan variabel ke fungsi yang tidak diinisialisasi. Kami juga mengharapkan bahwa fungsi akan menginisialisasi parameter. Itu akan menjadi jelas ketika kita melihat kode panggilan. Jadi, mari kita siapkan kode panggilannya.
Revisi kode panggilan menggunakan TryGetProperty
Sub Publik EditTableSubdatasheetProperty( _ Opsional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetName" Set db =Untuk Setiap tdf Dalam db.TableDefs Jika (tdf.Attributes Dan dbSystemObject) =0 Kemudian Jika Len(tdf.Connect) =0 Dan (Tidak tdf.Name Seperti "~*") Kemudian 'Tidak terpasang, atau temp. If TryGetProperty(tdf, SubDatasheetPropertyName, prp) Kemudian If prp.Value <> NewValue Then prp.Value =NewValue End If Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Property End If EndAppend prp Jika NextEnd Sub
Kode sekarang sedikit lebih mudah dibaca dengan pola Coba pertama. Kami telah berhasil mengurangi penanganan prp
. Perhatikan bahwa kami melewati prp
variabel ke dalam prp
akan diinisialisasi dengan properti yang ingin kita manipulasi. Jika tidak, prp
tetap Nothing
. Kami kemudian dapat menggunakan CreateProperty
untuk menginisialisasi prp
variabel.
Kami juga membalik negasi sehingga kode menjadi lebih mudah dibaca. Namun, kami belum benar-benar mengurangi penanganan NewValue
parameter. Kami masih memiliki blok bersarang lain untuk memeriksa nilainya. Bisakah kita melakukan yang lebih baik? Ya! Mari tambahkan fungsi lain:
Menambahkan TrySetPropertyValue
prosedur
Fungsi Publik TrySetPropertyValue( _ ByVal SourceProperty As DAO.Property, _ ByVal NewValue As Variant_) Sebagai Boolean Jika SourceProperty.Value =PropertyValue Kemudian TrySetPropertyValue =True Else On Error Resume Next SourceProperty.Value =PropertyValue GoToValue =NewValue 0 SourceProperty.Value =NewValue) Akhiri Fungsi IfEnd
Karena kami menjamin bahwa fungsi ini tidak akan membuat kesalahan saat mengubah nilai, kami menyebutnya TrySetPropertyValue
. Lebih penting lagi, fungsi ini membantu merangkum semua detail mengerikan seputar perubahan nilai properti. Kami memiliki cara untuk menjamin bahwa nilainya adalah nilai yang kami harapkan. Mari kita lihat bagaimana kode panggilan akan diubah dengan fungsi ini.
Memperbarui kode panggilan menggunakan keduanya TryGetProperty
dan TrySetPropertyValue
Sub Publik EditTableSubdatasheetProperty( _ Opsional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetName" Set db =Untuk Setiap tdf Dalam db.TableDefs Jika (tdf.Attributes Dan dbSystemObject) =0 Kemudian Jika Len(tdf.Connect) =0 Dan (Tidak tdf.Name Seperti "~*") Kemudian 'Tidak terpasang, atau temp. If TryGetProperty(tdf, SubDatasheetPropertyName, prp) Kemudian TrySetPropertyValue prp, NewValue Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append prp End If End If End If NextEnd SubKami telah menghilangkan seluruh
If
memblokir. Kami sekarang dapat dengan mudah membaca kode dan segera bahwa kami mencoba untuk menetapkan nilai properti dan jika terjadi kesalahan, kami terus melanjutkan. Itu jauh lebih mudah dibaca dan nama fungsinya menggambarkan dirinya sendiri. Nama yang baik membuatnya tidak perlu lagi mencari definisi fungsi untuk memahami apa yang dilakukannya.Membuat
TryCreateOrSetProperty
prosedurKode lebih mudah dibaca tetapi kami masih memiliki
Else
memblokir pembuatan properti. Bisakah kita melakukan yang lebih baik lagi? Ya! Mari kita pikirkan apa yang perlu kita capai di sini. Kami memiliki properti yang mungkin ada atau mungkin tidak ada. Jika tidak, kami ingin membuatnya. Apakah sudah ada atau belum, kita perlu mengaturnya ke nilai tertentu. Jadi yang kita butuhkan adalah fungsi yang akan membuat properti atau memperbarui nilainya jika sudah ada. Untuk membuat properti, kita harus memanggilCreateProperty
yang sayangnya tidak ada diProperties
melainkan objek DAO yang berbeda. Jadi, kita harus terlambat mengikat dengan menggunakanObject
tipe data. Namun, kami masih dapat menyediakan beberapa pemeriksaan runtime untuk menghindari kesalahan. Mari buatTryCreateOrSetProperty
fungsi:Public Function TryCreateOrSetProperty( _ ByVal SourceDaoObject As Object, _ ByVal PropertyName As String, _ ByVal PropertyType As DAO.DataTypeEnum, _ ByVal PropertyValue As Variant, _ ByRef OutProperty As DAO.Property _) Sebagai Boolean Select SourceDao True Case Type Adalah DAO.TableDef, _ TypeOf SourceDaoObject Adalah DAO.QueryDef, _ TypeOf SourceDaoObject Adalah DAO.Field, _ TypeOf SourceDaoObject Adalah DAO.Database Jika TryGetProperty(SourceDaoObject.Properties, PropertyName, OutProperty) Lalu TryCreateOrSetPropertyV Error Resume Next Set OutProperty =SourceDaoObject.CreateProperty(PropertyName, PropertyType, PropertyValue) SourceDaoObject.Properties.Append OutProperty If Err.Number Kemudian Set OutProperty =Tidak Ada yang Berakhir Jika Aktif Error GoTo 0 TryCreateOrSetProperty =(OutProperty Is Nothing) End If Case Else Err.Raise 5, , "Objek tidak valid disediakan untuk parameter SourceDaoObject. Itu harus berupa objek DAO yang berisi anggota CreateProperty." End SelectEnd FunctionBeberapa hal yang perlu diperhatikan:
- Kami dapat mengembangkan
Try*
sebelumnya fungsi yang kami definisikan, yang membantu mengurangi pengkodean badan fungsi, memungkinkannya untuk lebih fokus pada pembuatan jika tidak ada properti seperti itu. - Ini tentu lebih bertele-tele karena pemeriksaan runtime tambahan, tetapi kami dapat mengaturnya sehingga kesalahan tidak mengubah alur eksekusi dan kami masih dapat membaca dari atas ke bawah tanpa melompat.
- Alih-alih melempar
MsgBox
entah dari mana, kami menggunakanErr.Raise
dan mengembalikan kesalahan yang berarti. Penanganan kesalahan yang sebenarnya didelegasikan ke kode panggilan yang kemudian dapat memutuskan apakah akan menampilkan kotak pesan kepada pengguna atau melakukan sesuatu yang lain. - Karena penanganan kami yang cermat dan ketentuan bahwa
SourceDaoObject
parameter valid, semua jalur yang mungkin menjamin bahwa setiap masalah dengan membuat atau menyetel nilai properti yang ada akan ditangani dan kami akan mendapatkanfalse
hasil. Itu mempengaruhi kode panggilan seperti yang akan kita lihat sebentar lagi.
Versi terakhir dari kode panggilan
Mari perbarui kode panggilan untuk menggunakan fungsi baru:
Sub Publik EditTableSubdatasheetProperty( _ Opsional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetName" Set db =Untuk Setiap tdf Dalam db.TableDefs Jika (tdf.Attributes Dan dbSystemObject) =0 Kemudian Jika Len(tdf.Connect) =0 Dan (Tidak tdf.Name Seperti "~*") Kemudian 'Tidak terpasang, atau temp. TryCreateOrSetProperty tdf, SubDatasheetPropertyName, dbText, NewValue End If End If NextEnd Sub
Itu cukup meningkatkan keterbacaan. Dalam versi aslinya, kita harus meneliti sejumlah If
blok dan bagaimana penanganan kesalahan mengubah aliran eksekusi. Kami harus mencari tahu apa sebenarnya yang dilakukan konten untuk menyimpulkan bahwa kami mencoba mendapatkan properti atau membuatnya jika tidak ada dan menyetelnya ke nilai tertentu. Dengan versi saat ini, semuanya ada atas nama fungsi, TryCreateOrSetProperty
. Sekarang kita dapat melihat apa yang diharapkan dari fungsi tersebut.
Kesimpulan
Anda mungkin bertanya-tanya, “tetapi kami menambahkan lebih banyak fungsi dan lebih banyak baris. Bukankah itu banyak pekerjaan?" Memang benar bahwa dalam versi saat ini, kami mendefinisikan 3 fungsi lagi. Namun, Anda dapat membaca setiap fungsi secara terpisah dan masih dengan mudah memahami apa yang harus dilakukan. Anda juga melihat bahwa TryCreateOrSetProperty
fungsi dapat dibangun di atas 2 Try*
lainnya fungsi. Itu berarti kami memiliki lebih banyak fleksibilitas dalam menyusun logika bersama.
Jadi, jika kita menulis fungsi lain yang melakukan sesuatu dengan properti objek, kita tidak perlu menulis semuanya dan juga tidak menyalin-tempel kode dari EditTableSubdatasheetProperty
asli. ke dalam fungsi baru. Bagaimanapun, fungsi baru mungkin memerlukan beberapa varian yang berbeda dan dengan demikian memerlukan urutan yang berbeda. Terakhir, perlu diingat bahwa penerima manfaat sebenarnya adalah kode panggilan yang perlu melakukan sesuatu. Kami ingin menjaga agar kode panggilan tetap cukup tinggi tanpa terperosok ke dalam detail yang dapat berdampak buruk bagi pemeliharaan.
Anda juga dapat melihat bahwa penanganan kesalahan disederhanakan secara signifikan, meskipun kami menggunakan On Error Resume Next
. Kita tidak perlu lagi mencari kode kesalahan karena pada kebanyakan kasus, kita hanya tertarik pada apakah berhasil atau tidak. Lebih penting lagi, penanganan kesalahan tidak mengubah alur eksekusi di mana Anda memiliki beberapa logika di badan dan logika lain dalam penanganan kesalahan. Yang terakhir adalah situasi yang pasti ingin kita hindari karena jika ada kesalahan di dalam pengendali kesalahan, maka perilakunya bisa mengejutkan. Sebaiknya hindari hal itu agar tidak menjadi kemungkinan.
Ini semua tentang abstraksi
Tetapi skor terpenting yang kami menangkan di sini adalah tingkat abstraksi yang sekarang dapat kami capai. Versi asli EditTableSubdatasheetProperty
berisi banyak detail tingkat rendah tentang objek DAO sebenarnya bukan tentang tujuan inti dari fungsi tersebut. Pikirkan tentang hari-hari di mana Anda telah melihat prosedur yang panjangnya ratusan baris dengan loop atau kondisi yang sangat bersarang. Apakah Anda ingin men-debug itu? Saya tidak.
Jadi ketika saya melihat sebuah prosedur, hal pertama yang benar-benar ingin saya lakukan adalah merobek bagian-bagian itu menjadi fungsinya sendiri, sehingga saya dapat meningkatkan tingkat abstraksi untuk prosedur itu. Dengan memaksa diri kita untuk mendorong tingkat abstraksi, kita juga dapat menghindari kelas besar bug di mana penyebabnya adalah satu perubahan di bagian mega-prosedur memiliki konsekuensi yang tidak diinginkan untuk bagian lain dari prosedur. Saat kita memanggil fungsi dan meneruskan parameter, kita juga mengurangi kemungkinan efek samping yang tidak diinginkan mengganggu logika kita.
Oleh karena itu mengapa saya menyukai pola “Coba*”. Saya harap ini berguna untuk proyek Anda juga.