Code Metrics : Cyclomatic Complexity

Kompleks kod hata oranı fazla, anlaşılması zor, değiştirmesi zor, test etmesi zor kısacası ençok problem içeren kod olduğu için bunu ölçebilmek ve buna göre önlem almak oldukça önemli. Kodun kalitesini ölçmekte kullanılan birçok metrik(ölçüm) bulunuyor. Bunların kodun kompleksliğini değerini ortaya çıkaran en önemli metriklerden biride Cyclomatic Complexity.  Kim, ne zaman, nasıl, kaç tarihinde bulmuş onları linkteki wiki sayfasından öğrenebilirsiniz.

Metriklerın yaptığı iş kodun belirli kriterlere göre değerlendirmesini yapıp size bazı değerler hakkında fikir vermesi. Mesela geliştirdiğiniz projede kaç satır kod,kaç satır yorum satırı var,sınıflarınız ortalama kaç metod,kaç değişken içeriyor,sınıfların birbirlerine bağımlılıkları nedir… gibi birçok metrik aslında kodunuzu çeşitli konulardaki değerlerini izlemenizi sağlıyor.

Bu bakımdan kodun kalitesini bu çeşit ölçümler ile izleyip, problemli alanları ortaya çıkarıp ardından bunlara göre önlem almak oldukça faydalı oluyor. Hatta bu tarz metrikleri eğer düzgün bir sürüm otomasyon sisteminiz varsa sürüm çıkarma işlemine bile ekleyebiliyorsunuz. Mesela 1000 satır ve daha fazla kod içeren sınıf varsa bu sürümü çıkarma gibi. (Biraz saçma oldu ama idare edin :) ). Şimdi metriklerin neden önemli olduğu konusunda anlaştık sanırım. Anlaşmadıysakta örnekten sonra anlaşacağımızı umuyorum :)

Cyclomatic complexity metriği nedir onu açıklayalım : Cyclomatic complexity kodun ne kadar kompleks olduğu hakkında fikir veren en önemli ölçümlerden biridir. Bunu yapmak için kodun içerisindeki if,else, while, switch,for gibi yapıların sayısını kullanır.Bu değerin projemize nasıl yansıyacağını aşağıdaki tablodan yorumlayabilirsiniz.Bir metodun Cyclomatic complexity değerine göre taşıdığı risk aşağıdaki tabloda bulunuyor.

Cyclomatic Complexity Risk
1 – 10 Basit ,risk az
11 – 20 daha komplex,orta derece riskli
21 – 50 kompleks,yüksek derece riskli
> 50 test edilemeyen,çok yüksek riskli( silin daha iyi :) )

Matematiği sevenler için Wikipedia formülleri oldukça güzel anlatmış. Eğer formüllerle fazla uğraşmak istemiyorsanız Cyclomatic complexity daha basit olarak şu şekilde hesaplanıyor. Mesela aşağıdaki gibi bir metodumuz var diyelim.

        public int Hesapla(int rakam)
        {
            int value = 0;
            if (rakam == 0)
            {
                value = 4;
            }
            else
            {
                value = 0;
            }
            return value;
        }

Yukarıdaki kodun Cyclomatic complexity değerini basit olarak şu şekilde hesaplayabiliriz. Metoda giriş değeri olarak sabit 1 ekleriz, içerideki if için 1 ekleriz, else için 1 ekleriz ve hepsini toplarız: 1+1+1= 3. Yani kodun Cyclomatic complexity değeri 3.Tabi bunu hesaplarken tüm projedeki kodları bu şekilde saymak mantıksız olur. Bunun için birçok open-source ve paralı araç mevcut. Bu araçların bazıları kodu text olarak inceliyor,bazıları ise Java,.NET platformundaki ara dile çevirilmiş kodu inceleyerek bu değerleri otomatik olarak hesaplıyor. Aşağıdaki bunun için kullanılan araçlardan bir kısmını yazıyorum sizde bunlardan size uygun olanını kullanabilirsiniz.

Java İçin

    CheckStyle PMD JDepend Source Monitor

.NET İçin

    Reflector ve Code Metrics Plugin’i FxCop NDepend Source monitor

Şimdi gerçek bir koddan örnek alarak bu değer bizim ne işimize yarar bunları kodu daha iyi hale getirmek için nasıl kullanabiliriz ona bakalım. Aşağıda yazacağım örnek gerçek hayattan değiştirilerek mutasyona uğramış bir C# metodu.

using System;

namespace CyclomaticComplexity
{
    public class ConditionChecker
    {
        private readonly Logger logger;

        public ConditionChecker(Logger logger)
        {
            this.logger = logger;
        }

