pythonp etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
pythonp etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

8 Şubat 2022 Salı

Mikroservis Mimarisi ve Python Özelinde Uygulamalar

Merhaba,

Bugünkü yazımda Mikroservis mimarisine giriş yapıp, python yazılım dili özelinde bazı incelemeler yapmaya çalışacağım.

Mikroservis mimarisine geçmeden önce Monolitik uygulama mimarisine bakmakta fayda görüyorum. 



Üstteki resim Monolitik ve Mikroservis mimari yapıları arasındaki farkı anlamamız için oldukça güzel bir örnek olacaktır. Monolitik mimaride veritabanından sunumun yapıldığı kullanıcı arayüzüne kadar tüm akış genellikle tek dilde yazılan tek bir uygulama şeklindedir. Kullanıcı sunum katmanı, iş akışının oluşturulduğu katman ve veri erişim katmanları aynı codebase içindedir. Küçük projelerde bu yapı oldukça kolay anlaşılabilir ve yönetilebilir olabilir. 

Fakat uygulama büyürse ve codebase e yeni özellikler eklemek gerekirse ne olacak? 

Projenin yönetimi ile ilgili çeşitli zorluklar baş gösterecektir. Ayrıca projede hızlı bir büyüme ve çeşitlenme ölçeklenebilirlikle ilgili sıkıntıları ortaya çıkaracaktır. Codebase çok büyürse farklı codebaselere bölünerek farklı takımlar halinde ilerlemek ve kurulum aşamasında codebaselerin birleştirilmesi gibi bir çözüm düşünülebilir fakat buda ekstra efor ve iş yükü anlamına gelecektir. 

  • Monolitik yapıdaki en büyük sıkıntılardan biri ise uygulama çok büyüdüğünde bir hata keşfedilirse, hatanın düzeltilmesi, etki analizi ve testlerinin yapılması sıkıntılara neden olacaktır. Küçük bir bug tespitinde bile kapsamlı testler yapılması ve tüm uygulamanın tekrar dağıtılması gerekecektir. 
  • Her güncelleme uygulamanın bütünüyle tekrar deploy edilmesine sebep olur. Küçük değişikliklerde bile kapsamlı regresyon testi gerekir. 
  • Monolitik yapılarda genellikle tek bir dil kullanıldığı için çözüm ve modülerite gereken noktalarda o dile bağımlı durumda kalırsınız. 
  • Bu yapıda çok büyüyen projelerde yeni dahil olan mühendislerin uygulamaya hakim olma süreci de uzayacaktır. 
  • Monolitik mimaride geliştirilmiş bir uygulamanın herhangi bir bileşeni kullanıcılar tarafından çok yük ile karşı karşıya bırakılırsa performansı arttırmak için yazılımın sadece bu bileşenini ayırıp ikinci sunucuya koymamız mümkün değildir. Bu anlamda bir performans artışı için tüm yazılımı ikinci sunucuya kurmak gerekecektir. Fakat ikinci sunucuya diğer tüm bileşenleri de tekrar kurmuş olacaksınız. Bu da kaynakların verimsiz tüketilmesine de sebep olacaktır.
  • Herhangi bir modüldeki hata tüm sistemi etkileyecektir.
  • Yeni teknolojileri alıp kullanma konusunda birtakım bariyerlere sahiptir. Çünkü kullanılan dil ve frameworkteki değişiklikler tüm uygulamayı etkilemektedir. Yeni bir teknolojiyi bir bileşende entegre etmeye çalışmak, işi tüm uygulamayı yeniden yazmaya kadar götürebilir.



Mikroservis mimarisinde ise "Böl ve Yönet" sloganını aklımıza getirebiliriz. Küçük ve birbirinden bağımsız çalışabilen ama gerektiğinde birbiri ile konuşabilen uygulamalardan oluşan bu mimari, genişlemeye ve yatay büyümeye oldukça açıktır. Büyümeye açık olması çok iyi bir dizaynı da gerekli kılar. Yapılacak dizayn anlaşılabilir, yönetilebilir ve uzun vadede bu yönetilebilirliğini koruyabilmelidir. 

  • Bu mimaride problemlerin kök kaynağı ve etki ettikleri noktalar hızlıca tespit edilebilir ve daha hızlı müdahale edilerek yeni geliştirme gereken durumlarda daha hızlı deploy sağlanabilir.
  • Sistemin daha yönetilebilir alt parçalar olan servislere ayırmak karmaşıklık probleminin üstesinden gelmemizi sağlar.
  • Farklı mikroservisler için farklı yazılım dilleri rahatlıkla kullanılabilir. Servisi kullanacak taraflar için servisin geliştirildiği yazılım dili önemli değildir. Karşı taraf sadece servisin arayüzünü kullanacaktır.  Buda uygulama geliştirmeye büyük bir esneklik ve modülerlik getirir. 
  • Sisteme yeni bir özellik eklemek çok kolaydır. Yeni eklenen özellik için sistemin tamamının test edilmesine gerek duyulmaz, sadece etki edilen ve birbiri ile konuşan ve bağımlılığı olan mikroservislerin test edilmesi yeterlidir. 
  • Her bir mikroservisin tek bir işe odaklanmış olması sistem yapısını anlamamızı ve sorun tespitini kolaylaştırır. Ayrıca bu sayede daha performanslı çalışan bir sistem geliştirilmesine olanak sağlar. 
  • Tek ve büyük bir codebase içine gereken deploy süresi ile kıyaslarsak mikroservislerin bağımsız deploy ediliyor olması büyük avantajdır. Yani yeni bir özellik ekliyorsanız sadece yeni mikroservisi ve gerekiyorsa sadece yeni mikroservis ile konuşacak mikroservisi deploy etmeniz yeterli olacaktır. 
  • Ekibi tek bir codebase i öğrenecek şekilde yönlendirmek yerine parçalara bölüp her servis ve altındaki mikroservisleri farklı geliştiricilerin yönetmesi sağlanabilir. Buda herkesin yaptığı işte hakimiyetini arttıracaktır. Ayrıca yeni katılan kişi tüm yapı yerine kendisi ile ilgili servisi detaylı öğrenmek zorunda olacağı için adaptasyonu çok hızlı olacaktır. Mühendisler sistemin geneli hakkında bilgi sahibi olmakla birlikte kendilerine atanan mikroservisler konusunda uzman olacaktır.
  • Her servis bir takım tarafından diğer takımların seçim ve karar makanizmalarından ve kullanılan dillerden bağımsız olarak geliştirilir. Bir takım X teknolojisini kullanıyorsa diğerleri bunu kullanmak zorunda değildir. Önemli olan son durumda servislerin birbirleri ile, müşteri ike ve veri kaynakları ile konuşabiliyor olmasıdır.
  • Önemli noktalardan birisi ise bir mikroserviste bir sorun olduğunda sadece bu mikroservis ve hizmet ettiği mikroservisler etkilenir. Yazılım kalan mikroservisleri çalışmaya devam eder. Örnek vermek gerekirse bir web sitesinde arama işlevini yöneten 3 mikroservisten biri çökebilir ve bu 3 mikroservis etkilenerek arama servisi durabilir. Fakat diğer mikroservisler çalıştığı için web sitesindeki login, alışveriş, analitik, upload, yorumlama... gibi diğer servisleri hizmet vermeye devam ederler. Yani web sitesi %10 kullanılmaz hale gelirken %90 işlevsel kalacaktır.
  • Cassandra gibi sistemler ile her mikroservisi ayrı veritabanı yapısı ile çalıştırmanız da mümkündür. Buda veritabanlarında çıkacak sorun yada yapılacak güncellemeler sırasında diğer servislerin etkilenmesini önleyecektir.

