Pazartesi, Eylül 24, 2012

Ödeme Ekranı Refactoring 1

Altdotnet Türkiye Google grubundan gelen geri bildirimler ışığında SambaPOS üzerinde değişiklikler yapmaya ve yaptığım değişiklikleri dökümante etmeye devam ediyorum. Önceki yazımdan sonra test first geliştirme yapmam önerildi. Hazır yeri gelmişken bu yazıların ilham kaynağı olan ve grupta değerli paylaşımları ile bir çok kişiye yol gösteren Sidar OK dostumuza bir selam gönderelim.

Öncelikle söyleyim bir TDD uzmanı kesinlikle değilim ama takip ettiğim bir konu. Bu yazılarda öncelikli amacım birşeyler öğretmek değil . Amacım öğrenmek ve öğrenirken yaşadıklarımı paylaşmak. İsterseniz siz de yorumlarınızla katkıda bulunabilirsiniz. 

Eveeet konumuz test driven development olayı. Ve belki de test driven meselesinin en kazık konularından biri olan TDD ile geliştirilmeyen bir proje üzerinde test driven development yapmak. Evet bu elbette çok daha zor çünkü muhtelemen sınıflar arasındaki bağımlıklar nedeniyle test etmek istediğimiz sınıfları yapılandırmak (create) bile mümkün olmayacak. Bu nedenle öncelikle test yazmak istediğimiz sınıfı bağımlıklardan kurtarmak ve "Test Edilebilir" hale getirmek gerekecek.

İyi de buna neden katlanalım ? Zaten çalışıyor ne gerek var test edilebilir hale getirip test falan yazmaya?


Bir önceki yazımda da konu ettiğim ödeme ekranına geri dönelim. Yapmak istediğim döviz butonlarından birine basıldığı zaman solda listelenen fiyatları ilgili döviz kuruna göre güncellemekti. Sonuçta basit bir bölme işlemi olduğu için fazla önemsememiştim ama döviz ile ilgili özellikleri yaparken en çok bu kısım ile uğraştım.

Bu liste kullanıcıya ürün seçerek ödeme alma şansı veriyor. Yani aynı masada oturan beş kişi beraber kasaya gelip "ben kola içtim", "ben de pide yedim" diye saymaya başladığında kasiyer kolayca hesap toplamlarını görüp tahsilat yapabiliyor. Buraya kadar güzel ancak programın tasarımı gereği işin içine döviz girince bir kişinin yiyip içtiğini dolar olarak ödemesi diğer kişinin Türk Lirası olarak ödemesi gibi durumlar (case) ortaya çıktı. İşin içine para üstü girdi. Yani benim 12 lira hesabım vardı 15 verdim 3 lira para üstü aldım, arkadaşım turist olduğu için dolar olarak ödemek istedi ve 4 dolarlık hesabını 5 dolar olarak ödedi. Ayrıca 1 lira 80 kuruş para üstü aldı... Kasiyerin gün sonunda kasasını tutturabilmek için her bir para birimininden kasayı ayrı ayrı düzgün hesaplamak gerek. 

Bir de işin içine iskontolar, bahşişleri hesaplamakla ilgili özellikler, kur çarpımından doğan yuvarlamalardan doğan farklar falan girince ipin ucu gerçekten kaçtı ve ben yaptığım hesaplamaların doğru olup olmadığını anlayamayacağım bir noktaya geldim. Test falan yazmamış olduğum için elime hesap makinesini aldım, akla gelecek her durumu tek tek elimle yapıp hesaplayarak kontrol ettim. Allahtan geçmişte yüzlerce kez bu tür hesaplamalar yapan kodlar yazdığım için işin içinden çıkabildim ama bu ekranda yapılabilecek işlemlerin kombinasyonu neredeyse sınırsız olduğu için hatalı sonuç verecek bir işlem yapılabilme şansı var mı bundan emin değilim. Aslında yüzlerce kullanıcı her durumu test edip çıkan problemleri bildiriyor ama ben yapacağım en ufak değişiklikte bile bu hesap makinesi ile doğrulama işini baştan yapmak zorundayım. Aksi takdirde çalışan şeyi bozmuş olurum. Kötü bir durum.