        public ConditionDataType checkPeriodCondition(short eventTypeID, long unitID, DateTime time, bool maxPeriodTimeViolationShouldBeChecked, DateTime unitTime, double unitSpeed, string unitPlate)
        {
            
            ConditionDataType cdt = null;

            try
            {

                if (maxPeriodTimeViolationShouldBeChecked)
                {
                    if (unitTime.CompareTo(time) > 0)
                    {
                        if (unitSpeed != 0)
                        {
                            logger.log(" Period Violation will be created");
                            cdt = new ConditionDataType(eventTypeID, unitID, true, time, unitTime, "bitiş ", unitPlate);
                        }
                        else
                        {
                            logger.log(" Period Violation will NOT be created, unit speed is zero");
                        }
                    }
                    else
                    {
                        logger.log("Period violation will NOT be created, travelingTime is in the allowed time interval.");
                    }
                }
                else
                {
                    logger.log("Event startTime = " + time);
                    if (unitTime.CompareTo(time) < 0)
                    {
                        if (unitSpeed != 0)
                        {
                            logger.log(" Period Violation will be created");
                            cdt = new ConditionDataType(eventTypeID, unitID, true, time, unitTime, "başlangıç ", unitPlate);
                        }
                        else
                        {
                            logger.log(" Period Violation will NOT be created, unit speed is zero");
                        }
                    }
                    else
                    {
                        logger.log("Period violation will NOT be created, travelingTime is in the allowed time interval.");
                    }
                }
            }
            catch (Exception e)
            {
                logger.log(e.Message);
            }
            return cdt;
        }        
    }
}

Koda biraz bakınca zaten kötü olduğunu görebilirsiniz. Yani bunu ölçmek için bu tarz araçlara ihtiyaç bile yok. Fakat gerçek bir projede binlerce satır kod arasından bu ölçümü yapmak imkansız bu bakımdan bu tarz araçlar gerçek projede oldukça faydalı olabiliyor. Şimdi kodun Cyclomatic complexity değerini ölçmek için bedava olan Reflector aracını kullanacağım. Daha önceden .NET Reflector yazısında nasıl kurulup kullanılacağına dair bilgi vermiştim.

CCDegeri

CCDegeri1

Dipnot olarak değinmeden geçmeyeyim. Kodu Source monitor ile incelediğimde Cyclomatic complexity değeri 12 çıkıyor. Normalde de el ile yapıyı saydığımda böyle çıkmazı lazım. Fakat Reflector 6 olarak gösteriyor sebebini çok fazla incelemedim fakat .NET Compiler aradile çevirirken birtakım optimizasyon yapıp gereksiz else yapılarından kurtulmuş olabilir ya da Source Monitor’deki gibi ortalama komplekslik değerini gösteriyor olabilir.

Gördüğünüz gibi metoda 12 komplekslik değerini baz alarak baktığımızda risk içeren bir metod. Tabi siz metodlara bakarken tamamen değere göre karar vermeyin 10’dan düşük değere sahip olsa bile eğer daha basit hale getirilebiliyorsa mutlaka getirmeye çalışın. Fakat incelediğinizde aslında daha da basitleştirilebileceğini görüyorsunuzdur.

Kolları sıvayıp metodu basitleştirmek için neler yapabiliriz hemen bakalım. İlk olarak aklıma gelen daha önceden yazdığım Refactoring tekniklerinden Extract Method uygulamak geliyor. O yüzden refactoring yapıp kodu aşağı şekle getiriyoruz.

using System;

namespace CyclomaticComplexity
{
    public class ConditionChecker
    {
        private readonly Logger logger;

        public ConditionChecker(Logger logger)
        {
            this.logger = logger;
        }

        public ConditionDataType checkPeriodCondition(short eventTypeID, long unitID, DateTime time, bool maxPeriodTimeViolationShouldBeChecked, DateTime unitTime, double unitSpeed, string unitPlate)
        {
            
            ConditionDataType cdt = null;

            try
            {

                if (maxPeriodTimeViolationShouldBeChecked)
                {
                    cdt = CheckMaxPeriodTimeViolated(unitTime, time, unitSpeed, eventTypeID, unitID, unitPlate, cdt);
                }
                else
                {
                    cdt = CheckMinPeriodTimeViolated(unitTime, time, unitSpeed, eventTypeID, unitID, unitPlate, cdt);
                }
            }
            catch (Exception e)
            {
                logger.log(e.Message);
            }
            return cdt;
        }