Düzgün şekilde dizayn edilen mikroservis mimarisi yatayda ölçeklendirmeye imkan verecektir. Yatayda ölçeklemeden kastımız daha çok makine üzerinde loadbalancing yaparak mikroservisin birden fazla örneğinin çalıştırılmasıdır. Yani dikey ölçekleme yapıp tek bir sunucuda çalıştırıp ölçekleme gerektiğinde makineyi upgrade etmek yerine daha maliyetsiz küçük sistemlerde uygulamayı çalıştırarak yatayda genişlemiş oluyoruz. 

  • Küçük bir örnek vermek gerekirse. Web sitesinde arama yapan bir bileşen kullanıcılardan çok talep alıyor ve sunucuyu yormaya başladı ise bunu çözmenin iki yolu olacaktır. Ya sunucuyu dikey ölçekleme ile ram cpu yönünden upgrade edeceksiniz yada bir loadbalancer arkasına bu arama bileşenininden mikroservis olarak 5 tane koyarak yatay ölçekleme yapacaksınız. Sanallaştırma ve container teknolojileri ile birlikte yatay ölçekleme maliyetler açısında daha uygun olabilmekle birlikte birtakım risklerin bertaraf edilmesine de fayda sağlayacaktır. Kurulan 5 mikroservis aynı işi yaptığı için bir tanesi çökse bile loadbalancer üzerinde diğer 4 tanesi hizmet vermeye devam edecektir. Yani dikey ölçekleme yaparak, düşerse sistemi durduracak 1 servis ile hizmet vermek yerine, yatayda genişleyerek 5 tane aynı işi yapan servisi koşturarak servisi yedeklemek sureti ile hizmetin durma riskini minimize etmiş olacaksınız. Biryandan da performans sorununuzu çözmüş olacaksınız.

Mikroservisler için spesifik olarak şu sunucuda şu ortamda çalışsın gibi bir zorunluluk bulunmamaktadır. Kubernetes ve Docker teknolojileri ile birlikte bunu kolaylıkla sağlamak mümkündür. Mikroservisler stateless yapıda geliştirilirler. Çalıştıkları diskte veri saklamazlar. Böylece restart edilselerde, başka bir makinede çalıştırılsalarda işlerini yapmaya devam ederler. Verileri dışarıda sakladıkları için statefull servislere oranla daha kolay ölçeklenebilirler.  

Son yıllarda gelişen teknolojilere ve taleplere bakıldığında tabiki mikroservis mimarisi daha tercih edilebilir durmaktadır. Ama bu her durumda mikroservis mimarisini tercih etmemiz gerektiği anlamına gelmemektedir. Monolit mimarinin de avantajlı olduğu ve tercih edilmesi gereken projeler de olacaktır. Uygulamanız küçük ve çok fazla iş mantığı içermiyor ise ileriye dönük çok fazla ölçeklenebilirlik gerektirmiyor ise monolitik yapı kullanmak avantajlı olabilir. Karmaşık yapıda mikroservis mimari kuracak bilgi birikimi ve tecrübeniz yoksa aynı zamanda ekibiniz küçük ise yine monolitik mimaride başlamak avantajlı olabilir. 

Sıfırdan bir projeyi microservis mimari işe geliştirmek nispeten daha kolay olacaktır. Fakat monolit ağırlıklı yapıda halihazırda çalışan bir yapıyı mikroservis yapısına geçirmek her açıdan maliyetli ve sancılı olabilir. Evet son durumda harika bir yapıya sahip olacaksınız, peki bedel ödemeye hazır mısınız? 

Bugün tam performans ile kesintisiz çalışan Netflix firması tamda bahsettiğimize benzer bir bedel ödemişe benziyor. 2008 yılında 3 gün civarında hizmet veremeyen Netflix firması sorunun veritabanı sıkıntısı sebebi ile oluştuğunu keşfetmiştir. 

  • Netflix'in devasa sistemi tek bir veritabanına bağımlıydı. Takdir edersiniz ki bu veritabanı ile ilgili herhangi bir sıkıntı tüm sistemi etkileme potansiyeline sahiptir. Normal şartlarda firmalar düzgün çalışan sistemlerine pek dokunmak istemezler. Heleki anlık kesintilerin bile olay olabileceği Netflix gibi bir firma. Genelde de bu bakış açısı ciddi bir çöküntüye kadar devam eder ve bu çöküntü firmayı yeni bir arayışa ve önlem almaya iter. İşte 4 günlük kesinti Netflix için tamda böyle olmuştu. Netflix'in mikroservis mimariye geçiş kararını 2008 yılında aldı. Peki microservis mimariye tam geçiş nekadar sürede tamamlandı ? 2016 yılında. Yani tam 8 sene. Gerçekten bir bedel ödenmişe benziyor. Fakat sonuçta tüm dünyaya hizmet verebilecek yatay genişlemeye sahip mikroservis mimarisi üzerine kurulmuş bir yapıya kavuşuyorlar. Günümüzdeki pekçok büyük firmanın projelerine monolitik bir mimaride başladığını fakat ölçekleme ve yönetimsel problemlerden dolayı günümüzde mikroservis mimari ile devam ettiğini söyleyebiliriz.

