Kerumitan siklomatik ialah metrik yang mengukur kerumitan dan kekusutan kod.
Kerumitan siklomatik yang tinggi bukanlah perkara yang baik, sebaliknya.
Ringkasnya, kerumitan cyclomatic adalah berkadar terus dengan bilangan laluan pelaksanaan yang mungkin dalam program. Dalam erti kata lain, kerumitan siklomatik dan jumlah bilangan pernyataan bersyarat (terutamanya sarangnya) berkait rapat.
Jadi hari ini, mari kita bincangkan tentang pernyataan bersyarat.
Pada tahun 2007, Francesco Cirillo melancarkan gerakan yang dipanggil Anti-jika.
Francesco Cirillo ialah lelaki yang mencipta teknik Pomodoro. Saya sedang menulis catatan blog ini sekarang “di bawah Pomodoro.”
Saya rasa kita semua dengan cepat mengetahui maksud kempen ini daripada namanya. Menariknya, pergerakan itu mempunyai beberapa saintis komputer dalam kalangan pengikutnya.
Hujah mereka sangat kukuh — jika kenyataan adalah jahat, membawa kepada pertumbuhan eksponen dalam laluan pelaksanaan program.
Ringkasnya, itulah kerumitan siklomatik. Semakin tinggi ia, semakin sukar bukan sahaja untuk membaca dan memahami kod tetapi juga untuk menutupnya dengan ujian.
Pasti, kami mempunyai jenis metrik "bertentangan" — liputan kod, yang menunjukkan jumlah kod anda diliputi oleh ujian. Tetapi adakah metrik ini, bersama-sama dengan alat yang kaya dalam bahasa pengaturcaraan kami untuk menyemak liputan, membenarkan mengabaikan kerumitan siklomatik dan memercikkan jika kenyataan di sekeliling hanya berdasarkan "naluri"?
Saya rasa tidak.
Hampir setiap kali saya mendapati diri saya hendak bersarang jika di dalam yang lain, saya sedar bahawa saya melakukan sesuatu yang sangat bodoh yang boleh ditulis semula secara berbeza — sama ada tanpa bersarang jika atau tanpa jika sama sekali.
Anda memang perasan perkataan "hampir," bukan?
Saya langsung tidak perasan perkara ini. Jika anda melihat GitHub saya, anda akan menemui lebih daripada satu contoh kod lama dengan bukan sahaja kerumitan siklomatik yang tinggi tetapi kegilaan siklomatik lurus.
Apakah yang membantu saya menjadi lebih sedar tentang isu ini? Mungkin pengalaman dan beberapa perkara pintar yang saya pelajari dan peluk kira-kira setahun yang lalu. Itulah yang saya ingin kongsikan dengan anda hari ini.
Menyemak sesuatu apabila kita belum "mengetahui" perkara itu mungkin merupakan sumber yang paling biasa untuk menggunakan pernyataan bersyarat berdasarkan "naluri".
Sebagai contoh, katakan kita perlu melakukan sesuatu berdasarkan umur pengguna dan kita mesti memastikan umur itu sah (termasuk dalam julat yang munasabah). Kita mungkin akan mendapat kod seperti ini:
from typing import Optional def process_age(age: Optional[int]) -> None: if age is None: raise ValueError("Age cannot be null") if age < 0 or age > 150: raise ValueError("Age must be between 0 and 150")
Kita semua telah melihat dan mungkin menulis kod yang serupa beratus kali.
Bagaimanakah kita menghapuskan semakan bersyarat ini dengan mengikuti prinsip meta yang dibincangkan?
Dalam kes khusus kami dengan umur, kami boleh menggunakan pendekatan kegemaran saya — beralih daripada obsesi primitif ke arah menggunakan jenis data tersuai.
class Age: def __init__(self, value: int) -> None: if value < 0 or value > 150: raise ValueError("Age must be between 0 and 150") self.value = value def get_value(self) -> int: return self.value def process_age(age: Age) -> None: # Age is guaranteed to be valid, process it directly
Hore, kurang satu jika! Pengesahan dan pengesahan umur kini sentiasa "di mana umur diketahui" — dalam tanggungjawab dan skop kelas yang berasingan.
Kita boleh pergi lebih jauh/berbeza jika kita mahu mengalih keluar if dalam kelas Age, mungkin dengan menggunakan model Pydantic dengan validator atau malah menggantikan if dengan assert — tidak mengapa sekarang.
Teknik atau mekanisme lain yang membantu untuk menyingkirkan semakan bersyarat dalam meta-idea yang sama ini termasuk pendekatan seperti menggantikan keadaan dengan polimorfisme (atau fungsi lambda tanpa nama) dan fungsi penguraian yang mempunyai bendera boolean licik.
Sebagai contoh, kod ini (tinju yang mengerikan, bukan?):
class PaymentProcessor: def process_payment(self, payment_type: str, amount: float) -> str: if payment_type == "credit_card": return self.process_credit_card_payment(amount) elif payment_type == "paypal": return self.process_paypal_payment(amount) elif payment_type == "bank_transfer": return self.process_bank_transfer_payment(amount) else: raise ValueError("Unknown payment type") def process_credit_card_payment(self, amount: float) -> str: return f"Processed credit card payment of {amount}." def process_paypal_payment(self, amount: float) -> str: return f"Processed PayPal payment of {amount}." def process_bank_transfer_payment(self, amount: float) -> str: return f"Processed bank transfer payment of {amount}."
Dan tidak mengapa jika anda menggantikan if/elif dengan mancis/kes — ia adalah sampah yang sama!
Agak mudah untuk menulis semula sebagai:
from abc import ABC, abstractmethod class PaymentProcessor(ABC): @abstractmethod def process_payment(self, amount: float) -> str: pass class CreditCardPaymentProcessor(PaymentProcessor): def process_payment(self, amount: float) -> str: return f"Processed credit card payment of {amount}." class PayPalPaymentProcessor(PaymentProcessor): def process_payment(self, amount: float) -> str: return f"Processed PayPal payment of {amount}." class BankTransferPaymentProcessor(PaymentProcessor): def process_payment(self, amount: float) -> str: return f"Processed bank transfer payment of {amount}."
betul ke?
Contoh penguraian fungsi dengan bendera boolean kepada dua fungsi berasingan adalah setua masa, sangat biasa dan sangat menjengkelkan (menurut pendapat jujur saya).
def process_transaction(transaction_id: int, amount: float, is_internal: bool) -> None: if is_internal: # Process internal transaction pass else: # Process external transaction pass
Dua fungsi akan menjadi lebih baik dalam apa jua keadaan, walaupun 2/3 daripada kod di dalamnya adalah sama! Ini adalah salah satu daripada senario di mana pertukaran dengan DRY adalah hasil daripada akal sehat, menjadikan kod itu lebih baik.
The big difference here is that mechanically, on autopilot, we are unlikely to use these approaches unless we've internalized and developed the habit of thinking through the lens of this principle.
Otherwise, we'll automatically fall into if: if: elif: if...
In fact, the second technique is the only real one, and the earlier "first" technique is just preparatory practices, a shortcut for getting in place :)
Indeed, the only ultimate way, method — call it what you will — to achieve simpler code, reduce cyclomatic complexity, and cut down on conditional checks is making a shift in the mental models we build in our minds to solve specific problems.
I promise, one last silly example for today.
Consider that we're urgently writing a backend for some online store where user can make purchases without registration, or with it.
Of course, the system has a User class/entity, and finishing with something like this is easy:
def process_order(order_id: int, user: Optional[User]) -> None: if user is not None: # Process order for a registered user pass else: # Process order for a guest user pass
But noticing this nonsense, thanks to the fact that our thinking has already shifted in the right direction (I believe), we'll go back to where the User class is defined and rewrite part of the code in something like this:
class User: def __init__(self, name: str) -> None: self.name = name def process_order(self, order_id: int) -> None: pass class GuestUser(User): def __init__(self) -> None: super().__init__(name="Guest") def process_order(self, order_id: int) -> None: pass
So, the essence and beauty of it all is that we don't clutter our minds with various patterns and coding techniques to eliminate conditional statements and so on.
By shifting our focus to the meta-level, to a higher level of abstraction than just the level of reasoning about lines of code, and following the idea we've discussed today, the right way to eliminate conditional checks and, in general, more correct code will naturally emerge.
A lot of conditional checks in our code arise from the cursed None/Null leaking into our code, so it's worth mentioning the quite popular Null Object pattern.
When following Anti-if, you can go down the wrong path by clinging to words rather than meaning and blindly following the idea that "if is bad, if must be removed.”
Since conditional statements are semantic rather than syntactic elements, there are countless ways to remove the if token from your code without changing the underlying logic in our beloved programming languages.
Replacing an elif chain in Python with a match/case isn’t what I’m talking about here.
Logical conditions stem from the mental “model” of the system, and there’s no universal way to "just remove" conditionals entirely.
In other words, cyclomatic complexity and overall code complexity aren’t tied to the physical representation of the code — the letters and symbols written in a file.
The complexity comes from the formal expression, the verbal or textual explanation of why and how specific code works.
So if we change something in the code, and there are fewer if statements or none at all, but the verbal explanation of same code remains the same, all we’ve done is change the representation of the code, and the change itself doesn’t really mean anything or make any improvement.
Atas ialah kandungan terperinci Kebijaksanaan Mengelakkan Kenyataan Bersyarat. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!