Yazılımda Geviş Getirme Taktiği Nasıl Uygulanır?

Koyun, keçi, deve gibi hayvanlar otları çiğnemeden yutarlar. Daha sonra dinlenme esnasında yuttukları otları ağızlarına getirerek, çiğnerler. Buna işleme geviş getirme denir.

Geviş getirme hayvanların evrim sürecinde düşmanlarına karşı geliştirdikleri bir savunma mekanizmasıdır. Bu tür hayvanlar düşmanlarından kaçabilmek için buldukları besinleri çiğnemden yutarlar. Daha sonra kendilerini güvende hissettikleri bir yer ve anda çiğnemeden yuttukları bu besinleri geviş getirme yöntemiyle tekrar çiğnerler.

Bu yazımda geviş getirme mekanizmasının yazılımda nasıl kullanılabileceğini göstermek istiyorum.

Öncelikle yazdığımız kodların yazıcıdan çıktısını almamız gerekiyor. Daha sonra kağıtları küçük parçalara bölerek, çiğnemeden, yutuyoruz….. :)

İşin şakasını yaptıktan sonra, geviş taktiğine tekrar geri dönelim. Geviş getirme mekanizması iki kademeli işlemektedir. Birinci kademede hayvan çok hızlı bir şekilde otları dişleriyle kopararak, çiğnemeden yutar. İkinci kademede hayvan geviş getirerek, daha önce çiğnemeye zaman bulamadığı besinleri çiğner.

Şimdi kod yazma sürecini de bu iki kademeyle şekillendirmeye çalışalım. Birinci kademenin geviş getirme mekanizmasına göre hızlı bir şekilde, kodun hangi yapıda olduğu düşünülmeden yazılması gerekiyor. Ben bu kademede test güdümlü yazılım yaparak, kodu oluşturmayı tercih ediyorum. Amacım çalışır durumda olan testleri ve kodları oluşturmak. Birinci kademede kodun hangi durumda olduğu beni ilgilendirmiyor. Ot yiyen hayvanın yaptığı gibi kodu yazıyor ve kaçıyorum. Kaçıyorum, çünkü ikinci kademede kodu tekrar çiğneyerek, yani kodu yeniden yapılandırarak (refactoring), tekrar elden geçireceğim. Kaçabilmem için testlerin ve kodun çalışır durumda olması gerekiyor.

Şimdi bunun nasıl yapıldığını bir örnek üzerinde inceleyelim. Bir dosyada yer alan tüm kelimeleri, kaç kez kullanıldıklarını görecek şekilde bir dosyaya eklememiz gerekiyor. Örneğin dosyamız içerisinde şu satır olsun:

bu bir test

Elde edeceğimiz dosyanın içeriği şu şekilde olmalı:

test:   1
bir:    1
bu:     1

Her kelimenin dosya içinde kaç kez kullanıldığını görüyoruz. Bu kodu test güdümlü şu şekilde yazdım:

package test.the.west;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.io.FileUtils;
import org.hamcrest.CoreMatchers;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

public class WordSorterTest {

    private class FileNotFound extends RuntimeException {

        private static final long serialVersionUID = 1L;

    }

    private class FileIsEmpty extends RuntimeException {

        private static final long serialVersionUID = 1L;

    }

    private class SortError extends RuntimeException {

        public SortError(final Exception e) {
            super(e);
        }

        private static final long serialVersionUID = 1L;

    }

    private class WordSorter {