İşte TDD bu problemle başedebilmemizi sağlayan bir yötem. Tam olarak yaptığımız kodu doğru yazıp yazmadığımızı anlamamız için değil. Daha çok çalışan şeyi bozmamak için kullanılan bir yöntem.

Tamam test yazılması gerektiğine ikna olduk ama nasıl yapacağız? Bu ürün seçme işi ilk yazıldığında üç beş satırdan oluışan basit bir özellikti. Bu ekrana eklenen her özellikten sonra bikaç satır daha kod eklendi ve sonuçta elimizde geliştirmesi ve bakımı zor bir kod yığını var. Nereden başlayacağız? 

Öncelikle bir kod parçası inceleyerek problemli noktalara bakalım. Aşağıdaki kod soldaki her bir satırı temsil eden MergedItem sınıfı. Problemli noktaları comment ekleyerek açıkladım.



MergedItem aslında bir ViewModel sınıfı. Ancak zaman içerisinde hızlı kod yazma gerekliliklerinden (!) dolayı normalde bir modelde durması gereken kodlar da içine karışmış. Bu sınıf sadece görüntülenecek Label'ı ve Font görünümünü belirlemesi gereken bir sınıf olduğu halde bazı hesaplamalar da yapıyor.

Normalde ViewModel sınıflarını test etmek zordur çünkü bir MVVM framework'ü kullanıyorsanız çeşitli nedenlerle framework'e bağımlılıklar oluşur ve test yazmak için test context'i içinde framework'ü de kullanıma hazır hale getirmek gerekebilir. Aslında ViewModel sınıfları test edilmesi şart olan özellikler pek içermezler. Sonuçta temel görevi Model sınıfı ile View sınıfı arasında köprü vazifesi görmektir. Asıl önemli kısımlar model içindedirler ve bu nedenle MergedItem sınıfından Model sınıfını ayırmak ve test ile garantiye almak gerekiyor.

Ayrıca bu MergedItem kolleksiyonunu tutan ve kolleksiyon oluşturma işlerini yapan sınıf da PaymentEditorViewModel sınıfı ve bu kısmı ilgilendiren model sınıfını da ayırıp test ile garantilemek gerek.

İşe öncelikle isimlendirmeyi düzelterek başladım. MergedItem yerine Selector isimini ve kolleksiyonu yöneten sınıfa da OrderSelector ismini vermeyi tercih ettim. Testin yazılımı sürecine fazla girmeyeceğim ancak ilk olarak bir belgenin OrderSelector sınıfına aktarılarak Selector sınıflarının oluşturulması ile başladım ve eklemek istediğim her case için bir test yazarak devam ettim. Yazdığım her bir testten sonra yeni sınıflarımın özellikleri birer birer oluştu ve 10 kadar case'i test ederek sınıflarımı tamamladım.  https://github.com/emreeren/SambaPOS-3/blob/master/Samba.Modules.PaymentModule.Tests/PaymentOrderSelectorTests.cs adresinden testlere yukarıdan aşağı bakarak özellikleri ne sırada eklediğim ve sınıfın nasıl kullanılacağı görülebiliyor.

Sonuçta MergedItem sınıfı yerine geçecek Selector sınıfının son hali şu şekilde oldu.


Özelliklerin arabirimlere bağlanması ve diğer görsel işler de SelectorViewModel sınıfı içinde yapılıyor. Bu sınıfın görünümü de şu şekilde.






Eğer dikkatinizi çektiyse bu sınıf çok enteresan özellikler içermiyor. ViewModel sınıfları için test yazılmasa da olur dememin nedeni buydu. Belki Description özelliğinin doğru olup olmadığı veya diğer değerlerlerin doğru formatlanıp formatlanmadığı test edilebilir ama döviz hesaplamalarının doğru olup olmadığı kadar kritik bir konu değil.

