Kodunuz spagetti mi olsun?

Bu yazımda SAP projelerinde yapılan geliştirmeler hakkındaki görüşlerimi paylaşmak istedim. Projelerde genel olarak kavramsal tasarım aşamasından sonra başlanan uyarlama ve geliştirme aşamasında, geliştirmeler ile ilgili olarak bir kılavuz hazırlanır, veritabanı tablosu, program, fonksiyon gibi geliştirme nesneleri için isimlendirme kuralları konur ve projeye dahil olan ABAP geliştiricilerinin bu kılavuzdaki kurallara uymaları beklenir. Bu tür kılavuzlar geliştiriciler için iyi bir örnek olsa da tek başına genellikle yeterli olmaz. Hatta proje ilerledikçe bu kurallara pek uyulmadığı, her geliştiricinin kendi alışkanlığının  ağır bastığı ve üretilen kodlarda farklılıklar olduğu görülür. Bunun yanında projede görev alan her geliştirici bilgi, beceri ve deneyim açısından aynı düzeyde olmadığından (olmaması da çok doğaldır), isimlendirmedeki farklılıkların yanında kod kalitesinde ve temiz kod yazma yaklaşımlarında da farklılıklar ortaya çıkar. Buna ek olarak, ERP sistemleri şirketlerde uzun soluklu kullanıma sahiptir. Şirketin iş süreçlerinin değişmesi ve gelişmesi ile birlikte geliştirmeler de güncellendiğinden, kod standardının belli seviyede tutulması gittikçe zorlaşır. Projenin canlı kullanıma alınmasından sonra başka geliştiriciler de görev alabilir, eskiler de devam etmeyebilirler. Bir süre sonra bazı programlar iş süreçlerinin değişiminden dolayı tümüyle kullanılmaz hale gelirler. Bu sebeplerle, işin başından itibaren kod standartları, temiz kod ve kod bankası gibi yaklaşımlar korunmazsa, belli bir olgunluğa gelen ERP sistemlerinde ciddi oranda kirli kod oluşmaya başlar. Sürüm yükseltmelerde ve süreçlerdeki değişikliklerin koda yansıtılmasında da yazılım bakım maliyetleri yükselir.

Bu noktada durup, SAP sistemlerindeki gelişmeden kısaca bahsetmek istiyorum. Son yıllarda SAP, gerek dış alımlarla gerekse kendi yeni geliştirmeleri ile yazılım portföyünü oldukça genişletti. Müşteri firmaların belli süreler ile bu yeni ürünlere geçişleri bekleniyor. ERP bileşeni özelinde, firmaların belli seçenekleri var. Birincisi SAP ERP sistemlerini koruyup, veritabanını HANA sistemine çevirmektir. Diğeri ECC sisteminden S/4 HANA sistemine geçiştir. Bunu da kendi içinde iki şekilde yapabilirler. Birincisi sürüm yükseltmedir (teknik olarak oluyor diye biliyorum, düzeltmelere açıktır). İkincisi mevcut ECC sistemini kaldırıp yerine sıfırdan ERP projesi yaparak S/4 HANA’ya geçmektir. Sadece HANA veritabanına geçiş, teknik olarak mümkün ve S/4 HANA projesi yapmaktan daha az maliyetli bir seçenektir. Bu tercih edilirse, veritabanı geçişinden sonra sistemdeki geliştirmelerin belli kurallara göre gözden geçirilmesi, HANA’da değişen bazı durumlar için kod değişikliği yapılması gereklidir. Örneğin tablodan birden çok kayıt okunması sırasında kayıtların birincil anahtarlara göre sıralı geleceği varsayımı HANA’da geçerli değildir, programda tek satır kod değişmese bile, alınan sonuçlar farklı olabilir. Bu zorunlu kontrollerden sonra, özellikle performans problemi olan geliştirmelerde HANA veritabanının performansından gerçek anlamda yararlanmak için kodlarda belli değişikliklerin yapılması gereklidir. CDS görünümlerinin oluşturulması, AMDP olarak isimledirilen ABAP sınıfları ile veritabanı prosedürlerinin yazılması ve programların gerekli yerlerinde yapılan veritabanı erişimlerinin bu yeni yaklaşımlar ile değiştirilmesi faydalı olur. Benzer çalışmaların sürüm yükseltmesi yolu ile S/4 HANA’ya geçilmesinde de yapılması çok yerindedir. Tümüyle sıfırdan S/4 HANA projesinde de yeni yapılacak geliştirmelerde HANA veritabanının bu özelliklerinin öncelikli tercih edilmesi SAP tarafından da önerilmektedir.  Bunlar HANA veritabanı özelinde yapılabilecek çalışmalar. Veritabanı ya da SAP ERP sisteminizin sürümü ne olursa olsun, performans ve kod kalitesi açısından yapılabilecekler de var. Biraz da bunlardan bahsetmek istiyorum. Veritabanı ya da ERP bileşeni açısından herhangi bir sürüm yükseltmesi planlanmasa bile sistemdeki geliştirmelerin bakım maliyetlerini azaltacak yaklaşımlardır. Bunlar, başta bahsettiğim isimlendirme standartlarından daha önemlidir, temiz kod yazma prensiplerini içerir.

Her ERP projesinin olmazsa olmazı rapor geliştirmeleridir. Her modülün mutlaka rapor ihtiyaçları olur. Rapor tipi programlarda genelde bir seçim ekranı olur. Seçim ekranına girilen kriterlere göre veritabanından kayıt okuma kısmı olur ve son olarak da bulunan kayıtları görüntülemek için ALV ekran bileşeni kullanılır. Genellikle de bir rapor programı, hazırlandıktan sonra kendi başına yürütülür ve diğer programlar tarafından da pek çağırılmaz. Bu hali ile rapor programları monolitik yapıya sahiptir, tüm işlevleri kendi içinde tek parça olarak barındırır. Bu noktada yapılabilecek iyileştirme, bu monolitik yapıyı, modüler yapıya dönüştürmektir. Örneğin veri okuma kısmına başka bir programdan ya da başka bir sistemden erişilmesi gerekebilir. Bu durumda, seçim ekranından kriterleri alıp rapor verisini hazırlayan kısım, rapor programının içinden çıkarılıp bağımsız bir RFC fonkiyonu ya da global sınıfa çevirilebilir. Bu yaklaşım yazılım dünyasında MVC (Model-View-Controller) olarak isimlendirilir. Veri hazırlamak için geliştirilen foksiyon ya da sınıf, bu yaklaşımın “model” kısmına karşılık gelir. Rapor programının içindeki ALV ekran bileşeni “view” rolünü üstlenir. Eğer ALV raporunuz kullanıcıdan belli girişleri alıyorsa (seçilen satırlar için örneğin yeni bir belge oluşturma, satıra çift-tıklanınca başka bir programı çağırma gibi), (genellikle) rapor programı içinde tanımlanan ve ALV nesnesinin olaylarını karşılayan sınıf da “controller” görevini üstlenir. Controller bileşeni, model ve view arasındaki etkileşimi yönetir. Veri okuma kısmını rapor programında çıkardığınızda, elinizde mobil ya da diğer harici sistemlerden çağırılabilecek bir kod bileşeni olur. Örneğin ileride raporlarınızı SAPUI5 ya da başka bir web önyüz uygulaması ile geliştirmek isterseniz, OData geliştirmesi yapmanız gerekecektir, burada OData servisi içinde doğrudan kullanabileceğiniz kodunuz hazır olur. Raporun veri okuma ve hesaplama mantığı değiştiği zaman bunu tek bir yerde yapmanız, hem ALV raporunun hem de SAPUI5 uygulamasının aynı anda güncellenmesini sağlar. Özellikle OData servisleri ile daha uyumlu olması açısından birden çok kayıt döndüren fonksiyon ya da sınıflarınızda OData servisinde yerleşik olan sayfalama (pagination) parametrelerinin de olmasını öneririm. OData servislerinin GetList metotlarında “skip”, “top” gibi parametreler ile sıralama ve filtreleme için de parametreler bulunmaktadır. Veri okuma kısmını yapan servisiniz bunları OData’nın GetList metodundan alıp işleyebiliyorsa, Fiori uygulamalarınızı daha doğal biçimde desteklersiniz.