        public void sort(final File file) {

            if (!file.exists()) {
                throw new FileNotFound();
            }

            List<String> lines = null;

            try {
                lines = FileUtils.readLines(file);

                if (lines.size() == 0) {
                    throw new FileIsEmpty();
                }
            } catch (final IOException e) {
                throw new SortError(e);
            }

            final Map<String, Integer> values = new HashMap<>();

            for (final String line : lines) {

                final String[] words = line.split(" ");

                for (final String word : words) {

                    Integer value = values.get(word);
                    if (value == null) {
                        values.put(word, new Integer(1));
                    } else {
                        values.put(word, ++value);
                    }
                }
            }

            final StringBuilder newContent = new StringBuilder();

            final Set<Entry<String, Integer>> entrySet = values.entrySet();
            for (final Entry<String, Integer> entry : entrySet) {
                newContent.append(entry.getKey() + ":\t" + entry.getValue()).append("\n");
            }

            System.out.println(newContent);

            try {
                FileUtils.write(file, newContent);
            } catch (final IOException e) {
                throw new SortError(e);
            }
        }
    }

    @Test(expected = FileNotFound.class)
    public void when_file_does_not_exits_error_is_thrown() throws Exception {

        final WordSorter sorter = new WordSorter();
        sorter.sort(new File("xx"));

    }

    @Test(expected = FileIsEmpty.class)
    public void when_file_is_emptry_error_is_thrown() throws Exception {

        final File file = File.createTempFile("myfile", ".txt");

        final WordSorter sorter = new WordSorter();
        sorter.sort(file);

    }

    @Test
    public void words_are_sorted_by_count_and_written_to_the_same_file() throws Exception {

        final File file = File.createTempFile("myfile", ".txt");

        FileUtils.write(file, getDummyContent());

        final WordSorter sorter = new WordSorter();
        sorter.sort(file);

        final List<String> lines = FileUtils.readLines(file);

        assertThat(lines.size(), is(3));
        assertThat(lines.get(0), is(CoreMatchers.equalTo("test:\t1")));
        assertThat(lines.get(1), is(CoreMatchers.equalTo("bir:\t1")));
        assertThat(lines.get(2), is(CoreMatchers.equalTo("bu:\t1")));

    }

    private String getDummyContent() {
        return "bu bir test";
    }
}

İşletme mantığı WordSorter sınıfının sort() isimli metodunda yer alıyor. İlk bakışta bu metot bünyesinde ne olup, bittiğini anlamak mümkün değil, çünkü kod testleri tatmin edecek seviyeden ileri gitmiyor, yani metot temiz kod (clean code) prensiplerine uygun değil. Tüm testlerimiz çalışır durumda ise, o zaman birinci kademeyi tamamlamış oluyoruz. Bu kademede gerisi bizi ilgilendirmiyor. Şimdi tekrar geviş getiren hayvanı hatırlayalım. O da ilk kademede dişleriyle otları koparıp, yuttuktan sonra geviş getirebileceği bir yere gider. Hayvan ilk kademede otun çiğnenmesi ile ilgilenmez. Biz de ilk kademede kodun çiğnenmesi, yani yeniden yapılandırılarak, daha okunur hale gelmesi ile ilgilenmiyoruz.

Şimdi geviş getirme zamanı. İkinci kademede kodu yeniden yapılandırarak (çiğneyerek), daha okunur hale getiriyoruz. Kodun yeni hali aşağıda yer almaktadır.

private class WordSorter {

    public void sort(final File file) {

        checkFile(file);

        final List<String> lines = getLines(file);

        final Map<String, Integer> sortedKeys = new HashMap<>();

        sortWords(lines, sortedKeys);

        final String newContent = buildSortedContent(sortedKeys);

        logSortedContent(newContent);

        writeSortedContentToFile(file, newContent);
    }

    private List<String> getLines(final File file) {
        List<String> lines = null;

        try {
            lines = FileUtils.readLines(file);
            checkLines(lines);
        } catch (final IOException e) {
            throw new SortError(e);
        }
        return lines;
    }

    private void checkLines(final List<String> lines) {
        if (noLines(lines)) {
            throw new FileIsEmpty();
        }
    }

    private void checkFile(final File file) {
        if (!file.exists()) {
            throw new FileNotFound();
        }
    }

    private void writeSortedContentToFile(final File file, final String newContent) {
        try {
            writeFile(file, newContent);
        } catch (final IOException e) {
            throw new SortError(e);
        }
    }