Böylelikle iki farklı işi birden yapan bir sınıfı ikiye bölmüş olduk. Veri tutma ve değiştirme işlemlerini bir sınıfa, görünümle ilgili sorumlulukları ise başka bir sınıfa vermiş olduk. Belki bu ayırma işlemini kodu gözle inceleyerek ve yorumlarda belirttiğim yerleri başka bir sınıfa taşıyarak da yapabilirdik ancak burada test yönelimli hareket ettiğimiz için bölünme sihirli bir şekilde kendiliğinden oluşmuş oldu. Bu arada testlerimizi de yazmış olduk. Hatta sürekli dikkat çeken bir görsel problem de arada çözüldü ama onun nasıl çözüldüğünü tam anlamadım. Demek ki bazı kurallara uygun hareket edince yazdığımız kod daha başarılı oluyor.

PaymentEditorViewModel sınıfından çıkan kısımları da commentleyerek submit ettim. https://github.com/emreeren/SambaPOS-3/blob/16d7123ceaf49e0d54621082ed492a35e5eaabb5/Samba.Modules.PaymentModule/PaymentEditorViewModel.cs adresinde görülebilir.

Bu sınıftan ayrıştırdığımız OrderSelector sınıfı da şurada https://github.com/emreeren/SambaPOS-3/blob/16d7123ceaf49e0d54621082ed492a35e5eaabb5/Samba.Modules.PaymentModule/OrderSelector.cs

OrderSelector sınıfı da gayet temiz ve anlaşılır oldu. Ancak PaymentEditorViewModel sınıfı halen fazla büyük ve daha refactoring istiyor. 

Devam edeceğiz.

Commit : https://github.com/emreeren/SambaPOS-3/commit/16d7123ceaf49e0d54621082ed492a35e5eaabb5

Cuma, Eylül 21, 2012

Ödeme Ekranı - Para Üstü Seçimi

Selamlar. Alt.Net google grubunda açtığım ve SambaPOS projesini nasıl daha iyi değerlendirebileceğimi sorduğum bir konuda gelen öneriler neticesinde bazı SambaPOS özelliklerini kodlarken ya da refactoring yaparken yaptıklarımı bloglama gibi bir fikir gündeme geldi. Çok düşündüm ve biryerlerden başlamak adına sıradaki işimi kayda alarak yapmaya karar verdim. Şimdi bir SambaPOS özelliğini hem kodlayacak hem de anlık olarak yaptıklarımı bloga yazacağım.

Buna “Live Development” adını veriyorum. Canlı canlı kodlama yapacağız yani :) Aslında bir anlamda pair programlama yapmış da oluyorum :) Yer yer kısa açıklamalara da yer vererek kullandığım teknolojiler hakkında da bilgiler vermiş olacağım. Belki projenin dökümantasyon eksiğini kapatmak anlamında bir faydası olur.

Burası programın ödeme almak için kullanılan ekranı. Gerekli para birimi, kur, hesap ve para üzeri hesaplama ayarlarını yaptığımız zaman program sol alt kısımda göründüğü gibi belge toplamını çeşitli para birimlerinde hesaplayarak gösteriyor. Eğer müşteri ödemeyi TL olarak değil de Dolar ya da Euro olarak yapmak istiyorsa kullanıcı bu butonlardan birine basarak ödemeyi farklı para birimlerinden alabiliyor. 

Ancak burada şöyle bir durum var. Hesap 17,40 dolar ve müşteri muhtemelen 20 dolar verip para üzeri isteyecek. Müşteriler duruma göre para üstünü dolar ya da TL olarak isteyebiliyor. Bu nedenle tahsilatın hangi para biriminden yapıldığını seçtiğimiz gibi para üstünün de hangi para biriminden yapıldığını seçebilmemiz gerekiyor. 

Bu hesapları yapmak ve ilgili tutarları ilgili kasa hesaplarına aktarmak ile ilgili özellikler zaten yapılmış durumda. Bugünkü işimiz kullanıcının para üstünü hangi para biriminden vereceğini seçebilmesini sağlamak.