Mikroservis mimariye geçiş birçok değişimi de beraberinde getirebiliyor. Mikroservis mimaride alışık olduğunuz legacy DB'i yeni mimariye uyumlu hale getirmek için birçok sorguda değişiklik yapılması gerekebilir. Tabloları ve şemaları mikroservislere göre yapılandırmanız gerekecektir. Yapılacak bu ayrımlar ve ek join işlemleri sistemi yönetilmesi zor hale sokmakla birlikte performans sorunlarını da beraberinde getirebilir. Bu sebeple legacy veritabanınızdan vazgeçmeniz de gerekebilir. Mikroservis mimariye daha uyumlu bir veritabanı yapısı kullanmak daha mantıklı olabilir. 


Tıpkı Apache Cassandra gibi. Cassandra açık kaynak kodlu olmakla birlikte NoSQL ve dağıtık mimaride kullanılabilecek bir yapıya sahiptir. Tamda mikroservis mimarinin aradığı şey değil mi? Yatay ölçeklemeye çok uygun olan Cassandra, Netflix tarafından da yeni mikroservis yapısı içinde yoğun kullanılmaktadır. 

Tabiki bu bir projede sadece NoSQL yada sadece RDBMS sistemleri tercih etmemiz gerekiyor anlamına gelmiyor. Çeşitli kriterleri göz önüne alarak detaylı incleme ve dizayn yapmamız gerekiyor. Evet bir proje sadece NoSQL bir DB ile çalışabilir, yada RDBMS bir sistem ile. Ama önemli olan anlık ve ileriye dönük ihtiyaçlara uygun bir dizayndır. Çok fazla mikroservisin birarada çalıştığı bir mimaride bazı servisleri NoSQL, bazı servisleri ise RDBMS sistemleri ile çalıştırarak karma bir veritabanı altyapısı kullanmak da mümkündür. Mikroservis mimarisinde tavsiye edilen noktalardan birisi de mümkünse her mikroservisin kendine ayrı veritabanı olmasıdır. Böylece veritabanlarından birine bakım yaptığınızda diğer mikroservisleri etkilememiş olacaksınız. Yada veritabanı çöktüğünde bütün mikroservisleriniz çökmemiş oluyor. 

Mikroservis mimarisinin getirdiği bazı dezavantajlar da yok değildir.

  • Mikroservis mimarisinin kurulduğu sistemlerde networkün düzgün ve performanslı çalışması çok önemlidir. Çünkü olay mikroservislerin, müşterilerin ve veritabanlarının network üzerinde birbirilerinin konuşması üzerine kurgulanmıştır. Network sistemleri üzerinde yapılması gerekebilecek iyileştirmeler ek maliyetlere neden olabilir.
  • Bir mikroservisin doğru çalıştığının test edilmesi monolitik yapıda yapılan testlere göre daha karmaşık olabilir. Özellikle entegrasyon testleri. Servisin test edilmesi için konuştuğu diğer servisler ile  birlikte test edilmesi gerekecektir. İyi bir otomasyon ile bu işin altından kalkılabilir.
  • Mikroservis mimarilerinde otomasyonun daha etkin kullanımı ve bu konuda ekstra efor gerekir.
  • Veri tutarlılığını sağlayabilmek için çok iyi bir dizayn ve ekstra efor gerekecektir.
  • Mikroservis mimaride güvenlik konusunda daha fazla efor ve bütçe sarfetmemiz gerekecektir. Çünkü mikroservisler birbirleri ile ve veritabanları ile haberleşirken monolitik yapılara göre daha fazla saldırılara müsait haldedir. Çünkü ağ üzerinde çok fazla noktadan akan veri trafiği mevcuttur.
  • Servis ve veritabanı sayısı arttıkça monitoring işlemi daha maliyetli ve karmaşık hale gelebilir.


Biraz da Mikroservisler ve Python hakkında konuşalım. 

Python ile mikroservis oluşturmak oldukça kolaydır. Hedeflediğiniz proje ile ilgili kriterleri karşılayacak çeşitli kütüphane ve frameworkler ile performanslı mikroservis uygulamaları geliştirmeniz mümkündür.

Hadi gelin birkaç dakikada ayağa kaldırabileceğimiz basit bir mikroservis yazalım. Bu konuyu anlamanıza daha da yardımcı olacaktır. 

Yazacağımız bu mikroservis bir veri kaynağından istenen verileri müşterilere sağlayan bir uygulama olsun. Burada veri kaynağı herhangi bir şey olabilir. Bir veritabanı, json dosyası, xml dosyası, csv dosyası, başka bir microservis... 

Biz burada veri kaynağı olarak bir json dosyası kullanacağız. Bu json dosyamız içerisinde kullanıcı bilgileri içerecek ve istendiğinde mikroservisimiz bu json dosyasını okuyarak müşteriye sunacak.

Dosyamızın adı jobs.json olsun. Bu dosyayı ana proje klasörümüzde data/ klasörü altına koyalım.

{
"acan": {
"id":1,
"name":"Ali Can",
"job":"Muhendis",
"tel":"034275654235"
},
"msert": {
"id":2,
"name":"Mehmet Sert",
"job":"Yonetici",
"tel":"03567567234235"
},
"cselman": {
"id":3,
"name":"Cem Selman",
"job":"Muhendis",
"tel":"03423867864235"
},
"malan": {
"id":4,
"name":"Murat Alan",
"job":"Mimar",
"tel":"03429455674235"
}
}

İkinci oluşturacağımız dosyamız ise jobs.py isimli bir python dosyası olsun ve ana klasörde services/ klasörü altında bulunsun.


Burada flask frameworkü bulunduğumuz python environment a yüklememiz gerekiyor. 
Bunun için öncelikle bir virtual environment yaratıp aktive edelim ve içine girip flask ı yükleyelim.
Eğer PyCharm ile geliştirme yapıyorsanız PyCharm IDE settings içinden flaskı hızlıca yüklemeniz de mümkündür.

Virtual Environment için kullandığınız işletim sistemine göre alttaki komutları çalıştıralım:

Linux >

$ mkdir myproject
$ cd myproject
$ python3 -m venv venv

Windows>