Benzer yaklaşım, SmartForm, SAPscript, Adobe Forms gibi yazılı çıktı amacıyla yapılan geliştirmeler için de geçerlidir. Bazı projelerde, bu çıktı geliştirmelerinin içinde çeşitli hesaplamaların yapıldığını görebiliyoruz. Özellikle çıktının içinde vergi, indirim toplamı gibi kritik bilgiler hesaplanıyorsa bunların mümkün olduğunca çıktıyı üreten yazdırma programı içinde hesaplanması ve hatta tüm çıktı hesaplarını yapan ve çıktıya veri hazırlayan kısmın da ayrı fonksiyon ya da servis olması daha iyi bir tasarım olabilir. Yine harici sistemlerden çıktıdaki bilgilerin birebir aynısı istendiği durumda bunu yine servis olarak çalışan fonksiyon/sınıf ile sağlayabilirsiniz. Burada da MVC prensibinin “view” kısmı çıktının kendisidir (SmartForm vs). “model” kısmı, veri hazırlayan servistir. Uyarlamada çıktı türünün içinde tanımlanan yazdırma programının kendisini de “controller” olarak kabul edebiliriz.

Raporların haricindeki veri girişi sağlayan programlar için de benzer şeyler söylenebilir. Bu programların özelliği, sadece veri okumaları değil, veriyi güncellemeleridir. Bunlar, OData servislerindeki CRUD-Q (Create-Read-Update-Delete-Query) işlemlere karşılık gelir. Bu sebeple, veri girişi sağlayan diyalog program (module pool) geliştiriyorsanız, programın PAI kısımlarında yeralan veri kontrolu ve güncelleme kısmlarını da servis yaklaşımı ile hazırlayabilirsiniz. Sistem standardında bulunan BAPI fonksiyonlarını incelediyseniz, her iş nesnesi için (satınalma sipariş, müşteri siparişi, malzeme ana verisi gibi), BAPI_XXX_CREATE, BAPI_XXX_CHANGE, BAPI_XXX_DELETE, BAPI_XXX_GET_DETAIL ve BAPI_XXX_GET_LIST gibi fonksiyonların olduğunu görmüşsünüzdür. Kendi uygulamalarınız da bu şekilde hazırlarsanız, Fiori/mobil uygulama geçişlerine hazırlıklı olursunuz.

Kod kirliliğinin en çok görüldüğü yerlerden biri de standart uygulamalar içindeki kullanıcı-çıkışları ve Badi’lerdir. Özellikle satış süreçleri şirketlerde çok dinamik olduğundan ve farklılıklar gösterdiğinden, Satış ve Dağıtım modülünün kullanıcı çıkışları zamanla hem çok fazla kod barındırmaya başlar, hem de her süreç için kullanıcı-çıkışının metotlarında ya da FORM rutinlerinde  büyük kod blokları oluşmaya başlar, kullanım yoğunluğuna göre benzer durum diğer modüller için de geçerlidir. Özellikle IF-ELSE-ENDIF koşul blokları bazı durumlarda takip edilemez büyüklüğe gelebilir. Ayrıca belli bir süreç için yazılmış olan kodda CHECK komutu varsa, bunu takip eden diğer süreçlerin kodları tümüyle çalıştırılamaz duruma gelir. Kullanıcı çıkışı kodu, birden fazla ABAP geliştiricisinin aynı anda çalışması izin vermez (kodu sadece biri değiştirebildiği için) ve değişiklikleri canlı sisteme taşımak da sorunlu olabilir (birbirini bekleyen geliştirmelerden dolayı). Buna çözüm olarak genelde her süreç için ya ayrı FORM rutinleri ya da INCLUDE programlar hazırlanıyor ve asıl kullanıcı-çıkışı rutini içinde bu rutinler PERFORM ile çağırılıyor ya da INCLUDE programlar doğrudan bunun içine dahil ediliyor. Bu iki yaklaşım da yapılan geliştirmeleri tümüyle birbirlerinden izole edemez. Bunun yerine her kullanıcı çıkışında, amaca uygun olarak ana programdan ya da ilgili kullanıcı-çıkışından gerekli değişkenleri alıp çalışabilen arayüzler tasarlanabilir. Bu arayüzleri SE24 işlemi ile tanımlayabilirsiniz. Sonra yine SE24 işlemi ile her farklı süreç için bu arayüzü uygulayan bir sınıf oluşturabilirsiniz. Her sınıf diğerinden bağımsız bir ana program olacaktır. Arayüzün metodu içinde sadece kendisinin sorumlu olduğu kodu içerecektir. Asıl kullanıcı-çıkışı rutininde de bu arayüzü uygulayan sınıflar sistemden bulunur, her bir sınıf için bir nesne üretilip arayüzü metodu çağırılabilir. Böylece kullanıcı-çıkışına yeni bir süreç için kd eklenmek gerekirse, SE24 işlem kodu ile yeni bir global sınıf tanımlamak ve aynı arayüzü uygulamasını sağlamak yeterli olacaktır. Sınıfların içindeki kodların geliştirmesini farklı programcılar üstlenebilir, bunlar farklı zamanlarda ve transport requestler ile canlı sisteme taşınabilir.