Normalde 31.50 hesap varken 50 tuşuna ve Cash tuşuna bastığımız zaman bu 50 Nakit anlamına gelir ve program otomatik olarak para üstünü şekildeki gibi gösterir. Burada direk olarak para üstünü göstermek yerine hangi para biriminden para üstü verdiğimizi seçebilmemiz gerekiyor. 

Öncelikle ödemeyi işlediğimiz kod parçasını inceleyelim.

Seçilen nakit, kredi kartı gibi ödeme tipi PaymentTemplate parametresi olarak fonksiyona geliyor. Ne kadar tahsil edilmesi gerektiğini ve ne kadar tahsil edildiğini GetPaymentValue() ve GetTenderedValue() fonksiyonları ile alıyoruz. Bir yuvarlama işleminden sonra SubmitPaymentAmount() ile ödemeyi kayıt ediyoruz. Bu fonksiyon kullanılan para üstü şablonunu da döndürüyor ve görüntüleyeceğimiz para üstünün para birimini ve kurunu alabilmek için bu değeri DisplayReturningAmount() fonksiyonuna geçiyoruz. Böylece işlem tamamlanmış oluyor. 

Para üstü şablonu (ChangePaymentTemplate) kullanıcının para üstünün hangi para biriminden verilebileceğini belirlemek için yaptığı tanımlar. Yani Dolar ve Euro para üstü vermek isteniyorsa iki tane para üstü şablonu tanımlanacak ve işlem sırasında kullanıcıdan bir tanesini seçmesi istenecek. Şu anki durumda sadece bir para üstü şablonuna izin verildiği ve şablonun tanımlanmış ya da tanımlanmamış olmasına göre işlem yapıldığı için bu SubmitPaymentAmount() fonksiyonundan sonuç olarak dönüyor. Bu nokta problemli çünkü eğer şablon sayısı birden fazlaysa bunu kullanıcıya seçtirmek gerek. 

Kodu şöyle değiştirdim.


Öncelikle fonksiyonun Para Üstünü görüntüleyen kısmını SubmitPaymentAmount() fonksiyonuna taşıdık. GetChangePaymentTemplates() fonksiyonunu ise buraya alarak tanımlanmış tüm para üstü şablonlarını döndürecek şekilde düzenledik. Son olarak da eğer şablon sayısı birden faza ise ChangeTemplates kolleksiyonunu oluşturarak görüntüleyeceğimiz para üstü düğmeleri için gerekli veriyi hazırlıyoruz ve IsChangeOptionsVisible değişkenini true yaparak düğmelerin görüntülenmesini sağlayacağız.

Burada ChangeTemplates kolleksiyonu hakkında kısa bir bilgi verebiliriz. CommandButtonViewModel bir düğme için gerekli veriyi tutan bir jenerik sınıfımız. Düğme başlığını, düğmeye tıklantığında çalışacak ICommand implementasyonunu ve ICommand'a geçilecek parametreyi tutuyor. 

ICommand arabirimini gerçekleyen sınıflar WPF'de düğme gibi Command özelliği içeren kontrollere direk olarak bağlanabilirler. ViewModel e Command olarak tanımladığımız _selectChangePaymentTemplateCommand sınıfının yapılandırılması şöyle.


Çalıştırıldığında OnSelectChangePaymentTemplate() fonksiyonuna Parameter olarak belirlediğimiz PaymetData nesnesini parametre olarak geçerek çalıştıracak. PaymentData o anki tahsilat değerlerini OnChangePaymentTemplate fonksiyonuna geçmek için tanımladığımız bir sınıf. 

Bir para üstü seçildiğinde SubmitPaymentAmount() seçilen para üstü şablonu ve diğer tahsilat verilerini alarak çalışacak.

Kod kısmında yapacaklarımız bu kadar.

Şimdi Para Üstü seçimi düğmelerinin görüntülenmesini sağlayacağız. 