> mkdir myproject
> cd myproject
> py -3 -m venv venv

Sonrasında Virtual Environment aktivasyonunu yapalım:

Linux> 

$ . venv/bin/activate

Windows>

> venv\Scripts\activate

Son olarak Flask ı yükleyelim:

$ pip install Flask

Eğer yazılım geliştirme için PyCharm kullanıyorsanız yukarıdaki işlemleri PyCharm IDE üzerinden birkaç butona tıklayarak otomatik yaptırabilirsiniz.

Gelelim kodumuza. Alttaki kodumuzda database olarak json dosyamızı verdik ve dosyayı açarak job_list e atadık. Artık veriler job_list içinde elimizde. Şimdi bu verileri isteyenlere sunmamız gerekiyor. Bu noktada yazacağımız fonksiyonlar ile flask framework ün gücünü kullanarak elimizdeki verileri sunacağız.

Öncelikle hello() fonksiyonu ile mikroservisimizin ana istek sayfasına istek yapanlara mikroservisimizin ayakta olduğu cevabını döneriz.

Sonrasında show_lists() fonksiyonumuz ile elimizdeki tüm datayı istek yapanlara gösteririz.

Son olarak user_list() fonksiyonumuz ile ise elimizdeki datanın yanlızca belli bir bölümünü isteyen müşteriye gösteririz.

Böylece mikroservisimiz istek yapan müşterilere hangi veriyi istiyorlar ise onu dönecektir.

Alttaki 3 satır isteği yaparken hangi seçeneklerimiz olduğunu belirlememize yardımcı olar. Hangi şekilde çağırırsak ilgili fonksiyon cevap dönecektir.
@app.route("/", methods=["GET"])
@app.route("/lists", methods=["GET"])
@app.route("/lists/<user>", methods=["GET"])
Alttaki satır hangi port ile mikroservisimizin hizmet vermeye başlayacağını belirler.
app.run(port=6111, debug=True)
Yani mikroservisimizi çalıştırdığımızda alttaki şekillerde çağırabileceğiz.

  • http://127.0.0.1:6111/
  • http://127.0.0.1:6111/lists
  • http://127.0.0.1:6111/lists/msert

Kodumuzun tamamını içeren Jobs.py dosyamız.
from flask import Flask, jsonify, make_response
import json
import os

app = Flask(__name__)

database_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
file = "{}\data\\jobs.json".format(database_path)
with open(file, "r") as jsf:
job_list = json.load(jsf)


@app.route("/", methods=["GET"])
def hello():
"""Welcome Screen"""

return "JOBS Service Running"


@app.route("/lists", methods=["GET"])
def show_lists():
"""LIST"""

users_lists = []
for user in job_list:
users_lists.append(job_list[user])
return jsonify(lists=users_lists)


@app.route("/lists/<user>", methods=["GET"])
def user_list(user):
"""Get User Based Info"""

if user not in job_list:
return "Not Found"
return jsonify(job_list[user])


if __name__ == "__main__":
app.run(port=6111, debug=True)

Şimdi web browserdan mikroservisimizi çeşitli urller ile çağırıp çıktılarına bakalım.










Ve son olarak da kodumuzda tanımlanmamış bir URL nin hata vereceğini görüyoruz.


Evet Python da bir mikroservis oluşturmak bu kadar basit. Tabiki bu işleyişi göstermek açısından basit bir  örnekti. Umarım aklınızda birşeyler canlandırabilmişimdir. Burada 6111 nolu port üzerinden isteklere karşılık veren bir mikroservis yazmış olduk. Farklı bir port üzerinden çalışan onlarca mikroservis yazıp hepsini aynı ortamda çalıştırıp birbirleri ile ve veritabanları ile konuşturduğumuzda basit bire mikroservis mimarisine sahip olmuş oluyoruz.

Burada kullandığımız Flask framework çok sık kullanılan ve mikroservis oluşturmada kullanılabilecek bir framework. Python bunun gibi başka framework ve kütüphaneler de sunmaktadır. 

  • Django Rest framework çok daha karmaşık geliştirmeler yapabileceğiniz ve yine mikroservisler oluşturabileceğiniz bir yapı olarak karşımıza çıkmakta ve büyük projelerde yoğunlukla kullanılmaktadır. 
  • Yine minimalist yapıda olan python Fast API ve Falcon Web Framework hızlı bir şekilde yüksek performanslı mikroservisler yazmak isteyenlerin tercihi olmaktadır. 
  • Bunların yanısıra popüler frameworkler arasından Connexion, Hug, Eve, Bottle, Turbo Gears, Tornado ve Cornice'i sayabiliriz. 
  • Daha farklı alanlarda yapacağınız çalışmalar için de microservis geliştirebileceğiniz kütüphaneler bulunmaktadır. Bunlardan ilginç olanlarından biri ise ArcGIS API dir. Haritalama, mekansal analiz, veri bilimi, yapay zeka ve otomasyon için güçlü bir Python kütüphanesi sağlamaktadır. 
  • Hızlı, ölçeklenebilir, güvenli ve taşınabilir veri tabanına dayalı web tabanlı uygulamaların hızlı bir şekilde geliştirilmesi için ise Web2PY i tavsiye edebiliriz. Web2PY nin avantajlarından birisi, veritabanı soyutlama katmanının, SQLite, MySQL, PostgreSQL, Oracle, DB2, MSSQL, MongoDB ve birçok popüler veritabanını destekleyen bir dizi sürücü ile birlikte çalışmasıdır. 

Piyasada bunlar gibi daha pek çok spesifik işte kullanılabilecek ve mikroservisler üretebilecek kütüphane ve framework bulmanız mümkündür.

Evet bu yazımızda mikroservis mimarisine ve python ile mikroservis oluşturulması konularına giriş yaptık. İlerleyen yazılarımızda birkaç mikroservisten oluşan python kodları geliştirerek daha kapsamlı projeler yapacağız. Ayrıca yukarıda bahsettiğimiz python ile mikroservis oluşturabileceğimiz birkaç ürünü deneyip, farklı ürünlerle farklı mikroservisler hayata geçirerek birbirleri ile, çeşitli veritabanları ile ve müşteriler ile konuşmalarını sağlayacağız.

Teşekkürler,
Cem Selmanoğulları

27 Ocak 2022 Perşembe

Dependency Injection ve Dependency Inversion - PYTHON