    private void writeFile(final File file, final String newContent) throws IOException {
        FileUtils.write(file, newContent);
    }

    private void logSortedContent(final String newContent) {
        System.out.println(newContent);
    }

    private String buildSortedContent(final Map<String, Integer> values) {
        final StringBuilder content = new StringBuilder();

        final Set<Entry<String, Integer>> entrySet = values.entrySet();

        for (final Entry<String, Integer> entry : entrySet) {
            content.append(entry.getKey() + ":\t" + entry.getValue()).append("\n");
        }
        return content.toString();
    }

    private void sortWords(final List<String> lines, final Map<String, Integer> values) {
        for (final String line : lines) {

            final String[] words = line.split(" ");
            handleWords(values, words);
        }
    }

    private void handleWords(final Map<String, Integer> values, final String[] words) {
        for (final String word : words) {

            handleWord(values, word);
        }
    }

    private void handleWord(final Map<String, Integer> values, final String word) {
        Integer value = values.get(word);
        if (valueNull(value)) {
            putWith(values, word, new Integer(1));
        } else {
            putWith(values, word, ++value);
        }
    }

    private boolean valueNull(final Integer value) {
        return value == null;
    }

    private void putWith(final Map<String, Integer> values, final String word, final Integer value) {
        values.put(word, value);
    }

    private boolean noLines(final List<String> lines) {
        return lines.size() == 0;
    }
}

Yeni yapılandırma kademesinde metotların 3-5 satırdan fazla olmamasına dikkat ediyorum. Bir metot bünyesinde ne kadar az satır varsa, kodu kavrama oranı o oranda artacaktır. sort() metodu şimdi okuyucusuna hangi işlemi gerçekleştirdiğini daha iyi anlatmaktadır.

Birinci kademede işletme mantığını çalışır hale getirmeye çalışırken, ikinci kademede kodu refactoring yöntemleriyle daha okunur hale getirmeye odaklanıyorum. Eğer birinci kademede kodu yazarken aynı zamanda okunur hale getirmeye çalışsaydım, o zaman tek sorumluluk prensibine ters düşmüş olurdum. Aynı zamanda sadece bir işlemle ilgilenmeliyim. Geviş getiren bir hayvan da geviş getirirken kalkıp, tekrar ot yemiyor. Hayvan geviş getirirken sadece bu işe konsantre oluyor ve başka bir işle uğraşmıyor. Bu onun ikinci kademede yuttuğu otları verimli bir şekilde çiğnemesini sağlıyor ki bizde aynı şekilde her iki kademede yapılan işlemleri birbirlerinden ayırt ederek, belli safhalarda belli işleri yapmaya odaklanıyoruz. Bu bizim yaptığımız işte verimliliğimizi artırıcı bir durum.

Ben bu şekilde çalışmayı tercih ediyorum. İki kademeli çalışmak, çok hızlı bir şekilde işletme mantığını kodlamamı sağlıyor. İkinci kademede testlerin bana verdiği öz güvenle birlikte kodu istediğim şekilde yeniden yoğurabiliyor ve istediğim şekle sokabiliyorum. Buradan da anlaşılabileceği gibi yazılımda geviş getirme taktiğini uygulayabilmek için ikinci kademede yeniden yapılandırmak istediğim kodun tümünü kapsayan birim testlerine ihtiyaç duymaktayım. Eğer testlerim yoksa, ikinci kademede kodu yeniden yapılandırmaya cesaret etmem mümkün değil, çünkü elden yapılan yeniden yapılandırma işlemleri testler olmadan rus ruletinden farksızdır. Nasıl bir netice alacağınızdan emin olamazsınız ve yapılan değişikliklerin yan etkilerini ölçmeniz mümkün değildir. Bu yüzden geviş getirme taktiği test yazma zorunluluğunu beraberinde getirir.


EOF (End Of Fun)
Özcan Acar

Share Button
0.00 avg. rating (0% score) - 0 votes

Bir cevap yazın