Kullanıcı-çıkışlarındaki yaklaşımı, sıfırdan uygulama geliştirirken de kullanabilirsiniz. Nesne tabanlı programlamanın temeli aslında tek bir prensibe dayanır: Uygulamada değişebilecek yerleri diğer kısımlardan gizle ve bunları ana programın yapısını değiştirmek zorunda kalmadan başka kod parçaları ile değişebilecek şekilde tasarla. Tasarım kalıplarında anlatılan tüm problem tiplerinin temelinde bu yatar. Örneğin bir uygulamanız var, karmaşık veri girişi olan bir program olmasına da gerek yok, bir rapor programı da olabilir. Rapor programının içinde örneğin FI belge türüne göre farklı noktalarda IF-ELSE blokları yazmaya başladınız. Örneğin birinci IF-ELSE bloğunda tarih hesaplaması olsun. İkincisinde tutar hesaplansın, üçüncüde de farklı e-postalar gönderilsin. Programa ileride yeni bir belge türü eklenirse ne olacak? Programcı belge türüne göre koşul yazılan her yeri gözden geçirmek zorundadır. Farklı IF-ELSE kod bloklarının sayısı artarsa, hata yapma olasılığı da artar, programcı bunların bir kısmını atlayabilir, yeni belge türü için kod eklerken yanlışlıkla mevcut kodu bozabilir. Bunun yerine, belge türü bazında, farklı kod bloklarına karşılık gelecek şekilde metotlar içeren bir arayüz yapılabilir. Her belge türü için bir sınıf oluşturulabilir ve bu sınıf, arayüz metotlarını uygular. Hatta belge türü bazında sınıf adını içeren yeni bir Z’li tablo yapılabilir ve programa dahil edilen belge türleri için bu tabloya kayıt eklenir, karşılık gelen sınıf isimleri ile birlikte. Sonra ana rapor programında belge türünün kontrol edildiği yerlerde bu tablodan belge türüne göre okuma yapılır, belge türüne karşılık gelen sınıf bulunur ve bundan bir nesne üretilir (CREATE OBJECT ya da NEW ile). Sonra ilgili IF-ELSE bloğu arayüzde hangi metoda karşılık geliyorsa, nesne üzerinden o metot çağırılır. Böylece program içindeki IF-ELSE kısımlarını sadece tek bir noktada toplamış olursunuz, belge türüne göre ilgili sınıf için tek bir yerde  nesne üretilir ve diğer tüm IF-ELSE bloklarında sadece bu nesnenin ilgili metodu çağırılır. İleride yeni bir belge türü rapora dahil edilirse, SE24 işlemi ile yeni bir sınıf hazırlanır, arayüz metotları kodlanır ve belge türü ile yeni sınıf adı Z’li uyarlama (parametre) tablosuna eklenir. Rapor programında tek satır kod değiştirmek zorunda kalmazsınız.

Genel olarak projelerde çözüm üretirken kullandığım yaklaşımlar bunlar, yeni çözüm örnekleri ürettikçe paylaşacağım. Umarım sizlere de faydalı olur. Herkese projelerinde kolaylıklar dilerim.

Yollar boş kalmasın

İlk yazım Endüstri Mühendisliği’nin (EM) ana konularından biri olan Yöneylem Araştırması (YA) üzerine olmuştu ve giriş niteliğindeydi, “en kısa yol” algoritmasından bahsetmiştim. Bu yazımda da yine benzer konulara değinmek istedim. “En kısa yol” algoritmasını bir adım ilerleterek, bir ağ üzerinde belli noktalarda bulunan kaynakların, başka noktalarda bulunan talepleri karşılamak üzere hangi bağlantıların üzerinden sevkedilmesini bulan “en fazla ya da en büyük akış (max-flow)” algoritmasından bahsedeceğim. Bu tür algoritmalar genelde taşımacılık, ulaştırma ve atama problemlerinde kullanılır. Örneğin bir şirketin farklı şehirlerde üretim yapan fabrikaları ve benzer şekilde farklı şehirlerde de depoları bulunsun. Fabrikalarda üretilen mamullerin depolarda bulunan müşteri taleplerini karşılamak üzere şehirler arasındaki yollardan nakledilmesi ve taleplerin miktarsal açıdan mümkün olan en fazla şekilde karşılanması gerektiğini düşünelim, taşıma bedelleri dikkate alınmaz. Böyle bir problemde, çözüme gereksinim olması için birtakım kısıtların da olması gerekir, aksi durumda fabrikalardan depolara herhangi bir rotadan mamul nakletmenin bir önemi kalmaz. Kısıt olarak bazı bağlantılarda (şehirler arası yollarda) taşıma miktarları (akış miktarları) için alt ve üst sınırlar getirilebilir. Örneğin A şehrinden B şehrine en az X miktar mamul gönderilmelidir ya da en fazla Y miktar mamul gönderilebilir gibi. Bir yol üzerinde hem alt sınır hem de üst sınır tanımlanabilir.
Ulaştırma/atama problemleri için YA’da başka yöntemler de (atlama taşı yöntemi, MODI yöntemi, kuzeybatı köşe yaklaşımı, minimum maliyetli hücre yöntemi, Vogel’in yaklaşımı yöntemi) bulunmaktadır, burada aynı tür problemleri ağ algoritmaları (network algorithms) ile çözmeye çalışacağız.
“En fazla akış” problemine bağlantılar arasındaki taşıma maliyetlerini de dahil ederseniz, “minimum maliyetli akış (min-cost-flow)” algoritmasını elde edersiniz. Bu durumda amacınız hem ağ üzerindeki akışı alt ve üst sınırlara (kısıtlara) uymak koşulu ile miktar açısından en fazla olacak şekilde ayarlamak hem de toplam taşıma maliyetini en az düzeyde tutmaktır. “En fazla akış” algoritması, “minimum maliyetli akış” algoritmasının özel bir sürümüdür, tüm bağlantılardaki taşıma maliyetlerinin sıfır olduğu durumdur.
Genel olarak YA modellerinin çözümünde iki kavram önemlidir. Birincisi problemin amacının ne olduğudur. Problemler ulaşılması gereken amaç değere göre iki gruba ayrılırlar, en küçük ya da en büyük değerin bulunması istenir. Problem içinde maliyet gibi bir kavram sözkonusu ise, model en küçük toplam maliyetin bulunması üzerine kurulur. Problemde kar/getiri değerleri verildi ise model en büyük toplam getirinin bulunmasını hedefler. İkinci konu problemin çözümünün durumudur. Bu bakımdan problemleri üç gruba ayırabiliriz. Birinci grup çözümü mümkün olmayan problemlerdir. Bunlar matematiksel olarak sonuç üretmeyecek problemlerdir. Örnek olarak bir ağ üzerinde bir A noktasından B noktasına gidebilmek için bir yol bulunması istensin, ancak ağ üzerinde bu iki noktanın doğrudan ya da dolaylı (diğer noktalar üzerinden) bir bağlantısı olmayabilir, ağ kesik (disconnected) durumda olabilir. Ağın kesik durumda olması, üzerinde birtakım noktaların olması, bunların bir kısmının birbiri ile bağlantısının olması bir kısmının ise olmamasıdır. Bu tür ağlarda ağaç yapılarından ya da “orman (forest)” yapısından bahsedebiliriz. Öbekler şeklinde birbiri ile bağlantılı noktalar olabilir ancak bunlar birbirlerinden kopuktur. Bu durumda bir öbekte bulunan bir noktadan bir diğer öbekte bulunan bir noktaya ulaşmak imkansızdır. Bu tür durumlarda problem çözümsüzdür (infeasible). İkinci grup çözümü olan problemlerdir. Bunlar için uygulanabilir bir çözüm vardır (feasible). Örnekten devam edecek olursak, ağ üzerinde A noktasından B noktasına bir yolun bulunduğu durumdur. Üçüncü durum bulunan çözümün, mümkün çözümler (feasible solutions) arasındaki en iyi çözüm (optimum solution) olmasıdır. Örnekten devam edelim. A noktasından B noktasına ulaşmak için A-C-D-B ve A-G-H-K-B yolları izlenebilir, bunların içinde A-G-H-K-B yolu diğerine göre daha kısa sürebilir. Bu durumda A-G-H-K-B çözümü problemin en iyi çözümüdür. Bazı durumlarda problemin birden çok en iyi çözümü olabilir. A-C-D-B ve A-G-H-K-B yollarından gitmek toplamda aynı sürede oluyorsa ikisi de en iyi çözümdür. Bir çözümün, uygun çözüm (feasible solution) olması için problemde verilen kısıtlara uygun olması gereklidir. Örneğin bir A noktasından B noktasına en az 5 birim mamul taşınmalıdır gibi bir kısıt varsa, elde edilen çözümde A-B arası taşınan mamul değeri 4 ise, çözüm uygun değildir, kısıtları yerine getirmemektedir. Eğer hiçbir şekilde A-B arası taşınan mamul miktarı 5 birim olamıyorsa, problem çözümsüzdür. Çok basit olarak ağ üzerinde A ve B şeklinde sadece iki nokta olsun. A noktasında üretilen mamul sayısı 4, B noktasındaki talep 4 ve A-B arası taşınması gereken en düşük mamul sayısı 5 olarak verilirse, problem çözümsüzdür.
Çözümsüz olarak verilen bir problem, önce çözümlü duruma getirilip, çözüm adımları sonra işletilebilir. Problemin çözümsüzlüğünün yukarıda da görüldüğü gibi birden fazla sebebi olabilir. Çözümsüzlük sebebi arz ve talep toplamının eşitsizliği ise, az olan taraf için sanal mir miktar tanımlanabilir. Örneğin A, B ve C noktalarında toplamda 10 birim arz, Z noktasında da toplamda 13 birim talep olsun. Toplam talebin toplam arz tarafından karşılanamadığı durumda ağ üzerinde sanal bir nokta tanımlanır, bu noktadan doğrudan talep noktasına bir bağlantı kurulur ve sanal nokta üzerindeki sanal arz miktarı 3 yapılır. Talebin arzdan az olduğu tersi durumda da aynı yaklaşım geçerlidir, sanal bir nokta ve üzerinde de gerekli miktarda sanal talep oluşturulur, arz noktalarından birine bağlantı kurulur. Sanal noktalar ile gerçek noktalar arasındaki bağlantılarda beklenen akış miktarının alt ve üst sınırları (kısıtları), sanal olan noktadaki miktara eşitlenir. Bunun sebebi sanal nokta ile gerçek nokta arasında bu fazladan olan arz/talep miktarının akışının mutlaka sağlanması gerekmesidir.