erhaba,

Bugünkü yazımda Dependency Injection ve Dependency Inversion konularını inceleyip python ile örnek kodlar yazmaya çalışacağım.

Birçok kaynakta karışık bir şekilde anlatılan bu yapıları bakalım herkesin anlayabileceği bir şekle sokabilecek miyiz...

Dependency Inversion konsepti yazılımda S.O.L.I.D prensiplerinde tanıtılmaktadır ve bu kelimenin son harfi olan D ile gösterilmektedir. Dependency Injection ise Dependency Inversion uygulanma tekniklerinden biri olarak tanımlanabilir. Amaç diğer sınıflara bağımlılığı en aza indirecek bir yapı kurmaktır.



Bir memeli sınıfımız olsun. Bu sınıf içinde başka bir hayvan sınıfını kullanıyorsak, burda bir bağımlıktan söz etmek mümkündür. Yani memeli sınıfı hayvan sınıfına bağımlıdır. Bu bağımlılığı azaltabilmek için öncelikle dependency injection uygulanması gerekir. Alttaki kodlarımızda kısaca şunu yapacağız. Hayvan sınıfını da kullanarak bir memeli oluşturacağız. Memeli sınıfından oluşturulan nesne ye metodu aracılığı ile komut verip yürüteceğiz ve durduracağız.

class hayvan():
def yuru(self):
print("Yuru...Yuruyor.")

def dur(self):
print("Dur!...Durdu.")

class memeli():
def __init__(self):
self.hayvan = hayvan()
self.durum = False

def komut_ver(self):
if self.durum:
self.hayvan.yuru()
self.durum = False
else:
self.hayvan.dur()
self.durum = True

kedi = memeli()
kedi.komut_ver()
kedi.komut_ver()
kedi.komut_ver()
kedi.komut_ver()
kedi.komut_ver()
Çıktı :

Dur!...Durdu.
Yuru...Yuruyor.
Dur!...Durdu.
Yuru...Yuruyor.
Dur!...Durdu.

Dependency Injection bağımlılıkların dışardan alınması şeklinde sağlanmaktadır. Şimdi dependency injection ile üstteki kodumuzu alttaki şekilde düzenleyelim.

 
class hayvan():
def yuru(self):
print("Yuru...Yuruyor.")

def dur(self):
print("Dur!...Durdu.")

class memeli():
def __init__(self, memeliHayvan: hayvan):
self.memeliHayvan = memeliHayvan
self.durum = False

def komut_ver(self):
if self.durum:
self.memeliHayvan.yuru()
self.durum = False
else:
self.memeliHayvan.dur()
self.durum = True

kedi = hayvan()
sonic = memeli(kedi)

sonic.komut_ver()
sonic.komut_ver()
sonic.komut_ver()
sonic.komut_ver()
sonic.komut_ver()
Çıktı :

Dur!...Durdu.
Yuru...Yuruyor.
Dur!...Durdu.
Yuru...Yuruyor.
Dur!...Durdu.

Kodun yeni halini incelersek hayvan sınıfından üretilen nesne memeli sınıfına dışarıdan verilmektedir. Yani memeli sınıfı içinde hayvan sınıfı kullanılarak bir nesne üretimi yapılmamaktadır. Bu bağımlılığı dependency injection ile kaldırmış olduk.

Peki işi biraz ileriye götürürsek, memeli sınıfına gönderilecek hayvanları iki ayaklı ve dört ayaklı olarak ikiye ayırıp bunlar için birer ara sınıf oluşturmak istersek bunu nasıl yöneteceğiz. Burada iki ekstra sınıftan bahsediyoruz ama onlarca sınıf olabilir. Peki iki ayaklı ve dört ayaklı şeklinde ayarlayacağımız iki sınıftan üretilen hayvanları memeli sınıfına gönderebilir miyiz ? Yukarıdaki örnekte hayvan sınıfından üretilen hayvan nesnelerini memeli sınıfına gönderecek şekilde ayarlamalarımızı yaptık. Peki iki ayaklı hayvan nesnesi ve dört ayaklı hayvan sınıf nesnelerini memeli sınıfına nasıl gönderebiliriz ? Bunu direkt olarak yapmamız mümkün değildir, yani bu nesneler memeli sınıfı tarafından algılanamayacaktır. Çünkü memeli sınıfı hayvan sınıfı nesnelerini algılamak üzere geliştirildi.

Bunu interface kullanımı yardımı ile çözeriz. iki ayaklı hayvan ve dört ayaklı hayvan sınıfları bu interface'den implement alırsa sorunumuz çözülecektir. Bu durumda hayvan sınıfımız bir interface olacak. Memeli sınıfı hayvan sınıfından nesneleri kabul edecek şekilde ayarlanacak. İki ayaklı hayvan ve dört ayaklı hayvan sınıfları bu interface'den implement alarak bunlardan üretilen nesneler memeli sınıfına gönderilebilecek.

Şimdi bu senaryoya göre dependency inversion uygulayalım.

Tekrar özetlersek. Hayvan isimli bir interface olacak. İki ayaklı hayvan ve Dört ayaklı hayvan isimli birer sınıf bu interfaceden implement alacak ve bu sınıflardan üretilecek nesneler memeli sınıfına gönderilerek memeli metodu olan komut_ver() çağırılacak.


from abc import ABC, abstractmethod

class hayvan(ABC):
@abstractmethod
def yuru(self):
pass

@abstractmethod
def dur(self):
pass

class iki_ayakli_hayvan(hayvan):
def yuru(self):
print("İki Ayaklı Yuru...Yuruyor.")

def dur(self):
print("İki Ayaklı Dur!...Durdu.")

class dort_ayakli_hayvan(hayvan):
def yuru(self):
print("Dort Ayaklı Yuru...Yuruyor.")

def dur(self):
print("Dort Ayaklı Dur!...Durdu.")

class memeli():
def __init__(self, memeliHayvan: hayvan):
self.memeliHayvan = memeliHayvan
self.durum = False

def komut_ver(self):
if self.durum:
self.memeliHayvan.yuru()
self.durum = False
else:
self.memeliHayvan.dur()
self.durum = True

kedi = dort_ayakli_hayvan()
sonic = memeli(kedi)

maymun = iki_ayakli_hayvan()
jimny = memeli(maymun)