        private ConditionDataType CheckMinPeriodTimeViolated(DateTime unitTime, DateTime time, double unitSpeed, short eventTypeID, long unitID, string unitPlate, ConditionDataType cdt)
        {
            logger.log("Event startTime = " + time);
            if (unitTime.CompareTo(time) < 0)
            {
                if (unitSpeed != 0)
                {
                    logger.log(" Period Violation will be created");
                    cdt = new ConditionDataType(eventTypeID, unitID, true, time, unitTime, "başlangıç ", unitPlate);
                }
                else
                {
                    logger.log(" Period Violation will NOT be created, unit speed is zero");
                }
            }
            else
            {
                logger.log("Period violation will NOT be created, travelingTime is in the allowed time interval.");
            }
            return cdt;
        }

        private ConditionDataType CheckMaxPeriodTimeViolated(DateTime unitTime, DateTime time, double unitSpeed, short eventTypeID, long unitID, string unitPlate, ConditionDataType cdt)
        {
            if (unitTime.CompareTo(time) > 0)
            {
                if (unitSpeed != 0)
                {
                    logger.log(" Period Violation will be created");
                    cdt = new ConditionDataType(eventTypeID, unitID, true, time, unitTime, "bitiş ", unitPlate);
                }
                else
                {
                    logger.log(" Period Violation will NOT be created, unit speed is zero");
                }
            }
            else
            {
                logger.log("Period violation will NOT be created, travelingTime is in the allowed time interval.");
            }
            return cdt;
        }
    }

}

CCDegeri3

CCDegeri2

Gördüğünüz gibi koda Refactoring yaptıktan sonra değerler neredeyse yarı yarıya düştü. Tabi bundan sonrada çeşitli refactoring teknikleri uygulayacak kodu daha da basitleştirip bu değeri düşürebiliriz. Gerisini size bırakıyorum.

Sonuç olarak Cyclomatic complexity değerini inceleyerek kodun nerelerinin riskli olup refactoring’e ihtiyacı olduğunu belirleyebiliyoruz. Buradaki asıl amacımız bu değeri rakamsal olarak düşük tutmak değil tabiki. Asıl amacımız daha basit değiştirmesi kolay,okunabilir koda sahip olmak. Burada bu tarz metrikleri kodun riskli alanlarını belirlemek için kullanıyoruz. Özellikle Cyclomatic complexity değeri yüksek olan kod if-else,swtich,for tarzı yapıların bolca bulunduğu metod olduğu için her kontrol yapısı için ayrı bir test yazmak gerekiyor. Mesela Cyclomatic complexity değeri 20 olan bir metoda en az 20 adet test yazmak gerekir. Tabi buda test edilmesi problemli olan alanların tasarımının kötü olduğunu hemen ortaya çıkarmasını sağlıyor.

Kaliteli kodun izini sürmeye devam. Başka metriklerde görüşmek üzere…..

2 thoughts on “Code Metrics : Cyclomatic Complexity

  1. Emre Yazici

    “Dipnot olarak değinmeden geçmeyeyim. Kodu Source monitor ile incelediğimde Cyclomatic complexity değeri 12 çıkıyor. Normalde de el ile yapıyı saydığımda böyle çıkmazı lazım. Fakat Reflector 6 olarak gösteriyor”

    Merhaba,

    Cyclomatic complexity hesabında her ne kadar bazı kaynaklarda “else” sayıma dahil edilse de bu doğru değildir. Zira else bir karar cümleciği değildir ve “default execution path” üzerindedir, başka bir deyişle “else” çalışmada bir dallanmaya neden olmaz. Aynı şey swtich-case yapısındaki “default” deyimi için de geçerlidir. Bu bağlamda Reflector tarafından gösterilen 6 doğru bir sonuçtur.

  2. Buğra Taşdemir

    Yaptığın hesaplamalar tamamen yanlış.

    Cyclomatic Complexity hesaplamanın temel üç yolu vardır:

    1-Graph ı çizersin kapalı alanları sayarsın + 1 yaparak number of regiondan bulursun.

    2-Graph ı çizdikten sonra edgeleri ve nodeları sayarsın Edge-Node+2 yaparak bulursun.

    3-Graphı çizdikten sonra Predicateleri sayarsın + 1 yaparsın ve bulursun.

    Ve bu 3 sonuçta birbirine eşit olmak zorunda.Yani sağlaması gibi düşünülebilir.
    Ayrıca bu üçünden kodlama ile yapılabilecek olan sadece 3üncü yoldur programın içerisinde ifleri whileları forları andleri saydırırsın.
    Çok net biliyorum çünkü projemde bana verilen ödevdi ve kodlarını yazmıştım ve şuan hala elimde bulunmakta :)))

Comments are closed.