Şimdi aşağıdaki örnek ağ üzerinde verilen değerleri inceleyelim.

max flow graph001

Noktalar içinde parantez içinde verilen numaralar nokta numaralarıdır. 1 numaralı nokta içinde verilen (+10) değeri, bu noktanın bir arz noktası olduğunu gösterir (örneğin üretim yapan fabrika). 6 numaralı düğüm içinde verilen (-10) değeri de b noktanın bir talep noktası olduğunu gösterir (pazar yeri). 2-4 noktaları arasındaki bağlantıda U=5 şeklinde verilen ifade bu bağlantı üzerinde en fazla (Upper bound) 5 birim ürün taşınabileceğini gösterir. Alt limit (L) verilmemiştir, bu durumda 0 (sıfır) olarak alınabilir. Benzer şekilde 3-5 noktaları arasındaki bağlantı üzerinde de L=3 şeklinde alt limit (Lower bound) verilmiştir. Bu değere göre 3-5 bağlantısı üzerinden en az 3 birimlik ürün taşınması gerektiği anlaşılır. Aynı bağlantı için üst limit verilmemiştir, buna göre bu bağlantıdan 3 ve 3’ün üzerinde ürün akışı olması gerekmektedir. Bu örnekte sadece birer arz ve talep noktası verilmiştir, birden çok arz ve talep noktaları da olabilirdi.

Ağ üzerindeki değerlere göre problemi görsel olarak çözelim. Öncelikle arz ve talep arasındaki dengeyi kontrol edelim. Ağ üzerinde sadece 1 numaralı noktada arz bulunmaktadır, toplam arz değeri 10’dur. Talep de sadece 6 numaralı noktada bulunmaktadır ve değeri 10’dur. Toplam arz ve talep noktaları birbirine eşit olduğu için arz-talep açısından model çözümsüz değildir. Modelin çözümsüz olmasına sebep olabilecek diğer neden alt ve üst limitlerden dolayı akışın mümkün olamamasıdır. Örneğin 3-5 bağlantısında belirtilen alt limit 3 yerine 11 olsaydı, model çözümsüz olacaktı. Bu durumu farkedebilmek için “max-flow min-cut” isimli teoreme bakılabilir. Çözümün sonucunda herhangi bir noktada arz ya da talep miktarı kaldı ise model çözümsüzdür, tüm talepleri karşılayacak şekilde arz miktarları ağ üzerinde eritilememiştir.

Çözüme başlamadan önce bağlantılar üzerinde olan alt limitler için model üzerinde bir değişiklik yapmak gereklidir. 3-5 bağlantısı üzerindeki 3 birimlik alt limit için 3-5 bağlantısına ait akış değeri çözümün başında alt limite eşitlenir (x ya da f = 3). 3-5 bağlantısının başlangıç noktası olan 3 numaraları noktaya sanal olarak alt limit kadar talep eklenir, 5 numaralı noktaya da sanal olarak alt limit kadar arz eklenir. 3-5 bağlantısında en az 3 birimlik akış olacaksa 3 numaralı noktaya en az 3 birimlik ürün akışı olması gereklidir, bu sebeple 3 birimlik talep oluşturulur (3 nolu noktaya ürün çekebilmek için). Benzer şekilde 3-5 bağlantısından en az 3 birimlik akış olacak ise, 5 nolu noktada da en az 3 birimlik arz artışı var demektir, bu sebeple 5 nolu noktanın arz miktarı da 3 birim artırılır. Model üzerinde alt limit değeri olan tüm bağlantılar için bu işlem uygulanır. İşlem sonucunda doğal olarak model üzerinde yeni arz ve talep noktaları oluşabilir.

Bir bağlantı üzerindeki akış miktarı “x” ya da “f (flow)” harfi ile ifade edilir. Bir “i” noktasında “j” noktasında olan bağlantının akışı f(i, j) ya da x(i, j) şeklinde belirtilir. Bağlantı için alt ve üst limitler de sırasıyla l(i, j) ve u(i, j) olarak ifade edilir. Buna göre geçerli olan bir çözümde tüm bağlantılar için aşağıdaki koşulun sağlanması gereklidir.

I(i, j) <= f(i ,j) <= u(i, j) olmalıdır.

Bağlantı üzerindeki akış miktarı o bağlantının alt ve üst limitleri arasında yeralmalıdır. Bağlantı için üst limit belirtilmedi ise sonsuz gibi büyük bir değer alınabilir. Alt limit belirtilmedi ise değeri sıfırdır. Akış miktarı tamsayı olmalıdır ve sıfırdan küçük olamaz (alt limit koşulu). Bir i-j bağlantısı yönlü olarak verildi ise, üzerindeki akışı da yönlü olarak hesaplamak gerekir. İ-j bağlantısında yön bilgisi i noktasından j noktasında ise i i -> j yönündeki akış yukarıda verilen alt ve üst limit kuralına yine uymak zorundadır. j noktasından i noktasına ters yönde akış olabilir, aslında bu durumda i->j yönündeki akış miktarı azaltılır, akış geri çekilmiş olur. Örneğin alt limit 5, üst limit 12 ise i noktasından j noktasına akış değeri 5-12 arasında olmalıdır. Mevcut akış değeri örneğin 8 ise, j noktasından i noktasına akış en fazla 3 olabilir, bu aynı zamanda i noktasından j noktasına olan akışın en fazla 3 birim azaltılabileceği anlamına gelir. Mevcut akış alt limit olan 5 birimden daha düşük duruma geldiği için daha fazla azaltılamaz.