jimny.komut_ver()
jimny.komut_ver()
jimny.komut_ver()
print()
sonic.komut_ver()
sonic.komut_ver()
sonic.komut_ver()
Çıktımız :

İki Ayaklı Dur!...Durdu.
İki Ayaklı Yuru...Yuruyor.
İki Ayaklı Dur!...Durdu.

Dort Ayaklı Dur!...Durdu.
Dort Ayaklı Yuru...Yuruyor.
Dort Ayaklı Dur!...Durdu.

Evet böylece ilk kodumuza önce dependency injection uygulayarak sınıf bağımlılığından kurtardık. Sonrasında ise daha detaylı bir sınıf yapısı kurarak sınıf çeşitliliğimizi arttırma amacı ile kodumuzu bir interface den implement ederek inşaa ettik. Bu sayede dependency inversion uygulamış olduk. Bunun bir avantajı da artık aynı interfaceden implement eden yeni farklı ara sınıfları kodumuza eklemek ve memeli sınıfına göndermek sureti ile yeni tür nesneler yaratabileceğiz. Örneğin kırkayak adında üçüncü bir sınıfı hızlıca kodumuza ekleyebiliriz. Hemde interface ve memeli sınıflarını değiştirmeden.

Bunu farklı bir örnek tasarımı ile açıklamaya çalışalım. Bir adet Tır sınıfımız olsun, bir adet Uçak sınıfımız olsun, bir adet Otobüs sınıfımız olsun. Bir kargo lojistik firması için yazılım geliştiriyor olalım. Bu 3 sınıfı bir adet Operasyon isimli interface sınıfından implement ederiz. Bu Operasyon sınıfının Taşı isimli bir metodu olacaktır. Dolayısı ile Uçak, Otobüs, Tır sınıflarının da Taşı isimli metodu olacaktır. Bu 3 sınıftan üretilen nesneleri ana sınıfımız olan Kargo sınıfına göndereceğiz.
Kısaca  Operasyon()  ->  ( Uçak() , Otobüs() , Tır() )  ->  Kargo() şeklinde bir yapı olacaktır.
Görebileceğiniz üzere böyle bir yapısı olan kargo firması ilerleyen yıllarda bir gemi alıp gemi kargosu işlerine girerse Gemi() sınıfını sisteme dahil etmek oldukça kolay olacaktır ve bu ekleme diğer sınıfları etkilemeyecektir.

Teşekkürler,
Cem Selmanoğulları 










23 Ocak 2022 Pazar

Python ve Singleton Design Pattern (Singleton Tasarım Örüntüsü)

Merhaba,

Bu yazımda Singleton Design Pattern konusundan bahsederek python diliyle yazılmış bir örneği paylaşmak istiyorum. 

Tasarım örüntülerinde belirli bir problem için belirli bir çözüm sunulmaktadır. Bu sayede tasarım iyileştirilir ve takımın aynı dili konuşması sağlanır. Tasarım örüntüleri belirli bir işi yapmak yada çözüme ulaşmak için en iyi yöntemi sunar. Çatısı belirli yazılım örüntüleri kullanılarak oluşturulan yazılımların ilerleyen süreçlerde büyümesi, bakımının yapılması ve devamlılığının sağlanması daha kolay olacaktır.

Her tasarım örüntüsünün bir ihtiyaçtan yada bir sorunu çözme amacından ortaya çıktığını söyleyebiliriz. Singleton da bunlardan biridir. Creational Design Pattern (Yaratımsal Tasarım Örüntüsü) grubuna dahildir.



Bu tasarım örüntüsü çalışma zamanında yanlızca 1 obje yaratılmasını garanti eder. İkinci bir obje yaratılamayacaktır ve sadece bu obje çağırılıp kullanılacaktır. Kısaca tüm uygulama için bu sınıftan yanlızca bir nesne olacaktır. Bu nesneye ihtiyaç olduğunda her yerde aynı tek örnek çağırılacaktır. Böylece bir instance'a kontrollü erişim sağlayabiliriz. Singleton'da bir obje sadece ona ihtiyaç duyduğumuzda yaratılır. 

Bir sınıftan bir nesne oluşturabilirsiniz ve bu nesneyi program içinde değişik yerlerde defalarca kullanmak isteyebilirsiniz. Örneğin bir veritabanı bağlantı nesnesini bir kere oluşturup her veritabanı işlemi gerektiğinde bu nesneyi kullanabilirsiniz. Eğer burada Singleton deseni ile geliştirme yaptıysanız aynı veritabanı bağlantı sınıfından ikinci bir nesne üretilemeyecek ve her seferinde istek yapıldığında ilk yaratılan nesne örneği gelecek ve kullanılacaktır. Ayrıca bir nesneyi global bir değişkende tutmak da sıkıntılıdır, çünkü kodun herhangi bir yerinde o değişkenin üzerine yazılarak tüm kodun çökme ihtimali vardır. Singleton nesneyi global bir değişkene atamadan global bir şekilde istediğimiz yerde çağırıp kullanabilmemize izin verir. Böylece global bir kullanım sunarken başka bir kodun nesne üzerine yazması da engellenmiş olur. Kod Singleton'un statik metoduna erişebiliyorsa bu metod her çağırıldığında aynı nesne örneğinin getirilmesi sağlanacaktır.

Burada önemli noktalardan birisi de şudur. Singleton ile global değişkenler üzerinde daha sıkı bir kontrol sahibi olmuş oluruz. Singleton sınıfının kendisi dışında hiçbir kod parçası nesne örneğini değiştiremez.

Biraz da kodlar üzerinden anlamaya çalışalım. Öncelikle bir sınıf oluşturup bu sınıf ile sadece bir adet nesne üretilmesini garanti edecek basit bir kod parçacığı yazmak istiyorum.


class Singleton:
__instance = None

def __init__(self):
""" Virtually private constructor. """
if Singleton.__instance != None:
raise Exception("This class is a singleton!")
else:
Singleton.__instance = self

singObj1 = Singleton()
print(singObj1)

Yukarıdaki kodda sınıf üzerinden bir nesne yaratıyoruz. Nesne yaratıldığında sınıf içindeki __instance değişkenini kontrol ederek daha önce bu sınıftan bir nesne yaratılıp yaratılmadığını kontrol ediyoruz. Eğer daha önce nesne yaratılmadıysa nesnemiz yaratılıyor ve sınıf içindeki __instance değişkenine nesnemiz atanıyor. Ekrana bastırdığımızda da alttaki şekilde hatasız bir şekilde nesnenin yaratıldığını görüyoruz.