Bu XAML kodu ChangeTemplates kolleksiyonuna bind edilmiş bir ItemsControl kontrolü tanımlar. ChangeTemplates kolleksiyonu tanımlı para üstü şablonlarını içeriyordu. ItemsControl ise kolleksiyon tipinde veriye bağlanan bir kontroldür.

ItemsControl.ItemTemplate bölümünde kolleksiyonun her bir elemanı için olşturulacak kontrolü ve veri bağlantısını tanımlıyoruz. Burada her bir şablon için bir FlexButton yaratılacak ve her bir FlexButton ChangeTemplates kolleksiyonunun içerdiği her bir CommandButtonViewModel nesnesinin özelliklerine bağlanacak (bind). 

ItemsControl.ItemsPanel bölümündeki tanımlama ise oluşan düğmelerin tek bir satırda mevcut alana sığdıracak bir UniformGrid içine ekleneceğini belirliyor. 

En dışarıda ise görünüp görünmediği IsChangeOptionsVisible özelliğine bağlı bir Border var. Bir ağaç yapısı şeklinde. En dışta bir Border, içinde bir ItemsControl ve ItemsControl'e bağlı kolleksiyonun (ChangeTemplates) her bir elemanı için bir FlexButton oluşacak. Border'ı gizlediğimizde içerideki düğmeleri de gizlemiş olacağız.

Sonuç olarak Dolar tahsilatina geçip 4,97 tutarındaki belge için 5 dolarlık bir tahsilat yaptığımızda para üstünün hangi para biriminden verileceği aşağıdaki gibi sorulmuş oluyor.


Burada Dollar Change ya da EuroChange seçeneklerinden birini seçerek para üstünün hangi para biriminden verildiğini belirlemiş oluyoruz.

Tabii para üstü şablonu isimlerini kullandığımız için Dollar Change gibi isimler görüyoruz. Buna göre direk olarak para üstünün ne kadar olduğunu göstermemiz biraz daha pratiklik sağlayacaktır. Bunun için CommandButtonViewModel nesnesini oluşturduğumuz yerde Caption özelliğini para üstünü hesaplayarak atayabiliriz.

Kodumuzun son hali böyle. CommandButtonViewModel sınıfının Caption özelliğine para üstünü hesaplayarak yazıyoruz. Böylelikle kullanıcı ne kadar para üstü vermesi gerektiğini anlamış oluyor. 


Son iki satırda ekran kontrollerine bağladığımız özelliklerde değişiklikler olduğunu ve güncellenmesi gerektiğinden WPF'i haberdar ediyoruz :) Tabi kodu kısa tutmak için böyle yaptım. Normalde ChangeTemplates kolleksiyonunu ObservableCollection tipinde tanımlamak ve her seferinde yeni bir kolleksiyon oluşturmak yerine kolleksiyonu temizleyip yeniden oluşturmak gerek. Ayrıca IsChangeOptionsVisible özelliğinin güncellenmesini özelliğin set metodu içinde yapabiliriz.

Bu ufak değişiklikleri ve kodun son halini incelemek için https://github.com/emreeren/SambaPOS-3/blob/master/Samba.Modules.PaymentModule/PaymentEditorViewModel.cs adresini ziyaret edebilirsiniz.



















9 TL'lik satış 4.97 dolar ediyor. Müşteri 10 dolar veriyor ve para üstünü Euro istiyor. Sonuçta €4.29 para üstü vermemiz gerekiyor. Belgenin alt toplamından göreceğimiz gibi Dolar kasamıza 18,10 TL karşılığı dolar girmiş, Euro kasamızdan da 9,10 TL değerinde Euro çıkmış oluyor. Mükemmel!

Bu arada farkettim bu sınıf bayağı bir büyümüş. Ekran elemanlarını farklı UserController (View) içine
taşıyarak ilgili metodları farklı ViewModel sınıfları içine taşımanın zamanı yaklaşmış...

Başka bir Live Development yazısında görüşmek üzere.