Burada biraz modellerin ürün çeşitliliğine göre olan ayırımlarından da bahsetmek gerekir. Max-flow ve min-cost-flow gibi ağ temelli modellerin çözümü sadece bir ürün için geçerlidir (örneğin tek bir buzdolabı modeli). Problemde birden fazla ürün varsa (birden fazla buzdolabı modeli ya da hem buzdolabı hem de çamaşır makinası gibi), max-flow ve min-cost-flow algoritmaları ile doğrudan çözüm mümkün değildir. Modelleri bu bakımdan tek-ürünlü (single-commodity) ve çok-ürünlü (multi-commodity) olarak ikiye ayırmak gerekir. Tek-ürünlü modellerin ağ problemlerini doğrudan max-flow ya da min-cost-flow yöntemleri ile çözebilirsiniz, ancak çoklu-ürün durumlarında model üzerindeki kısıt tanımları değişmektedir. Modele birleşik kısıtlar (composite-constraints) eklenmeye başlar. Örneğin bir bağlantı üzerinde toplam alt ve üst limitler tanımlı olabilir (alt limit 10, üst limit 50 olsun). Buna göre bağlantı üzerinde birden fazla ürüne ait akış olacaksa bu değerler nasıl hesaplanacaktır? Bir ürünün bir a-b bağlantısındaki akış miktarı, hem bu ürünün diğer bağlantılardaki akış miktarlarını hem de diğer ürünlerin tüm ağ üzerindeki akış miktarlarını etkiler. Bu sebeple yardımcı birtakım yaklaşımlar mevcuttur. En bilineni “Lagrange gevşetmesi (relaxation)” olarak bilinir. Burada probleme dahil olan her bir ürün için max-flow ya da min-cost-flow problemi çözülür, en az bir ürün için çözüm yoksa zaten çok-ürünlü durum için de çözüm yoktur. Her ürün için ayrı ayrı çözümler elde edildikten sonra birleşik kısıtlara uygunluk durumu kontrol edilir. Örneğin a-b bağlantısından toplamda en fazla 50 birim akış olabiliyorsa, tüm ürünlerin kendi ağları üzerindeki a-b bağlantısındaki akışları toplanır, toplam 50’den fazla değil ise kısıt sağlanmıştır. Benzeri birleşik alt limitler için de yapılır. Birleşik alt ya da üst limitlere uyum sağlanmıyorsa gevşetme denen yaklaşım uygulanır. Birleşik üst limit kısıtı karşılanamadı ise (ürünlerin a-b bağlantısındaki toplam akışları 50’den büyük ise), ürünlerin kendi a-b bağlantıları için sanal üst limitler konur, böylece tüm ürünlerin a-b bağlantısı üzerindeki üst limitlerinin toplamı 50 yapılır. Bu hali ile tüm ürünler için ağ modelleri çözülür ve birleşik kısıtlar tekrar aynı şekilde kontrol edilir. Ürün bazındaki ve birleşik olan tüm kısıtlar sağlanıncaya kadar çözüm tekrarlanır. Çözüm tekrarlarının çok fazla olması olasılığına karşı belli bir tekrar sayısı kabul edilebilir. Birleşik kısıtlara göre ürünlerinin her birinin kendi kısıtlarının bollaştırılmasından dolayı yaklaşıma gevşetme (relaxation) denmektedir. Sonuçta olması gereken optimum noktadan biraz uzak olmakla birlikte yine de optimuma yakın bir çözüm elde edilmiş olunur.

Çoklu-ürün çözümlerinde gevşetme yaklaşımının alternatifi olarak doğrusal programlama da kullanılabilir. Aslında ister tek-ürünlü ister çok-ürünlü bir model olsun, her ağ modeli bir doğrusal programlama problemidir. Bir ağ üzerindeki arz, talep, alt ve üst limitlerin doğrusal denklemler ile ifade edilmesi mümkündür, bu sebeple doğrusal programlama ile çözülebilmektedir. Simplex yöntemi bu amaçla kullanılabilir.

Bu yazıma tek-ürünlü modelin çözümü ile devam edeceğim. Çok-ürünlü modellerin gerek ağ algorimaları gerekse doğrusal programlama ile çözülmeleri başka bir yazı konusu olabilir.

Aşağıdaki ağ modelinde bir öncekine göre biraz daha farklıdır. Modele yeni alt limitler eklenmiştir.

max flow graph002

Bu modele göre çözüme başlamadan önce alt limitlerin modele yeni arz-talep değerleri olarak eklenmesi gereklidir. 2-4 bağlantısının alt limit olan 5 değeri, 2 nolu noktaya talep olarak eklenir, 4 nolu noktaya arz olarak eklenir. 2 nolu noktanın talep değeri 5, 4 nolu noktanın arz değeri 5 olur. Sonra 4-6 bağlantısı için aynı işlem uygulanır. 4 nolu noktaya 6 birimlik talep eklenir, noktanın net değeri 1 birimlik talep olur (öncesinde 2-4 bağlantısından gelen 5 birimlik arz vardı). 6 nolu noktaya da 6 birimlik arz eklenir, 6 nolu noktanın ne durumu 4 birimlik tale olur. Son olarak 3-5 bağlantısı için işlem uygulanır, 3 nolu noktanın 3 birimlik talebi, 5 nolu noktanın 3 birimlik arzı olur. Modelin son durumu aşağıdaki gibidir.

max flow graph003

Görüldüğü gibi modeldeki toplam arz ve talep dengesi bozulmamıştır. Bundan sonra çözüme geçilebilir ya da öncesinde istenirse tüm arz noktaları sanal bir arz noktasına bağlanabilir, tüm talep noktaları da sanal bir talep noktasına bağlanabilir ve bu sanal arz ve talep noktalarına toplam arz ve talep miktarları yazılır, gerçek arz ve talep noktalarının arz/talep miktarları sıfırlanır, sanal arz noktasından gerçek arz noktalarına olan bağlantılar üzerinde alt ve üst limitler ilgili gerçek arz noktasının arz miktarına eşitlenir (mutlaka ilgili gerçek arz noktasına olması gereken akış sağlanmasını garanti altına almak için). Benzer düzenleme gerçek talep noktaları ile sanal talep noktası arasında da yapılır. Sonuç olarak tek-kaynaklı tek-bitişli (single-source, single-sink ya da single-terminal) ağ modeli ortaya çıkar.

max flow graph004

Şimdi bu modelin çözümü için olması gereken veri modeli üzerinde düşünelim.