<__main__.Singleton object at 0x0000024535C47C10>

Şimdi alttaki şekilde ikinci bir obje yaratmaya çalışalım.


class Singleton:
__instance = None

def __init__(self):
""" Virtually private constructor. """
if Singleton.__instance != None:
raise Exception("This class is a singleton!")
else:
Singleton.__instance = self

singObj1 = Singleton()
print(singObj1)
singObj2 = Singleton()
print(singObj2)

İlk nesnenin yaratılmasına kadar herşey aynı. Artık sınıf içindeki __instance değişkenine nesnemiz atanmış durumda. İkinci kes bu sınıftan nesne üretmeye çalıştığımızda ise yine __init__ içindeki kontrol yapılacaktır. Bu sefer __instance değeri None olmayacaktır. Bu sebeple "This class is a singleton!" şeklinde bir exception fırlatacaktır. Burda dikkat ederseniz nesneyi global bir değişkende tuttuk.

Exception: This class is a singleton!

Bu örneği temel mantığı anlamanız için yaptık. Şimdi biraz daha kapsamlı bir örnek paylaşmak istiyorum. Bu örnekte singleton sınıfından bir nesne üreteceğiz ve global bir değişkene bu nesneyi atamadan her istediğimizde sınıfın metodu ile çağıracağız. Böylece nesneyi global değişkende tutarak çağırmanın getireceği risklerden de kurtulmuş olacağız. Birçok yerde paylaşılan standart bir örneği olduğu gibi paylaşarak açıklamak istiyorum.


class SingletonMeta(type):
"""
The Singleton class can be implemented in different ways in Python. Some
possible methods include: base class, decorator, metaclass. We will use the
metaclass because it is best suited for this purpose.
"""

_instances = {}

def __call__(cls, *args, **kwargs):
"""
Possible changes to the value of the `__init__` argument do not affect
the returned instance.
"""
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]


class Singleton(metaclass=SingletonMeta):
def some_business_logic(self):
"""
Finally, any singleton should define some business logic, which can be
executed on its instance.
"""
# ...


if __name__ == "__main__":
# The client code.

s1 = Singleton()
s2 = Singleton()
print(s1)
print(s2)

if id(s1) == id(s2):
print("Singleton works, both variables contain the same instance.")
else:
print("Singleton failed, variables contain different instances.")

Bu kod örneğimizde sınıfımız içinde __call__ metodunun kullanıldığını göreceksiniz. Bu metod sınıfımızdan üretilecek nesne örneğinin istenildiği bir anda bir fonksiyon gibi çağırılarak getirilebilmesini sağlar.

Yani Singleton sınıfımızdan üretilecek nesne örneğini Singleton() ile defalarca çağırabiliriz ve her seferinde aynı nesne örneği gelecektir ve ikinci bir nesne oluşturulmayacaktır. Bu tamda Singleton Design Pattern'de istediğimiz şey.

Yine burada _instances isimli bir değişkenimiz var. Bu değişken sınıf ilk nesneyi ürettiğinde oluşturulan nesne örneğini tutacak ve her çağırıldığında aynı örneği verecektir. Kontrol __call__ metodu içinde yapılacak eğer daha önce nesne üretilmediyse nesne üretilecek ve ikinci kes nesne üretilmesine izin verilmeyecektir.

Koda bakarsanız s1 ve s2 de iki kere bu sınıf nesnesi çağırılmış. Peki her seferinde aynı nesne mi gelmiş ? Evet. Aşağıdaki çıktıya baktığınızda aynı nesne örneğinin geldiğini göreceksiniz. Singleton() çağrısını yüzlerce kez de yapsak artık hep aynı nesne örneği gelecektir.

<__main__.Singleton object at 0x000002B5D5F4FF70>

<__main__.Singleton object at 0x000002B5D5F4FF70>

Singleton works, both variables contain the same instance.


Teşekkürler,

Cem Selmanoğulları

22 Ocak 2022 Cumartesi

Python ile InfluxDB operasyon scripti

Merhaba,

Bugün InfluxDB ile ilgili çeşitli işlemleri yapabileceğiniz bir python script paylaşmak istiyorum.

Alttaki script ile InfluxDB üzerinde veritabanı yaratabilir, measurement yaratabilir, veri ekleyebilir, veritabanları ve measurementları listeletebilir, var olan bir measurement ı başka bir measurementa taşıyabilirsiniz. Henüz temel birtakım işlemleri yapan scriptimizi önümüzdeki süreçte daha da geliştirip InfluxDB ile alakalı birçok işlemi yapan bir araç haline getirmeyi planlamaktayım.

   


from influxdb import DataFrameClient, InfluxDBClient
from pandas import DataFrame
import pandas as pd
import numpy as np


class InfluxMain:

def __init__(self, host, port, user, password, dbname):
self.host, self.port = host, port
self.user, self.password = user, password
self.dbname = dbname
self.client = DataFrameClient(host, port, user, password, dbname)
self.__create_database__(dbname) # Creates if db does not exist

def __create_database__(self, db):
try:
print("Creating DB: " + db)
self.client.create_database(db)
except Exception as e:
print(e)

def insert_data(self, data, measurement, tag_columns):
self.__write_to_database__(data, measurement, tag_columns)

def drop_measurement(self, measurement):
print("Dropping measurement: " + measurement)
self.client.drop_measurement(measurement)

def __write_to_database__(self, data, measurement, tag_columns, protocol="line"):
try:
print("Create Measurement: " + measurement)
self.client.write_points(data, measurement, tag_columns=tag_columns, protocol=protocol, batch_size=10000)
print("Done!")
except Exception as e:
traceback.print_exc()


class InfluxAnalyser:

def __init__(self, host, port, user, password, dbname):
self.host, self.port = host, port
self.user, self.password = user, password
self.dbname = dbname
self.influxdb_client = InfluxDBClient(host, port, user, password, dbname)

def close_connection(self):
self.influxdb_client.close()

def get_databases(self, print_to_screen):
try:
df_databases = DataFrame(self.influxdb_client.query("SHOW DATABASES").get_points())
if print_to_screen == True:
print("\n| INFLUX DATABASES |\n")
for i in range(len(df_databases)): print("DB-" + str(i + 1), "> ", df_databases['name'].loc[i])
return df_databases
except Exception as e:
print(e)