Noktalar üzerinde arz ve talep miktarları için bir bilgi olması lazım. Bu tek bir değişken olabilir, eksi değere sahip olduğu durumda nokta bir talep noktasıdır, artı değere sahip olduğunda nokta bir arz noktasıdır. Sıfır olduğu durumda nokta sadece bir geçiş noktasıdır (transshipment node ya da vertex).

Bağlantılar üzerinde de bağlantıya ait akış miktarı, alt ve üst limitler için değişkenlerin olması gereklidir. Üst limitin olmadığını ifade etmek için yine bağlantı bazında ek olarak üst limit var/yok gibi mantıksal bir değişken tanımlanabilir. Üst limit yoksa üst limit değeri hesaplamalarda dikkate alınmaz, diğer durumda alınır.

Bir noktanın arz ya da talep noktası olup olmadığı, üzerindeki arz/talep miktarı değişkeninden başka, noktaya gelen bağlantılar ve noktadan çıkan bağlantıların üzerindeki akışların toplamına göre de hesaplanabilir. Örneğin noktaya gelen bağlantılardaki toplam akıştan, noktadan çıkan bağlantılardaki toplam akış çıkarıldığında elde edilen net değer sıfırdan büyükse nokta bir arz noktasıdır, sıfırdan küçükse talep noktasıdır. Anca modelin başlangıcında tüm akışlar sıfır olacağından bu formül her zaman geçerli olmayabilir. Nokta bazında arz ve talep miktarlarının modelde verilebilmesi gereklidir.

En kısa yol bildiğin yoldur. Acaba?

Endüstri Mühendisliği (EM)’nin en temel konularından biri olan Yöneylem Araştırması (YA)’dır. YA’yı ilginç kılan özelliği, diğer EM konuları içinde belki de disiplinler arası uygulamaların en çok başvurulduğu konu olmasıdır. Matematik modelleme, algoritma kurma becerisi ve bilgisayar bilgisinin bir araya geldiğini söylersek yanlış olmaz. Doğrusal programlama, doğrusal olmayan programlama, kuyruk modelleri (aklınıza hemen dört ayaklı dostlarımız gelmesin, market kasalarındaki bekleme kuyruklarından bahsediyoruz), ulaştırma/atama problemleri, dinamik programlama gibi farklı başlıklar altında sanayide ve günlük yaşantımızda doğrudan ya da dolaylı etkisi olan problemlerin çözümleri ile ilgilenir.

Bahsettiğim konularda uzun süreler çalışmadığımı baştan söylemek isterim, çünkü genel olarak YA çok kapsamlı, başlı başına bir doktora konusudur, hatta alt konuları bile hem teoride hem de pratikte ciddi emek ister, bu sebeple yazımda bazı yanlış ya da atlanmış noktaların bulunabileceğini belirtmek isterim. Bu konuda yazmak istememin sebebi, profesyonel çalışmalarımda biraz da zorunluluklardan dolayı, EM’nin lisans programlarında anlatılanın ötesinde hem modelleme hem matematik hem de veri yapıları açısından çözüm bulmak zorunda kaldığım problemler ile karşılaşmış olmam. Çalışmalarım sırasında konuların özellikle birden fazla disiplinin bir araya gelmesiyle daha keyifli hale geldiğini söylemem lazım. Çözümler kolay mı oldu? Tabii ki hayır. Bu tür problemlerde çözüm üretmek için ciddi boyutta zamana ve bütçeye sahip olmak, teknoloji/platform gibi unsurlarda doğru kararlar vermek gerekir. Yine de sahip olduğumuz imkanlar içinde özgün çalışmalar olduğu kanısındayım. Bu konular üzerine kafa yoran, emek veren ve neredeyse tüm akademik hayatını bunların üzerine kuran değerli insanların yanında benim ve birlikte çalıştığım iş arkadaşlarımın çabaları çok zayıf kalır. Çalışmalarından yararlandığımız akademisyenlere teşekkür ederim.

YA konuları genelde problem çeşitlerine göre matematik modelleme, algoritma kurma ve bunun için gerekli veri yapılarının tanımlanması üzerine dayanır. Örneğin ulaştırma/atama problemlerinin çözümünde ağ modellerinden ve yöntem olarak da ağ  algoritmalarından (graph algorithms) yararlanılır. Ağ algoritmaları da DFS (Depth-First-Search) (Derin Öncelikli Arama), BFS (Breadth-First-Search) (Sığ Öncelikli Arama) gibi algoritmaları kullanır. Ağ algoritmaları arasında “en kısa yol (shortest-path)”, “en fazla akış (max-flow)”, “minimum maliyetli akış (min-cost-flow)” başlıklarını sayabiliriz. Yeri geldiğinde bu kavramlardan da bahsedeceğim, bir kısmı ileriki yazılarımda olabilir.

Bu yazımda işe basit kısmından başlayıp, “en kısa yol (shortest-path)” algoritmasından bahsetmek istiyorum. Bu algoritma ne için ortaya çıkmıştır? Diyelim ki yolculuk yapıyoruz, bir yerden yola çıkıp başka bir yere ulaşmak istiyoruz. Bunun için birden fazla seçeneğimiz olabilir. Örneğin arabayla İstanbul’dan yola çıkıp İzmir’e gitmek istiyoruz. Batı kesiminde sahil yolunu izleyip Balıkesir üzerinden de gidebiliriz, daha içeride kalıp Manisa üzerinden de. Normal olarak düşündüğünüzde içerideki yolu izleyerek gitmemiz gerektiğini hemen söyleyebilirsiniz. Hatta elinizde doğrudan her iki rotanın da mesafe bilgisi varsa, yol şartlarının eşit olduğunuz kabul edersek, doğrudan tek seferde içeriden gidilmesi gerektiğini kolayca söyleyebilirsiniz. Peki elimizde böyle bir veri yoksa ve ulaşmamız gereken son noktaya kadar yol üzerinde uğrayabileceğimiz noktalar ve bunların arasındaki mesafeler biliyorsa nasıl bir çözüm izleyebiliriz? Bu noktada ağ modeli denilen kavram karşımıza çıkıyor. Ağ modeli belli noktaları ve bunların arasındaki bağlantıları içeren bir veri modelidir. Noktaları istasyon, durak, havaalanı gibi varılacak ve yola çıkılacak yerler olarak kabul edebilirsiniz. Noktalar arasındaki bağlantılar da otobanları, demiryollarını ya da uçuşları ifade edebilir.  Ağ modelinin görsel örneğini aşağıda görebilirsiniz.

shortest path sample

Şemada görüldüğü gibi A, B, C gibi noktalar varılacak/yola çıkılacak noktaları, aralarındaki bağlantı çizgileri de iki nokta arasındaki ulaşım süresini gösterir. Benzerlik kurmak gerekirse, şemadaki noktaları, şehirler, havaalanları, istasyonlar, aralarındaki bağlantıları da ulaşım süresi ya da  taşıma maliyeti gibi düşünebilirsiniz.