def show_measurements(self):
try:
df_databases = self.get_databases(False)
for i in range(len(df_databases)):
db_name = df_databases['name'].loc[i]
print("\nDATABASE : " + db_name + "\n")
df_measurements = DataFrame(self.influxdb_client.query("show measurements on " + db_name).get_points())
print("Measurements >")
print(df_measurements)
except Exception as e:
print(e)

def migrate_measurement(self, source, target, influx, tag_columns, influx_index):
select = "select * from " + source
df = DataFrame(self.influxdb_client.query(select).get_points())
df['Index_Time'] = pd.to_datetime(df[influx_index])
df.set_index('Index_Time', inplace=True)
influx.insert_data(df, target, tag_columns)


if __name__ == "__main__":
host, port = "localhost", 8086
user, password = "", ""
database = "TestDB"
measurement = "TestMeasurement"

influx = InfluxMain(host, port, user, password, database) # Create DB if not exists, initiate connection object
# influx.drop_measurement(measurement)

# Test data : Dataframe with Python Dataframe
df = pd.DataFrame(columns=['Name', 'City', 'Market_Type', 'Par_Val', 'Core_Val'])
dfrow = {'Name': 'Mert', 'City': 'ist', 'Market_Type': 'marmar', 'Par_Val': '23443234', 'Core_Val': '7567567',
'Start_Time': '2021-01-01 00:10:33'}
df = df.append(dfrow, ignore_index=True)
df['Index_Time'] = pd.to_datetime(df['Start_Time'])
df.set_index('Index_Time', inplace=True) # Setting index time for influxdb measurement

tag_columns = ['Name', 'City', 'Market_Type'] # Define tag column names. Fields come with data.
influx.insert_data(df, measurement, tag_columns) # Create measurement if not exists, and add data

analyser = InfluxAnalyser(host, port, user, password, database)
analyser.get_databases(True)
analyser.show_measurements()
source, target, dbname = "TestMeasurement", "NewMeasurement", "TestDB"
influx_index = "Start_Time" # Influx DB table index time
analyser.migrate_measurement(source, target, influx, tag_columns, influx_index) # Read from source, write to target
analyser.close_connection()

20 Ocak 2022 Perşembe

Python metod ve fonksiyonları - Python Methods and Functions

Merhaba,

Sıklıkla karıştırılabilen bir konuya açıklık getirmek isterim. Python yazılım dilinde fonksiyon ve metod konseptleri farklı şeyleri ifade etmektedir. 

Metodlar sınıfların bir parçasıdır ve sınıf ile oluşturulan objenin durumunu değiştirebilir. Fonksiyonlar ise sınıflardan bağımsız tek başına çalışan ve çağrılabilen yapılardır. Bir objenin parçası değildirler.

Örnek ile açıklamak gerekirse.

class Toplam:
    def __init__(self, a, b):
        self.a = a
        self.b = b
    # Metod Ornegi
    def islem(self):
        return self.a + self.b

#Fonksiyon Ornegi
def topla(a, b):
    return a + b

Toplam sınıfı içinde tanımlı islem() bir metoddur. Sınıf dışında tanımlı topla() ise bir fonksiyondur.







Teşekkürler,

Cem Selmanoğulları

17 Ocak 2022 Pazartesi

Python yazılım dilinde Dekoratör (Decorator) kullanımı

Merhaba,

Bugün Python yazılım dilinde Decorator kullanımına bir örnek paylaşmak istiyorum. Kısaca diğer fonksiyonları yada sınıfları geliştirip modifiye etmeye yarayan python objeleri şeklinde tanımlayabiliriz. 

Örnek vermek gerekirse, bir fonksiyonumuz olsun. Bu fonksiyon bir string döndürüyor olsun. Bu döndürülen stringin büyük harfle başlamasını istiyoruz. Bunu direkt fonksiyon içinde yapmamız mümkün, ama biz bunu her yazdığımız fonksiyon içinde tekrar tekrar yapacak mıyız ?  İşte bunun için her ihtiyacımız olduğunda decorator fonksiyonunu çağırarak bu işi yapmasını sağlayabiliriz. Alttaki örnekte capitalizer isimli bir decorator fonksiyonumuz var. birde string dönen fonksiyonumuz var. @capitilizer referansını stringer fonksiyonunun başına yazdığımızda artık capitilizer fonksiyonu bir nevi stringer fonksiyonunu sarmalayarak geliştirip dönen değeri büyük harfle başlayacak şekilde geliştirecektir.

def capitalizer(func):
def wrapper(string):
innerfunc = func(string)
capitalized = innerfunc.capitalize()
return capitalized
return wrapper

@capitalizer
def stringer(string):
return string

print(stringer("tHiS is a DeCORator EXAmple"))

Çıktı :  This is a decorator example


Bir başka örneği ise Python Django Framework üzerinden verebiliriz. Django otantikasyon sistemi view fonksiyonları ile birlikte kullanabileceğimiz bir @login_required dekoratörü sunmaktadır. Bu dekoratörün refere edildiği view eğer sisteme login olunduysa gösterilecek olunmadıysa gösterilmeyecektir.

Alttaki örnekte @login_required dekoratörünün nasıl uygulandığını görebilirsiniz. Login olmayanlara my_view gösterilmeyecektir.


from django.contrib.auth.decorators import login_required

@login_required
def my_view(request):
    ...






Teşekkürler,

Cem Selmanoğulları

30 Aralık 2021 Perşembe

Python ile uzak makineye ssh bağlantısı yaparak komut çalıştırmak

Merhaba,

Python pexpect kütüphanesini kullanarak kullanıcı adı ve şifre ile uzak makineye (linux server) ssh ile bağlanıp komut çalıştıralım. 

from pexpect import pxssh
import getpass
try:
    s = pxssh.pxssh()
    hostname = "ip.ip.ip.ip"
    username = "root"
    password = "732he+?hs7"
    s.login(hostname, username, password)
    s.sendline('ll')  
    s.prompt()            
    print(s.before)        
    s.sendline('ls -l')
    s.prompt()
    print(s.before)
    s.sendline('df')
    s.prompt()
    print(s.before)
    s.logout()
except pxssh.ExceptionPxssh as e:
    print("pxssh login hata.")
    print(e)</code>