Örneğe göre A noktasından doğrudan J noktasına ya da E noktasına erişim yoktur. Bu durumda en kısa yoldan iki nokta arasındaki ulaşım nasıl hesaplanabilir? Bunun için en kısa yol algoritması çözüm olmaktadır. A ve E arasındaki ulaşımı örnek alacak olursak, A->B->E, A->C->E, A->D->E gibi üç alternatif vardır. Burada bir özelliği de belirtmekte yarar var. Bu tür ağ modellerinde bağlantıların yönlü ya da yönsüz olması durumu da problemin tanımında verilmelidir. Örnek şemada bağlantı okları yönlü olarak verilmiştir, bu durumda bahsettiğimiz üç alternatif rota sözkonusudur. Yön önemli olmasaydı, alternatiflerin sayısı da ciddi şekilde artardı, örneğin A->D->G->H->E rotası da kullanılabilirdi. Şimdilik bağlantıların yönlü olduğunu kabul edelim. Buna göre bahsettiğimiz üç rotanın hangisinin daha kısa olduğunu nasıl hesaplarız?

En kısa yol algoritması başlangıç olarak kabul edilen noktadan, ulaşılması istenen noktaya gelinceye kadar sıradaki komşu noktalara olan mesafeleri hesaplar, sonra en kısa mesafedeki komşudan devam ederek aynı hesaplamaları onun için yapar. Bu işlem hedef noktaya varılıncaya kadar devam eder. Burada önemli olan, her nokta için birikimli (kümülatif) mesafenin hesaplanmasıdır, hedef noktaya ilk seferde ulaşılması her zaman doğru sonucu vermeyebilir. Bunu daha basit bir örnek ile açıklayayım. Basit bir ağ modeli düşünün, üç noktası olsun, A, B ve C. A noktasından B’ye, oradan da C’ye varabiliyoruz. Ayrıca A noktasından doğrudan C noktasına da varılabilsin. Bu durumda A’dan C’ye ulaşmak için en kısa yol, A->C arasıdır diyebilir miyiz? A->B arası 5 saat, B->C arası 3 saat, A->C arası 10 saat ise yanıtımız hatalı olur. En kısa yol algoritmasında A’dan başlanır, A’nın komşuları olan B ve C’ye ulaşım mesafeleri hesaplanır. B (5), C (10) olur, A noktası işlenenler grubuna dahil edilir. C’ye ulaşıldığı için çözüm durdurulmaz, B’den devam edilir. B’nin komşusu C’dir, A da B’nin komşusudur (bağlantılarda yön olmasa) ancak A daha önce işlendiği için hesaba katılmaz. C’nin birikimli mesafesi tekrar hesaplanır, bu durumda C’nin yeni birikimli mesafesi, B’nin birikimli mesafesine (5), B->C arasındaki mesafe (3) eklenerek bulunur ve sonuçta 8 elde edilir. 8 değeri, C’nin o andaki birikimli mesafesinden (10) daha küçük olduğu için C’nin birikimli mesafesi 8 olarak değiştirilir. B noktası da işlenenler grubuna dahil edilir. İşlenmeyen noktalar arasında bir tek C noktası kaldığı için ve hedef nokta olduğu için çözüm sonlanır. Burada bir noktanın komşu noktalarının tek tek değerlendirilip, her biri için mesafe hesaplanmasını ve aynı işlemin sıradaki komşu noktadan devam ettirilmesini BFS (Breadth-First-Search) (Sığ Öncelikli Arama) algoritması olarak isimlendiriyoruz. Sıradaki komşu noktalardan ilkini alıp hemen onunla devam eden ve onun da ilk komşu noktasını alıp ilerleyen algoritama da DFS (Depth First Search – Derin Öncelikli Arama) olarak isimlendirilir. DFS yöntemi, ağ üzerinde bir noktadan başka bir noktaya mesafeleri dikkate almaksınız ulaşmada BFS algoritmasına göre çok daha hızlı sonuç verir. En kısa yol probleminde ise komşu noktaların her biri, hedef noktaya ulaşmada birer alternatif olduğu için ve hangisinin en kısa yol üzerinde olduğu bilinmediği için BFS yöntemini tercih etmek daha doğru olur.

Üç noktalı bir ağ modelinde çözümün iki adımda (iterasyonda) bulunması kolaydır, ancak noktalar ve aralarındaki bağlantı sayıları arttıkça problem kağıt kalemle çözümlenemeyecek kadar karmaşık hale gelir. Bunun için bilgisayar yardımıyla çözülebilen yazılım modeli kurmak gerekir. Öncelikle yazılım oluşturacak veri modeli nasıl olmalıdır? Noktalar ve aralarındaki bağlantılar nasıl ifade edilecektir?

Ağın içinde noktalar kümesi ve bağlantılar kümesi olmak zorundadır. Bağlantıları nasıl tarif edebiliriz? Her bir noktanın komşularının bilinmesi gereklidir. Bu sebeple nokta bazında bir komşu listesi tanımlamak gerekecektir. Noktaların komşu listesi, ilgili noktaya komşu olan diğer noktaları içereceğinden, bağlantı listesi de kendiliğinden tanımlanmış olur, sonuçta bir bağlantı, başlangıç ve bitiş noktası ile birlikte ifade edilir. Her bağlantı için mesafe bilgisinin saklanması gerekecektir. Her nokta için de, o noktanın birikimli mesafesi ve ek kısa yolda kendisine gelen önceki komşu noktanın numarası saklanmalıdır.

Buna göre Nokta, Komsu, Ag isimli yapılar tanımlanmıştır. Ag yapısı, nokta listesini içerir, nokta ekleme çıkarma, bağlantı ekleme çıkarma gibi metotları eklenmiştir.

Aşağıdaki program kodunda algoritmanın uygulaması verilmiştir.


using System;
using System.Collections.Generic;
using System.Linq;



namespace ConsoleApplication4
{
    public class Nokta
    {
        public int NoktaId { get; set; }

        private Dictionary<int, Komsu> komsular;

        public Nokta()
        {
            komsular = new Dictionary<int, Komsu>();
        }

        // En kısa yol algoritmasında bu noktaya ilk noktadan 
        // ulaşmanın toplam maliyetini tutar.
        public double BirikimliMesafe { get; set; }

        // En kısa yol algoritmasında bu noktaya hangi noktadan
        // gelindiğini tutar.
        public int OncekiNoktaId { get; set; }

        // Noktanın komşu noktalarını tutar.
        public IEnumerable<Komsu> KomsuListesi
        {
            get
            {
                return komsular.Values;
            }
        }

        public void KomsuListesiTemizle()
        {
            komsular.Clear();
        }

        public Komsu KomsuAl(int NoktaId)
        {
            if (KomsuMu(NoktaId))
                return komsular[NoktaId];

            return null;
        }

        public bool KomsuMu(int NoktaId)
        {
            return komsular.ContainsKey(NoktaId);
        }

        public void KomsuEkle(int NoktaId, Komsu.KomsuYonu Yon)
        {
            KomsuEkle(NoktaId, Yon, 0);
        }

        public void KomsuEkle(int NoktaId, Komsu.KomsuYonu Yon, double mesafe)
        {
            if (KomsuMu(NoktaId)) return;

            komsular.Add(NoktaId, new Komsu() { NoktaId = NoktaId, Yon = Yon, Mesafe = mesafe });
        }

        public void KomsuCikar(int NoktaId)
        {
            if (!KomsuMu(NoktaId)) return;

            komsular.Remove(NoktaId);
        }

    }


    public class Komsu
    {
        public enum KomsuYonu
        {
            Onceki,
            Sonraki,
        }

        public int NoktaId { get; set; }

        public KomsuYonu Yon { get; set; }

        public double Mesafe { get; set; }
    }



    public class Ag
    {
        private Dictionary<int, Nokta> noktalar;

        public Ag()
        {
            noktalar = new Dictionary<int, Nokta>();
        }

        public IEnumerable<Nokta> NoktaListesi
        {
            get
            {
                return noktalar.Values;
            }
        }

        public int NoktaSayisi
        {
            get { return noktalar.Count; }
        }

        public Nokta NoktaAl(int NoktaId)
        {
            if (NoktaMi(NoktaId))
                return noktalar[NoktaId];

            return null;
        }

        public bool NoktaMi(int NoktaId)
        {
            return noktalar.ContainsKey(NoktaId);
        }

        public void NoktaEkle(int NoktaId)
        {
            if (NoktaMi(NoktaId)) return;

            noktalar.Add(NoktaId, new Nokta() { NoktaId = NoktaId, BirikimliMesafe = 0 });
        }

        public void NoktaCikar(int NoktaId)
        {
            if (!NoktaMi(NoktaId)) return;

            var cikarilacakNokta = NoktaAl(NoktaId);

            var komsular = cikarilacakNokta.KomsuListesi;

            foreach (var kom in komsular)
            {
                var komNokta = noktalar[kom.NoktaId];

                komNokta.KomsuCikar(NoktaId);
            }

            noktalar.Remove(NoktaId);
        }

        public void BaglantiEkle(int baslangicNoktaId, int bitisNoktaId)
        {
            BaglantiEkle(baslangicNoktaId, bitisNoktaId, 0);
        }

        public void BaglantiEkle(int baslangicNoktaId, int bitisNoktaId, double mesafe)
        {
            if (!(NoktaMi(baslangicNoktaId) && NoktaMi(bitisNoktaId)))
                return;

            var basNokta = noktalar[baslangicNoktaId];

            if (basNokta.KomsuMu(bitisNoktaId)) return;

            var bitNokta = noktalar[bitisNoktaId];

            basNokta.KomsuEkle(bitisNoktaId, Komsu.KomsuYonu.Sonraki, mesafe);

            bitNokta.KomsuEkle(baslangicNoktaId, Komsu.KomsuYonu.Onceki, mesafe);
        }

        

        public void BaglantiCikar(int baslangicNoktaId, int bitisNoktaId)
        {
            if (!(NoktaMi(baslangicNoktaId) && NoktaMi(bitisNoktaId)))
                return;

            var basNokta = noktalar[baslangicNoktaId];

            var bitNokta = noktalar[bitisNoktaId];

            basNokta.KomsuCikar(bitisNoktaId);

            bitNokta.KomsuCikar(baslangicNoktaId);
        }
    }


    public class AgAlgoritmalari
    {
        private class MesafeSirala: IComparer<Nokta> {

            public int Compare(Nokta x, Nokta y)
            {
                return x.BirikimliMesafe.CompareTo(y.BirikimliMesafe);
            }
        }

        public void EnKisaYolHesapla(Ag ag, int baslangicNoktaId, int bitisNoktaId, List<Nokta> yol) 
        {
            yol.Clear();

            if (!(ag.NoktaMi(baslangicNoktaId) && ag.NoktaMi(bitisNoktaId)))
                return;

            var noktalar = ag.NoktaListesi;

            // tüm birikimli mesafelere sonsuz büyüklükte değer atanıyor.
            // her adımda bunlar azaltılacak.
            // her noktanın önceki nokta değeri temizleniyor

            foreach (var nokta in noktalar)
            {
                nokta.BirikimliMesafe = Double.MaxValue;

                nokta.OncekiNoktaId = 0;
            }

            // başlangıç noktasının birikimli mesafesi sıfır yapılıyor.

            var baslangicNokta = ag.NoktaAl(baslangicNoktaId);

            baslangicNokta.BirikimliMesafe = 0;

            var islenecekler = new List<Nokta>();

            var islenenler = new HashSet<int>();

            islenecekler.Add(baslangicNokta);

            var siralayici = new MesafeSirala();

            while (islenecekler.Count > 0)
            {
                var nokta = islenecekler[0];

                islenecekler.RemoveAt(0);

                if (islenenler.Contains(nokta.NoktaId))
                    continue;

                islenenler.Add(nokta.NoktaId);

                var komsular = nokta.KomsuListesi;

                foreach (var komsu in komsular)
                {
                    if (islenenler.Contains(komsu.NoktaId))
                        continue;

                    var komsuNokta = ag.NoktaAl(komsu.NoktaId);

                    double yenimesafe = nokta.BirikimliMesafe + komsu.Mesafe;

                    if (yenimesafe < komsuNokta.BirikimliMesafe)
                    {
                        komsuNokta.BirikimliMesafe = yenimesafe;

                        komsuNokta.OncekiNoktaId = nokta.NoktaId;

                        islenecekler.Add(komsuNokta);
                    }                    
                }

                // bir sonraki adıma geçmeden önce işlenecek noktaları 
                // birikimli mesafelerine göre sırala, en küçük
                // birikimli mesafeli noktadan devam edilsin.
                islenecekler.Sort(siralayici);
            }

            // işlenen nokta sayısı, ağdaki nokta sayısından az ise
            // başlangıç ve bitiş noktaları ağda bağlantılı değildir

            if (islenenler.Count < ag.NoktaSayisi)
                return;

            // son noktadan geri giderek çözüm yolunu üret.

            var bitisNokta = ag.NoktaAl(bitisNoktaId);

            yol.Insert(0, bitisNokta);

            var noktaId = bitisNokta.OncekiNoktaId;

            while (true)
            {
                var oncekiNokta = ag.NoktaListesi.FirstOrDefault(n => n.NoktaId == noktaId);

                if (oncekiNokta == null)
                    break;

                yol.Insert(0, oncekiNokta);

                noktaId = oncekiNokta.OncekiNoktaId;
            }
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Başladı");

            // Ağ yapısında 1->2, 2->3 ve 1->3 bağlantıları olacak.
            // 1->2 mesafesi 5 birim, 2->3 mesafesi 3 birim, 1->3 mesafesi 10 birimdir.
            // Çözüm yolu bu değerler ile 1->2->3 şeklindedir.
            // 2->3 mesafesini 6 veya daha büyük değer yaparsanız çözüm yolu
            // 1->3 olarak değişir

            var ag = new Ag();

            ag.NoktaEkle(1);
            
            ag.NoktaEkle(2);
            
            ag.NoktaEkle(3);

            ag.BaglantiEkle(1, 2, 5);

            ag.BaglantiEkle(2, 3, 3);

            ag.BaglantiEkle(1, 3, 10);

            var alg = new AgAlgoritmalari();

            List<Nokta> cozum = new List<Nokta>();

            alg.EnKisaYolHesapla(ag, 1, 3, cozum);

            Console.WriteLine("İzlenen yol");

            foreach (var nokta in cozum)
            {
                Console.WriteLine("Nokta no: {0}", nokta.NoktaId);
            }

            Console.WriteLine("Bitti");

            Console.WriteLine("Press any key to exit, do not search for the \"ANY\" key on keyboard :)");

            Console.ReadKey();            
            
        }        
    }
}