Quantcast
Channel: Burak Selim Şenyurt
Viewing all 351 articles
Browse latest View live

Angular ile Yazılmış Bir Web Uygulamasını PWA Uyumlu Hale Getirmek

$
0
0

Geminin neredeyse tüm seyrüsefer sistemi ve radarı arka arkaya gelen alarm sinyalleri sonrası bozulmuştu. Güney pasifiği terk etmek üzere olan koca tekne en son Arjantin kıyılarına yakın seyrediyordu. Gecenin zifiri karanlığında ilerlerken kaptanın en büyük yol bulma ümitlerinden olan kuzey yıldızı bulutlarla kaplı gökyüzünden saatlerdir görülmüyordu.

Altı kişilik güverte mürettabı normal şartlarda geminin seyri için fazlasıyla yeterliydi. Yaklaşık yirmibin groston ağırlığındaki gemi son teknloji cihazlarla donatıldığı için az sayıda personel ile kıtalar arası seyahat edebiliyordu.

Ama şimdi kaptan ve yardımcısı dışındaki mürettebat geminin çeşitli noktalarına yayılmış, dürbünleriyle bir şeyler arıyordu. Korkutucu olan, yönü belirleyemezlerse kendilerini Antartika güzergahında bulabilecek olmalarıydı. Koca okyanusta bu derecede bir rota sapması işlerin daha da korkunç bir hal almasına neden olabilirdi.

Derken kıç tarafa giden denizcinin sesi duyuldu telsizden. Güverte umuttan daha da fazla bir hisle dolmuştu anında. Gemi, güçlü bir manevra ile geri dönüp ufukta belirmiş ve aralıklarla yanıp sönen ışığa doğru yöneldi. Kaptan mürettabatı tekrar güverteye davet ederken yardımcısına şöyle seslendi "Tanrıya şükür Eric. Sonunda Les Eclaireurs yüzünü gösterdi" 

Les Eclaireurs...1920 yılında inşa edilen bu deniz feneri, Arjantinin en güney şehri olarak bilinen Ushuaia'nın yaklaşık 9.3 km doğusundadır. "Dünyanın Ucundaki Fener" olarak da bilinir. Turistlerin popüler uğrak noktası olan ve küçük bir ada üzerinde duran fener tarih boyunca bir çok denizci için yol gösterici olmuştur. Deniz feneri kelimesinin İngilizce karşılığı Lighthouse'dur ve bu kelime bir cumartesi gecesi çalışmasında bana yol göstericilik yapmıştır. Öyleyse gelin derlememize başlayalım (Biraz eski bir araştırma olsa da dünyanın 10 ünlü feneri için şu yazıya bakabilirsiniz. Ben özellikle İskoçya'daki Bell Rock deniz fenerinden çok etkilendim)

PWA(Progressive Web App) tipindeki uygulamalar özellikle mobil cihazlarda kullanılırken sanki AppStore veya PlayStore'dan indirilmiş native uygulamalarmış gibi görünürler. Ancak native uygulamalar gibi dükkandan indirilmezler ve bir web sunucusundan talep edilirler. Https desteği sunduklarından hat güvenlidir. Bağlı olan istemcilere push notification ile bildirimde bulunabilirler. Cihaz bağımsız olarak her tür form-factor'ü desteklerler. Bu uygulama modelinde Service Worker'lar iş başındadır ve sürekli taze kalınmasını sağlarlar. Düşük internet bağlantılarında veya internet olmayan ortamlarda çevrim dışı da çalışabilirler. URL üzerinden erişilen uygulamalar olduklarından kurulum ihtiyaçları yoktur.

Benim bu cumartesi gecesi çalışmasındaki amacım ise gayet basitti. Biraz yabancısı olduğum Angular ile basit bir web uygulaması yazmak ve bunu PWA uyumlu hale getirmek.

Peki bir web sayfasından gelen içeriğin PWA uyumluluğunu nasıl test edebiliriz? Bunun için Google'ın geliştirdiği ve Chrome üzerinde bulunan Lighthouse isimli uygulamadan yararlanabiliriz(Ta taaaa...Hikayeyi bağladım işte) F12 ile açılan Developer Tools'tan kolayca erişilebilen Lighthouse ile o anki sayfa için uyumluluk testleri yapabiliriz. Örneğin kendi blogum için bunu yaptığımda mobile cihazlardaki PWA uyumluluğunun %50 olarak çıktığını gördüm :/ Yarı yarıya uyumsuz. Bu nedir arkadaş ya?

Bakalım boş bir uygulama için bu durumu değiştirebilecek miyiz?

Ön Hazırlıklar

Angular ile ilgili işlemler için command-line interface(CLI) aracından yararlanabiliriz. Yoksa aşağıdaki ilk komutla kurmak lazım tabii. Angular CLI komut satırı bir çok konuda yardımcı olacaktır. Projenin oluşturulması, angular için yazılmış paketlerin kolayca eklenmesi vb...İkinci komutla projemizi hazır şablonla oluşturuyoruz. UI tarafında Material Design kullanmayı öğrenmeye çalışacağım. Bu nedenle proje klasörüne girdikten sonra ng add komutu ile material'ın angular sürümünü de projeye ilave etmemiz lazım (Prebuilt tema seçimini Indigo/Pink olarak bıraktım)

sudo npm install -g @angular/cli
ng new quotesify
cd quotesify
ng add @angular/material

Kod Tarafı

Kodları mümkün mertebe açıklamalarla desteklemeye çalıştım ancak genel hatları ile önyüz bileşenini değiştirdiğimiz, farklı bir adresle haberleşecek bir servis yazdığımızı ifade edebiliriz. İşe ilk olarak app.module.ts dosyasından başlayalım. app.module.ts dosyasında HTTP çağrılarını yapmamızı sağlayan HttpClientModule modülünü tanımlıyoruz. Böylece HttpClient, ana modüle bağlı tüm bileşen ve servislere enjekte edilebilir(Evet burada da Dependency Injection var. O her yerde :P ) Ayrıca UI tarafı kontrolleri için ilgili Material modüllerini de eklememiz lazım. Örnekte Toolbar, Card ve Button kontrollerine ait modülleri ele almaktayız. Kodda geçen diğer modüller zaten şablon ile birlikte gelmiş olanlar.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

/*
UI tasarımında kullanacağımız Material bileşenlerine ait modül bildirimleri
*/
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';

/* 
 HttpClientModule'ü burada import ettik.
 Böylece HTTP çağrıları yapabilmemizi sağlayan
 HttpClient nesnesini ana modüle bağlı olan 
 tüm componenetlere enjekte edebiliriz.

 HttpClient'ı arayüze veri döndüren dummy bir API
 servisine Get çağrısı yapmak için kullanacağız.
 */
import { HttpClientModule } from '@angular/common/http';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    NoopAnimationsModule,
    HttpClientModule, //Buraya da eklemeyi unutmamak lazım
    // Aşağıdakilerde Material modülleri için yapılan ilaveler
    MatToolbarModule,
    MatCardModule,
    MatButtonModule,
    // PWA güncellemesi sonrası eklenen Worker Service kaydının yapılması
    ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Bu adımdan sonra ng g service dummy terminal komutunu kullanarak DummyService isimli bir servis sınıfı ekliyoruz. Şuradaki dummy servis adresinden veri çekip sunmakla görevli bir modül esas itibariyle. Sunmak derken uygulama arayüzündeki bileşenleri besleyecek diyebiliriz.

/*
Servisin görevi https://jsonplaceholder.typicode.com/posts adresinden
dummy veri çekmek ve bunu bir Observable nesne olarak sunmak.
*/
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; // HttpClient nesnesini içeriye constructor üzerinden enjekte edeceğiz
import { Observable } from 'rxjs'; //RxJS kütüphanesinden Observable tipini kullanıyoruz

/*
JSON servisinden dönen öğeleri ifade eden arayüz tanımı.
Post tipini temsilen bazı alanlar içeriyor.
*/
export interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

@Injectable({
  providedIn: 'root'
})

/*
DummyService'i üretmek için komut satırından 
ng g service dummy
komutunu kullandık
*/
export class DummyService {

  // Constructor bazlı dependency injection
  constructor(private httpClient: HttpClient) { }

  /* 
    get metodu Observable tipte bir koleksiyon döndürür 
  */
  get(): Observable<Post[]> {
    var url = "https://jsonplaceholder.typicode.com/posts";
    /*
      url ile belirtilen adrese get talebi gönderiyor
      ve içeriğini Post dizisi olarak alıp
      Observable nesnesiyle geriye dönüyoruz
    */
    return <Observable<Post[]>>this.httpClient.get(url);
  }
}

Oluşturulan servisi app.component.ts dosyasında kullanabilmek içinse bir takım eklemeler yapmalıyız.

import { Component, OnInit } from '@angular/core';
import {DummyService} from './dummy.service'; // yeni eklediğimiz servisi kullanacağımızı belirtiyoruz
import {Post} from './dummy.service'; //ki Post arayüz tipinide oradan export etmiştik

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

// AppComponent, OnInit metodunu uygulamalı
export class AppComponent implements OnInit {
  title = 'Dummy Posts';
  posts: Array<Post>; // çekilen Post verilerini saklamak için kullanacağımız dizi

  constructor(private dummyService:DummyService){

  }

  /*
  OnInit, Angular bileşeninin yaşam döngüsünde çalışan metodlardan birisi.
  Component oluşturulurken devreye girip ilgili servisten veriyi çeken bir 
  işlevi yürütecek şekilde programlandı.

  OnInit AppComponent bileşeni oluşurken bir seferliğine çağrılır.
  */
  ngOnInit(){
    /*
    Constructor'dan enjekte edilen DummyService örneğini kullanarak
    get metoduna başvuruyor ve Post dizisini çekiyoruz.

    DummyService servisindeki get metodu Observable bir nesne döndürüyor.
    Burada ona abone(subscribe) oluyoruz. Asenkron çalışma durumu söz konusu
    olduğunda servis ilgili veriyi çektiğinde kendisine abone olanları da bilgilendirecektir.
    Yani çekilen Post dizisindeki değişim(bu senaryoda servisten alınması) component'e
    bildirilmiş olacaktır. 
    
    */
    this.dummyService.get().subscribe((data:Array<Post>)=>{
      this.posts=data;
    },(err)=>{
      console.log(err);
    });
  }
}

Önyüz bileşeni olarak src/app/app.component.html içeriği de tamamen değiştirildi. Material bileşenlerine yer verildi.

<mat-toolbar><mat-toolbar-row><span>Some dummy posts from universe</span></mat-toolbar-row></mat-toolbar><main><mat-card *ngFor="let post of posts"><mat-card-header><mat-card-title>{{post.title}}</mat-card-title></mat-card-header><mat-card-content>
      {{post.body}}</mat-card-content></mat-card></main>

Toolbar tipinde bir Navigation kontrolü, Post bilgilerini göstermek içinse Card kontrolünden yararlanıyoruz. UI, bağlı olduğu AppComponent içerisindeki posts dizisini kullanıyor. Tüm dizi elemanlarında gezmek içinse *ngFor komutundan yararlanılmakta. Bir özellik değerini arayüzde göstermek istediğimizde {{post.title}} benzeri notasyonlar kullandığımız da gözden kaçmamalı.

PWA Uyumluluğu için Hazırlıklar

Amacımız uygulamanın PWA uygunluğunu kontrol etmek olduğu için öncelikle onu canlı ortam için hazırlamalıyız(Yani Production Build işlemini yapmamız gerekiyor) Nitekim PWA özelliklerinin bir çoğu geliştirme ortamına dahil edilmemekte. Build işlemi için ng CLI aracını aşağıdaki gibi kullanabiliriz.

ng build --prod

Uygulama dist klasörüne build edilmiş olur. Hizmete sunmak için http-server gibi bir araçtan yararlanılabilir. Eğer sistemde yüklü değilse npm ile kurmamız gerekir. İlk komutla bunu yapıyoruz. İkinci terminal komutuysa uygulamayı localhost üzerinden ayağa kaldırmakta.

sudo npm install -g http-server
cd dist
cd quotesify
http-server -o

Bunun sonucu olarak 127.0.0.1:8080 veya 8081 portundan yayın yapılır ve uygulama açılır.

Uygulama çalıştıktan sonra F12 ile Audits kısmına gidip 'Run Audit' ile PWA testi başlatılırsa, Lighthouse bize aşağıdakine benzer sonuçlar verecektir(Tabii sizin denediğiniz vakitlerde bu kurallar değişmiş olabilir. O nedenle bilgileri güncellemekte yarar var)

PWA uyumluluğu oldukça düşük ki bu zaten şu aşamada beklediğimiz bir şey. PWA uyumlu hale getirmek için neler yapılabilir bakalım.

İhlal Edilen PWA Kriterleri

Önce hangi kuralların ihlal edildiğinde ve bunların ne anlama geldiğine bir bakalım.

  • Uygulamanın HTTPS desteği olmazsa olmazlardandır. Development tarafında sıkıntı olmasa da uygulamayı üretim ortamlarına aldığımızda sertifika tabanlı iletişim sağlanmalıdır.
  • Service Worker olmaması sebebiyle offline çalışma ve cache kabiliyetlerinin yanı sıra push notification kabiliyletleri de ortada yoktur. Service Worker, ağ proxy'si gibi bir görev üstlenir ve uygulamanın çektiği öğeler(asset) ile veriyi taleplerden(requests) yakalayıp önbelleğe alma operasyonlarında işe yarar.
  • Manifesto dosyasının bulunmayışı ki bu dosyada uygulama adı, kısa açıklaması, icon'lar ve diğer gerekli bilgiler yer alır. Ayrıca manifesto dosyası sayesinde add-to-home-screen ve splash screen özellikleri de etkinleşir.
  • Progressive Enhancment desteğinin olmaması da bir PWA ihlalidir. Uygulamanın çağırıldığı tarayıcıya göre ileri seviye özelliklerin kullanılabileceğinin ifade edilmesi beklenmektedir.

PWA Uyumluluğu için Yapılanlar

Angular tarafında uygulamayı PWA uyumlu hale getirmek için aşağıdaki terminal komutunu çalıştırmak yeterlidir. (Proje klasöründe çalıştırdığımıza dikkat edelim)

ng add @angular/pwa

Komut çalıştırıldığında eksik olan manifesto ve service worker dosyaları eklenir. Ayrıca assets altındaki icon'ların form factor desteği açısından farklı boyutları oluşur.

Yeni bir dağıtım paketi çıktığımızda PWA için eklenen Service Worker ve manifesto dosyalarını da görebiliriz.

Tekrardan Lighthouse raporunu çektiğimizde aşağıdaki gibi %92lik bir karşılama oranı oluştuğunu görebiliriz. Fena değil ama eksik. Çünkü HTTPS desteğini göremedi.

Peki ya kalan HTTPS ihlalini development ortamında nasıl aşabiliriz? Aşabilir miyiz? Eğer buraya kadar gelebildiyseniz bir adım daha ilerleyebilirsiniz sevgili okur ;)

Ben Neler Öğrendim?

Yirmisekiz numaralı bu cumartesi gecesi çalışmasının da bana kattığı değerli bilgiler oldu elbette. Bunları kabaca aşağıdaki gibi sıralayabilirim.

  • Angular CLI'ın(command-line interface) temel komutlarını
  • Component'lere servislerin nasıl enjekte edilebileceğini
  • Çok basit anlamda Material bileşenlerini arayüzde nasıl kullanabileceğimi
  • PWA tipindeki uygulamaların genel karakteristiklerini ve avantajlarını
  • PWA ihlallerinin kısaca ne anlama geldiklerini ve tespitinde Lighthouse'un nasıl kullanılabileceğini

Böylece geldik bir maceramızın daha sonuna. Bu yazıda basit bir Angular uygulaması geliştirip bunu Progressive Web App modelinde yayınlanabilecek kıvama getirmeye çalıştık. Umarım sizler için de faydalı bir çalışma olmuştur. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.


Sunucu Bazlı Blazor Uygulaması ve Firestore Kullanımı

$
0
0

Mavi renkli teknoloji firmasına henüz yeni başlamıştım. Yaş ve önceki dönem tecrübeleri nedeniyle standart olarak uygulanan oryantasyon hızlıca atlanmış ve 2002 yılında geliştirilmeye başlanmış Web Forms kurugulu ERP uygulamasından ilk görevimi almıştım. Henüz çevik dönüşüme başlanmamıştı. Elimde tek sayfalık bir analiz dokümanı bulunuyordu. Otomotiv tarafındaki iş bilgim az olduğundan dokümanda yer almayan şeyler hakkında pek bir fikrim yoktu. Görevim kağıt üstünde oldukça basitti. Popup pencere açtırıp içerisinde bir araca ait veriler gösterecektim. Ne kadar zor olabilirdi ki :))

İlk haftanın sonunda popup açılıyor ancak engin front-end bilgim nedeniyle ekranın üstündeki nesnelerin hiç biri olması gerektiği yerde durmuyordu. Back-end servisinin kodlanması, Data Access Layer tarafı, veri tabanı nesneleri...Hepsini kısa sürede halledebilmiştim ama işte o önyüz tarafı yok mu? O görselliği arttıran CSS ayarlamaları yok mu? (CSS demişken yandaki Simpson karakterlerinin Chris Pattle tarafından nasıl yazıldığına bir bakmak ister misiniz? Şöyle buyrun öyleyse)

Sıfırdan bir şeyler yazacak olsam hiç zorlanmayacaktım belki ama yaşayan, belli kuralları ve sırları bulunan bu ürün içinde epey mücadele vermiştim. Bir tam gün boyunca ekrandaki o düğmenin olması gerektiği yere gelmesi için Chrome DevTools'ta gözlerimi kanatacak kadar uğraştığımı hatırlıyorum. İç içe gelen master page'lerin, sayısız div'in arasında bir o yana bir bu yana savrulup durmaktaydım.

Tabii zaman hızla aktı. Aradan aylar geçti. Hem bu yaşlı ürüne hem yeni nesil programlara aşina oldukça gereksinimleri bir öncekine göre daha hızlı karşılayabildiğimi fark ettim. Ne varki yeni nesil ürünlerde de en çok zorlandığım şey işte bu popup pencereleriydi. Vue tabanlı olanda olsun Angular tabanlı olan da olsun gözümün önünde yapılmış örnekleri bile varken onlara bakmadan gelişitirmekte halen zorlanmaktayım. Web API tarafı tamam, veri tabanı nesneleri tamam, aradaki iletişim için gerekli köprüleri kurmak tamam...Ama işte o önyüz yok mu? Bence beynimdeki front-end hücreleri tamamen yanmış :D Sıradaki cumartesi gecesi derlememizde de bir Popup var ama bu kez çok fazla zorlanmadım diyebilirim. Nitekim odağımız Blazor kıyıları olacak.

Blazor çoğunlukla client-side web framework olarak düşünülmekte. Bu kabaca, Component ve DOM etkileşiminin aynı process içerisinde olması anlamına geliyor ancak process'lerin ayrılması konusunda esnek bir çatı. Öyle ki Blazor'un bir Web Worker içinde çalıştırılıp UI(User Interface) thread'inden ayrıştırılabileceği ifade edilmekte. Diğer yandan 0.5 sürümü ile birlikte Blazor uygulamalarının sunucu tarafında çalıştırılması mümkün hale gelmişYani .Net Core ile etkileşimde olacak şekilde Blazor bileşenlerini sunucu tarafında çalıştırabiliriz. Bu senaryoda .Net tarafı WebAssembly yerine CoreCLR üzerinde koşmakta ve .NET ekosisteminin pek çok nimetinden(JIT, debugging vb) yararlanabilmekte. Kullanıcı önyüz tarafı ile etkileşimde olayların yakalanması ve Javascript Interop çağrıları içinse SignalR ele alınmakta. Aşağıdaki kötü çizim konuyla ilgili olarak size bir parça daha fikir verebilir.

Benim bu çalışmadaki amacım Server Side tipinden Blazor uygulamalarının Ubuntu gibi bir platformda nasıl geliştirilebileceğini öğrenmek ve bunu yaparken de Google Cloud Firestore'u kullanarak basit CRUD(Create Read Update Delete) operasyonları içeren bir ürün tasarlamaktı. Araştırmalarıma göre Server Side Blazor modelinin belli başlı avantajları bulunuyor. Bunları şöyle sıralayabiliriz.

  • Uygulamanın indirme boyutu nispeten küçülür
  • Blazor bileşenleri(component) .Net Core uyumlu sunucu kabiliyetlerinin tamamını kullanabilir
  • Debugging ve JIT Compilation imkanlarına sahip olunur
  • Server-Side Blazor tarafı Mono WebAssembly yerine .Net Core process'i içinde çalışır ve WebAssembly desteği olmayan tarayıcılar için de bir açık kapı bırakır
  • UI tarafının güncellemeleri SignalR ile gerçekleşir ve gereksiz sayfa yenilemeleri olmaz

Belki dezavantaj olarak arayüz etkileşimi için SignalR kullanılmasının ağ üzerinde ekstra hareketlilik anlamına geleceğini belirtebiliriz. Bu bilgilerden sonra gelin örneğimizi geliştirmeye başlayım.

Ön Gereksinimler

Visual Studio Code'un olduğu WestWorld'de(Ubuntu 18.04,64bit) Visual Studio 2017/2019 nimetleri olmasa da Server Side Blazor uygulamaları geliştirebiliyorum. Sizin için de geçerli bir durum. Bunun için terminalden aşağıdaki komutu vermek yeterli.

sudo dotnet new --install "Microsoft.AspNetCore.Blazor.Templates"
dotnet new --help

Görüldüğü gibi dotnet aracının new şablonlarına Blazor eklentileri gelmiş durumda.

Cloud Firestore Tarafının Hazırlanması

Kod tarafına geçmeden önce Google Cloud Platform üzerindeki veri tabanı hazırlıklarımızı gerçekleştirelim. Önce Firebase Console'a gidelim ve yeni bir proje oluşturalım. Ben aşağıdaki özelliklere sahip enbiey(NBA) isimli bir proje oluşturdum.

Ardından database sekmesinden Create Database seçeneği ile ilerleyip Security rules for Cloud Firestore penceresindeki Start in locked mode seçeneğini işaretli bırakalım.

Varsayılan olarak Cloud Firestore tipinden bir veri tabanı oluşturacağız(Realtime Database tipini de kullanabilirsiniz) Sonrasında bir koleksiyon(collection) ve örnek doküman(document) ile ilk veri girişimizi yapabiliriz. Söz gelimi players isimli koleksiyonu açıp,

içine fullname, length, position ve someinfo alanlarından oluşan örnek bir oyuncuyu ekleyebiliriz.

Sonuçta aşağıdakine benzer bir dokümanımızın olması gerekiyor.

Yazılacak Blazor uygulamasının(başka uygulamalar içinde benzer durum söz konusu aslında) Firestore veri tabanını kullanabilmesi için Credential ayarlamalarını da yapmalıyız. Yeni açılan projenin Service Account'u için bir key dosyası üretmemiz lazım. Öncelikle Google IAM adresine gidip projemizi seçelim ve ardından istediğimiz service account'u işaretleyip üç nokta düğmesini kullanarak Create Key tuşuna basalım.

Gelen penceredeki varsayılan JSON seçimini olduğu gibi bırakalım.

İndirilen JSON uzantılı dosya içeriği Blazor uygulaması için gerekli olacak, unutmayın.

Server Side Blazor Uygulamasının İnşası

Veri tabanı hazırlıklarımız tamam. Artık Blazor uygulamasının omurgasını hazırlayıp gerekli kodları yazmaya geçebiliriz. Terminalden aşağıdaki komutu vererek Hosted in ASP.NET Server tipindeki blazor çözümünü inşa ederek işlemlerimize devam edelim.

dotnet new blazorhosted -o NBAWorld

Komutun çalışması sonrası üç adet proje oluşur. Shared kütüphanesi, Client ve Server projeleri tarafından ortaklaşa kullanılmaktadır. Client projesi Server tarafına da referans edilmiştir(csproj dosyalarını kontrol ediniz) ve tarayıcıda gösterilecek bileşenleri içerir. Firestore'a erişeceğimiz API Controller tarafı Server projesinde bulunur. Yani back-end'in server projesi olduğunu düşünebiliriz. Model sınıfları gibi hem istemci hem sunucu projelerince paylaşılacak tiplerse Shared uygulamasında bulunur. Shared ve Server projeleri Google Cloud Firestore ile çalışacaklar. Bu nedenle her iki projeye de Google.Cloud.Firestore nuget paketini eklememiz gerekiyor. Bunu aşağıdaki terminal komutu ile ilgili uygulama klasörlerinde yapabiliriz.

dotnet add package Google.Cloud.Firestore --version 1.0.0-beta19

Örneği çalıştığım tarihte bu paket sürümü mevcuttu. Siz denerken güncel versiyona bir bakın.

Kodlayalım

Artık kodlarımızı geliştirmeye başlayabiliriz. NBAWorld.Shared isimli projede Models klasörü açıp içine aşağıda kodları yer alan Player sınıfını ekleyelim. 

using System;
using Google.Cloud.Firestore;

/*
    Firestore tarafındaki players koleksiyonundaki her bir 
    dokümanın kod tarafındaki karşılığını ifade eden sınıfımız

    Koleksiyon eşleştirmesi için FirestoreData kullanıldı.
    Sadece FirestoreProperty niteliği ile işaretlenen özellikler
    Firestore tarafında işleme alınır.
 */
namespace NBAWorld.Shared.Models
{
    [FirestoreData]
    public class Player{
        public string DocumentId{get;set;}
        [FirestoreProperty]
        public string Fullname { get; set; }
        [FirestoreProperty]
        public string Length { get; set; }
        [FirestoreProperty]
        public string Position { get; set; }
        [FirestoreProperty]
        public string SomeInfo { get; set; }

    }
}

NBAWorld.Server projesinde de Data isimli bir klasör açıp Firestore tarafındaki players koleksiyonu için Data Access Layer görevini üstlenecek PlayerDAL sınıfını ekleyelim.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBAWorld.Shared.Models;
using Google.Cloud.Firestore;
using Newtonsoft.Json;

namespace NBAWorld.Server.Data
{
    /*
    Google Cloud Firestore ile iletişimde kullanılan
    Data Access Layer sınıfı.
     */
    public class PlayerDAL
    {
        string projecId = "enbiey-94b53"; // Firebase proje id
        FirestoreDb db;

        /*
            Firestore veri tabanı nesnesini, proje id ve credential 
            bilgileri ile üretmek için sınıfın yapıcı metodu oldukça
            uygun bir yer.
         */
        public PlayerDAL()
        {
            // Client iletişimi için gerekli Credential bilgisini taşıyan dosya. Firebase'den indirmiştik hatırlayın.
            // Siz tabii dosyayı hangi adrese koyduysanız orayı ele almalısınız
            string credentialFile = "/home/burakselyum/enbiey.json";
            // Environment parametrelerine GOOGLE_APPLICATION_CREDENTIALS bilgisini ekliyoruz
            Environment.SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", credentialFile);
            // FirebaseDb nesnesini projeId ile oluşturuyoruz
            db = FirestoreDb.Create(projecId);
        }

        /*
        Oyuncu listesini getirecek olan metot
         */
        public async Task<List<Player>> GetPlayers()
        {
            // Koleksiyon için sorguyu hazırlıyoruz
            Query selectAll = db.Collection("players");
            // Snapshot nedir?
            QuerySnapshot selectAllSnapshot = await selectAll.GetSnapshotAsync();
            var players = new List<Player>();

            // Tüm dokümanları dolaşıyoruz
            foreach (var doc in selectAllSnapshot.Documents)
            {
                // Eğer doküman varsa
                if (doc.Exists)
                {
                    // koleksiyondaki dokümanı bir dictionary'ye al
                    Dictionary<string, object> playerDoc = doc.ToDictionary();
                    // json formatında serialize et
                    string json = JsonConvert.SerializeObject(playerDoc);
                    // gelen JSON içeriğini player örneğine çevir
                    Player player = JsonConvert.DeserializeObject<Player>(json);
                    player.DocumentId = doc.Id; //Delete ve Update işlemlerinde Firestore tarafındaki Document ID değerine ihtiyacımız olacak
                    // List koleksiyonuna ekle
                    players.Add(player);
                }
            }

            // Listeyi döndür
            return players;
        }

        /*
        Firestore'a doküman olarak yeni bir oyuncu ekleyen fonksiyonumuz
         */
        public async void NewPlayer(Player player)
        {
            // players koleksiyonuna ait referansı al
            CollectionReference collRef = db.Collection("players");
            // awaitable AddAsync metodu ile ekle
            await collRef.AddAsync(player);
        }

        /*
        Firestore'dan doküman silme işlemini üstlenen metodumuz
         */
        public async void DeletePlayer(string documentId)
        {
            // documentId bilgisini kullanarak players koleksiyonda ilgili dokümanı bul
            DocumentReference document = db.Collection("players").Document(documentId);
            // bulunan dokümanı sil
            if (document != null)
            {
                await document.DeleteAsync();
            }
        }

        /*
        Firestore'dan bir dokümanı güncellemek için kullanılan metodumuz
         */
        public async void UpdatePlayer(Player player)
        {
            // Önce parametre olarak gelen oyuncunun referansını bulmaya çalış
            DocumentReference document = db.Collection("players").Document(player.DocumentId);
            if (document != null) //eğer bulduysan
            {
                // Overwite seçeneği ile üstüne yaz
                await document.SetAsync(player, SetOptions.Overwrite);
            }
        }

        /*
        Tek bir oyuncu bilgisini dokümand ıd değerine göre çeken fonksiyonumuz
         */
        public async Task<Player> GetPlayerById(string documentId)
        {
            // Doküman referansını bulup
            DocumentReference document = db.Collection("players").Document(documentId);
            // bir görüntüsünü çekiyoruz
            DocumentSnapshot snapshot = await document.GetSnapshotAsync();
            Player player = new Player();

            if (snapshot.Exists) // Eğer snapshot içeriği mevcutsa
            {
                player.DocumentId = snapshot.Id;
                // oyuncu bilgilerini dokümandan GetValue ile alıyoruz
                player.Fullname=snapshot.GetValue<string>("Fullname");
                player.Position=snapshot.GetValue<string>("Position");
                player.SomeInfo=snapshot.GetValue<string>("SomeInfo");
                player.Length=snapshot.GetValue<string>("Length");
            }

            return player;
        }
    }
}

Ardından Controller klasörüne API Controller görevini üstlenen PlayersController isimli aşağıdaki bileşeni ilave ederek devam edelim.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NBAWorld.Server.Data;
using NBAWorld.Shared.Models;
using Microsoft.AspNetCore.Mvc;

namespace NBAWorld.Server.Controllers
{
    /*
        İstemci tarafına CRUD operasyon desteği sunacak olan API servisimiz.
     */
    [Route("api/[controller]")]
    public class PlayersController
        : Controller
    {
        PlayerDAL playerDAL = new PlayerDAL();

        // Tüm oyuncu listesini döndüren HTTP Get metodumuz
        [HttpGet]
        public Task<List<Player>> Get()
        {
            return playerDAL.GetPlayers();
        }

        /*
        HTTP Post çağrısı ile yeni bir oyuncuyu Firestore'a eklemek için kullandığımız servis metodu.
        Mesaj gövdesinden JSON formatında gelen oyuncu içeriğini kullanır.
        DAL'daki ilgili metodu çağırır. Firestore'a asıl ekleme işini PlayerDAL içindeki metod gerçekleştirir.
         */
        [HttpPost]
        public void Post([FromBody]Player player)
        {
            playerDAL.NewPlayer(player);
        }

        /*
        Silme işlemini üstlenen metodumuz.
        Querystring ile gelen id değerini kullanır.
        Data Access Layer nesnesindeki DeletePlayer metodunu çağırır.
         */
        [HttpDelete("{documentId}")]
        public void Delete(string documentId)
        {
            playerDAL.DeletePlayer(documentId);
        }

        /*
        Güncelleme işlemini üstlenen API metodumuz.
        HTTP Put ile çalışır.
        Request Body ile gelen içerik kullanılır.
         */
        [HttpPut]
        public void Upate([FromBody]Player player)
        {
            playerDAL.UpdatePlayer(player);
        }

        /*
        Tek bir dokümanı almak için kullanılan metodumuz.
        Bunu var olan oyuncu bilgilerini güncelleme akışında kullanıyoruz.
         */
        [HttpGet("{documentId}")]
        public Task<Player> Get(string documentId)
        {
            return playerDAL.GetPlayerById(documentId);
        }
    }
}

Gelelim istemci rolünü üstlenen NBAWorld.Client projesine. Öncelikle proje oluşturulduğunda varsayılan olarak gelen bazı dosyalar(Counter, Fetch Data vb) göreceksiniz. Bunlara ihtiyacımız olmadığından silebiliriz. Projenin Pages klasörüne PlayerData(Tüm oyuncuları gösteren bileşenimiz) ve NewPlayer(Yeni oyuncu ekleme işini üstlenen bileşenimiz) isimli razor sayfalarını ekleyeceğiz. İçeriklerini aşağıdaki gibi geliştirebiliriz.

PlayerData.cshtml

<!--
    Razor sayfamızın adı playerspage. Navigasyonda bu ismi kullanıyoruz.
    Kullandığı model PlayerDataModel isimli BlazorComponent türevli bileşen.
    playerList, component sınıfı içerisindeki bir özellik.
-->

@page "/playerspage"
@inherits PlayerDataModel

<h1>Efsane Oyuncularımın Listesi</h1>

@if (playerList == null)
{
    <p><em>Yükleniyor...</em></p>
}
else
{<table class='table'><thead class="thead-dark"><tr><th>Adı</th><th>Boyu</th><th>Mevkisi</th><th>Hakkında</th><th></th><th></th></tr></thead><tbody><!--
                Eğer playerList hazırsa tüm içeriğini dolaşıyoruz.
                Ve özelliklerini TD hücrelerine yazdırıyoruz.
                Sağ tarafa yer alan ve silme işlemini üstlenen bir button kontrolü var.
                onclick olay metodunda bileşendeki DeletePlayer fonksiyonu çağırılıyor ve
                döngü ile kontroller bağlanırken güncel p değişkeninin sahip olduğu
                DocumentId bilgisi yollanıyor.
                Güncelleme operasyonları için modal popup kullanılmakta.
                Bu popup'a ulaşırken GetPlayerForEdit metodu kullanılarak güncel değerleri de çekiliyor.
                Modal Popup, yine bu sayfa içerisinde tanımlı bir div elementi. data-toggle ve data-target niteliklerine 
                atanan değerlerle, button kontrolü arasında ilişki kuruluyor.
                -->
            @foreach (var p in playerList)
            {<tr><td>@p.Fullname</td><td>@p.Length</td><td>@p.Position</td><td>@p.SomeInfo</td>  <td><button class="btn btn-outline-danger" 
                        onclick="@(async () => await DeletePlayer(@p.DocumentId))">
                        Sil</button></td>   <td><button class="btn btn-outline-primary" data-toggle="modal" data-target="#EditPlayerModal" 
                        onclick="@(async()=>await GetPlayerForEdit(@p.DocumentId))">
                        Güncelle</button></td>               </tr>
            }</tbody></table>
}<!--
Modal popup bileşenimiz.
ID bilgisini button kontrolü kullanmakta.
Bir bootstrap modal penceresi genelde üç ana kısımdan oluşuyor.
Başlık ve X işareti gibi bilgileri içeren modal-header,
Asıl içeriği bulunduran modal-body
ve kaydet, vazgeç gibi button kontrollerini veya özet bilgileri bulunduran modal-footer.
Edit işlemi yapılırken documentId bilgisi ile elde edilen oyuncu verisi,
Razor bileşenindeki currentPlayer değişkeninde yer almakta. Dolayısıyla modal
kontrollerini bu değişkene bind ediyoruz.

Modal popup kullanabilmek için jquery ve bootstrap javascript kütüphanelerine ihtiyacımız var.
Bunları index.js içerisinde bildirdik.
--><div class="modal fade" id="EditPlayerModal"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h3 class="modal-title">Bilgileri güncelleyebilirsin</h3><button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">X</span></button></div><div class="modal-body"><form><div class="form-group"><label class="control-label">Adı</label><input class="form-control" bind="@currentPlayer.Fullname"/></div><div class="form-group"><label class="control-label">Boyu</label><input class="form-control" bind="@currentPlayer.Length"/></div><div class="form-group"><label class="control-label">Mevkisi</label><input class="form-control" bind="@currentPlayer.Position"/></div><div class="form-group"><label class="control-label">Hakkında</label><textarea class="form-control" rows="4" cols="30" bind="@currentPlayer.SomeInfo" /></div></form></div><div class="modal-footer"><button class="btn btn-primary" 
                onclick="@(async ()=> await UpdatePlayer())" 
                data-dismiss="modal">Kaydet</button></div></div></div></div>

PlayerData.cshtml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NBAWorld.Shared.Models;
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;

namespace NBAWorld.Client.Pages
{
    /*
    Razor sayfamız tarafından kullanılan Blazor bileşeni.
    Doğrudan PlayersController APIsi ile konuşur.
     */
    public class PlayerDataModel
    : BlazorComponent
    {
        /*
        API servisine göndereceğimiz talepleri ele alan HttpClient nesnesini
        Property Injection ile içeriye alıyoruz.
         */
        [Inject]
        protected HttpClient Http { get; set; }
        protected List<Player> playerList = new List<Player>();
        protected Player currentPlayer = new Player();

        protected override async Task OnInitAsync()
        {
            await GetAllPlayers();
        }
        protected async Task GetAllPlayers()
        {
            // api/Players tahmin edileceği üzere PlayersController'a yapılan bir çağrıdır
            playerList = await Http.GetJsonAsync<List<Player>>("api/Players");
        }

        /*
            bir dokümanı (yani oyuncuyu) silmek için kullandığımız fonksiyon
         */
        protected async Task DeletePlayer(string documentId)
        {
            // Doğrudan HTTP delete tipinden bir çağrı yapıyoruz
            // QueryString parametresi olarak arayüzden gelen doküman Id bilgisini kullanıyoruz
            await Http.DeleteAsync($"/api/Players/{documentId}");
            // Silme işlemi sonrası listeyi tekrar güncellemekte yarar var.
            await GetAllPlayers();
        }

        /*
        Güncelleme işleminden önce documentId ile oyuncu bilgilerini
        bulmaya çalıştığımız metod.
         */
        protected async Task GetPlayerForEdit(string documentId)
        {
            // Web API tarafına bir HTTP Get çağrısı yapıyoruz.
            // adresin son kısmında doküman id bilgisi bulunuyor.
            currentPlayer = await Http.GetJsonAsync<Player>("/api/Players/" + documentId);
        }

        /*
        Oyuncu bilgilerini güncellemek için kullanılan metodumuz.
        Parametre almadığına bakmayın. Razor sayfasındaki bileşenlere bağlanan
        currentPlayer içeriği kullanılıyor. Bu değişken güncelleme için
        açılan Modal Popup tarafından değiştirilebilmekte.
         */
        protected async Task UpdatePlayer()
        {
            // Web API tarafına HTTP Put metodu ile bir çağrı yapıyoruz
            // Request Body'de currentPlayer içeriği yollanıyor.
            await Http.SendJsonAsync(HttpMethod.Put, "api/players/", currentPlayer);
            await GetAllPlayers();
        }
    }
}

NewPlayer.cshtml

<!--Razor sayfamızın adı newplayer. Navigasyonda bu ismi kullanıyoruz.
    Kullandığı model NewPlayerModel isimli BlazorComponent türevli bileşen.
    Kontrolleri, bileşendeki player isimli değişkene player.Özellike Adı 
    notasyonu ile bağlıyoruz.Button kontrolüne basıldığında onclick niteliği ile belirttiğimiz 
    kod parçası çalışıyor ve bileşendeki AddPlayer metodu tetikleniyor.
-->

@page "/newplayer"
@inherits NewPlayerModel

<h1>Yeni bir efsane eklemek istersen doldur, gönder...</h1><table class='table'><tbody><tr><td><p>Adı</p></td><td><input class="form-control" bind="@player.Fullname" /></td></tr><tr><td><p>Boyu</p></td><td><input class="form-control" bind="@player.Length" /></td></tr><tr><td><p>Mevkisi</p></td><td><input class="form-control" bind="@player.Position" /></td></tr><tr><td><p>Hakkında</p></td><td><textarea class="form-control" rows="4" cols="30" bind="@player.SomeInfo" /></td></tr><tr><td colspan="2"><button class="btn btn-primary" onclick="@(async () => await AddPlayer())">Ekle</button></td></tr></tbody></table>

NewPlayer.cshtml.cs

using System;
using System.Net.Http;
using System.Threading.Tasks;
using NBAWorld.Shared.Models;
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;

namespace NBAWorld.Client.Pages
{
    /*
    Razor sayfamız tarafından kullanılan Blazor bileşeni.
    Doğrudan PlayersController APIsi ile konuşur.
    Temel görevi yeni bir oyuncuyu eklemektir. (Firestore veri tabanına)
     */
    public class NewPlayerModel 
    : BlazorComponent
    {
        /*
        API servisine göndereceğimiz talepleri ele alan HttpClient nesnesini
        Property Injection ile içeriye alıyoruz.
         */
        [Inject]
        protected HttpClient Http { get; set; }
        // Önyüzdeki HTML elementlerini bu özelliğe bağlayacağız (bind)
        protected Player player = new Player();

        protected async Task AddPlayer()
        {
            /* api/Players tahmin edileceği üzere PlayersController'a yapılan bir çağrıdır
            HTTP Post tipinden bir çağrı söz konusu ve parametre olarak player bilgisini gönderiyoruz.
            Dolayısıyla API tarafındaki Post isimli metot (farklı bir isimde verilebilir, HttpMethod.Post ile karıştırmayın) çağırılacaktır.
            player değişkeni, önyüz tarafına bind edildiği için, kontrollerin verisini içerecektir.
            */
            await Http.SendJsonAsync(HttpMethod.Post, "/api/Players/", player);            
        }
    }
}

Piuvvvv :) Çok kod yazdık belki ama sıkın dişinizi az kaldı! Son değişikliklerimiz ana sayfa ve navigasyon çubuğu ile ilgili. Ön yüz tarafında bootstrap kullandığımız dikkatinizi çekmiştir. Bunun tüm bileşenler için etkin olmasını wwwroot klasöründeki index.cshtml içerisindeki gerekli js kütüphane bildirimleri ile sağlayabiliriz. Diğer kısımlarda çok ufak tefek değişiklikler var.

<!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width"><title>NBAWorld</title><base href="http://www.buraksenyurt.com/" /><!--CDN veya diğer URL adreslerinden de bootstrap ve jquery için link verilebilir.
        Ben local'e indirdiklerimi kullandım.
    --><link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" /><link href="css/site.css" rel="stylesheet" /><script src="js/jquery.min.js"></script><script src="js/bootstrap.min.js"></script></head><body><app>Loading...</app><script src="_framework/blazor.webassembly.js"></script></body></html>

NavMenu.cshtml dosyasına da yeni razor sayfaları için gerekli linkleri eklemeliyiz.

<div class="top-row pl-4 navbar navbar-dark"><a class="navbar-brand" href="">NBA World</a><button class="navbar-toggler" onclick=@ToggleNavMenu><span class="navbar-toggler-icon"></span></button></div><div class=@(collapseNavMenu ? "collapse" : null) onclick=@ToggleNavMenu><ul class="nav flex-column"><li class="nav-item px-3"><NavLink class="nav-link" href="playerspage"><span class="oi oi-list-rich" aria-hidden="true"></span> Oyuncular</NavLink></li>        </ul><ul class="nav flex-column"><li class="nav-item px-3"><NavLink class="nav-link" href="newplayer"><span class="oi oi-list-rich" aria-hidden="true"></span> Yeni Oyuncu</NavLink></li>        </ul></div>

@functions {
    bool collapseNavMenu = true;

    void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

Çalışma Zamanı

Artık yazdığımız ürünü test etmeye hazırız. Uygulamayı Visual Studio Code ile geliştirdik ve önümüzde bir Solution var. Visual Studio Code'da NBAWorld klasörünü ayrıca açıp F5 tuşuna bastığımızda bize çözümü hangi derleyici ile debug etmek istediğimiz sorulacaktır. .Net Core seçeneğini işaretlersek ilgili Debug ayarları JSON dosyasına eklenir ve Build işlemi başlar. Ardından uygulama ayağa kalkıp(ki oraya gelene kadar aldığım hataları düzelttim) http://localhost:5888/ adresinden yayına başlar. Sizin de aşağıdakine benzer bir görüntü elde etmeniz gerekiyor.

Oyuncular linkine basıldığında da aşağıdaki gibi...

Yeni bir efsane eklemek istersek NewPlayer sayfasını kullanabiliriz.

Güncelleme fonksiyonelliğini ekledikten sonraki durum da şöyle olacaktır. Görüldüğü üzere bir popup ile gerekli düzenlemeleri yapabiliyoruz(Bootstrap ile modal popup tasarlamak gerçekten kolay)

Ben Neler Öğrendim?

Hepsi bu kadar. Çok temel seviyede basit CRUD operasyonlarını gerçekleştiren bir Blazor uygulamasını inşa etmeyi başardık. Üstelik veriler Google Firestore üzerinde tutuluyor. Örneği geliştirmek pekala elinizde. Çok daha şık tasarıma sahip kendi alanınızla ilgili veri yönetim işlemlerini yapabileceğiniz bir uygulama haline getirebilirsiniz. Gelelim benim bu çalışmadan neler öğrendiğime.

  • Blazor proje şablonlarını Ubuntu gibi bir platformda .Net Core için nasıl kullanabileceğimi
  • Google Cloud üzerinde Firestore veri tabanı oluşturmayı
  • Credential dosyasının ne işe yaradığını
  • Basit Blazor bileşenleri yazmayı
  • Blazor bileşeni ile Razor sayfasının nasıl etkileştiğini
  • FirestoreData ve FirestoreProperty niteliklerinin kullanımını
  • Ortak kütüphanede model sınıfı(Entity tipi olarak da düşünebiliriz) oluşturmayı
  • Server Side tarafında Firestore ile haberleşen bir Data Access nesnesi yazmayı
  • Firestore tarafındaki asıl CRUD operasyonlarını yapan DAL nesnesine önyüzden, API Controller yardımıyla nasıl gelinebileceğini
  • Bir Bootstrap Modal Popup bileşeninin nasıl tasarlanabileceğini(jquery.min.js ve bootstrap.min.js ler olmadan işletemediğimi)

Böylece geldik birinci faza ait 35nci bölüm derlemesinin sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

MongoDb,Express,Vue ve Node Birlikteliği

$
0
0

Aranızdan kaç kişi akıllı telefonundaki herhangi bir arkadaşının numarasını ezbere söyleyebilir? Eminim bazılarımız bizi dokuz ay karnında taşıyan annesinin telefonunu dahi hatırlamıyordur. Peki ya birlikte sıklıkla vakit geçirdiğiniz ama çok da yakın çevrenizden olmayan kankanızın doğum günü ne zaman? Teknolojik cihazlarınızdaki hatırlatıcılar olmadığında kaç arkadaşınızın doğum gününü unutacaksınız hiç düşündünüz mü?

İletişim bilgisi ve doğum günleri bizi yakın çevremize bağlayan veya uzağı yakın eden unsurlar arasında yer alıyor. Hatırlanmak güzel olduğu kadar hatırlamak da gerekiyor. Ortaokul sıralarında kullandığım bir fihrist defterim vardı. İçinde yakın arkadaşlarımın ev telefonları ve doğum günü bilgileri yazardı. Pek tabii çok sık iletişimde olduğum sıra arkadaşım sevgili Burak Gürkan gibi dostlarımı aramak için o deftere ihtiyacım yoktu. Neredeyse her gün telefonla konuştuğumuz için numarayı ezberlemiştim.

Aradan yıllar geçti ve Yıldız Teknik Üniversitesi Matematik Mühendisliği bölümünü kazandım. Okulun ikinci yılındaki bilgisayar programlama dersinde Cobol görüyorduk ve dönem sonuna doğru hocamızla birlikte yaptığımız o uzun metrajlı çalışmanın konusu fihrist defteriydi(O yıl Cobol ile ilk ve son karşılaşmam olur diye düşünsem de hayatımın ilerleyen yıllarında iki kez karşıma çıkarak bende hoş anıların oluşmasına neden olacaktı) Bu kez arkadaşlarımızın iletişim bilgilerini ve doğum günlerini tutmak için 3.5 inçlik floppy diskten, 1ler ve 0lardan yararlanacaktık. Her ne kadar o zamanlar için heyecan verici bugün içinse çok sıradan bir örnek olsa da benim için geçmişe yapılan manevi bir yolculuk. Dolayısıyla zaman zaman bu örnek konsepti bir şeyleri öğrenmeye çalışırken kullanıyorum. İşte sıradaki saturday-night-works birinci faz derlememizin konusu da bir fihrist. Gelin hiç vakit kaybetmeden notlarımızı toparlamaya başlayalım.

Amacım başlıkta geçen enstrümanları kullanarak Web API tabanlı basit bir web uygulaması geliştirmekti. Veriyi tutmak için MongoDB'yi, sunucu tarafı için Node.js'i, Web Framework amacıyla express'i ve önyüz geliştirmesinde de Vue'yu kullanmak istemiştim. Kobay olarakta doksanlı yıllardan aklıma gelen ve Cobol öğretirlerken gösterdikleri Fihrist örneğini seçtim(O vakitler sanırım hepimiz öğrendiğimiz dillerle arkadaşlarımızın telefon numaralarına yer verdiğimiz bir fihrist uygulaması yazmışızdır)Özellikle Vue tarafında bileşen(component) geliştirmenin nasıl yapılabileceğini, bunlar arasındaki haberleşmenin nasıl tesis edileceğini merak ediyordum. İşin içerisine WebPack de girince güzel bir çalışma alanı oluştu diyebilirim.

Projenin İskeleti

Projenin genel iskelet yapısı ve kullanacağımız dosyaları aşağıda görülen hiyerarşide oluşturabiliriz.

Fihrist/
|----- app/
|----- config.js (Mongo bağlantısı gibi ortam parametrelerini tuttuğumuz konfigurasyon modülü)
|----- Routers.js (HTTP Get,Post,Put,Delete operasyonlarını üstlenen WebAPI tarafı)
|----- Contact.js (mongodb tarafı için kullanılan entity modeli)
|----- public/
|----- src/
|------------ bus.js (vue component'leri arasındaki iletişimi sağlayan event bus dosyası)
|------------ main.js (vue tarafının giriş noktası)
|------------ vue.js (vue npm paketi yüklendikten sonra dist klasöründen alınıp buraya kopyalanmıştır)
|------------ components/ (vue componentlerini tuttuğumuz yer)
|----------------- createContact.vue (yeni bir bağlantı eklemek için kullanılan bileşen)
|----------------- contacts.vue (tüm kontak listesini gösteren bileşen)
|----------------- app.vue (ana vue bileşeni)
|----- index.html (kullanıcının etkileşimde olacağı ana sayfa)
|----- server.js (sunucu tarafı)
|----- webpack.config.js (webpack build işleminin kullandığı konfigurasyon dosyası)

app klasöründe model sınıf, yönlendirme paketi ve bir konfigurasyon dosyası yer alıyor. public klasöründe HTML ve gerekirse CSS, image gibi öğlere ve vue uygulamasının kendisine yer veriliyor. server.js tahmin edileceği üzere node server rolünü üstleniyor. 

Gerekli Kurulumlar

Sistemde node, npm ve mongodb'nin yüklü olduğunu varsayıyoruz. Buna göre kök klasörde,

npm init

ile başlangıcı yapıp gerekli paket kurulumlarını tamamlayabiliriz.

npm install body-parser express mongoose morgan

JSON bazlı servise ait mesaj gövdelerini kolayca parse etmek için body-parser, HTTP sunucusu ve servis taleplerinin karşılanması için express, mongodb veri tabanı ile konuşmak için mongoose, mongo loglarını console tarafından izleyebilmek için morgan paketlerini kullanıyoruz. Buna göre server.js dosyasının içeriğini aşağıdaki gibi kodlayabiliriz.

var express = require('express')
var morgan = require('morgan')
var path = require('path')
var app = express()
var mongoose = require('mongoose')
var bodyParser = require('body-parser')
var config = require('./app/config')
var router = require('./app/router')

mongoose.connect(config.conn, { useNewUrlParser: true }) // konfigurasyon dosyasındaki bilgi kullanılarak mongoDb bağlantısı tesis edilir

// static dosyaların public klasöründen karşılanacağı belirtilir
app.use(express.static(path.join(__dirname, '/public')))

// Middleware katmanına morgan'ı enjekte ederek loglamayı etkinleştirdik
app.use(morgan('dev'))

// İstemci taleplerinden gelecek Body içeriklerini JSON formatından kolayca ele almak için 
// Middleware katmanına body-parser modülünü ekledik
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))

var port = config.default_port || 8080 // port bilgisi belirlenir. config'de varsa 5003 yoksa 8080

app.listen(port)
app.use('/api', router) // /api adresine gelecek taleplerin router modülü tarafından karşılanacağı belirtilir.

app.get('/', function (req, res, next) {
    res.sendFile('./public/index.html') // eğer / adresine talep gelirse (yani http://localhost:5003/ şeklinde) index.html sayfasına yönlendiriyoruz
})

console.log('Sunucu hazır ve dinlemede')

Sunucu tarafında kullandığımız yardımcı modüllerimiz olacak. Web API tarafı için router.js'i ve MongoDb veri tabanı bağlantısını konfigurasyon dosyasından beslememizi sağlayan config.js ki içeriğini aşağıdaki gibi oluşturabiliriz.

// genel konfigurasyon ayarlarımız
// veritabanı bağlantısı, sunucu için varsayılan port bilgisi vs
module.exports={
    conn:'mongodb://localhost:27017/fihristim', 
    default_port:5003 
}

Web API görevini üstlenen router.js'in temel görevi CRUD operasyonları için HTTP desteği sunmak(Read için Get, Create için Post vb) Her ne kadar ilişkisel bir veri tabanı kullanmıyor olsak da, Mongo tarafındaki koleksiyon için bir şema bilgisi tanımlamamız gerekiyor. Yani bir model oluşturmalıyız. Bunun için contact isimli modülü oluşturabiliriz. Arkadaşımızın tam adını, telefon numarasını, yaşadığı yeri ve doğum tarihi bilgilerini tutan bu modelde sadece String ve Date veri türlerine yer vermiş olsak da siz veri farklı tiplerle yapıyı pekala zenginleştirebilirsiniz.

// MongoDb'de koleksiyonunun karşılığı olan model tanımı
var mongoose = require('mongoose')

// contact isimli bir şemamız var
// örnek olması açısından bir kaç özellik içeriyor
var contact = new mongoose.Schema({
    fullname: { type: String },
    phoneNumber: { type: String },
    location: { type: String },
    birtdate: { type: Date }
},
    {
        collection: 'contacts' // kontaklarımızı tuttuğumuz koleksiyon
    }
)

module.exports = mongoose.model('Contact', contact)

Bu şemayı kullanan ve esas itibariyle CRUD operasyonlarının karşılığı olan servis taleplerini ele alan router dosyasının içeriğini de aşağıdaki gibi geliştirerek devam edebiliriz.

// Web API Router sınıfımız
// gerekli modülleri tanımlıyoruz
var express = require('express')
var operator = express.Router()
var contact = require('./contact')

// HTTP Post ile yeni bir contact eklenmesini sağlıyoruz
operator.route('/').post(function (req, res) {
    // request body'den gelen değerlere göre contact oluşturuluyor
    contact.create({
        fullname: req.body.fullname,
        phoneNumber: req.body.phoneNumber,
        location: req.body.location,
        birtdate: req.body.birtdate
    }, function (e, c) { //callback fonksiyonu
        if (e) { //hata oluşmuşsa HTTP 400 döndük
            res.status(400).send('kayıt işlemi başarısız')
        }
        res.status(200).json(c) // Hata yoksa HTTP 200 Ok dönüyor ve cevabın içine oluşturulan contact nesnesini gömüyoruz
    }
    )
})

// HTTP Get talebi için tüm kontakların listesini dönüyoruz
operator.route('/').get(function (req, res, next) {
    contact.find(function (e, contacts) {
        if (e) { //hata varsa sonraki fonksiyona bunu yollar
            return next(new Error(e))
        }
        res.json(contacts) // hata yoksa tüm kontaları json serileştirip döner
    })
})

// Belli bir ID'ye ait kontak bilgisini döndürür
// HTTP Get ile çalışır
// Querystring'teki id kullanılır
operator.route('/:id').get(function (req, res, next) {
    var id = req.params.id //id parametresinin değeri alınır
    contact.findById(id, function (e, c) {
        if (e) //hata varsa kayıt bulunanamış diyebiliriz
        {
            return next(new Error('Bu ID için bir kontak bilgisi mevcut değil'))
        }
        res.json(c)
    })
})

// ID bazlı kontak silmek için çalışan fonksiyon
// HTTP Delete kullanılır
operator.route('/:id').delete(function (req, res, next) {
    var id = req.params.id
    contact.findByIdAndRemove(id, function (e, c) {
        if (e) {
            return next(new Error('Bu ID için bir kontak bulunamadığından silme işlemi yapılamadı'))
        }
        res.json('Başarılı bir şekilde silindi')
    })
})

// Güncelleme işlemi
// HTTP Put kullanılır
operator.route('/').put(function (req, res, next) {
    var id = req.body.id
    // önce id'den contact bulunur
    contact.findById(id, function (e, c) {
        if (e) {
            return next(new Error('Güncellenme için bir kayıt bulunamadı'))
        } else { //bulunduysa özellikler body'den gelenler ile değiştirilir

            c.fullname = req.body.fullname ? req.body.fullname : c.fullname
            c.phoneNumber = req.body.phoneNumber ? req.body.phoneNumber : c.phoneNumber
            c.location = req.body.location ? req.body.location : c.location
            c.birtdate = req.body.birtdate ? req.body.birtdate : c.birtdate

            // contact yeni haliyle kayıt edilir
            c.save()
            res.status(200).json(c)
        }
    })
})

module.exports = operator

server.js ve App klasörü içindeki dosyalarımız hazır. An itibariyle sunucu tarafını çalıştırabilir ve çeşitli servis çağrıları gereçekleştirerek Mongo üzerinde CRUD operasyonlarını deneyimleyebiliriz. Sunucu tarafını başlatmak için terminalden

npm start

komutunu vermek yeterli. Tabii bu aşamada MongoDb'nin de çalışır olduğundan emin olmak lazım. mongod ile mongodb servisini başlatabiliriz. Sonrasında veri tabanı üzerindeki operasyonlar için mongo komutunu kullanarak arabirim haberleşmesini de açabiliriz. Eğer mongod ile servis başarılı bir şekilde çalışırsa 27017 port veri tabanı haberleşmesi için aktif hale gelecektir. Sonrasında örneğin db komutunu kullanılabilir ve örneğin veri tabanlarını listeyebiliriz.

mongod
mongo
db

Servis Testleri

Servis tarafının işlerliğini kontrol etmek için curl aracıyla aşağıdaki denemeler yapılabilir. Ben denemeler sırasında en yakın arkadaşlarımdan dördünü ekledim. Sonrasında listeleme, belli bir id'ye bağlı kişi çekme, bilgi güncelleme ve silme operasyonlarını icra ettim. 

curl -H "Content-Type: application/json" -X POST -d '{"fullname":"M.J.","phoneNumber":"555 55 23","location":"chicago","birtdate":"1963-05-18T16:00:00Z"}' http://localhost:5003/api

curl -H "Content-Type: application/json" -X POST -d '{"fullname":"Çarls Barkli","phoneNumber":"555 55 34","location":"phoneix","birtdate":"1963-05-18T16:00:00Z"}' http://localhost:5003/api

curl -H "Content-Type: application/json" -X POST -d '{"fullname":"meycik cansın","phoneNumber":"555 55 32","location":"los angles","birtdate":"1959-05-18T16:00:00Z"}' http://localhost:5003/api

curl -H "Content-Type: application/json" -X POST -d '{"fullname":"leri börd","phoneNumber":"555 55 33","location":"boston","birtdate":"1956-05-18T16:00:00Z"}' http://localhost:5003/api

curl http://localhost:5003/api

curl http://localhost:5003/api/5c29222522433f0234e71e1b

curl -H "Content-Type: application/json" -X PUT -d '{"id":"5c29222522433f0234e71e1b","fullname":"maykıl cordın"}' http://localhost:5003/api

curl -X DELETE http://localhost:5003/api/5c29222522433f0234e71e1b

Belli dokümanlar için kullanılan ve MongoDb tarafından otomatik olarak üretilen ID değerleri elbette siz kendi denemelerinizi yaparken farklılıklar gösterecektir.

Front-End Tarafı

Servis tarafı hazır. Elimizde veri kaynağı ile haberleşen ve temel işlemleri gerçekleştiren bir sunucu mevcut. Şimdi bu servisle konuşan arayüz uygulamasını tasarlamaya başlayalım. Tüm front-end enstrümanları public klasörü altında konuşlanmış durumdadır. Javascript ve css öğelerini tek bir paket haline getirmek için WebPack'ten faydalanacağız. Vue tarafındaki HTTP çağrıları için istemci olarak axios paketini kullanacağız. Gereken paket kurulumları için terminalden şöyle ilerleyebiliriz.

npm install babel-core babel-loader@7 babel-preset-env babel-preset-stage-3 css-loader vue-loader vue-template-compiler webpack webpack-dev-server bootstrap

Vue tarafındaki ana bileşenimiz app.vue dosyasında bulunuyor. Kendi içinde iki alt bileşene sahip. Bir kontak eklemek için kullanacağımız newContact.vue ve listeleme için ele alacağımız contacts.vue. Bu bileşenleri aşağıdaki gibi kodlayabiliriz.

newContact.vue

<template><div><h1>Yeni Bağlantı</h1><form><div class="form-group"><label>Fullname</label><input
          type="text"
          class="form-control"
          aria-describedby="inputGroup-sizing-default"
          placeholder="nasıl isimlendirirsin?"
          v-model="contact.fullname"
        ></div><div class="form-group"><label>Phone</label><input
          type="text"
          class="form-control"
          aria-describedby="inputGroup-sizing-default"
          placeholder="nereden ulaşırsın?"
          v-model="contact.phoneNumber"
        ></div><div class="form-group"><label>Location</label><input
          type="text"
          class="form-control"
          aria-describedby="inputGroup-sizing-default"
          placeholder="nerede yaşıyor?"
          v-model="contact.location"
        ></div><div class="form-group"><label>Birthdate</label><input
          type="text"
          class="form-control"
          aria-describedby="inputGroup-sizing-default"
          placeholder="1976-04-12T11:35:00Z"
          v-model="contact.birtDate"></div><div class="form-group"><button type="button" class="btn btn-primary" @click="createContact($event)">Kaydet</button></div></form></div></template><script>
import axios from "axios"; // API servis haberleşmesi için
import bus from "./../bus.js"; // bileşenler arası haberleşme için
// HTML elementlerindeki input kontrollerinde dikkat edileceği üzere v-model attribute'ları kullanıldı. Bunlar modelimizin özellikleri.

export default {
  data() {
    return {
      contact: {
        fullname: "",
        phoneNumber: "",
        location: "",
        birtDate: ""
      }
    };
  },
  methods: {
    createContact(event) {
      //Button'un @click niteliğinde yüklenen olay metodu
      if (event) event.preventDefault();
      let url = "http://localhost:5003/api";
      let param = {
        //parametre değerleri input kontrollerinden geliyor
        fullname: this.contact.fullname,
        phoneNumber: this.contact.phoneNumber,
        location: this.contact.location,
        birtDate: this.contact.birtDate
      };
      axios
        .post(url, param) //HTTP Post çağrısını gönderdik
        .then(response => {
          console.log(response); // tarayıcının developer tool kısmından log takibi için. Canlı ortamda kullanmaya gerek yok.
          this.clear();
          this.refresh(); // yeni bir bağlantı oluşturlduğunda refresh metodu çağırılır
        })
        .catch(error => {
          console.log(error);
        });
    },
    clear() {
      this.contact.fullname = "";
      this.contact.phoneNumber = "";
      this.contact.location = "";
      this.contact.birtDate = "";
    },
    refresh() {
      bus.$emit("refresh"); // metod evenbus'a bir olay fırlatır. refresh isminde. bunu diğer bileşende yakalayarak bağlantılar listesini anında günelleyebiliriz
    }
  }
};</script>

contacts.vue bileşenimiz

<template><div><div class="col-md-12" v-show="contactList.length>0"><h3>Tüm bağlantılarım</h3><div class="row mrb-10" v-for="contact in contactList" :key="contact.id"><div class="card bg-light border-dark mb-3" style="width: 18rem;"><div class="card-body"><h5 class="card-title">{{ contact.fullname }}</h5><p class="card-text">{{ contact.phoneNumber }}</p><p class="card-text">{{ contact.location }}</p><p class="cart-text">{{ contact.birtdate }}</p><span v-on:click="deleteContact(contact._id)" class="btn btn-primary">Sil</span></div></div></div></div></div></template><script>
import axios from "axios";
import bus from "./../bus.js";

export default {
  data() {
    return {
      contactList: [] //modelimizin verisini içerecek array elemanı
    };
  },
  created: function() {
    // başlangıçta çalışacak fonksiyonumuzda iki işlem yapılıyor
    this.getAllContacts(); // tüm bağlantıları al
    this.listenToBus(); // ve diğer bileşenden yeni eklenecek bağlantıları alabilmek için eventBus'ı dinlemeye başla
  },
  methods: {
    getAllContacts() {
      let uri = "http://localhost:5003/api"; // klasik axios ile web api'mize HTTP Get talebi gönderdik
      axios.get(uri).then(response => {
        this.contactList = response.data; //dönen veriyi contactList dizisine aldık
        console.log(this.contactList);
      });
    },
    deleteContact(id) {
      // bir arkadaşımızı silmek istediğimizde
      let uri = "http://localhost:5003/api/" + id;
      axios.delete(uri); // HTTP Delete talebini gönderiyoruz. id parametresi Mongo'nun ürettiği Guid
      this.getAllContacts(); // listemizi tazeleyelim
    },
    listenToBus() {
      bus.$on("refresh", $event => {
        this.getAllContacts(); // Diğer bileşen tarafından yeni bir bağlantı eklenirse dinlediğimiz refresh isimli hattan bunu yakalayabileceğiz. Bu uyarı sonrası bağlantı listesini tekrar çekiyoruz
      });
    }
  }
};</script>

Özellikle yukarıdaki iki bileşenin kullandığı bus isimli modüle dikkat etmek lazım. Vue tarafında bileşenler arasında olay bazlı çalışan bir eventbus modeli ile iletişim sağlanabilir. Bu sayede bir bileşende yapılan değişiklikleri başka birisine göndermek mümkündür. Örneğimizdeki newContact bileşeninde refresh isimli bir olay tetiklenir (Yeni bir kontak eklendiğinde çalıştırıyoruz) Contacts bileşeni de refresh isimli olayı dinlemektedir. Dolayısıyla yeni bir kontak bilgisi eklediğimizde diğer bileşen otomatik olarak güncel kontak listesini çekecektir.

ve son olarak app.vue isimli ana bileşenimiz.

<template><div id="app"><div class="container"><div class="row col-md-6 offset-md-3"><create-contact></create-contact><!-- createContact bileşeni buraya yerleşecek diğeri de aşağıya --><contacts></contacts></div></div></div></template><script>
import createContact from "./newContact.vue"; // yeni bağlantı eklediğimiz bileşeni aldık
import contacts from "./contacts.vue"; // contact bileşenini aldık
export default {
  name: "app",
  data() {
    return {};
  },
  components: { createContact, contacts } // bileşenlerimizi tanıttık
};
</script>

Frontend tarafı geliştmeleri bittikten sonra bir build işlemi ile mypackage.js dosyasını oluşturacağız. Bu içerik Vue bileşenlerinin de sunulacağı index.html dosyasında referanslanacak. Çok sade bir HTML içeriği elde edeceğimizi ifade edebilirim :) Tabii burada build süreci için gerekli bir webpack.config.js dosyasına ihtiyaç var. İlgili dosyayı aşağıdaki gibi kodlayabiliriz.

const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {

    entry: './public/src/main.js',
    output: {
        filename: './public/build/mypackage.js'
    },
    resolve: {

        alias: {
            vue: './vue.js'
        }
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    'vue-style-loader',
                    'css-loader'
                ]
            }, {
                test: /\.vue$/,
                loader: 'vue-loader',
                exclude: /node_modules/,
                options: {
                    loaders: {
                    }
                }
            },
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin()
    ],
    devServer: {
        port: 3000
    }
}

Build sonrası oluşan paket için bir takım bildirimlere yer verilmektedir. Giriş noktası olarak src klasöründeki main.js tanımlanmıştır. Build sonrası çıktı output sekmesinde belirtilen ortama yapılacaktır. Devam eden kısımlarda css, vue vs js formatlı dosyalar için henüz ne anlama geldiklerini öğrenemediğim kural setleri vardır. Dosya tamamlandıktan sonra build işlemine geçilebilir. Aşağıdaki terminal komutunu bu amaçla kullanabiliriz.

npm run build

Eğer sorunsuz bir şekilde(sorunsuz diyorum çünkü webpack.config.js dosyasını ayarlarken epey problem yaşadım) build işlemi gerçekleşirse dist/public/build/mypackage.js şeklinde bir dosya oluşur. Bu bohçayı index.html public klasörüne alıp aşağıdaki gibi kullanıma açabiliriz.

<!DOCTYPE html><html><head><meta charset="utf-8"><title>Benim en iyi arkadaşlarım</title></head><body><app></app><script src="./build/mypackage.js"></script></body></html>

Hepsi bu kadar ;) Yazdığımız uygulamayı test etmek için node ve mongodb sunucularını çalıştırmamız ve http://localhost:5003/ adresine gitmemiz yeterli. Malum burası varsayılan olarak index sayfasına yönlendirilmekte(Nereden yapıldığını hatırlıyor musunuz?) index.html içinde Vue kodlarımızı paketlediğimiz mypackage.js dosyası referans edildiğinden ilgili bileşenler de buraya render edilecek.

Build sonrası bileşenlerde bir değişiklik yapılırsa tekrardan build işleminin çalıştırılması ve bundle paketinin kullanıma alınması gerekir.

Ekleme işlemi ile ilgili ilk test sonucu aşağıdaki gibidir.

Listeleme ve listenen bağlantıları silme işini contacts.vue isimli bileşen üstlenmekte. Buda app.vue içerisinde tanımlanıp sayfaya yerleştiriliyor. contacts.vue içerisinde bootstrap card stillerini kullanmayı tercih ettim. Çok berbat bir tasarım olmadı ama çok iyi de olmadı. Fonksiyonel olarak bağlantıları listeletebiliyor, silme ve yenilerini ekleme işlemlerini gerçekleştirebiliyoruz.

Update işlemi içinde buraya bir şeyler eklemek lazım. Mesela bir link'le farklı bir adrese yönlendirilme ve onun üstünden güncelleme işlemlerinin yapılması sağlanabilir. Bu kutsal görevi...... :D

İşte örnek bir ekran görüntüsü daha...

Ben Neler Öğrendim?

Olayın başlangıç noktası olan index.html son derece sadedir. İçinde bootstrap ve vue bileşenleri gibi gerekli diğer kütüphaneleri barındırmaktadır. Bu sadeliği webpack sağlıyor ama bu ifadem mutlaka webpack'i kullanın anlamına gelmemeli. Farklı alternatif bundler araçları da mevcut. Benim bu çalışma sonrası öğrendiklerim ise şöyle;

  • Vue'da component nasıl geliştirilir
  • template üzerinde model kullanımı nasıldır
  • axios ile HTTP Post,Get,Put gibi metodlar nasıl çağrılır
  • webpack.config dosyası nasıl hazırlanır
  • bir bileşenden eventbus'a bildirim nasıl yapılır ve diğer bileşenlerden bu değişiklik nasıl yakalanır
  • Bootstrap Card'ları nasıl kullanılır

Böylece geldik bir cumartesi gecesi çalışmasına ait derlemenin daha sonuna. 13 numaralı örnekle geçmişe bir yolculuk daha yaptım. Tekrardan okuyunca unuttuğum bir çok noktayı yeniden hatırladığım bir yazı olduğu için kendi adıma kardayım. Umarım sizler için de faydalı olmuştur. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Bir Web Uygulamasında Gantt Chart Kullanımı

$
0
0

Beğenerek dinlediğim Scorpions grubunun en güzel şarkılarından birisidir Wind of Change. Değişim rüzgarları uzun zamandır hayatımın bir parçası aslında. Sanıyorum ilk olarak 2012 yılında o zamanlar çalışmakta olduğum turuncu bankada başlamıştı esintiler. Çevik dönüşüm süreci kapsamında uzun zamandır var olan şelale modelinin ağır ve hantal işleyişi yerine daha hızlı reaksiyon verme kabiliyeti kazanmak içindi her şey. Benzer bir dönüşüm süreci geçtiğimiz sene içerisinde şu an çalışmakta olduğum mavi renkli teknoloji şirketinde de başlatıldı.

Her iki şirketin bu dönüşüm sürecindeki en büyük problemi ise oturmuş kültürel işleyiş yapısının değişime karşı geliyor olmasıydı. Hal böyle olunca her iki firmada dışarıdan yetkin danışmanlıklar alarak dönüşümü daha az sancılı geçirmeye çalıştı. Genelde ölçek olarak büyük bir firmaysanız bu tip dijital dönüşümler hem uzun hem de sancılı olabiliyor. 

Lakin bu acıyı azaltmak için yapılanlar bazen bana çok garip gelir. Servisten iner girişe doğru ilerlersiniz. Girdiğiniz andan itibaren masanıza varıncaya dek o dijital dönüşümün bilinçaltınıza yollanan mesajlarını görürsünüz. Koridorun duvarında, bindiğiniz asansörün aynasında, tuvaletin kapısında, tavandan sarkan kartonlarda, takım panolarında, bir önceki gün dağıtılan mouse pad'lerin üzerinde, bardağınızda, bilgisayarınızın duvar kağıdında...Tüm eşyalar çoktan dijitalleşmiş ve çevikleşmiştir esasında ama önemli olan bireyin değişimidir. Kurumsal kimliğin en temel yapı taşı olan çalışanların her birinin dönüşüme ayak uydurması gerekir. Farkındalığı olan takımların bu tip dönüşümleri daha çabuk kabullendiği ve kolayca adapte olduğu gözden kaçırılmamalıdır. Olay renkli temalarla binaları giydirmekten, ilkeleri oyunlaştırarak anlatmaktan çok daha ötedir. Bu esas itibariyle bir felsefe kabülü, ciddi bir dönüşümdür. 

Diğer yandan dijital dönüşüm başlar başlamaz bunu sorgulamadan kabul etmek de çok doğru değildir. Değişime direnç göstermek değil ama neden öyle olması gerektiğini sorgulamaktan bahsediyorum. Sorgusuz sualsiz kabullerin sonucu çoğunlukla çevik süreçlerin mükemmel olduğu görüşü ifade edilir ve fakat pekala buna ihtiyaç duyuluncaya kadar şelale modeli ile de başarılar elde edilmiştir. Değişen dünya artık o model tarafından yönetilememekte ve müşteri ihtiyaçları atik bir şekilde giderilememektedir. En basit terk ediş sebebi belki de bu şekilde özetlenebilir.

Pekala ben neden bu kadar felsefik konuşmaya çalışıyorum? Çeviklikten yanayım ama şelale modeli ile de yıllarca çalışmış birisiyim ve o saturday-night-works seansında daha çok şelale modelinde karşımıza çıkan bir tablo ile haşır neşirdim. Konu Gantt şemasıydı. Başlayalım mı?

Henry Gantt tarafından icat edilen Gantt tabloları proje takvimlerinin şekilsel gösteriminde kullanılmaktadır. Temel olarak yatay çubuklardan oluşan bu tablolarda proje planlarını, atanmış görevleri, tahmini sürelerini ve genel olarak gidişatı görmek mümkündür. Excel üzerinde bile kullanılabilen Gantt Chart'lar sanıyorum proje yöneticilerinin de vazgeçilmez araçlarındandır. Benim 23 numaralı saturday-night-works çalışmasındaki amacım ise dhtmlxGantt isimli Javascript kütüphanesinden yararlanarak bir Asp.Net Core projesinde Gantt Chart kullanabilmekti. 

Kısaca kurgudan da bahsedeyim. Görevlere ait bilgiler SQLite veri tabanıyla beslenecek. Önyüz bu veriyi kullanırken REST tipinden servis çağrıları gerçekleştirilecek. Malum veri sunucu tarafında, Gant Chart ise kullanıcı etkileşimiyle birlikte HTML sayfasında. Yani dhtmlxGantt kütüphanesi listeleme, ekleme, silme ve güncelleme gibi operasyonlar için Web API tarafına Post, Put, Delete ve Update çağrıları gönderecek. Sunucu tarafında daha çok servis odaklı bir uygulama olacağını ifade edebiliriz. Kütüphanenin kullandığı veri modelini C# tarafında konumlandırabilmek için DTO(Data Transform Object) nesnelerinden yararlanırken, sunucu tarafı operasyonlarında Model ve Controller katmanlarına başvuracağız. Heyecanlandınız, motive oldunuz, hazırsınız değil mi? :) Öyleyse notların derlenmesine başlayalım.

Bu arada örneği her zaman olduğu gibi WestWorld (Ubuntu 18.04 64bit) üzerinde geliştirmişim. İlk olarak boş bir web uygulaması oluşturalım. Ardından wwwroot klasörü ve içerisine index.html dosyasını ekleyerek devam edelim.

dotnet new web -o ProjectManagerOZ

Örnekteki gantt chart çizimi için kullanılan CSS dosyasına şu adresten, Javascript dosyasına da bu adresten ulaşabilirsiniz. Bu kaynakları offline çalışmak isterseniz bilgisayara indirdikten sonra wwwroot altındaki alt klasörlerde(css, js gibi) konuşlandırabilirsiniz.

Index.html

<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width" /><title>Project - 19</title><link href="css/dhtmlxgantt.css"
          rel="stylesheet" type="text/css" /><script src="js/dhtmlxgantt.js"></script><script>
        // index.html dokükamanı yüklendiğinde ilgili fonksiyon devreye girerek 
        // proje veri içeriğini ekrana basacak
        document.addEventListener("DOMContentLoaded", function(event) {
            // standart zaman formatını belirtiyoruz
            gantt.config.xml_date = "%Y-%m-%d %H:%i";
            gantt.init("project_map");
 
            // veri yükleme işinin üstlenildiği kısım
            // tahmin edileceği üzere /api/backlog şeklinde bir REST API çağrısı olacak
            // bu kod tarafındaki Controller ile karşılanacak
            gantt.load("/api/backlog");
            // veri işleyicisi (bir web api servis adresi gibi düşünülebilir)
            var dp = new gantt.dataProcessor("/api/");
            dp.init(gantt);
            // REST tipinden iletişim sağlanacak
            dp.setTransactionMode("REST");
        });
    </script></head><body><h2>Apollo 19 Project Plan</h2><div id="project_map" style="width: 100%; height: 100vh;"></div></body></html>

Uygulamamız grafik verilerini göstermek için SQLite veri tabanını kullanıyor. Bu enstrümanı Entity Framework kapsamında ele alabilmek için projeye Microsoft.EntityFrameworkCore.SQLite paketini eklemeliyiz. Bunun için terminalden aşağıdaki komutu çalıştırabiliriz. 

dotnet add package Microsoft.EntityFrameworkCore.SQLite

Sonrasında appsettings.json içeriğine bir bağlantı cümlesi ilave edebiliriz. Bunu ilerleyen kısımlarda startup dosyasında kullanacağız. Apollo.db fiziki veri tabanı dosyamızın adı ve root altındaki db klasörü içerisinde yer alacak.

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "ConnectionStrings": {
    "ApolloDataContext": "Data Source=db/Apollo.db"
  },
  "AllowedHosts": "*"
}

Pek tabii modellerimizi, API servis tarafı haberleşmesi için controller tiplerimizi ve hatta gantt chart kütüphanesindeki tiplerle entity modelleri arasındaki dönüşümleri kolaylaştıracak DTO nesnelerimizi geliştirmemiz gerekiyor. Uzun bir maraton olabilir. İlk olarak model sınıflarını yazarak başlayalım. Bir Models klasörü oluşturup altına Context ile model sınıflarımızı(ki gantt chart kütüphanesine göre Link ve Task isimli sınıflarımız olmalı) ekleyerek çalışmamıza devam edebiliriz.

Link sınıfı

using System;

/*
Task'lar arasındaki ilişkinin tutulduğu Entity sınıfımız
Eğer iki Task birbiri ile bağlıysa bu sınıfa ait nesne örnekleri üzerinden ilişkilendirebiliriz.
*/
namespace ProjectManagerOZ.Models
{
    public class Link
    {
        public int Id { get; set; }
        public string Type { get; set; }
        public int SourceTaskId { get; set; }
        public int TargetTaskId { get; set; }
    }
}

Task sınıfı

using System;
/* 
proje görevlerinin verisinin tutulduğu Entity sınıfımız
Tipik olarak görevle ilgili bilgiler yer alır. 
Açıklaması, süresi, hangi durumda olduğu, bağlı olduğu başka bir task varsa O, başlangıç tarihi, tipi vs
*/
namespace ProjectManagerOZ.Models
{
    public class Task
    {
        public int Id { get; set; }
        public string Text { get; set; }
        public DateTime StartDate { get; set; }
        public int Duration { get; set; }
        public decimal Progress { get; set; }
        public int? ParentId { get; set; }
        public string Type { get; set; }
    }
}

ve ApolloDataContext sınıfı ki bu alışkın olduğumuz tipik DataContext tipimiz. Görüldüğü üzere içerisinde görevleri ve aralarındaki ilişkileri temsil eden veri setleri sunmakta.

using Microsoft.EntityFrameworkCore;

namespace ProjectManagerOZ.Models
{
    // Entity Framework DB Context sınıfımız
    public class ApolloDataContext
        : DbContext
    {
        public ApolloDataContext(DbContextOptions<ApolloDataContext> options)
            : base(options)
        {
        }

        // Proje görevleri ile bunlar arasındaki olası ilişkileri temsil eden özelliklere sahip
        public DbSet<Task> Tasks { get; set; }
        public DbSet<Link> Links { get; set; }
    }
}

Küçük Bir Middleware Ayarı

Uygulamamız ayağa kalktığında veri tabanının boş olma ihtimaline karşın onu doldurmak isteyebiliriz. Bunun için DataFiller isimli sınıfımız ve içerisinde Prepare isimli static bir metoduz var. Ancak söz konusu metodu host çalışma zamanında ayağa kalkarken çağırmak istiyoruz. Bunun için Program sınıfında kullanılan IWebHostBuilder üzerinden işletilebilecek bir operasyon tesis etmek lazım. Bunun için IWebHost türevlerine uyarlanabilecek bir genişletme metodu(extension method) işimizi görecektir. Bu metod çalışma zamanında entity servisinin yakalanması ve Prepare operasyonun enjekte edilmesi açısından dikkate değerdir.

DataFiller sınıfı

using System;
using System.Collections.Generic;
using System.Linq;
using ProjectManagerOZ.Models;

namespace ProjectManagerOZ.Initializers
{
    /*
    Bu sınıfın amacı başlangıçta boş olan veritabanı tablolarına
    örnekte kullanabilmemiz için ilk verileri eklemek.
    Bu amaçla örnek task ve link'ler oluşturuluyor.
    Mesela Epic bir Work Item ve ona bağlı User Story'ler gibi
     */
    public static class DataFiller
    {
        public static void Prepare(ApolloDataContext context)
        {
            if (context.Tasks.Any()) //Eğer veritabanında en az bir Task varsa zaten veri içeriyor demektir. Bu durumda initalize işlemine gerek yok.
                return;

            // Parent task'ı oluşturuyoruz (ParentId=null)
            var epic = new Task
            {
                Text = "JWT Implementation for Category WebAPI",
                StartDate = DateTime.Today.AddDays(1),
                Duration = 5,
                Progress = 0.4m,
                ParentId = null,
                Type = "Epic"
            };
            context.Tasks.Add(epic); //Task örneğini context'e ekleyip
            context.SaveChanges(); //tabloyada yazıyoruz
            var story1 = new Task
            {
                Text = "I want to develop tokenizer service",
                StartDate = DateTime.Today.AddDays(1),
                Duration = 4,
                Progress = 0.5m,
                ParentId = epic.Id, //story'yi epic senaryoya ParentId üzerinden bağlıyoruz. Aynı bağlantı Story2 içinde gerçekleştiriliyor
                Type = "User Story"
            };
            context.Tasks.Add(story1);
            context.SaveChanges();

            var story2 = new Task
            {
                Text = "I have to implement tokinizer service",
                StartDate = DateTime.Today.AddDays(3),
                Duration = 5,
                Progress = 0.8m,
                ParentId = epic.Id,
                Type = "User Story"
            };
            context.Tasks.Add(story2);
            context.SaveChanges();

            var epic2 = new Task
            {
                Text = "Create ELK stack",
                StartDate = DateTime.Today.AddDays(3),
                Duration = 3,
                Progress = 0.2m,
                ParentId = null,
                Type = "Epic"
            };
            context.Tasks.Add(epic2);
            context.SaveChanges();

            var story3 = new Task
            {
                Text = "We have to setup Elasticsearch",
                StartDate = DateTime.Today.AddDays(6),
                Duration = 6,
                Progress = 0.0m,
                ParentId = epic2.Id,
                Type = "User Story"
            };
            context.Tasks.Add(story3);
            context.SaveChanges();

            var story4 = new Task
            {
                Text = "We have to implement Logstash to Microservices",
                StartDate = DateTime.Today.AddDays(6),
                Duration = 2,
                Progress = 0.3m,
                ParentId = epic2.Id,
                Type = "User Story"
            };
            context.Tasks.Add(story4);
            context.SaveChanges();

            var story5 = new Task
            {
                Text = "We have to setup Kibana for Elasticsearch",
                StartDate = DateTime.Today.AddDays(6),
                Duration = 2,
                Progress = 0.0m,
                ParentId = epic2.Id,
                Type = "User Story"
            };
            context.Tasks.Add(story5);
            context.SaveChanges();

            // Oluşturduğumuz proje görevleri arasındaki ilişkileri oluşturuyoruz
            List<Link> taskLinks = new List<Link>{
                new Link{SourceTaskId=epic.Id,TargetTaskId=story1.Id,Type="1"},
                new Link{SourceTaskId=epic.Id,TargetTaskId=story2.Id,Type="1"},
                new Link{SourceTaskId=epic2.Id,TargetTaskId=story3.Id,Type="1"},
                new Link{SourceTaskId=story3.Id,TargetTaskId=story4.Id,Type="1"},
                new Link{SourceTaskId=story4.Id,TargetTaskId=story5.Id,Type="1"},
                new Link{SourceTaskId=epic.Id,TargetTaskId=epic2.Id,Type="2"}
            };
            taskLinks.ForEach(l => context.Links.Add(l));
            context.SaveChanges();
        }
    }
}

DataFillerExtension

using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using ProjectManagerOZ.Models;

/*
    DataFillerExtension sınıfı InitializeDb isimli bir extension method içeriyor.
    Bu metodu IWebHost türevli nesne örneklerine uygulayabiliyoruz.
    Amaç çalışma zamanında host ortamı inşa edilirken Middleware katmanında araya girip
    veritabanı üzerinde Prepare operasyonunu icra ettirmek.
    Bu genişletme fonksiyonunu Program.cs içerisinde kullanmaktayız.
 */
namespace ProjectManagerOZ.Initializers
{
    public static class DataFillerExtensions
    {
        public static IWebHost InitializeDb(this IWebHost webHost)
        {
            // çalışma zamanını servislerinin üreticisini örnekle
            var serviceFactory = (IServiceScopeFactory)webHost.Services.GetService(typeof(IServiceScopeFactory));

            // Bir Scope üret
            using (var currentScope = serviceFactory.CreateScope())
            {
                // Güncel ortamdan servis sağlayıcısını çek
                var serviceProvider = currentScope.ServiceProvider;
                // Servis sağlaycısından sisteme enjekte edilmiş entity context'ini iste
                var dbContext = serviceProvider.GetRequiredService<ApolloDataContext>();
                // context'i kullanarak veritabanını dolduran fonksiyonu çağır
                DataFiller.Prepare(dbContext);
            }
            // IWebHost örneğini yeni bezenmiş haliyle geri döndür
            return webHost;
        }
    }
}

Bu yapılanmayı kullanbilmek için program sınıfını şöyle değiştirelim.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using ProjectManagerOZ.Initializers;

namespace ProjectManagerOZ
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args)
            .Build()
            .InitializeDb() // IWebHost için yazdığımız genişletme metodu.
            .Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseUrls("http://localhost:5402");
    }
}

Dikkat edileceği üzere InitiateDb isimli metod CreateWebHosBuilder dönüşünden kullanılabiliyor.

Çalışmamıza startup sınıfına geçerek devam edelim. Burada Entity Framework servisinin çalışma zamanına enjekte edilmesi, statik web sayfası hizmetinin açılması, Web API tarafı için MVC özelliğinin etkinleştirilmesi, SQLite veri tabanı için gerekli bağlantı bilgisinin konfigurasyon dosyasından alınması gibi işlemlere yer veriyoruz.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.EntityFrameworkCore; //EF Core kullanacağımız için eklendi
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ProjectManagerOZ.Models;

namespace ProjectManagerOZ
{
    public class Startup
    {
        /*
        Configuration özelliği ve Startup'ın overload edilmiş Constructor metodu varsayılan olarak gelmiyor.
        ApolloDbContext için gerekli connection string bilgisine ulaşacağımız Configuration nesnesine
        erişebilmek amacıyla eklendiler 
         */
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            // appsettings'den SQLite için gerekli connection string bilgisini aldık
            var conStr = Configuration.GetConnectionString("ApolloDataContext");
            // ardından SQLite için gerekli DB Context'i servislere ekledik
            // Artık modellerimiz SQLite veritabanı ile çalışacak
            // Bu işlemler runtime'de gerçekleşecek
            services.AddDbContext<ApolloDataContext>(options => options.UseSqlite(conStr));
            services.AddMvc(); // Web API Controller'ının çalışabilmesi için ekledik
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            // wwwroot altındaki index.html benzeri sayfaları kullanabileceğimizi belirttik
            app.UseDefaultFiles();
            // ayrıca wwwroot altındaki css, image gibi asset'lerinde kullanılacağı ifade edildi
            app.UseStaticFiles();
            app.UseMvc(); // Web API Controller'ının çalışabilmesi için ekledik
        }
    }
}

Veri modeli, verinin başlangıçta oluşturulması için gerekli adımlar ile çalışma zamanına ait bir takım kodlamaları halletmiş durumdayız. Veri tabanı tarafı ile konuşurken işimizi kolaylaştıracak DTO(Data Transform Object) nesneleri ile bu işin kontrolcülerini kodlayarak ilerleyelim. dto isimli bir klasör oluşturup içerisine aşağıdaki kod parçalarına sahip TaskDTO ve LinkDTO sınıflarını ekleyelim.

TaskDTO

using System;
using System.Text.Encodings.Web;
using ProjectManagerOZ.Models;
/*
    DTO sınıfımız WebAPI tarafında, Gantt kütüphanesi ile olan haberleşmedeki mesajlaşmalarda kullanılan modeli tanımlıyor.
    Arka plandaki Task nesnemizden ziyade Gantt kütüphanesinin istediği alan adlarına sahip. Sözgelimi Task tipinde StartDate varken
    burada start_date kullanılmakta.

    Peki tabii API Controller metodlarındaki Task ve TaskDTO arasındaki dönüşümleri kolayaştırmak adına bilinçli olarak operatörlerin
    aşırı yüklendiğini görüyoruz.
*/

namespace ProjectManagerOZ.DTO
{
    public class TaskDTO
    {
        public int id { get; set; }
        public string text { get; set; }
        public string start_date { get; set; }
        public int duration { get; set; }
        public decimal progress { get; set; }
        public int? parent { get; set; }
        public string type { get; set; }
        public string target { get; set; }
        public bool open
        {
            get { return true; }
            set { }
        }

        public static explicit operator TaskDTO(Task task)
        {
            return new TaskDTO
            {
                id = task.Id,
                text = HtmlEncoder.Default.Encode(task.Text),
                start_date = task.StartDate.ToString("yyyy-MM-dd HH:mm"),
                duration = task.Duration,
                parent = task.ParentId,
                type = task.Type,
                progress = task.Progress
            };
        }

        public static explicit operator Task(TaskDTO task)
        {

            return new Task
            {
                Id = task.id,
                Text = task.text,
                StartDate = DateTime.Parse(task.start_date, System.Globalization.CultureInfo.InvariantCulture),
                Duration = task.duration,
                ParentId = task.parent,
                Type = task.type,
                Progress = task.progress
            };
        }
    }
}

LinkDTO

using System;
using System.Text.Encodings.Web;
using ProjectManagerOZ.Models;
/*
    DTO sınıfımız WebAPI tarafında, Gantt kütüphanesi ile olan haberleşmedeki mesajlaşmalarda kullanılan modeli tanımlıyor.
    Arka plandaki Link nesnemizden ziyade Gantt kütüphanesinin istediği alan adlarına sahip. Peki tabii API Controller metodlarındaki 
    Link ve LinkDTO arasındaki dönüşümleri kolayaştırmak adına bilinçli olarak operatörlerin aşırı yüklendiğini görüyoruz.
*/

namespace ProjectManagerOZ.DTO
{

    public class LinkDTO
    {
        public int id { get; set; }
        public string type { get; set; }
        public int source { get; set; }
        public int target { get; set; }

        public static explicit operator LinkDTO(Link link)
        {
            return new LinkDTO
            {
                id = link.Id,
                type = link.Type,
                source = link.SourceTaskId,
                target = link.TargetTaskId
            };
        }

        public static explicit operator Link(LinkDTO link)
        {
            return new Link
            {
                Id = link.id,
                Type = link.type,
                SourceTaskId = link.source,
                TargetTaskId = link.target
            };
        }
    }
}

WebAPI tarafının web sayfası üzerinden gelecek HTTP çağrılarına cevap vereceği yerler Controller sınıfları. Kullanılan gantt chart kütüphanesinin işleyiş şekli gereği Link ve Task tipleri için ayrı ayrı controller sınıflarının yazılması gerekiyor.

LinkController

using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using ProjectManagerOZ.Models;
using ProjectManagerOZ.DTO;
using Microsoft.EntityFrameworkCore;

/*
    Link nesneleri ile ilgili CRUD operasyonlarını üstlenen Web API Controller sınıfımız
 */
namespace ProjectManagerOZ.Controllers
{
    [Produces("application/json")] // JSON formatında çıktı üreteceğimizi belirtiyoruz
    [Route("api/link")] // Gantt Chart kütüphanesinin beklediği Link API adresi
    public class LinkController
        : Controller
    {
        // Controller içerisine pek tabii ApolloDataContext'imizi geçiyoruz.
        private readonly ApolloDataContext _context;
        public LinkController(ApolloDataContext context)
        {
            _context = context;
        }

        // Yeni bir Link eklerken devreye giren HTTP Post metodumuz
        [HttpPost]
        public IActionResult Create(LinkDTO payload)
        {
            var l = (Link)payload;

            _context.Links.Add(l);
            _context.SaveChanges();

            /*
                Task örneğinde olduğu gibi istemci tarafına oluşturulan Link
                örneğine ait Id değerini göndermemiz lazım ki, takip eden Link bağlama,
                güncelleme veya silme gibi işlemler çalışabilsin.
                tid, istemci tarafının beklediği değişken adıdır.
             */
            return Ok(new
            {
                tid = l.Id,
                action = "inserted"
            });
        }


        /*
        Bir Link'i güncellemek istediğimizde devreye giren metodumuz
         */
        [HttpPut("{id}")]
        public IActionResult Update(int id, LinkDTO payload)
        {
            // Gelen payload içeriğini backend tarafındaki model sınıfına dönüştür
            var l = (Link)payload;
            // id eşlemesi yap
            l.Id = id;
            // durumu güncellendiye çek
            _context.Entry(l).State = EntityState.Modified;
            // ve değişiklikleri kaydedip
            _context.SaveChanges();
            // HTTP 200 döndür
            return Ok();
        }

        /*
        HTTP Delete operasyonuna karşılık gelen ve
        parametre olarak gelen id değerine göre silme işlemini icra eden metodumuz
         */
        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            // Link örneğini bul
            var l = _context.Links.Find(id);
            if (l != null)
            {
                // Entity Context'inden ve
                _context.Links.Remove(l);
                // Kalıcı olarak veritabanından sil
                _context.SaveChanges();
            }

            return Ok();
        }

        // Tüm Link örneklerini döndüren HTTP Get metodumuz
        [HttpGet]
        public IEnumerable<LinkDTO> Get()
        {
            return _context.Links
                .ToList()
                .Select(t => (LinkDTO)t);
        }

        // Belli bir Id değerine göre ilgili Link nesnesinin DTO karşılığını döndüren HTTP Get metodumuz
        [HttpGet("{id}")]
        public LinkDTO GetById(int id)
        {
            return (LinkDTO)_context
                .Links
                .Find(id);
        }
    }
}

TaskController

using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using ProjectManagerOZ.Models;
using ProjectManagerOZ.DTO;

namespace ProjectManagerOZ.Controllers
{
    [Produces("application/json")]
    [Route("api/task")] // Gantt Chart kütüphanesinin beklediği Task API adresi
    public class TaskController
        : Controller
    {
        // Controller içerisine pek tabii ApolloDataContext'imizi geçiyoruz.
        private readonly ApolloDataContext _context;
        public TaskController(ApolloDataContext context)
        {
            _context = context;
        }

        // HTTP Post metodumuz
        // Yeni bir Task eklemek için kullanılıyor
        [HttpPost]
        public IActionResult Create(TaskDTO task)
        {
            // Mesaj parametresi olarak gelen TaskDTO içeriğini Task tipine dönüştürdük
            var payload = (Task)task;
            // Task'ı Context'e ekle
            _context.Tasks.Add(payload);
            // Kalıcı olarak kaydet
            _context.SaveChanges();

            /*HTTP 200 Ok dönüyoruz
             Dönerken de oluşan Task Id değerini de yolluyoruz
             Bu Child task'ları bağlarken veya bir Task'ı silerken
             gerekli olan bir bilgi nitekim. Aksi halde istemci
             tarafındaki Gantt kütüphanesi kiminle işlem yapması gerektiğini bilemiyor.
             İnanmıyorsanız sadece HTTP 200 döndürüp durumu inceleyin :)
             */
            return Ok(new
            {
                tid = payload.Id,
                action = "inserted"
            });
        }

        // HTTP Put ile çalıştırılan güncelleme metodumuz
        // Parametrede Task'ın id bilgisi gelecektir
        [HttpPut("{id}")]
        public IActionResult Update(int id, TaskDTO task)
        {
            // Mesaj ile gelen TaskDTO örneğini dönüştürüp id değerini verdik
            var payload = (Task)task;
            payload.Id = id;

            // id'den ilgili Task örneğini bulduk
            var t = _context.Tasks.Find(id);

            // alan güncellemelerini yaptık
            t.Text = payload.Text;
            t.StartDate = payload.StartDate;
            t.Duration = payload.Duration;
            t.ParentId = payload.ParentId;
            t.Progress = payload.Progress;
            t.Type = payload.Type;

            // değişiklikleri veritabanına kaydettik
            _context.SaveChanges();

            // HTTP 200 Ok dönüyoruz
            return Ok();
        }

        // HTTP Delete yani silme işlemi için çalışacak metodumuz
        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            // Task'ı bulalım ve eğer varsa
            var task = _context.Tasks.Find(id);
            if (task != null)
            {
                // önce Context'ten 
                _context.Tasks.Remove(task);
                // sonra veritabanından silelim
                _context.SaveChanges();
            }

            // HTTP 200 Ok dönüyoruz
            return Ok();
        }

        // HTTP Get karşılığı çalışan metodumuz
        // Tüm Task'ları geri döndürür
        [HttpGet]
        public IEnumerable<TaskDTO> Get()
        {
            return _context.Tasks
                .ToList()
                .Select(t => (TaskDTO)t);
        }

        // HTTP Get ile ID bazlı çalışan metodumuz
        // Belli bir ID'ye ait Task bilgisini verir
        [HttpGet("{id}")]
        public TaskDTO GetById(int id)
        {
            return (TaskDTO)_context
                .Tasks
                .Find(id);
        }
    }
}

Web sayfasına HTTP Get ile çekilen görev listesi ve ilişkilerin gant chart'ın istediği tiplere dönüştürülmesi gerekiyor. İşte DTO dönüşümlerinin devreye girdiği yer. Bunun için MainController isimli tipi kullanmaktayız.

using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using ProjectManagerOZ.Models;
using ProjectManagerOZ.DTO;

namespace ProjectManagerOZ.Controllers
{
    [Produces("application/json")]
    [Route("api/backlog")] // Bu adres bilgisi index.html içerisinde de geçiyor. Bulun ;)
    public class MainController : Controller
    {
        // Controller içerisine pek tabii ApolloDataContext'imizi geçiyoruz.
        private readonly ApolloDataContext _context;
        public MainController(ApolloDataContext context)
        {
            _context = context;
        }

        // HTTP Get ile verinin çekildiği metodumuz. Talebi index.html sayfasından yapıyoruz
        [HttpGet]
        public object Get()
        {
            // Task ve Link veri setlerini TaskDTO ve LinkDTO tipinden nesnelere dönüştürdüğümüz dikkatinizden kaçmamıştır.
            // Bunun sebebi Gantt'ın beklediği veri tipini sunan DTO sınıfı ile backend tarafında kullandığımız sınıfların farklı olmasıdır.

            // Dönüş olarak kullandığımız nesne data ve links isimli iki özellik tutuyor.
            // data özelliğinde Task bilgilerini
            // links özelliğinde de tasklar arasındaki bağlantı bilgilerini dönüyoruz
            // bu format özelleştirilmediği sürece Gantt Chart'ın beklediği tiptedir
            
            return new
            {
                data = _context.Tasks
                    .OrderBy(t => t.Id)
                    .ToList()
                    .Select(t => (TaskDTO)t),
                links = _context.Links
                    .ToList()
                    .Select(l => (LinkDTO)l)
            };
        }        
    }
}

SQLite Ayarlamaları

Kodlarımızı tamamladık lakin testlere başlamadan önce SQLite veri tabanının oluşturulması gerekiyor. Tipik bir migration süreci çalıştıracağız. Bunun için terminalden aşağıdaki komutları kullanabiliriz.

dotnet ef migrations add InitialCreate
dotnet ef database update

İlk satır işletildiğinde DataContext türevli sınıf baz alınarak migration planları çıkartılır. Planlar hazırlandıktan sonra ikinci komut ile update işlemi yürütülür ve ilgili tablolar SQLite veri tabanı içerisine ilave edilir.

Çalışma Zamanı

Kod ilk çalıştırıldığında eğer Tasks tablosunda herhangibir kayıt yoksa aşağıdaki gibi bir kaç verinin eklendiği görülecektir. 

Benzer şekilde Links tablosuna gidilirse görevler arası ilişkilerin eklendiği de görülecektir.

Visual Studio Code tarafında SQLite veri tabanı ile ilgili işleri görsel olarak yapabilmek için şu eklentiyi kullanabilirsiniz.

Uygulamamızı terminalden

dotnet run

komutu ile çalıştırdıktan sonra Index sayfasını talep edersek bizi bir proje yönetim ekranının karşıladığını görebiliriz ;) Bu sayfanın verisi tahmin edeceğiniz üzere MainController tipine gelen HTTP Get çağrısı ile sağlanmaktadır.

Burada dikkat edilmesi gereken bir nokta var. Gantt Chart için yazılmış olan kütüphane standart olarak Task ve Link tipleri ile çalışırken REST API çağrılarını kullanmaktadır. Yeni bir öğe eklerken POST, bir öğeyi güncellerken PUT ve son olarak silme işlemlerinde DELETE operasyonlarına başvurulur. Eğer örnek senaryomuzda TaskController ve LinkController tiplerinin POST, PUT, DELETE ve GET karşılıklarını yazmassak arabirimdeki değişiklikler sunucu tarafına aktarılamayacak ve aşağıdaki ekran görüntüsündekine benzer hatalar alınacaktır.

HTTP çağrıları LinkController ve TaskController sınıflarınca ele alındıktan sonra ise grafik üzerindeki CRUD(CreateReadUpdateDelete) operasyonlarının SQLite tarafına da başarılı bir şekilde aktarıldığı görülebilir. Örnekte üçüncü bir ana görev ile alt işi girilmiş, bir takım görevler üzerinde güncellemeler yapılmış ve görevler arası bağlantılar kurgulanmıştır. WestWorld çalışma zamanına yansıyan örnek ekran görüntüsü aşağıdaki gibidir.

Bu oluşumun sonuçları SQLite veritabanına da yansır.

Tüm CRUD operasyonları aşağıdaki ekran görüntüsüne benzer olacak şekilde HTTP çağrıları üzerinden gerçeklenir. Bunu F12 ile geçeceğiniz bölümdeki Network kısmından izleyebilirsiniz.

Çalışma zamanı testlerini de tamamladığımıza göre yavaş yavaş derlememizi noktalayabiliriz.

Ben Neler Öğrendim?

Kopyala yapıştır yasağım nedeniyle yazılması uzun süren bir örnekti ama öğrenmek için tatbik etmek en güzel yöntemdir. Üstelik bu şekilde hatalar yaptırıp neyin ne için kullanıldığını ve nasıl olması gerektiğini de anlamış oluruz. Söz gelimi POST metodlarından üretilen task veya link id değerlerini döndürmezseniz bazı şeylerin ters gittiğini görebilirsiniz. Gelelim neler öğrendiğime...

  • Gantt Chart'ları xdhtmlGantt asset'leri ile nasıl kolayca kullanabileceğimi
  • IWebHost türevli bir tipe extension method yardımıyla yeni bir işlevselliği nasıl kazandırabileceğimi
  • Bu işlevsellik içerisinde servis sağlayıcısı üzerinde Entity Context'ini nasıl yakalayabileceğimi
  • Gantt Chart'ın ön yüzde kullandığı task ve link tipleri ile Model sınıfları arasındaki dönüşümlerde DTO tiplerinden yararlanmam gerektiğini
  • DTO'lar içerisinde dönüştürme(cast) operatörlerinin nasıl aşırı yüklenebileceğini(operator overloading)
  • Gantt Chart kütüphanesinin backend tarafı ile REST tipinden Web API çağırıları yaparak konuştuğunu
  • Gantt Chart için kullanılan API Controller'larda HTTP Post için tid'nin önemini

Bu uzun ve komplike örnekte ele almaya çalıştığımız Gantt Chart kütüphanesini eminim ki kullanmayacaksınız. Malum bir çoğumuz artık VSTS gibi ortamların bize sunduğu Scrum panolarında işlerimizi yürütüyor ve iterasyon bazlı planlamalar yaptığımızdan bu tip Waterfall'a dönük tabloları çok fazla ele almıyoruz. Yine de örneğe uçtan uca yazılan bir uygulama gözüyle bakmanızı tavsiye edebilirim. Böylece geldik bir maceramızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Firebase Cloud Messaging ile Abonelere Bildirim Yollamak

$
0
0

Servis kapısı açıldığında gözlerini herkesten kaçırıp araca binerken heyecanlı ses tonuyla "Günaydın" diyerek en arka koltuğa geçen kadının ruh hali her yönüyle tanıdık geliyordu. Bir buçuk yıl kadar önce yine bu servise bindiğim ilk gün bende benzer kaygıları hissetmiştim. Oysa hayatımda ilk kez servis binmiyordum.

Ama işte o ilk biniş sırasında söylenen "Günaydın" kelimesi ardından ben ve şoförümüz İhsan Bey dışında kimsenin karşılık vermediği ve onun gözlerini aradığım sırada geçen kısa zaman diliminde aklından geçenleri tahmin ettiğim anlar, en arka koltuğa oturduğunu gördükten sonra toplum psikolojisine ayak uydurup önüme doğru bakmamla son bulmuştu.

Servis şirkete vardıktan sonra her birimiz fabrikanın farklı noktalarına doğru yürümeye başladık. Bizim binamız yolun karşı tarafında kalıyordu ve can güvenliği nedeniyle bir üst geçitten geçilerek ulaşılabiliyordu. Merdivenleri çıkarken onun ne durumda olduğunu bile unutmuşum. Tesadüfen arkamı dönüp baktığımda tek başına ve biraz da şaşkın bir şekilde ne yöne gideceğini anlamaya çalıştığını fark ettim. Yanlış yöne gittiği apaçık ortadaydı. Çelimsizliğinden, gencecik yüzünden ve taşıdığı not defterinden üniversite talebesi olan bir stajyer olduğu biraz da olsa anlaşılıyordu. Geçen yılın aynı vakitlerinde de benzer manzaralar fabrikanın çeşitli sabahlarında yaşanmıştı.

Merdivenleri tekrar indim ve arkasından ona yetişerek "Merhaba...Yeni başladınız sanırım. Nereye gidecektiniz?" dedim. Aynı günün akşamında koltuğuma oturmuş hareket saatini beklerken kapıda yine o tedirgin duruşuyla beliriverdi. İlk adımını attığında yüzündeki gerginlik az da olsa okunabiliyordu. Aklından geçen "iyi akşamlar diyeceğim ve sanırım kimse sallamayacak" düşüncesi bir bulut olup kafasının üzerinden belirmişti. Ama o sabah ki yardımın etkisinde olsa gerek bu kez yüzümü aradı ve görünce hafif bir tebessümle "iyi akşamlar" dedi. "İyi akşamlar. Eee ilk günün nasıl geçti bakalım..." diye karşılık verince bir yanımdaki koltuğa oturdu. Artık daha iyi hissediyordu.

Biraz sonraki derlemeye nasıl giriş yapacağımı bilemediğim günlerden birindeyiz anlayacağınız üzere. O yüzden yakın zamanda başıma gelen bir olayı sizinle paylaşarak başlamak istedim. Kıssadan hisse herkesin kendisine pay çıkaracağını düşünüyorum. Hepimiz stayjer olduk. Onların daha çok farkına varmamız gerektiği konusunda ufak bir hatırlatmam olsun burada.

...

Cumartesi gecesi çalışmalarının 31nci örneğinde Firebase Cloud Messaging sistemini kullanarak uygulamalara(örnek özelinde bir PWA programına) nasıl bildirimde bulunulabileceğini anlamaya çalışmışım. Her zaman olduğu gibi örneği WestWorld(Ubuntu 18.04, 64bit)üzerinde geliştirmişim. Şimdi notların üstünden geçme, unutulanları hatırlama ve eksikleri giderme zamanı. Öyleyse gelin notlarımızı derlemeye başlayalım.

Tarayıcı üzerinde yaşayan ve çevrim dışı ya da çok düşük internet hızlarında da çalışabilme özelliğine sahip olan PWA(Progressive Web Applications) uygulamalarının en önemli kabiliyetlerinden birisi de Push Notification'dır. Bu, mobil platformlardan yapılan erişimler düşünüldüğünde oldukça önemli bir nimettir. Uygulamaya otomatik bildirim düşmesi veya arka plan veri güncellemeleri kullanıcı deneyimi açısından bakıldığında değerli bir işlevselliktir. Bu yetenekler uygulama için tekrardan submit operasyonuna gerek kalmadan güncel kalabilmeleri anlamına gelir.

Geliştireceğimiz örnek bir basketbol maçı için güncel haber bilgisinin abone olan uygulamalara gönderilmesi üzerine kurgulanmakta. Burada mesajlaşma servisi olarak Firebase Cloud Messaging altyapısını kullanacağımız için ilgi çekici bir örnek olduğunu ifade edebilirim. Şimdi hazırlıklarımıza başlayabiliriz.

Ön Hazırlıklar

İki uygulama geliştireceğiz. Birisi çok sade HTML içeriğine sahip olan PWA uygulaması. İkinci uygulama ise bir servis. Kullanıcıların bildirim servisine abone olma ve çıkma işlemlerinin yönetimi ile bağlı olanlara bildirim yapma görevini(işte bu noktada Firebase Cloud Messaging sisteminden yararlanacak)üstlenecek. İlk olarak basketbol haberlerini takip edeceğimiz basit önyüz uygulamasını geliştirmeye başlayım. Klasör yapısını aşağıdaki terminal komutları ile oluşturabiliriz.
mkdir basketkolik
cd basketkolik
touch index.html
touch sworker.js
touch main.js
touch efes_barca.html
Ayrıca uygulama testlerini HTTP üzerinden kolayca yapabilmek için serve isimli bir npm paketinden yararlanacağız. Kurulumu için aşağıdaki terminal komutunu kullanmak yeterli.
sudo npm install -g serve
Kodları yazmaya başlamadan önce Google tarafıyla haberleşme noktasında gerekli olan bir manifesto dosyasının oluşturulması gerekiyor.

Manifesto Dosyası

Manifesto dosyası PWA'nın Firebase tarafında etkinleştirilecek Push Notification özelliği için gereklidir. İçerisinde Sender ID değerini barındırır(ilerde karşınıza çıkacak) ve önyüzün kullandığı main modülü, abonelik başlatılırken bu değeri karşı tarafa iletmekle yükümlüdür. Peki bu dosyayı nasıl üreteceğiz?
 
Öncelikle Firebase kontrol paneline gidilir ve PWA için metadata bilgilerini tutacak bir Web App Manifest dosyası üretilir. Ben örnekte aşağıdaki bilgileri kullandım.
 
Sonrasında Zip dosyasını bilgisayara indirip proje klasörüne açmamız gerekiyor. Manifest.json dosyası ile birlikte images isimli bir klasör de gelecektir. Images klasöründe kendi eklediğimiz active.png dosyasının farklı cihazlar için standartlaştırılmış boyutları yer alır. Bu bilgiler manifest.json dosyasına da konur.
 
İşimiz henüz bitmedi! Uygulama için Push Notification özelliğini de etkinleştirmek gerekiyor. Bunun için Firebase Console arabirimine gidip yeni bir proje oluşturmalı ve ardından proje ayarlarına(Project Overview -> Project Settings) ulaşıp Cloud Messaging sekmesine gelinmeli (Ben "basketin-cepte-project'" isimli bir proje oluşturdum :P Hayaller başka tabii ama eldeki malzeme şimdilik bu) 

Bu bölümde proje için oluşturulan Server Key ve Sender ID değerleri yer alır. Az önce bahsedildiği gibi Sender ID değerinin manifest.json dosyasına eklenmesi gerekiyor (gcm_sender_id yazan kısma bakınız)

PWA Kodları

Artık basketkolik klasöründeki dosyalarımızı kodlamaya başlayabiliriz. index.html sayfası aslında haber kaynağına aboneliği başlatıp durdurabileceğimiz bir test sahası gibi. Tek dikkat edilmesi gereken nokta manifest dosyası ile bağ kurulmuş olması. Tasarım son derece aptalca yapıldı ama esas amacımız elbette şukela bir görsellik sunmanın dışında push notification kabiliyletlerini deneyimlemek. Index sayfası ile işe başlayalım.
<!DOCTYPE html><html><head><title>Basketin Cepte</title><link rel="manifest" href="manifest.json"><!--Bunu eklemeyi unutmamak lazım--></head><body><h3>Euroleague Haberleri</h3><p><i>Düğmeye bas ve ne olacağına bakalım</i></p><div id="divPush"><img id="buttonPush" width="50px" src="active.png" /></div><script src="main.js"></script></body></html>
efes_barca.html isim dosya da şimdilik bildirim alındığında gösterilecek içeriği barındırıyor. Bunun dinamik olduğunu bir düşünsenize. Abone olduktan sonra yeni bir haber geldiğinde efes_barcha benzeri dinamik HTML içerikleri kullanılacak. Tüm uygulamayı tamamladıktan sonra bu tip bir özellikle örneğinizi daha da zenginleştirebilirsiniz.
<!DOCTYPE html><html><head><title>Euroleague'de Efes Farkı</title></head><body><p>Anadolu Efes, kritik maçta Barcelona'yı kendi evinde farklı yenerek...
    </p></body></html>
Sworker modülü aslında Service Worker görevini üstlenmekte. Kodlar arasına katmaya çalıştığım yorumlarla mümkün mertebe neler olduğunu anlamaya ve anlatmaya çalıştım.
/*
main.js içerisinde Service Worker olarak bildirimi yapılan dosyamız.
İki olay metodu içerir.
Uygulama herhangibir bildirim aldığında push metodu tetiklenir. 
Bildirim almasını sağlamak için yazdığımız PusherAPI servisine talep göndermemiz yeterlidir. http://localhost:8080/news/push gibi.
Bu metod içerisinde showNotification ile bir bildirim penceresi gösterilir.
Pencereye tıklandığındaysa notificationclick isimli olay tetiklenir.
Bu olayın ele alındığı fonksiyonda ise temsilen bir web sayfası içeriği açtırılır.
*/

self.addEventListener('push', function (event) {
    console.log('push olayı tetiklendi');
    var title = 'Euroleague Haberleri';
    var body = {
        'body': 'Güncel skor bilgileri için tıklayın',
        'tag': 'pwa',
        'icon': './images/48x48.png'
    };
    event.waitUntil(
        self.registration.showNotification(title, body)
    );
});

self.addEventListener('notificationclick', function (event) {
    //TODO: Burada statik bir sayfa içeriği gösterilmesi yerine haber bilgisini servisten alacak kodu ekleyebilirsiniz
    console.log('Şimdi bildirime ait sayfa açılacak');
    var url = './efes_barca.html';
    event.notification.close();
    event.waitUntil(clients.openWindow(url));
});
Bu taraf için son olarak main içeriğini de aşağıdaki gibi geliştirebiliriz.
// load aşamasında Service Worker için register işlemi yapılır
window.addEventListener('load', e => {
    if (!('serviceWorker' in navigator)) {
        console.log('Service worker desteklenmiyor');
        return;
    }
    navigator.serviceWorker.register('sworker.js')
        .then(function () {
            console.log('Servis Worker kaydoldu');
        })
        .catch(function (err) {
            console.log('Hımm...Sanırım bir hata oluştu : ', err);
        });
});

// Push Notification durumunu günceller
// Aktifse active.png, değilse passive.png
function updateStatus(status) {
    divPush.dataset.checked = status; // statu bilgisini set et
    if (status) { //true ise aktif
        buttonPush.src = "./assets/active.png";
    }
    else { // değilse pasif olan ikonu göster
        buttonPush.src = "./assets/passive.png";
    }
}

function isPushNotifyEnabled() {
    if (Notification.permission === 'denied') {
        alert('Kullanıcı Push Notification hizmetini bloklamış');
        return;
    }

    if (!('PushManager' in window)) {
        alert('Üzgünüm ama Push Notification bu tarayıcı modelinde desteklenmiyor');
        return;
    }

    /*
    Kullanıcı Push Notification'ı engellememiş ve tarayıcı da bunu destekliyorsa
    buraya geliriz.
    Eğer Service Worker hazır ve kaydolmuşsa abonelik yönetimine başlanabilir. 
    */
    navigator.serviceWorker.ready.then(function (reg) {
        reg.pushManager.getSubscription() //abone ol
            .then(function (subs) { // abonelik durumuna göre statüleri güncelle
                if (subs) {
                    updateStatus(true);
                }
                else {
                    updateStatus(false);
                }
            })
            .catch(function (err) { // bir hata oluştuysa tarayıcı console'una hata basıyoruz.
                console.error('Bir hata oluştu : ', err);
            });
    });
}

// Uygulamayı Push Notification hizmetine abone etmek için
function subscribe() {
    navigator.serviceWorker.ready
        .then(function (reg) {
            if (!reg.pushManager) {
                alert('Tarayıcı Push Notification hizmetini desteklemiyor');
                return false;
            }
            reg.pushManager.subscribe(
                { userVisibleOnly: true }
            ).then(function (subs) {
                console.log('Abonelik başladı.');
                console.log(subs);
                sendSubsID(subs); // Abonelik IDsini REST servise gönderen metodu çağırdık
                updateStatus(true);
            }).catch(function (err) {
                updateStatus(false);
                console.error('Abone olma işlemi sırasında hata oluştu: ', err);
            });
        });
}

// Abonelikten çıkartmak için
function unsubscribe() {
    navigator.serviceWorker.ready
        .then(function (reg) {
            reg.pushManager.getSubscription()
                .then(function (subs) {
                    if (!subs) {
                        alert('Abonelikten çıkılamıyor yahu :S');
                        return;
                    }
                    subs.unsubscribe()
                        .then(function () {
                            console.log('Abonelikten çıkıldı');
                            console.log(subs);
                            deleteSubsID(subs); // Aboneliği silerken ID'yi çıkartacak olan REST servis metodunu çağırdık
                            updateStatus(false);
                        })
                        .catch(function (err) {
                            console.error(err);
                        });
                })
                .catch(function (err) {
                    console.error('Abonelikten çıkarken bir hata oluştu : ', err);
                });
        })
}

// Sunucuya Subscription ID bilgisini göndermek için kullanıyoruz
// Bunun için yazdığımız PusherAPI node servisini kullanıyoruz
function sendSubsID(subscription) {
    var id = subscription.endpoint.split('gcm/send/')[1];
    fetch('http://localhost:8080/subscribers', {
        method: 'post',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({ subscriptionid: id })
    });
}

function deleteSubsID(subscription) {
    var id = subscription.endpoint.split('gcm/send/')[1];
    fetch('http://localhost:8080/subscribers/' +
        id, {
            method: 'delete',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            }
        });
}

// div ve button elementlerini alıyoruz
var divPush = document.getElementById('divPush');
var buttonPush = document.getElementById('buttonPush');

/*
div elementinde mouse ile tıklandığı zaman gerçekleşecek olayı
dinleyecek bir listener bildirimi yaptık
*/
divPush.addEventListener('click', function () {
    //console.log('Click');
    if (divPush.dataset.checked === 'true') {
        //console.log('Unsubscribe');
        unsubscribe();
    }
    else {
        //console.log('Subscribe');
        subscribe();
    }
});

isPushNotifyEnabled();

İlk Test

Kodları tamamladıktan sonra kısa bir test ile push notification hizmetinin çalışıp çalışmadığı hemen kontrol edilebilir. Bunun için terminalden
serve
komutunu verip uygulamayı ayağa kaldırmamız yeterli. Eğer aşağıdaki ekran görüntülerindekine benzer sonuçlar elde edebiliyorsak REST API uygulamasını yazmaya başlayabiliriz.
 
 
Uygulama, Push Notification hizmeti için abone olunurken benzersiz bir ID değeri alır. Firebase Cloud Messaging sistem bu değeri kullanarak kime bildirim yapılacağını bilebilir.

REST API Uygulamasının Yazılması

Abone olan uygulamaların ID bilgilerini yönetmek için Node.js tabanlı bir REST servisi yazabiliriz. Servis temel olarak PWA'nın FCM ile olan iletişiminde devreye girmektedir. Hem abonelik yönetimi hem de istemcilere bildirim yapılması ki bunu tek başına yapmayacaktır. Service Worker dolaylı olarak FCM üzerinden bu servisle yakın ilişki içerisindedir. 
 
Bu servisi ayrı bir klasörde projelendirmek iyi olur. Pek tabii node.js tarafında REST Servisi yazımını kolaylaştırmak için bazı paketlerden destek alınabilir. express dışında HTTP mesajlarındaki gövdeleri kolayca ele almak için body-parser, servisin Firebase Cloud Messaging ile konuşabilmesini sağlamak amacıyla da fcm-node paketi kullanılablir. morgan modülünü ise sunucu tarafındaki HTTP trafiğini loglamak için değerlendirebiliriz. Aşağıdaki terminal komutları ile klasör ağacını oluşturup server dosyasını kodlayarak derlememize devam edelim.
mkdir PusherAPI
cd PusherAPI
touch server.js
sudo npm install express body-parser fcm-node morgan
server modülü
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var FCM = require('fcm-node');
var morgan = require('morgan');
app.use(morgan('combined'));

// Firebase Cloud Messaging nesnesini örneklerken
// bizim için üretilen server key değerini veriyoruz
var fcm = new FCM('bu kod sizde var...');

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
    extended: true
}));

app.use(function (req, res, next) {
    // CORS problemi yaşamamak için gerekli header tanımlamaları yapılır
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
    res.setHeader('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
    next();
})

//abonelerin ID bilgilerini tutacağımız array. 
// Bunun yerine daha kalıcı bir repository tercih edilebilir
var subscribers = []

// Web uygulaması HTTP Post ile subscriptionID gönderdiğinde çalışan metod 
app.post('/subscribers/', function (req, res) {
    // Mesaj gövdesinden abonelik bilgisini almaya çalışr
    if (!req.body.hasOwnProperty('subscriptionid')) {
        res.statusCode = 400;
        res.send('Gönderilen bilgilerde eksik veya hata var');
        return;
    }
    // bulduysan diziye ekle ve başarılı olduğu bilgisini ilet
    subscribers.push(req.body.subscriptionid)
    res.statusCode = 200; //HTTP 200 Ok basıyoruz
    res.send('ID alındı');
});

// Web uygulaması abonelikten çıkarken HTTP Delete ile çağırdığı metod
app.delete('/subscribers/:id', function (req, res) {
    // dizideki indis değerini URLden gelen id değerine göre bul
    const index = subscribers.indexOf(req.params.id)
    // varsa diziden çıkart
    if (index !== -1) {
        subscribers.splice(index, 1)
    }
    res.statusCode = 200;
    res.send('ID silindi');
});

/*
    Abonelere bildirim göndermek için tetiklenen REST metodu
    localhost:8080/news/push çağrısı geldiğinde çalışır.
*/
app.get('/news/push', function (req, res) {
    // Aboneler için mesaj hazırlanır
    var message = {
        registration_ids: subscribers,
        collapse_key: 'i_love_this_game',
    };
    // Firebase Cloud Messaging üzerinden mesaj gönderilir
    fcm.send(message, function (err, response) {
        if (err) {
            console.log(err)
        } else {
            console.log("Mesaj başarılı bir şekilde gönderildi: ", response);
        }
    });
    res.sendStatus(200);
});

app.listen(8080);
console.log('Pusher API servisi 8080 üstünden dinlemede');

Çalışma Dinamikleri

Uygulamanın çalışma dinamiklerini anlamak oldukça önemli. Index.html olarak düşündüğümüz web uygulamamız çalıştırıldığında iki aksiyonumuz var. Basketbol topuna basıp bir abonelik başlatmak veya tekrar basarak aboneliği durdurmak.

Abonelik başlatıldığında FCM benzersiz bir ID değeri üretir ve bunu PusherAPI servisi kendisine gelen çağrı ile kayıt altına alır(diziye eklediğimiz yer) Sonraki herhangi bir t zamanında PusherAPI servisinin abonelere bildirim gönderen HTTP metodu tetiklenirse, Firebase Cloud Messaging devreye girer ve dizideki ID bilgilerini kullanarak abonelerine bildirimde bulunur. Bildirimler web uygulaması tarafındaki Service Worker(sworker.js) tarafından push olayıyla yakalanır. Push olayı şimdilik sadece statik bir sayfa gösterimi yapmakta ki aslında asıl içeriği yine servis üstünden veya web aracılığıyla başka bir adresten aldırabiliriz.

Çalışma Zamanı(Development Ortamı)

Testler için PWA ve servis tarafını ayrı ayrı çalıştırmalıyız.
serve
terminal komutu ile web uygulamasını
node server.js
ile de REST servisini başlatabiliriz. Aboneliği başlattıktan sonra http://localhost:8080/news/push adresine talepte bulunursak bir bildirim mesajı ile karşılaşırız(sworker daki push olayı tetiklenir) Aynen aşağıdaki ekran görüntüsünde olduğu gibi.
 
 
Bildirim kutusuna tıklarsak statik olarak belirlediğimiz sayfa açılacaktır(yani notificationclick olayı tetiklenir)
 

PWA ve Service Uygulamalarının Firebase Hosting'e Alınması

Her iki uygulamada local geliştirme ortamında gayet güzel çalışıyor. Ancak bunu anlamlı hale getirmek için her iki ürünü de Firebase üzerine alıp genel kullanıma açmamız lazım. Web uygulamasını Firebase Hosting ile REST servisini de Firebase Function ile yayınlamalıyız. Bu işlemler için firebase-tools aracına ihtiyacımız olacak. Terminalden aşağıdaki komutu kullanarak ilgili aracı sisteme yükleyebiliriz.
sudo npm install -g firebase-tools

Basketkolik'in Dağıtımı

Yeni bir dağıtım klasörü oluşturmalı, initializion işlemini gerçekleştirip basketkolik uygulama kodlarını oluşan public klasörü içerisine atmalıyız. Ardından üzerinde çalışacağımız projeyi seçip deploy işlemini yapabiliriz. Bu işlemler için aşağıdaki terminal komutlarından yararlanılabilir.
mkdir dist
cd dist
firebase init
cp -a ../basketkolik/. ./public/
firebase use --add basketin-cepte-project
firebase deploy
firebase init işleminde bize bazı seçenekler sunulacaktır. Burada aşağıdaki görüntüde yer alan seçimlerle ilerleyebiliriz. En azından ben öyle yaptım.
 
 
Eğer dağıtım işlemi başarılı olursa aşağıdaki ekran görüntüsündekine benzer sonuçlar elde edilmelidir.

PusherAPI servisinin Dağıtımı

Hatırlanacağı üzere web uygulaması bir REST Servisi yardımıyla FCM sistemini kullanıyordu. PusherAPI isimli uygulama, Fireabase tarafı için bir Function anlamına gelmektedir(Serverless App olarak düşünelim)Ölçeklenebilirliği, HTTPS güvenliği, otomatik olarak ayağa kalkması gibi bir çok iş Google Cloud ortamı tarafından ele alınır. Şimdi aşağıdaki terminal komutu ile fonksiyon klasörünü oluşturalım (dist klasörü içerisinde çalıştığımıza dikkat edelim)
firebase init functions
Yine bazı seçenekler karşımıza gelecektir. Burada gelen sorulara şöyle cevaplar verebiliriz;
  • Dil olarak Javascript seçelim.
  • ESLint kullanımına Yes diyelim.
  • npm dependency'lerin kurulmasına da Yes diyelim ki uygulamanın gereksinim duyduğu node paketleri de yüklensin.
Devam eden adımda functions klasöründeki index dosyasının içeriğini PusherAPI'deki server içeriği ile değiştirmeliyiz. Ancak bu kez express'in firebase-functions ile kullanılması gerekiyor. İhtiyacımız olan express, body-parser ve fcm-node paketlerini üzerinde çalıştığımız functions klasörü içinede de yüklemeliyiz. Son olarak dist klasöründeki firebase.json dosyasına rewrites isimli yeni bir bölüm ekleyip fonksiyonumuzu deploy edebiliriz.
cd functions
sudo npm install express body-parser fcm-node
firebase deploy
Yapmamız gereken bir şey daha var. Web uygulamasının kullandığı main dosyasının içeriğini, yeni eklediğimiz google functions ile uyumlu hale getirmek. Tahmin edileceği üzere gidilen servis adreslerini, oluşturulan firebase proje adresleri ile değiştirmemiz lazım (dist/public/main.js içeriğini kontrol edin) Web uygulamasındaki bu değişikliği Cloud ortamına taşımak içinse public klasöründeyken yeniden bir deploy işlemi başlatmamız yeterli olacaktır.
firebase deploy

Çalışma Zamanı(Production Ortamı)

Uygulama artık https://basketin-cepte-project.firebaseapp.com/ adresinden yayında (En azından bir süre için yayındaydı ki aşağıdaki ekran görüntüsü de bunun kanıtıdır)
 

Ben Neler Öğrendim?

Bu çalışmanın da bana kattığı bir çok şey oldu elbette. Özellikle bir uygulamaya uzak sunuculardan bildirim yollanması ve bunun abonelik temelli yapılması merak edip öğrenmek istediğim konulardan birisiydi. İşin içerisine basit bir PWA modeli de ekleyince çalışma ilgi çekici bir hal almıştı. Yapılan hazırlıklar düşünüldüğünde aslında bizi çok fazla yormayacak bir geliştirme süreci olduğunu ifade edebiliriz. Derlemeyi sonlandırmacan önce yanıma kar kalanların neler olduğunu aşağıdaki maddeler ile özetleyebilirim.
  • Firebase Cloud Messaging(FCM) sisteminin kabaca ne işe yaradığını
  • PWA uygulamasının FCM ile nasıl haberleşebileceğini
  • Abone olan istemciye bildirimlerin nasıl gönderilebileceğini
  • Service Worker üzerindeki push ve notificationclick olaylarının ne anlama geldiğini
  • serve paketinin kullanımını
  • firebase terminal aracı ile deployment işlemlerinin nasıl yapıldığını
  • Web uygulaması ve Functions'ın Google Cloud tarafından bakıldığında farklılıklarını
Bu arada proje büyük ihtimalle Google platformundan kaldırılmıştır. Malum istenmeyen bir yüklenme sonrası yüklü bir fatura ile karşılaşmamak adına küçük bir tedbir olduğunu ifade edebilirim. O nedenle kendiniz başarmaya çalışırsanız daha kıymetli olacaktır. Konseptin basketbol olması önemli değil. Abonelerinize bildirimlerde bulunacağınız herhangi bir senaryo olması yeterli olur. Bildirim yapan servisi de planlanmış bir düzeneğe bağlayabiliriz. Söz gelimi günün belirli anlarında bir konu ile ilgili bildirimlerin yönlendirilmesi işini üstlenebilir. Böylece geldik bir saturday-night-works macerasının daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Socket-IO Yardımıyla RealTime Çalışan Bir Angular Uygulaması Geliştirmek

$
0
0

Dünyanın aslen hukukçu olmasına rağmen en ünlü matematikçilerinden olan Fermat'nın(1601-1665) asal sayıları bulduğunu iddia ettiği denklemini bir diğer matematikçi Euler(1707-1773), n=5 değeri için bozmuştur. Lakin matematikçilerin ve diğer pek çok kişinin asalları bulma tutkusu bitmemiştir. Bilim, felsefe ve müzikle haşırneşir olmayı seven Fransız rahibi Marin Mersenne(1558-1648) 2n-1 şeklindeki formülü ile ünlenmiştir. Formüldeki n değerinin asal sayı olarak kabul edildiği hallerde bulunan sayıların da asal olduğunun belirtildiği bir teorem söz konusudur(Bu formül ile bulunan bir sayının asal olup olmadığı Lucas-Lehmer testi ile kontrol edilebilir)

Nitekim mesele 1000-2000 arası asalları bulmakla ilgili değildir. En büyük asal değeri bulabilmektir. Çünkü n değeri büyüdükçe en büyük asalı bulmak da zorlaşır(Nadir olan her zaman daha kıymetlidir) Mersenne sayıları olarak adlandırılan bu asalların en kocamanı 2018 yılında elektrik mühendisi Jonathan Pace tarafından keşfedilmiştir. n = 82.589.933 değeri için bulunan 50nci Mersenne asalı tam 24.862.048 rakamdan oluşmaktadır(Ocak 2019 itibariyle) Dilerseniz 51nci Mersenne asalını bulmak için siz de katkıda bulunabilir hatta bulursanız küçük bir ödül bile alabilirsiniz. Şu adrese girip GIMPS(Great Internet Mersenne Prime Search) sistemine gönüllü olarak katılmanız yeterli.

Lakin hangi formül olursa olsun çıkan sonucun asal sayı olacağının garantisi veya ispatı henüz yoktur(May Be Prime!) Hatta dünyadaki tüm asal sayılarının dizisini bize getirebilecek bir denklem de henüz mevcut değildir. Peki bugünkü konumuzun Mersenne asalları ile bir ilgisi var mı dersiniz? Bu cumartesi gecesi derlemesinin 29ncu çalışmaya ait olması haricinde pek yok ;)

Bilindiği üzere istemci-sunucu geliştirme modelinde gerçek zamanlı ve çift yönlü iletişim için WebSocket yaygın olarak kullanılan protokollerden birisi. Klasik HTTP request/response modelinden farklı olarak WebSocket protokolünde sunucu, istemcinin talep göndermesine gerek kalmadan mesaj gönderebiliyor. Chat uygulamaları, çok kullanıcılı gerçek zamanlı oyunlar, finansal bildirim yapan ticari programlar, online doküman yönetim sistemleri ve benzerleri WebSocket protokolünün kullanıldığı ideal ortamlar. Benim 29 numaralı Saturday Night Works çalışmasındaki amacım Socket.IO kütüphanesinden yararlanan bir Node sunucusu ile Angular'da yazılmış bir web uygulamasını WebSocket protokolü tabanında deneyimlemekti. Hazırsanız notlarımızı toparlamaya başlayalım.

Öncelikle örneğimizde neler yapacağımızdan bahsdelim. Kullanıcıların aynı doküman üzerinde ortaklaşa çalışabileceği bir örnek geliştirmeye çalışacağız. İstemciler yeni bir doküman başlatabilecek. Dokümanların tamamı tüm kullanıcılar tarafından görülebilecek ve yazılanlar her istemcinin penceresine yansıyacak. Bir nevi ortak dashboard üzerindeki post-it'lerin herkes tarafından düzenlenebildiği bir ortam gibi düşünebiliriz. Ben örneği her zaman olduğu gibi WestWorld(Ubuntu 18.04, 64bit)üzerinden denemeye çalışıyorum. Bu arada makinenizde node, npm, angular CLI'ın yüklü olduğunu varsayıyorum.

Uygulamanın İnşası

Uygulama iki önemli parçadan oluşuyor. Soket mesajlaşmasını yönetecek olan sunucu(node.js tarafı) ve istemci(Angular tarafı) Sunucu tarafının inşası için aşağıdaki terminal komutları ile işe başlayabiliriz.

mkdir docserver
cd docserver
mkdir src
npm init
npm install --save-dev express socket.io @types/socket.io
cd src
touch app.js

Bize yardımcı olacak sunucu ve soket özellikleri için bir epxress ve socket.io paketlerini yüklüyoruz. app.js dosyasının içeriğini ise aşağıdaki gibi geliştirebiliriz.

/*
    sunucu özelliklerini kolayca kazandırmak için express modülünü kullanıyoruz.
    WebSocket kullanımı içinse socket.io paketi dahil ediliyor
*/
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);

const articles = {}; // Üzerinde çalışılacak yazıların tutulacağı repo. Canlı ortamlar için fiziki alan ele alınmalı.

/* 
on metodları birer event listener'dır. İlk parametre olayın adı,
ikinci parametrede olay tetiklendiğinde çalışacak callback fonksiyonudur.

connection Socket.IO için tahsis edilmiş bir olaydır. 
Burada soket haberleşmesi tesis edilir ve bağlı olan
istemciler için broadcasting başlatılır.
*/

io.on("connection", socket => {

    /*
    updateRoom metodu bağlı olan tüm istemcilerin aynı doküman üzerinde çalışmasını garanti etmek içindir.İstemci bağlantı gerçekleştirip dokümanla çalışmak üzere bir odaya bağlanır(room).
    Bağlı olan istemci bu odadayken başka bir dokümanla çalışmasına izin verilmez.
    N sayıda istemci aynı odadayken aynı doküman üzerinde güncelleme yapabilir.İstemci bir başka dokümanla çalışmak isterse bulunduğu odadan ayrılır ve yeni bir tanesine katılır.
    Tabii Socket.IO ile n sayıda oda(room) ile çalışmak mümkündür. Ancak bu senaryoda istenmemektedir.
    */
    let preId;
    const updateRoom = currentId => {
        socket.leave(preId);
        socket.join(currentId);
        preId = currentId;
    };

    /*
    istemci get isimli bir olay yayınladığında çalışır.
    istemci bir odaya gelen id ile dahil edilir.
    sonrasında sunucu dokümanı istemciye yollar. 
    Bunun için ready isimli bir olay yayınlar ki istemci de bu olayı dinlemektedir.
    */
    socket.on("get", id => {
        console.log("get event id: " + id);
        updateRoom(id);
        socket.emit("ready", articles[id]);
    });

    /*
    add yeni bir dokümanın eklenmesi için kullanılır.
    istemci tarafından yayınlanan olayda payload olarak
    dokümanın kendisi gelir. 

    io üzerinden yayınlanan warnEveryone isimli olay
    istemcilerin tümünü yeni bir dokümanın eklendiği bilgisini vermek üzere tasarlanmıştır.

    socket üzerinden yapılan olay bildirimi payload dokümanı ile birlikte sadece bağlı
    olan istemci için geçerlidir. 

    socket ile io nesnelerinin emit kullanımları arasındaki farka dikkat edelim.
    io.emit bağlı olan tüm istmecileri ilgilendirirken, socket.emit o anki olayın
    sahibi bağlı olan istemciyi ilgilendirir.
    */
    socket.on("add", payload => {
        articles[payload.id] = payload;
        updateRoom(payload.id);
        console.log("add event " + payload.id);
        io.emit("warnEveryone", Object.keys(articles));
        socket.emit("ready", payload);
        console.log(articles);
    });


    /*
    İstemcilerin üzerinde çalıştıkları dokümanda yaptıkları herhangibir tuş darbesi
    bu olayın tetiklenmesi ile ilgilidir.
    Payload içeriğine göre odadaki doküman güncellenir ve
    sadece bu doküman üzerinde çalışanların bilgilendirimesi sağlanır.
    */
    socket.on("update", payload => {
        //console.log("update event");
        articles[payload.id] = payload;
        socket.to(payload.id).emit("ready", payload);
    });

    // Tüm bağlı istemcileri template dizisindeki key değerleri için bilgilendir
    io.emit("warnEveryone", Object.keys(articles));
});

http.listen(5004);
console.log("Ortak makale yazma platformu :P 5004 nolu porttan dinlemede...");

Kodu içerisindeki yorumlar ile mümkün mertebe açıklamaya çalıştım. Buraya kadar her şey yolunda gittiyse istemci uygulamanın inşası ile devam edebiliriz.

İstemcinin(Angular tarafı)İnşası

Soket yöneticisi ile konuşacak olan istemciyi bir Angular uygulaması olarak geliştireceğiz. İşe aşağıdaki terminal komutları ile başlayabiliriz.

ng new authorApp --routing=false --style=css
cd authorApp
sudo npm install --save-dev ngx-socket-io
ng g class article
ng g component article-list
ng g component article
ng g service article

İlk komutla authorApp isimli bir Angular uygulaması oluşturulur. Socket.IO ile Angular tarafında konuşmamızı sağlayacak ngx-socket-io paketi proje klasörü içindeyken npm yardımıyla yüklenir. Yine aynı klasörde article isimli sınıf, article-list ve article isimli bileşenler ve soket sunucusuyla iletişimde kullanacağımız article isimli servis oluşturulur (g sonrasında gelen component ve service anahtar kelimeleri için c ve skısaltmaları da kullanılabilir)

Gelelim istemci tarafındaki kodlarımıza. Öncelikle app.module.ts dosyasında SocketIoModule ile ilgili bir kaç konfigurasyon ayarlaması yapalım. Böylece hangi sunucu ile web socket haberleşmesi yapılacağı tüm modüller için ayarlanmış olur.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { ArticleListComponent } from './article-list/article-list.component';
import { ArticleComponent } from './article/article.component';
import { FormsModule } from '@angular/forms';
/*
  Angular tarafından socket haberleşmesi için gerekli modül
  bildirimleri. Web Socket sunucusunun adresi de konfigurasyon bilgisi olarak tanımlanmakta.
*/
import { SocketIoModule, SocketIoConfig } from 'ngx-socket-io';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
const config: SocketIoConfig = { url: 'http://localhost:5004' };

@NgModule({
  declarations: [
    AppComponent,
    ArticleListComponent,
    ArticleComponent
  ],
  imports: [
    BrowserModule,
    // Üstte belirtilen url bilgisi ile birlikte socket modülünü hazır hale getirip içeri alıyoruz
    SocketIoModule.forRoot(config),
    BrowserAnimationsModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Odalardaki makaleleri temsil eden article.ts sınıfını da şöyle yazabiliriz.

/*
    Ortaklaşa çalışılacak dokümanı temsilen kullanılacak tip
*/
export class Article {
    id: string;
    content: string;
}

Pek tabii asıl iş yükü proxy sınıfı görevi üstlenen article.service.ts içerisinde. Socket sunucusu ile haberleşecek ve arayüz tarafında kullanacağımız servis kodlarını aşağıdaki gibi geliştirebiliriz.

import { Injectable } from '@angular/core';
import { Socket } from 'ngx-socket-io';  // Socket sunucusuna event fırlatıp yakalayacağımız için
import { Article } from '../app/article'; //Article tipini kullanacağımız için

@Injectable({
  providedIn: 'root'
})
export class ArticleService {

  /*
   Socket sunucusundan yayınlanan ready ve warnEveryOne isimli olaylar için kullanacağımız özellikleri tanımlıyoruz.
   
   Sunucu tüm istemcilere makale listesini string array olarak gönderirken warnEveryOne olayını yayınlamakta.
   Doküman ekleme, güncelleme ve tek birisini çekme işlemlerine karşılık olarak da ready olayını yayınlıyordu.
   
   fromEvent dönüşleri Observable tiptedir. Yani değişiklikler otomatik olarak abonelerine yansıyacaktır. 
*/
  currentArticle = this.socket.fromEvent<Article>('ready');
  allOfThem = this.socket.fromEvent<string[]>('warnEveryone');

  constructor(private socket: Socket) { } //Constructor injection ile Socket modülünü yükledik

  /*
  Boş bir doküman üretmek için kullanılıyor.
  emit metodu add olayını tetiklemekte. 
  Sunucuya ikinci parametrede belirtilen içerik gönderiliyor.

  emit metodlarındaki ilk parametrelerdeki olaylar sunucunun dinlediği olaylardır.
  */
  add() {
    let randomArticleName = Math.floor(Math.random() * 1000 + 1).toString();
    this.socket.emit('add', {id: randomArticleName,content:'' });
    // console.log(this.allOfThem.forEach(a=>console.log(a)));
  }

  /*
  makale içeriğinin güncellenmesi halinde sunucu tarafına update olayı basılır
  */
  update(article:Article){
    this.socket.emit('update',article);
  }

  /*
  id değerine göre bir makaleyi almak için get olayını fırlatıyor.
  */
  get(id:string){
    this.socket.emit('get',id);
  }
}

Ön yüz tarafında daha çok bileşenleri kodlayacağız. article ve article-list bileşenleri ana bileşen olan app içerisinde kullanılmaktalar. Tüm bu bileşenlere ait html ve typescript içeriklerini aşağıdaki gibi düzenleyebiliriz.

article-component.html

<textarea [(ngModel)]='article.content' (keyup)='updateArticle()' placeholder='Haydi bir şeyler yazalım...'></textarea><!--textarea'yo ngModel niteliği ile arka plandaki article sınıfının content özelliğine bağlıyoruz
    keyup olayı parmağımızı her tuştan çektiğimizde çalışacak ve bileşenin typescript tarafındaki updateArticle
    metodunu çağıracak.
-->

article-component.ts

import { Component, OnInit, OnDestroy } from '@angular/core';
import { ArticleService } from 'src/app/article.service';
import { Subscription } from 'rxjs';
import { Article } from 'src/app/article';
import { startWith } from 'rxjs/operators';

@Component({
  selector: 'app-article',
  templateUrl: './article.component.html',
  styleUrls: ['./article.component.css']
})

export class ArticleComponent implements OnInit, OnDestroy {
  article: Article;
  private _subscription: Subscription;

  /*
  ArticleService, Constructor Injection ile içeriye alınır.
  */
  constructor(private ArticleService: ArticleService) { }

  /*
  Bileşen initialize edilirken güncel makale için bir abonelik başlatılır.
  Böylece gerek bu aboneliğin sahibinin değişiklikleri
  gerek diğerlerinin değişiklikleri aynı makalede çalışan herkese yansır.
  */
  ngOnInit() {
    this._subscription = this.ArticleService.currentArticle.pipe(
      startWith({ id: '', content: 'Var olan bir makaleyi seç ya da yeni bir tane oluştur' })
    ).subscribe(a => this.article = a);
  }

  // Bileşen ölürken üzerinde çalışan makalenin aboneliğinden çıkılır
  ngOnDestroy() {
    this._subscription.unsubscribe();
  }

  /*
   Arayüzdeki keyup olayı ile bağlanmıştır
  Yani tuştan parmak kaldırdıkça servise bir güncelleme olayı fırlatılır 
  ki bu tüm abonelerce alınır.
  */
  updateArticle() {
    this.ArticleService.update(this.article);
  }
}

article-list.component.html

<div><button (click)='newArticle()'>Yeni makale başlat</button></div><div style="height: 100%;"><span *ngFor='let a of articles | async' (click)='getArticle(a)'><b>{{a}}</b>  <br/></span></div><!--
  *ngFor ile Typescript tarafındaki articles isimli diziyi dönüyoruz.
  Her bir elemanı için {{a}} ile dizi elemanını basıyoruz ki bu içerde
  rastgele üretilen dosya numarası oluyor.

  click olayı tetiklendiğinde dosya numarasının içeriğini çeken getArticle metoduçağırılıyor ki o da article-list.component.ts içerisinde yer alıyor.

  button kontrolüne basıldığındaysa yine article-list.component.ts içerisindeki
  newArticle metodu çağırılıyor.
-->

artcile-list.component.ts

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { ArticleService } from 'src/app/article.service';

@Component({
  selector: 'app-article-list',
  templateUrl: './article-list.component.html',
  styleUrls: ['./article-list.component.css']
})

/*
Bileşen, OnInit ve OnDestroy fonksiyonlarını implemente ediyor.
Yani bileşen oluşturulurken ve iade edilirken yaptığımız bağzı işlemler var.

Init'te güncel makale listesi için bir stream açılmakta ve o an üzerinde çalışılan 
makale için bir abonelik başlatılmakta. Destroy metodunda ise üzerinde çalışılan makalenin aboneliğinden çıkılmakta.

articles değişkeni Observable tipinden bir string dizisi ve servisin allOfThem 
özelliği ile ilişkilendirilip bir stream oluşması sağlanıyor.

Bileşen üzerinden socket sunucusuna fırlatılan olayların karşılığından fırlatılan olaylar,
Observable değişkenin güncel kalmasını sağlayacaktır.
*/
export class ArticleListComponent implements OnInit, OnDestroy {

  articles: Observable<string[]>;
  currentArticle: string;
  private _subscription: Subscription;

  constructor(private articleService: ArticleService) { }

  ngOnInit() {
    this.articles = this.articleService.allOfThem;
    this._subscription = this.articleService.currentArticle.subscribe(a => this.currentArticle = a.id);
  }

  ngOnDestroy() {
    this._subscription.unsubscribe();
  }

  // id değerine göre makale çekilmesi için gerekli sunucu olayını tetikler
  getArticle(id: string) {
    this.articleService.get(id);
  }

  // Yeni bir makale oluşturulması için gerekli olayı tetikler
  newArticle() {
    this.articleService.add();
  }
}

app.component.html (HTML tablosunun üst tarafında article-list, alt satırında ise article bileşenlerini gösterecektir)

<table><tr><td><app-article-list></app-article-list></td><td style="width: 200px;"><app-article></app-article></td></tr></table>

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'authorApp';
}

Sunucu ve istemci tarafı uygulamalarımız artık hazır. Çalışma zamana geçip testlerimize başlayabiliriz.

Çalışma Zamanı

İstemcilerin dokümanlar üzerinde çalışmasını sağlamak için öncelikle node sunucusunu ayağa kaldırmamız gerekiyor. Bunu için

npm run start

komutunu kullanabiliriz. İstemci tarafını çalıştırmak içinse,

ng serve

terminal komutundan yararlanılabilir.

Servis localhost:4200 nolu port'tan ayağa kalkar. Bu zorunlu değildir ve isterseniz geliştirme ortamı için angular.json dosyasındaki serve kısmına yeni bir options elementi olarak port bilgisi ekleyebilirsiniz veya ng komutu ile --port anahtarını kullanabilirsiniz.

ng serve --port 4003

gibi.

Örneği daha iyi anlamak için iki veya daha fazla istemci çalıştırmakta yarar var. Bir istemcide yeni bir sayfa açıp üzerinde yazarken diğer istemcide de aynı dosya numarası görünür ve değişiklikler karşılıklı olarak taraflara yansır. Yani Cenifır'ın 399 nolu dokümanda yaptığı değişikliği aynı dokümana bakan Brendon görebilir ve üstüne kendi değişikliklerini yazıp bunları Jenifer'ın görmesini sağlayabilir. Chat uygulaması gibisinden ama değil gibi...

Tasarım gerçekten çok kötü ancak amaç Socket.IO'nun Angular tarafında nasıl kullanılabileceğini anlamak olduğu için bir kaç fikir vermiş olmalı. En azından bana verdi ve aşağıda yazdığım maddelerdeki bilgileri öğrendiğimi ifade edebilirim.

Ben Neler Öğrendim?

  • WebSocket protokolünün Node.js tarafında Socket.IO paketi yardımıyla nasıl kullanılabileceğini
  • emit ile bağlı istemciye ya da tüm istemcilere canlı yayının(broadcasting) nasıl yapılabileceğini
  • on, event olay dinleyicilerinin ne işe yaradığını
  • ng komutları ile proje oluşturulmasını, class, component ve service öğelerinin eklenmesini
  • Angular component'lerinin bir üst component içerisinde nasıl kullanılabileceğini
  • Bileşenlerin HTML tabanlı ön yüzünden, Typescript tarafındaki enstrümanlara(metod, property vb) nasıl ulaşılabileceğini

Böylece geldik bir cumartesi gecesi çalışmasına ait derlemenin daha sonuna. Bu derlememizde Angular ile yazılmış istemcilerin Web Socket üzerinden bir birleriyle nasıl haberleşebileceğini incelemeye çalıştık. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Web API Tarafında Dapper Kullanımı

$
0
0

cumartesi gecesi çalışmasını bitirdiğimde, yaptığım örneğin beni çok da tatmin etmediği gerçeğiyle karşı karşıyaydım. Bazen böyle hissediyordum. Boşa kürek çektiğim hissine kapılıyor ve neden tüm bunlarla uğraştığımı sorguluyordum. Belki de sonraki yıllar boyunca kullanmayacağım bir şeyler üzerinde çalışmıştım. Ne aldığım notları ne de senaryo için kullandığım Westwind adını beğenmiştim. Örnek çok sığdı. Zengin değildi. Bir şekilde beni rahatsız ediyordu.

Gürültülü soğutma sistemi ile zamanın pancar motorlu tekneleriyle karıştırdığım emektar WestWorld'ün başından kalkıp evin basketbol sahasına bakan çalışma odasının penceresine doğru yürüdüm. Gün henüz batmak üzereyken havadaki az sayıda buluta, karşı okulun damına tünemiş bir kaç martıya, sahada şuuruzca oradan oraya koşuşturup duran çocuğa, her zaman ki gibi yanındaki bakkalla dükkanının önünde tavla oynayan Ekrem amcaya baktım. Gelip giden düşünceler eşliğinde dengeye gelmeyi ve bir kaç saattir beni terk eden iç motivasyonumu bulmaya çalışıyordum. Benimle tekrar iletişim kurması biraz zaman alsa da sonunda bir şeyler fısıldamaya başlamıştı.

İncelediğim konuyu belki şirket projelerinde veya farklı bir yerde kullanmayacaktım ama farkında olacaktım. Basit dahi olsa denediğim örnek bana konuşma hakkı verecekti. Onu en azından bir kişiyle bile paylaşabilir, karşılıklı fikir alışverişi yapıp göremediğim noktaları fark edebilirdim. Şunu unutmamak lazım ki kişisel gelişim adına yaptığımız çalışmaların hiçbiri boşa değil. Hele ki rutine bağlayıp düzenli olarak yaptıklarımızın. Mutlaka size ve öğrendiklerinizi paylaştığınız çevrenize faydası var. Öyleyse notlarımızı derlemeye başlayalım mı?

Veri odaklı uygulamalar düşünüldüğünde kalıcı depolama enstrümanları ilişkisel veya dağıtık sistemler olarak karşımıza çıkar(RDBMS veya NoSQL araçları ifade etmeye çalışıyorum) Bu sistemlerin temel görevi veriyi tutmak ve yönetmektir. Lakin son kullanıcıya hitap eden etkileşim noktalarına gelindiğinde farklı program arayüzlerinin kullanımı söz konusudur(Müşteri son yaptığı rezarvosyonla ilgili bilgileri kontrol etmek için veri tabanına bağlanıp bir sorgu cümlesi çalıştırmak istemez öyle değil mi?)Özellikle nesne yönelimli dünya söz konusuysa verinin programatik ortamda ifade ediliş biçimi de önem arz eden bir konu olarak karşımıza çıkar. Tablo, kolon gibi şematik yapıların programlama dünyasında ifade ediliş biçimleri domain odaklı kodlama yaparken işleri kolaylaştırmalıdır. İlk zamanlardaki çözümler sonrasında Object Relational Mapping adı verilen ara katman hayatımıza girmiştir. Yani veri tabanındaki nesnel varlıklar ile programatik dünyadaki örneklerin eşleştirilmesi konusundan bahsediyoruz.

Bugün bir çok ORM(Object Relational Mapping) aracı mevcut. Ben daha önceden Entity Framework, Hibernate, LLBLGen gibi araçlarla çalışma fırsatı buldum. Genel olarak veriyi tuttuğumuz taraf ile nesne yönelimli dünya arasındaki iletişimde devreye giren bu araçlarda felsefe az çok aynı. Bir noktadan sonra kullanım kolaylıkları, entegre olabildikleri sistemler, performans ve açık kaynak olma halleri ön plana çıkıyor. Ben o geceki çalışmada Stackoverflow ekibi tarafından açık kaynak olarak geliştirilen ve iyi bir Micro ORM olarak nitelendirilen Dapper aracını incelemiştim. SQLite, MySQL, SQLCE, SQL Server, Firebird ve daha bir çok veri tabanı platformu ile çalışabilen Dapper'ın performans olarak da iyi sonuçlar verdiği ifade edilmekte. Genel olarak uygulamadaki amacımsa, Dapper'ı bir Web API uygulamasında SQLite ile birlikte kullanabilmek.

SQLite Tarafındaki Hazırlıklar

Örneği her zaman olduğu gibi WestWorld(Ubuntu 18.04, 64bit)üzerinde geliştirmeye çalıştım. Sistemde o gün itibariyle SQLite yüklüydü ve hatta Visual Studio Code tarafında veri tabanı nesnelerini görebilmek için şu adresteki eklentiyi kullanıyordum. Senaryoya göre dünya çapındaki üretici firmaların temel bilgilerini tutacağım bir tablo kullanacaktım. Westwind veri tabanının adıydı(Northwind çakması :P) Firm isimli bir tablo kullanmaya ve içerisinde şirket adı, merkez şehri, güncel bütçe bilgisi gibi alanlara yer vermeye karar verdim. Bu hazırlıkları sqlite3 komut satırı aracını kullanarak pekala yapabiliriz(ki artık bu andan itibaren siz benimle birlikte yazmaya başlıyor olmalısınız)

sqlite3 Westwind.db
.databases
CREATE TABLE FIRM(
ID INT PRIMARY KEY NOT NULL,
NAME TEXT NOT NULL,
CITY CHAR(50) NOT NULL,
SALARY REAL
);

INSERT INTO FIRM (ID,NAME,CITY,SALARY) VALUES (1,'Pedal Inc','Los Angles',10000);

INSERT INTO FIRM (ID,NAME,CITY,SALARY) VALUES (2,'Cycling Do','London',9000000);

SELECT * FROM FIRM;

İlk komutla dosya sisteminde Westwind.db isimli SQLite veri tabanı nesnesi oluşturulur. .databases komutu sayesinde var olan veri tabanını görebiliriz. Takip eden CREATE, INSERT, SELECT komutları çoğunuzun aşina olduğu standart SQL cümleleridir. Tablonun oluşturulmasını takiben örnek olarak iki firma bilgisi girilmiş ve tablonun tüm içeriği terminal penceresine istenmiştir.

Web API Projesinin Oluşturulması

SQLite tarafındaki başlangıç hazırlıklarımız tamamlandığına göre .Net Core Web API projesinin oluşturulmasıyla örneğe devam edebiliriz. WestwindAPI isimli projemiz iki nuget paketi kullanacak. Bunlardan birisi SQLite'ın .Net Core için yazılmış sürümü. Diğeri ise ORM aracımız olan Dapper ile ilgili. Aslında var olan SQLite tiplerini genişleteceğimiz bir paket olduğunu ifade edebiliriz (Bu arada siz örneği yazarken bulunduğunuz uzay zaman dilimine göre güncel sürümleri kontrol ederek ilerleyin. Core 3.0 ve sonrası için paketler değişikliğe uğraşmış olabilir)

dotnet new webapi -o WestwindAPI
cd WestwindAPI
dotnet add package System.Data.SQLite.Core
dotnet add package Dapper

Artık kod tarafına geçebiliriz. SQlite tarafı için gerekli bağlantı bilgisini appsettings.json dosyasına alarak devam edelim(Westwind dosyasını db isimli bir klasör altında tutuyoruz)

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "ConnectionStrings": {
    "WestWindConStr": "Data Source=db/Westwind.db"
  },
  "AllowedHosts": "*"
}

Şirket bilgilerinin tutulduğu SQL tablosunu kod tarafında Firm isimli entity ile karşılayabiliriz. Entity sınıfını models isimli klasör altında aşağıdaki gibi oluşturalım. Tipik bir POCO(Plain Old CLR Object) sınıfı...

using System;

namespace WestwindAPI.Models
{
    public class Firm
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public float Salary { get; set; }
    }
}

Bilindiği üzere başlangıç şablonuyla ValuesController isimli bir sınıf geliyor. İçerisinde temel CRUD operasyonlarına karşılık gelecek HTTP metodlarını barındırdığı için onu kullanabiliriz. Bu sınıfı FirmsController olarak yeniden isimlendirip Dapper kullanılacak hale getirmeye çalışalım. Mümkün mertebe koda yorum satırları katarak açıklamaya çalıştım.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using WestwindAPI.Models;
using Microsoft.Extensions.Configuration;
using Dapper;
using System.Data.SQLite;

namespace WestwindAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class FirmsController : ControllerBase
    {
        private readonly IConfiguration _configuration;
        private string conStr;

        // appsettings içerisindeki ConnectionStrings bilgisine ihtiyacımız olacak
        // Bu nedenle .net core'un built-in configuration yöneticisini içeri alıyoruz.
        public FirmsController(IConfiguration configuration)
        {
            _configuration = configuration;
            conStr = _configuration.GetConnectionString("WestwindConStr");
        }

        [HttpGet]
        public ActionResult<IEnumerable<Firm>> Get()
        {
            // Standart get talebi sonrası Firm listesini döndürüyoruz
            IEnumerable<Firm> firms = new List<Firm>();
            // SQLite connection nesnesini oluştur
            using (var conn = new SQLiteConnection(conStr))
            {
                conn.Open(); // bağlantıyı aç
                // standart bir SQL sorgusu çalıştırıyoruz
                // isme göre sıralayarak firma bilgilerini alıyoruz
                firms = conn.Query<Firm>("SELECT * FROM FIRM ORDER BY NAME");

            }
            return new ActionResult<IEnumerable<Firm>>(firms);
        }

        // Belli bir şehirdeki firmaların bilgilerini döndüren metodumuz
        [HttpGet("{city}")]
        public ActionResult<IEnumerable<Firm>> GetByCity(string city)
        {
            IEnumerable<Firm> firms = new List<Firm>();
            // SQLite connection nesnesini oluştur
            using (var conn = new SQLiteConnection(conStr))
            {
                conn.Open(); // bağlantıyı aç
                // Bu kez işin içerisinde bir where koşulu var
                firms = conn.Query<Firm>("SELECT * FROM FIRM WHERE CITY = @FirmCity ORDER BY NAME", new { FirmCity = city });

            }
            return new ActionResult<IEnumerable<Firm>>(firms);
        }

        [HttpPost]
        public IActionResult Post([FromBody] Firm payload)
        {
            try
            {
                using (var conn = new SQLiteConnection(conStr))
                {
                    conn.Open(); // bağlantıyı aç
                    // INSERT cümleciğini çalıştır
                    // ikinci parametreye dikkat. Burada API'ye talebin body'si ile gelen JSON içeriğini kullanıyoruz.
                    conn.Execute(@"INSERT INTO FIRM (ID,NAME,CITY,SALARY) VALUES (@ID,@NAME,@CITY,@SALARY)", payload);
                    return Ok(payload);
                }
            }
            catch (SQLiteException excp) // Olası bir SQLite exception durumunda HTTP 400 Bad Request hatası verip içerisine exception mesajını gömüyoruz
            {
                return BadRequest(excp.Message); //Bunu production ortamlarında yapmayın. Loglama yapın başka bir mesaj verin. Exception içerisinde koda ve sorguya dair ipuçları olabilir.
            }
        }

        // Güncelleme işlemleri için kullanacağımız metot
        [HttpPut()]
        public IActionResult Put([FromBody] Firm payload)
        {
            try
            {
                using (var conn = new SQLiteConnection(conStr))
                {
                    conn.Open(); // bağlantıyı aç
                    // UPDATE cümleciğini çalıştır
                    // Parametreler diğer metodlarda olduğu gibi @ sembolü ile başlayan kelimelerden oluşuyor
                    // Bu parametrelere değer atarken anonymous type de kullanabiliyoruz.

                    //TODO Aslında gelen JSON içeriğinde hangi alanlar varsa sadece onları güncellemeye çalışalım
                    var result = conn.Execute(@"UPDATE FIRM SET NAME=@firmName,CITY=@firmCity,SALARY=@firmSalary WHERE ID=@firmId",
                        new
                        {
                            firmName = payload.Name,
                            firmCity = payload.City,
                            firmSalary = payload.Salary,
                            firmId = payload.ID
                        });
                    if (result == 1)
                        return Ok(payload); // Eğer güncelleme sonucu 1 ise (ki ID bazlı güncelleme olduğundan 1 dönmesini bekliyoruz) HTTP 200
                    else
                        return NotFound(); // ID değerinde bir firma yoksa HTTP 404
                }
            }
            catch (SQLiteException excp) // Olası bir SQLite exception durumunda HTTP 400 Bad Request hatası verip içerisine exception mesajını gömüyoruz
            {
                return BadRequest(); // HTTP 400 
            }
        }

        // Silme operasyonları için çalışan metot
        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            using (var conn = new SQLiteConnection(conStr))
            {
                conn.Open(); // bağlantıyı aç
                var result = conn.Execute(@"DELETE FROM FIRM WHERE ID=@firmId",new { firmId = id });
                if (result == 1)
                    return Ok(); // Eğer silme operasyonu başarılı ise etkilenen kayıt sayısı (ki bu senaryoda 1 bekliyoruz) 1 döner HTTP 200
                else
                    return NotFound(); // Aksi durumda bu ID de bir kayıt yoktur diyebiliriz. HTTP 404
            }
        }
    }
}

HTTP'nin Post, Put, Delete ve Get metodlarının tamamına yer verdik. Tüm operasyonlarımız IActionResult ve türevlerini döndürmekte. Pek çok noktada REST servis standartlarını yakalamaya çalışıp HTTP 200, HTTP 404 gibi mesajların karşılıkları olan Ok, NotFound benzeri metod çağrılarına yer vermekteyiz.

Kodu incelerken Dapper'ın nerede devreye girdiği sorusunu aklınızı kurcalayabilir. SQLiteConnection nesne örneği üzerinden uygulanan Query ve Execute metodları, Dapper'a ait genişletme fonksiyonlarıdır. Yani sorguların SQLite'a iletilmesi noktasında Firm entity örnekleri ile temas edilen yerlerde çalışmaktadır.

Esasında açık kaynak olan bu mikro çatıyı derinlemesine incelemekte fayda var. Nitekim dinamik parametre seçeneği ile SQL injection saldırılarından koruma gibi imkanlar da sunuyor. Üstelik bir şekilde performansı oldukça iyi. Kendim test edip sonuçları görmediğim için kesin bir şey diyemiyorum ama neyi nasıl yapıyor ya da diğerlerinden hangi noktada farklılaşıyor araştırıp öğrenmek lazım. Github kodlarını incelemek bu açıdan önemli.

Çalışma Zamanı

Kod tarafı tamamlandığına göre servisi test etmeye başlayabiliriz. Uygulamayı terminalden

dotnet run

komutu ile çalıştırdıktan sonra Postman veya muadili bir aracı kullanarak API fonksiyonellikleri denenebilir. Örnek bir veri girişiyle başlayalım. Adres, HTTP metodu ve gövdede göndereceğimiz JSON içeriği şöyle olsun;

http://localhost:5404/api/Firms 
POST
{"Id":21,"Name":"Dust&Dones Guitars","City":"Detroit","Salary":5250000}

Sonuç aşağıdaki gibi olmalıdır.

Aynı ID ile tekrar giriş yapmak istersek Primary Key alanı nedeniyle bir çalışma zamanı hatası alınması gerekir. Nitekim tekillik ihlal edilmektedir.

Şimdi de belli bir şehirdeki şirketleri listeleyelim.

http://localhost:5404/api/Firms/Detroit
GET

Tavsiye edilen bir servis çağrımı olmamakla birlikte tüm firmalar(1000 satırlık veriyi birden vermek çok da anlamlı olmaz gerçek hayat senaryolarında) için aşağıdaki talebi kullanalım.

http://localhost:5404/api/Firms
GET

ID bazlı bir güncelleme ile testlere devam edebiliriz.

http://localhost:5404/api/Firms
PUT
{"Id":55,"Name":"Queen Marry Music LTD","City":"London","Salary":4350000}

Son olarak ID bilgisiyle bir firmayı silerek testlerimizi tamamlayalım.

http://localhost:5404/api/Firms/103
DELETE

Tabi o ID için bir kayıt yoksa HTTP 404 NotFound döndürdüğümüzü de görmemiz lazım.

Ben Neler Öğrendim?

Testler tamamlandı. Eğer buraya kadar geldiyseniz neler öğrendiğinizi düşünmeye çalışabilirsiniz. Ben Web API, ORM ve SQLite tarafına çok da yabancı olmadığımdan bazı şeyleri tekrar etmiş gibi oldum. Lakin SQLite ve Dapper araçlarını deneyimleme fırsatı bulduğum için bir kaç şey de öğrendim.

  • Dapper Micro ORM aracının .net core tarafında nasıl kullanıldığını
  • CRUD(Create Read Update Delete) operasyonlarını SQLite ile çalıştırmayı
  • IActionResult/ActionResult ile Web API metodlarından nasıl sonuçlar dönebileceğimizi ve bu sayede REST standartlarına nasıl uyacağımızı

22 numaralı örnekte mikro ORM araçlarının gözlerinden olan Dapper'ı, SQLite tabanlı olacak şekilde .Net Core 2 ile yazılmış bir Web API projesinde deneyimlemeye çalıştık. Üstüne fazlasını katmak sizin elinizde. Hoş bir arabirimin bu servisi kullanması sağlanabilir. Örneğin progressive tarzda bir web uygulaması denenebilir. SQLite tarafı bir bulut hizmetine devredilebilir. Belki de servis dockerize edilerek aynı bulut sistemi üzerinde konuşlandırılabilir. Senaryoları çeşitlendirmek mümkün. Günümüz mini mikro servis uyarlamaların çoğunda bu tip ORM araçlarının kullanıldığı da ortada diyerek son mesajımı vereyim. Böylece geldik pratik kazandıran bir cumartesi gecesi derlemesinin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Microsoft Custom Vision Servisini Python ile Kullanmak

$
0
0

Yandaki resme baktığınızda aklınıza gelen ilk şey nedir? Bir surat? Belki de bir kurbağa. Kedi olabilir mi? Bu mürekkep baskısı gösterildiği kişide yarattığı algıyı anlamak için kullanılan Rorschach(Roşa olarak okunuyormuş) isimli psikolojik testten. Ünlü İsviçreli psikiyatrist Hermann Rorschach(1884-1922) tarafından geliştirilen test özellikle kişilik tahlili ve şizofreni vakalarında kullanılmakta. Sonuçların manipule edilmesinin zorluğu nedeniyle adli vakalarda ve hatta kariyerle ilgili kişilik testlerinde bile ele alınmakta. Hermann yandakine benzer kırk mürekkep baskısı tasarlamış. Kaynaklardan öğrendiğim kadarıyla doktorlar bu setteki kartların neredeyse yarısını kullanıp kişinin o anda nevrotik veya psikotik olup olmadığını anlayabiliyormuş. Tabi konunun uzmanı olmadığım için ancak giriş hikayemde kullanabilecek kadar bilgi aktarabiliyorum.

Filmlerde ve internette sıklıkla gördüğümüz bu mürekkep baskılarına az çok aşinayızdır. Peki bu fotoğrafa baktığında bir yapay zeka ne düşünür? Onun tamamen rasyonel olan dünyasında duygulara yer olmadığını varsayarsak tüm yapay zekalar için sonuç aynı mı olacaktır? Duygusal zeka ile donatılmış bir yapay zekanın tepkimeleri çeşitlilik gösterir mi? Sanırım onu bu resimlerle ilgili yeterince iyi eğitirsek düşüncelerini kolayca öğrenebiliriz. Tabii o cumartesi gecesi çalışmasında ben Rorschach resimlerini sınıflandıracak bir yapa zeka servisi aramak yerine elimdeki Lego fotoğraflarını öğretebileceğim birisine bakıyordum. Sonunda Microsoft'un Custom Vision servisini incelemeye karar verdim. Öyleyse derlememize başlayalım.

Amacım, Microsoft Azure platformunda yer alan ve fotoğraf/nesne sınıflandırmaları için kullanılabilen Custom Vision servisini basit bir Python uygulaması ile deneyimlemek. Custom Vision API geliştircilere kendi fotoğraf/nesne sınıflandırma servislerini yazma imkanı sunuyor. Onu, imajları belli karakteristik özelliklerine göre çeşitli takılar(tag) altında sınıflandırıp sıralayan bir AI(Artificial Intelligence) servisi olarak düşünebiliriz.

Örnek çalışmada belli takılar için belli sayıda imajı sisteme öğretmeye çalışacağız(Custom vision api için bu oran en az iki tag ve her bir tag için en az beş fotoğraf/nesne şeklinde) Öğretiyi tamamladıktan sonra sisteme bir fotoğraf gösterip ne olduğunu tahmin etmesini isteyeceğiz. Sistem bizim öğrettiklerimize göre bir tahminlemede bulunacak ve yüzdesel değerler verecek. Son olarak bu sonuçları sınıfla birlikte tartışacağız :P

Ön Hazırlıklar

Her zaman olduğu gibi ben uygulamayı Python SDK'sini kullanarak WestWorld(Ubuntu 18.04, 64bit)üzerinde geliştiriyorum. Platform bağımsız olarak Python ve pip aracının sistemde yüklü olması gerekiyor. Python tarafından Custom Vision API hizmetini kullanabilmek için ilgili paketin yüklenmesi lazım. Aşağıdaki terminal komutu ile bunu yapabiliriz.

pip install azure-cognitiveservices-vision-customvision

Custom Vision API için Credential Bilgilerinin Alınması

Diğer pek çok 3ncü parti serviste olduğu gibi istemci tarafının ilgisi servisi kullanmasını sağlayacak bir ehliyete(Credentials) sahip olması gerekiyor. Bu nedenle servis için abone olmamız ve uygulama anahtarını almamız lazım. İlk olarak şu adrese gidip login olmalıyız. Ardından Create new project sekmesini kullanarak yeni bir proje oluşturmalıyız. Ben buradaki ayarları varsayılan değerlerinde bırakıp CIA çakması bir proje oluşturdum. Buna göre projemiz sınıflandırma görevini üstleniyor. Sınıflandırılmaya tabi olan tipler birden fazla takıyla işaretlenebilir. Özel bir domain belirtmedik ama ihtiyaca göre bu seçenek general haricindekilerden birisi de olabilir.

Proje oluşturulduktan sonra özelliklerine ulaşıp bizim için üretilen Training Key ve Prediction Key değerlerini almamız gerekiyor. Bu bilgiler istemci tarafı için gerekli.

Kodun çalışma dinamiklerini anlamadan önce Custom Vision API için ilk başta oluşturduğumuz projeyi tarayıcıdan denemenizi öneririm. Belirtildiği gibi elinizdeki imajları en az 2 farklı tag ile eşleşecek şekilde ayrıştırıp sisteme yükleyin. Sonra eğitim programını(training kısmı) başlatın ki bu işi Azure tarafı halledecek. Program işleyişini tamamlayınca bir kaç imaj yükleyip hangi takılardan yüzde kaç oranında karşılandığına bakın. Örnek kümesi zenginleştikçe tahminlerin doğruluk oranları da yükselecektir.

Kodlama ve Çalışma Zamanı

Azure tarafından Vision servis için ehliyetimizi aldığımıza göre python tarafını kodlamaya başlayabiliriz. İki python dosyamız var. pgadget.py isimli olanı fotoğraf eğitimi için kullanıyoruz. client.py ise servisi tüketip sonuçları almak için çalıştırılıyor.

pgadget kodlarımıza gelince;

# -*- coding: utf-8 -*-

# Custom Vision API'sini kullanabilemek için gerekli modüllerin bildirimi ile işe başladık
from azure.cognitiveservices.vision.customvision.training import CustomVisionTrainingClient
from azure.cognitiveservices.vision.customvision.training.models import ImageFileCreateEntry

# Eğitici servise ait endpoint bilgisi
apiEndpoint = "https://southcentralus.api.cognitive.microsoft.com"

# Bizim için üretiken traning ve prediction key değerleri
tKey = "c3a53a4fb5f24137a179f0bcaf7754a5"
pKey = "bf7571576405446782543f832b038891"

# Eğitmen istemci nesnesi tanımlanıyor. İlk parametre traning_key
# ikinci parametre Cognitive servis adresi
coach_Rives = CustomVisionTrainingClient(tKey, endpoint=apiEndpoint)

# Projeyi oluşturuyoruz
print("Lego projesi oluşturuluyor")
legoProject = coach_Rives.create_project("Agent_Leggooo")  # projemizin adı

# Şimdi deneme amaçlı tag'ler oluşturup bu tag'lere çeşitli fotoğraflar yükleyeceğiz
technic = coach_Rives.create_tag(legoProject.id, "technic")
city = coach_Rives.create_tag(legoProject.id, "city")

# Aşağıdaki tag'ler şu anda yorum satırı. Bunları açıp, create_images_from_files metodlarındaki tag_ids dizisine ekleyebiliriz.
# Ancak Vision servisi her tag için en az beş adete fotoğraf olmasını istiyor. Bu kümeyi örnekleyemediğim için sadece iki tag ile ilerledim.

'''
helicopter = coach_Rives.create_tag(legoProject.id, "helicopter")
truck = coach_Rives.create_tag(legoProject.id, "truck")
yellow = coach_Rives.create_tag(legoProject.id, "yellow")
plane = coach_Rives.create_tag(legoProject.id, "plane")
car = coach_Rives.create_tag(legoProject.id, "car")
racecar = coach_Rives.create_tag(legoProject.id, "racecar")
f1car = coach_Rives.create_tag(legoProject.id, "f1car")
crane = coach_Rives.create_tag(legoProject.id, "train")
building = coach_Rives.create_tag(legoProject.id, "building")
station = coach_Rives.create_tag(legoProject.id, "station")
orange = coach_Rives.create_tag(legoProject.id, "orange")
'''

file_name = "Images/technic/choper.jpg"
with open(file_name, mode="rb") as image_contents:
    coach_Rives.create_images_from_files(legoProject.id, [ImageFileCreateEntry(
        name=file_name, contents=image_contents.read(), tag_ids=[technic.id])])

file_name = "Images/technic/f1car.jpg"
with open(file_name, mode="rb") as image_contents:
    coach_Rives.create_images_from_files(legoProject.id, [ImageFileCreateEntry(
        name=file_name, contents=image_contents.read(), tag_ids=[technic.id])])

file_name = "Images/technic/truck.jpg"
with open(file_name, mode="rb") as image_contents:
    coach_Rives.create_images_from_files(legoProject.id, [ImageFileCreateEntry(
        name=file_name, contents=image_contents.read(), tag_ids=[technic.id])])

file_name = "Images/technic/truck_2.jpg"
with open(file_name, mode="rb") as image_contents:
    coach_Rives.create_images_from_files(legoProject.id, [ImageFileCreateEntry(
        name=file_name, contents=image_contents.read(), tag_ids=[technic.id])])

file_name = "Images/technic/vinc.jpg"
with open(file_name, mode="rb") as image_contents:
    coach_Rives.create_images_from_files(legoProject.id, [ImageFileCreateEntry(
        name=file_name, contents=image_contents.read(), tag_ids=[technic.id])])

file_name = "Images/city/plane.jpg"
with open(file_name, mode="rb") as image_contents:
    coach_Rives.create_images_from_files(legoProject.id, [ImageFileCreateEntry(
        name=file_name, contents=image_contents.read(), tag_ids=[city.id])])

file_name = "Images/city/policestation.jpg"
with open(file_name, mode="rb") as image_contents:
    coach_Rives.create_images_from_files(legoProject.id, [ImageFileCreateEntry(
        name=file_name, contents=image_contents.read(), tag_ids=[city.id])])

file_name = "Images/city/porsche.jpg"
with open(file_name, mode="rb") as image_contents:
    coach_Rives.create_images_from_files(legoProject.id, [ImageFileCreateEntry(
        name=file_name, contents=image_contents.read(), tag_ids=[city.id])])

file_name = "Images/city/racecar.jpg"
with open(file_name, mode="rb") as image_contents:
    coach_Rives.create_images_from_files(legoProject.id, [ImageFileCreateEntry(
        name=file_name, contents=image_contents.read(), tag_ids=[city.id])])

file_name = "Images/city/snowmobile.jpg"
with open(file_name, mode="rb") as image_contents:
    coach_Rives.create_images_from_files(legoProject.id, [ImageFileCreateEntry(
        name=file_name, contents=image_contents.read(), tag_ids=[city.id])])

# Fotoğrafları çeşitli tag'ler ile ilişkilendirdiğimize göre öğretimi başlatabiliriz

print("lego fotoğraflarım için eğitim başlıyor")
iteration = coach_Rives.train_project(legoProject.id)
while (iteration.status != "Completed"):
    iteration = coach_Rives.get_iteration(legoProject.id, iteration.id)
    print("Durum..." + iteration.status)

coach_Rives.update_iteration(legoProject.id, iteration.id, is_default=True)
print("Eğitim tamamlandı...")

client.py

# -*- coding: utf-8 -*-

# Bu kod ile test klasöründe yer alan imajları custom vision api servisine sorgulatıyoruz

import requests  # HTTP Post talebini gönderirken kullanacağımız modül
import os  # Klasördeki dosyaları okumak için kullandığımız modül
import filetype  # Dosya tipi kontrolü için ekledik.

# tahminleme servisine ait endpoint
prediction_url = "https://southcentralus.api.cognitive.microsoft.com/customvision/v2.0/Prediction/334ee5e4-4fc8-4a5f-a209-a145ef857dcb/image"
# Servisi kullanabilmek için gerekli API Key
prediction_key = "bf7571576405446782543f832b038891"
# HTTP Post header bilgilerimiz
headers = {"Prediction-Key": prediction_key,
           "content-type": "application/octet-stream"}

files = os.listdir('./Images/test')  # test klasöründeki dosyaları alıyoruz
for f in files:
    filepath = os.path.join('./Images/test', f)
    extension = filetype.guess(filepath).extension # dosya tipini kontrol etmek için bakıyoruz
    if extension == 'jpg': # sadece jpg tipinden dosyalarla çalışıyoruz
        # sıradaki dosyayı binary olarak okuyoruz
        fileData = open(filepath, 'rb').read()
        # Post talebini gönderiyor ve cevabı response değişkenine atıyoruz
        result = requests.post(url=prediction_url,
                               data=fileData, headers=headers)
        print(f)
        for i in range(0, 2):  # taglerimize göre tahminleme bilgilerini okuyoruz
            print(result.json()['predictions'][i]['tagName'])
            print(result.json()['predictions'][i]['probability'])

İlk örnekte belli karakteristiklerine göre lego imajlarını sınıflandırmaya çalışıyoruz. Koddaki tag yapısı buna göre kurgulandı. Birinci örneği çalıştırmak için aşağıdaki terminal komutunu kullanabiliriz.

python pgadget.py

Local makinedeki sonuçlar şöyle olacaktır.

Azure projesine gidersek de aşağıdaki sınıflandırmalarla karşılaşırız.

Görüldüğü üzere fiziki depolama alanından seçilen fotoğraflar ilgili Azure projesine yüklendiler ve hatta iki kategori ile de tag bazında ilişkilendirildiler. Bu haliyle proje özetine baktığımızda şu sonuçları görürüz.

Artık servisimize bir fotoğraf gönderip ne olduğunu tahmin etmesini isteyebiliriz. Bu çok basit anlamda Postman gibi bir araçla da olabilir, tercih ettiğimiz programlama diliylede.

Postman ile Test

Oluşturduğumuz eğitmeni test etmek için bize açılan prediction API servisini kullanmak ve Postman üzerinden basit bir POST talebi göndermek yeterlidir(Kendi örneğinizle ilgili servise ait adres bilgisini site ayarlarından bulabilirsiniz)

Postman ayarlarında Header kısmında ki bilgileri de aşağıdaki gibi doldurmalıyız. Sonuçta ehliyetimizi göstermemiz gerekiyor. Bu nedenle Prediction-Key değerini girmemiz şart.

Ben WestWorld'de bulunan bir imajı deneme amaçlı olarak göndermek istediğimden Body kısmında Binary seçeneğini kullandım.

Deneme olarak kullandığım fotoğraf ise şuydu. Hani şimdilerde almaya kalksak bir yıl öncesine göre neredeyse iki katından fazla para vermek zorunda olduğumuz bir kutu ne yazık ki :(

Tahminleme servisim bu fotoğraf için aşağıdaki sonuçları verdi. %99 ihtimalle Lego City olduğunu ifade ediyor. Oldukça başarılı ;)

{
    "id": "eb190c6e-57a9-404f-ab58-ca106afc895e",
    "project": "334ee5e4-4fc8-4a5f-a209-a145ef857dcb",
    "iteration": "82712433-8e16-4e1a-9c52-7d1dc108085f",
    "created": "2019-02-25T11:12:04.3505905Z",
    "predictions": [
        {
            "probability": 0.9998578,
            "tagId": "0e0fe67a-9377-426b-88db-98081766c042",
            "tagName": "city"
        },
        {
            "probability": 0.0000050534627,
            "tagId": "d6ea80b8-8f44-488a-9e18-20227ef70fd2",
            "tagName": "technic"
        }
    ]
}

Alakalı alakasız fotoğraflar ile örneği denemekte yarar var. Eğitmene ne kadar çok örnek anlatır ve tag kullanırsak tahminleme sonuçları da o oranda başarılı olacaktır. Tabi sistemi yanılgıya da düşürmeliyiz. Söz gelimi bir pırasa resmi göstersek ne yapar şu kıt bilgisiyle, sorarım?

Python Kodları ile Test Etmek

Postman ile test yapmak işin kolay yollarından birisi. Diğer yandan client.py isimli uygulamayı çalıştıraraktan da denemeler yapabiliriz. Bu uygulama test klasörü altındaki imajları tarar ve her biri için POST talebi göndererek tahminleme sonuçlarını ekrana basar (Eğitime tabii olan örnek fotoğraflar images klasörü altında yer alıyor. Github üzerinden alıp kullanabilirsiniz)

python client.py

Test klasöründeki imajlar için aşağıdaki sonuçlar elde edildi.

Einstein ve havadaki uçak için çok başarılı tahminlemeler yapılmadığını görebiliriz. Bunun sebebi eğitmeni sadece 10 imajla yetiştirmiş olmamızdır. Yani görüp gördüğü ve yorumladığı küme çok sığ. Örnek kümeyi ve tag yapısını ne kadar geniş tutarsak tahminleme oranlarında o kadar isabetli sonuçlar elde ederiz. Bunu sanırım üçüncü kez söyledim :S

Bu arada dosya tip kontrolü için client.py'de filetype modülünü kullandık. Yüklemek için terminalden pip install filetype yazmamız yeterli.

Ben Neler Öğrendim?

Doğruyu söylemek gerekirse böyle hazır Cognitive servislerle eğlencesine de olsa örnek çalışmalar yapmak son derece keyifli. Sonuçta kafam AI dünyasına basmadığı için sınıflandırma algoritmalarını yazmaya çalışmak yerine onu ele alan servisleri kullanmak daha cazip geliyor. Benim bu çalışmada torbama kattıklarımı ise şöyle özetleyebilirim.

  • Vision API'ye bir fotoğrafı nasıl öğretebileceğimi
  • Python ile kod tarafında bunu nasıl yapabileceğimi
  • Temel olarak Azure Custom Vision Service'in AI çalışma mantığını (bir takı için en az beş örnekten oluşan fotoğraf kümeleri oluştur. Ne kadar çok olursa o kadar iyi olur. Bu nedenle koddaki gibi imgeleri tek tek öğretmek yerine, bir klasör altına n tane imge koyup onları bir tag ile ilişkilendirmek daha mantıklı)
  • Oluşturulan servisin python tarafında nasıl tüketilebileceğini
  • Python tarafında request modülünü kullanarak HTTP Post talebinin nasıl yapılabileceğini
  • request modülü kullanılırken Header ve Data bilgilerinin nasıl eklendiğini
  • Bir klasördeki dosyaları nasıl dolaşabileceğimi

Böylece geldik 26 numaralı saturday-night-works derlemesinin sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.


React Üzerinde Socket.IO Kullanımı

$
0
0

Bir zamanlar sıkı bir Formula 1 izleyicisiydim. O dönemde dünyanın bir numaralı pilotu üç kez F1 dünya şampiyonu olan Ayrton Senna'ydı. Yağmurlu havalardaki ustalığı nedeniyle Rainman lakabını almış bir yarışçı olmakla birlikte virajları hız kesmeden dönmeyi becerirdi. Monaco yarışından bir görüntüsü geldi şimdi gözümün önüne. Sağ eliyle kokpitin sağındaki vitesi sol eliylede direksiyonu tutuyordu.

Kullandığı McLaren Honda MP4/4 marka aracın 1987'de 950 beygir güç üreten bir canavar olduğunu düşününce o hızlarda o yağmurlu havalardaki sürüş tekniği ile gelmiş geçmiş en iyi yarışçı olduğunu adeta ispat ediyordu. İlk başarılarını Lotus ile eden Senna'nın en büyük rakiplerinden birisi Williams takımından Alain Prost'tu(ki bir dönem McLaren'de takım arkadaşı da oldular) Lise yıllarıma denk gelen bu iki kahramanın özellikle kullandıkları canavarların dev posterleri oda duvarlarımı süslerdi. Yarışçı olmak gibi bir hayalim yoktu ama onların meydan okuyuşları, takımlarının otomobil dünyasındaki öncülükleri ilgimi çekiyordu. 

I'll be honest with you; I was never a Senna fan. I always thought Gilles Villeneuve was the greatest racing driver of them all. But, to make this film, I've watched hours and hours and hours of footage. And the thing is, Villeneuve was spectacular on a number of occasions. Senna...He was spectacular every single time he got in a car.

Size karşı dürüst olacağım; Asla bir Senna hayranı olmadım. Her zaman Gilles Villeneuve'un hepsinin en iyi yarış pilotu olduğunu düşündüm. Ancak, bu filmi yapmak için saatlerce, saatlerce ve saatlerce çekimleri izledim. Mesele şu ki, Villeneuve birkaç kez muhteşemdi. Senna...Arabasında geçerdiği her anda muhteşemdi.

Jeremy Clarkson, 2010, Top Gear, Series 15, Episode 5

Senna, ne yazık ki 1 mayıs 1994 günü henüz 34 yaşındayken San Marino grand prixinde İmola pistinde geçirdiği kaza sonucu hayatını kaybetmişti. Sonraki yıllarda Formula 1 yarışlarına olan ilgim epeyce azaldı. Şimdilerde televizyondaki canlı yayınlarını bile izlemiyorum desem yeridir. Ama arada bir baktığımda en çok dikkatimi çeken şey bilgisayar oyunlarındakine benzer ekranlar oluyor. Aracın iç kamerasından gelen sürüş görüntüleri üzerine eklenen anlık hız, ivme, vites vb bilgilerin sunulduğu grafikler gerçekten müthiş. Üstelik bu verilerin neredeyse hiç gecikme yaşanmadan ekrana ulaşması da bende hayranlık uyandıran başka bir konu. Bu grafikler tekrar nasıl mı gündeme geldi? İzin verin anlatayım.

Bir süre önce eski bir meslektaşım OBD2 portlarından nasıl bilgi okunabileceğini sormuştu. Bu konuyu araştırırken kendimi çok farklı bir yerde buldum. OBD2 portu ile bir arabadan veri almak mümkün. Peki bir yarış sırasında tüm araçların hız, motor sıcaklığı, anlık devir vb bilgilerini bu şekilde bir yerlere aktarabildiğimizi düşünsek. Bu verileri yarışı mobil uygulamalarından takip edenlere anlık gönderimi için nasıl bir yol izleyebiliriz? İşte araştırma sırasında geldiğim nokta buydu. Donanımsal gereksinimleri bir kenara bırakırsak bunun minik bir POC(Proof of Concept)çalışmasını yapmak istedim.

En ideal senaryolardan birisi Web Socket kullanmaktı. Socket.IO kütüphanesi bu amaçla değerlendirilebilirdi. Bir yarış aracının WebSocket haberleşmesi ile veri yayınlayacağını düşünelim. Haberleşme ağı üzerinde olan başka bir sunucu uygulama ile araç verileri abone olan istemcilere gönderilecek. Veri yayıncısı ve broadcast yönetimini üstlenecek sunucu için Node.js, görsel arayüzle yarış araçlarının gönderilen bilgilerine bakacak istemci tarafı içinse bir React uygulaması geliştirmeye karar verdim. E ne duruyoruz öyleyse. Kodlamaya başlayalım.

Bizim senaryomuzda tek bir yarış aracının bilgi yayınladığını varsayıyoruz. Şekildeki gibi n sayıda aracın ve dinleyicinin olduğu bir senaryoda, yayın yapan araçların verilerini diğerleri ile karışamayacak şekilde konsolide ederek göndermemiz gerekir ki istemciler n sayıda aracın verisini ya da istedikleri belli bir aracın verisini kullanabilsin.

Ön Hazırlıklar

Örneği her zaman olduğu gibi WestWorld(Ubuntu 18.04, 64bit)üzerinde geliştirmekteyim. Sistemde node.js, npm ve react projesi oluşturmak için gerekli ekipmanlarım mevcut. Dolayısıyla hızlı bir şekilde proje iskeletini aşağıdaki terminal komutlarını kullanarak oluşturabilirim/oluşturabiliriz.

mkdir Zion
mkdir VehicleDataPublisher
cd Zion
npm init
npm i --save express socket.io
touch server.js
cd ..
cd VehicleDataPublisher
npm init
npm i --save socket.io-client
touch index.js
cd ..
sudo npx create-react-app dashboard
sudo npm i --save react-d3-speedometer save socket.io-client

Klasör yapısından ve içindeki uygulamalardan bahsetmek yarar var. Zion isimli klasörde sunucu uygulama kodlarımız olacak(Aslında yayıncı ve aboneler arasında bir veri aktarım organı) Veri yayını yapan uygulamamız burayı kullanacak. Sunucu, kendisini dinleyenlere ilgili verileri yayınlayacak. Web Sockets tabanlı bir iletişim söz konusu. Bu nedenle express ve socket.io paketleri kullanılıyor. VehicleDataPublisher uygulaması sembolik olarak veri yayını yapan program kodlarını içeriyor(Yani yarış aracımızdan veri gönderen parçayı taklit ediyor) Socket sunucusu ile haberleşmesi gerektiğinden socket.io-client paketini kullanıyor. Son olarak dashboard isimli bir react uygulamamız var. Bunu yarış aracından yayınlanan veriyi grafik formatında göstermek için kullanacağız. Bu nedenle react-d3-speedometer(gerekirse benzerleri) paketini içeriyor. Pek tabii bu uygulama soket dinleyicisi olarak sunucu ile konuşmak durumunda. Bu nedenle socket.io-client paketini de referans ediyor. Son olarak react uygulamasını oluşturmak için npx paket çalıştırıcısından yararlandığımızı da belirteyim.

Gelelim Kodlarımıza

İskeletimiz hazır. Artık gerekli kodlamaları yapabiliriz. İşe Zion projesindeki server.js dosyasını yazarak başlayalım. Daha önceki derlemelerde olduğu gibi kodları aralardaki yorum satırları ile mümkün mertebe anlatmaya çalıştım.

/*Önce sunucu için gerekli modülleri ekleyelim.
Socket.Io kullanımı için socketIo modülü kullanılıyor.
Web server ve http özellikleri içinse epxress ve http modülleri.
*/

const http = require("http");
const express = require("express");
const socketIo = require("socket.io");

const app = express(); // express nesnesini örnekle
const appServer = http.createServer(app); // express'i kullanan http server oluşturuluyor
const channel = socketIo(appServer); // Socket.io middleware'e ekleniyor.

// ya çevre değişkenlerinden gelen port bilgisini ya da 5555 portunu kullanıyoruz
const port = process.env.PORT || 5555;

// Yeni soketler için connection isimli bir olay dinleyici açılıyor.
// İstemci connection namespace'ini kullanarak bağlanıyor
channel.on("connection", socket => {
    console.log(`${Date(Date.now()).toLocaleString()}: yeni bir istemci bağlandı`);
    // TODO: İstemci hakkında daha fazla bilgiyi nasıl alabilirim? IP adresi gibi.

    // gelen veriyi dinleyeceğimiz bir olay metodu olarak düşünebiliriz.
    // bir publisher sokete veri yolladığında devreye giriyor
    // Yayıncı, "input road" isimli namespace'den yararlanarak veri gönderebiliyor
    socket.on("input road", (data) => {

        console.log(`${Date(Date.now()).toLocaleString()}:Gelen veriler\n\tHız:${data.speed}\n\tDevir:${data.rpm}\n\tMotor sıcaklığı:${data.heat}`);
        // gelen veriyi, göndericiyi hariç tutaraktan bağlı olan ne kadar dinleyici varsa onlara yolluyoruz.
        // aslında bir broadcast yayın yapıyoruz diyebiliriz.
        // istemcilere yayın "output road" isimli namespace üzerinden yapılıyor.
        // emit metodunun ikinci parametresinde, yayıncının yolladığı verinin serileştirilerek kullanıldığını görebilirsiniz.
        socket.broadcast.emit("output road", { engineData: data });  // burası callback metodumuz olarak düşünülebilir
    });

    // istemcilerin bağlantı kesmelerini ele aldığımız olay
    // Bu kez "disconnect" isimli bir namespace söz konusu
    // disconnect, socket.io için rezerve edilmiş anahtar kelimelerden.
    socket.on("disconnect", () => {
        /* 
        Burada çeşitli temizleme operasyonları yapılabilir.
        Mesela istemcinin geliş gidiş hareketlerini takip ediyorsak,
        burada state değişikliği yaptırtabiliriz.
        */
        console.log(`${Date(Date.now()).toLocaleString()}istemci bağlantıyı kapattı`);
    });
});

// Sunucuyu ayağa kaldırıyor ve dinlemeye başlıyoruz
appServer.listen(port, () => {
    console.log(`${Date(Date.now()).toLocaleString()}: Sunucu ${port} nolu port üzerinden aktif konumda.`);
});

Araçla ilgili veri yayını yapan VehicleDataPublisher projesindeki index.js içeriğini de aşağıdaki gibi yazalım.

/*
Bu kodun aslında bir araç üzerinde olduğunu varsayalım.
*/

// soket sunucusuna bağlantı oluşturuyoruz
// socket.io-client modülünü kullanıyoruz
let socket = require('socket.io-client')('http://localhost:5555');

// örnek simülasyon verimiz. Hız, devir ve motor sıcaklığı gibi
let engineData = {
    "speed": 0,
    "rpm": 0,
    "heat": 0
};

// Her 5 saniyede bir çalışacak bir fonksiyon.
setInterval(function () {
    // Rastgele veriler üretiyoruz.
    engineData.speed = getRandomValue(70, 180);
    engineData.rpm = getRandomValue(1000, 10000);
    engineData.heat = getRandomValue(100, 500);

    console.log(`Üretilen veri\nHız:${engineData.speed}\nDevir:${engineData.rpm}\nMotor sıcaklığı:${engineData.heat}`);
    /* 
        Veriyi emit metodu ile "input road" namespace'ini kullanarak sunucuya yolluyoruz
        oradaki callback'de devreye girip bu veriyi bağlı olan diğer istemcilere 
        (output road, namespace'ini kullanan) yayınlayacak.
    */
    socket.emit("input road", engineData);
}, 5000);

/* 
    Rastgele veri üertmek için kullandığımız basit fonksiyon.
    İki değer aralığında veri üretiyor.
*/
function getRandomValue(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
}

Verileri grafiksel ortamda gösterecek olan dashboard isimli react uygulamasının app.js dosyası da şu şekilde tasarlanabilir.

import React, { Component } from 'react';
import socketIOClient from "socket.io-client";
import ReactSpeedometer from "react-d3-speedometer";
/*
React uygulaması broadcast dinleyicisi rolünde.
Socket.io-client modülünü bu nedenle referans ediyor.
Ayrıca görsel metrikler için react-d3-speedometer paketini kullanıyor.
*/

class App extends Component {
  constructor() {
    super();

    // state değişkenlerimizde hızı, sıcaklığı, devri ve endpoint adresini tutuyoruz
    this.state = {
      speed: 0,
      rpm: 0,
      heat: 0,
      endpoint: "http://localhost:5555"
    };
  }

  /*
  componentDidMount yaşam döngüsü düşünüldüğünde
  component Document Object Model'e eklendiğinde devreye giren metodumuz.
  soket bağlantısını gerçekleştirip, "output data" yayınına abone oluyoruz.
  */
  componentDidMount() {
    const { endpoint } = this.state;
    const socket = socketIOClient(endpoint);
    //console.log(`${endpoint} adresine bağlantı yapılıyor...`);
    // output road'dan veri geldikçe bunları state değişkenlerine atıyoruz
    socket.on("output road", data => {
      this.setState({
        speed: data.engineData.speed,
        heat: data.engineData.heat,
        rpm: data.engineData.rpm
      });

      //console.log(`Gelen bilgi : ${data.engineData.speed}`);
    });
  }

  /*
  Bileşenin render edildiği metod.
  state değişkenlerini alıp, div elementindeki ReactSpeedometer kontrollerinde gösteriyoruz.
  */
  render() {
    const { heat } = this.state;
    const { rpm } = this.state;
    const { speed } = this.state;

    return (
      <div style={{ textAlign: "center" }}><h2>Hız</h2><ReactSpeedometer
          maxValue={200}
          minValue={70}
          value={speed}
          needleColor="gray"
          startColor="orange"
          segments={10}
          endColor="red"
          needleTransition={"easeElastic"}
          ringWidth={20}
          textColor={"black"}
        /><h2>RPM</h2><ReactSpeedometer
          maxValue={10000}
          minValue={1000}
          value={rpm}
          needleColor="gray"
          startColor="orange"
          segments={100}
          maxSegmentLabels={10}
          endColor="red"
          needleTransition={"easeElastic"}
          ringWidth={20}
          textColor={"black"}
        /><h2>Motor Isısı</h2><ReactSpeedometer
          maxValue={500}
          minValue={100}
          value={heat}
          needleColor="gray"
          startColor="orange"
          segments={5}
          endColor="red"
          needleTransition={"easeElastic"}
          ringWidth={20}
          textColor={"black"}
        /></div>
    )
  }
}

export default App;

Çalışma Zamanı

Program kodlarımız hazır. Artık uygulamaları çalıştırıp sonuçlarına bakabiliriz. En az 3 terminal penceresi ile ilerlemek lazım. Birisinde sunucu, diğerinde publisher ve sonuncusunda da react tabanlı dinleyici çalıştırılmalı. Aşağıdaki terminal komutu ile sunucu ve veri yayıncılarını başlatabiliriz(Ayrı terminal pencrelerinde tabii ki)

npm run serve

React uygulaması içinse şu komutu kullanabiliriz.

npm run start

React uygulaması başlatıldığında http://localhost:3000 adresi tarayıcıda açılır ve app.js'den render edilen html içeriği buraya basılır.

WestWorld üzerinde yakaladığım çalışma zamanına ait iki ekran görüntüsü aşağıda bulabilirsiniz. Aslında göstergeler canlı ortamda hareket ettiklerinden çok daha hoş ve etkileyici bir sonuç ortaya çıkıyor. Veri her 5 saniyede bir yenilenmekte.

Bir başka t anında;

Hepsi bu kadar :) Tabii örneği zenginleştirmek lazım. Benim ki epey aceleye geldi. Mesela senaryonun n sayıda araç(yayıncı) için n sayıda istemcide tekil veya toplu halde çalışabileceği farklı bir versiyonunu yazılabilir. Bu size güzel bir ev ödevi olsun.

Ben Neler Öğrendim?

Bu yoğun çalışmada deneyimlediğim bir çok yeni şey oldu. WebSocket kavramına aşina olsam da onu bu örnekteki gibi daha görünür bir şekilde uygulamak değerliydi. Öğrendiklerimi şu şekilde özetleyebilirim.

  • socket.io ile websocket bazlı iletişim trafiğinin node.js'de nasıl tesis edilebileceğini
  • socket.on olay dinleyicilerinin ne amaçla ele alındığını
  • broadcasting'in nasıl yapıldığını
  • disconnect ve connection namespace'lerinin ayrılmış kelimelerden(reserved words) olduğunu(bunları doğru yazmassak istemciler bağlanamaz veya çevrim dışı olamazlar)
  • node.js tarafında rastgele sayı üretimini
  • belirli periyotlarda sürekli olarak çalışan bir fonksiyonun nasıl yazılacağını
  • yayıncıların abonelere olan mesajları gönderdiğimiz fonksiyonun bir callback metodu olduğunu
  • React bileşeninde state nesne kullanımını
  • React üzerinden web socket haberleşmesinin nasıl yapılabileceğini
  • component DOM'a bağlandığında hangi olay metodunun tetiklendiğini
  • ReactSpeedometer'ın temel kullanımını

Böylece geldik 40 numaralı cumartesi gecesi derlemesinin sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Bir Python Uygulamasını git Tekniği ile Azure Platformuna Taşımak

$
0
0

Rey evrenin taaa bir ucundan kalkıp ahch-to gezegenine gelmiş ve Jedi ustasının onu eğitmesini istemişti. Galaksinin bir kez daha Luke Skywalker'a ihtiyacı vardı. Uzun zamandır inzivada olan Luke ise Kylo Ren'den sonra buna pek gönüllü değildi. İzleyenler bilir. Luke, neredeyse sadece sudan ibaret ahch-to gezegenindeki bir adada, eski Jedi tapınağında yaşamını sürdürmektedir(Sevgili ekşi sözlük yazarı John Harrison bu girişi beğenmeyecektir ama olsun :D )

Peki bu gezegeninin gerçekte nerede olduğunu biliyor musunuz?

Ahch-to, İrlanda'nın Kerry ilinin yaklaşık 11.6 km batısında yer alan Skellig Michael isimli bir ada aslında. Atlantik okyanusunda yer alan bu küçük ada üstünde 6ncı yüzyılda kurulan bir de manastır bulunuyor. Zaten filmde de Luke'un inzivaya çekildiği yer benzer dini ve mistik karakteristikliklere sahip. Tarihi yapısını önemli derecede korumuş ve UNESCO tarafından 1996 yılında Dünya Mirasları arasına alınmış bu adanın benim için anlamı ise yeni bir macera.

Nitekim o cumartesi gecesi Westworld'ü(Ubuntu 18.04, 64bit) kenara koymuş çok sık kullanılan Azure öğretilerinden birisini ahch-to üstünde deniyordum. Öğretinin temel amacı git yardımıyla Azure üzerinden deployment işlemi başlatabilmekti. Ben bunu bir python uygulaması için denemek istiyordum. Bu sefer işim biraz daha zorluydu. Çünkü cumartesi gecesi çalışmalarının ikinci fazını yapmayı planladığım ahch-to(Mac Mini, High Sierra) adasındaydım. Derken düğmeye bastım ve öğretiyi uygulamaya koyuldum. Pek tabii ahch-to gezegeninde eksikler vardı...

Ön Gereksinimler ve Kurulumlar

Geliştireceğimiz örnek python flask paketini kullanan basit bir web uygulaması olacak. ahch-to sisteminde python'un 2.7 sürümü mevcut(ki bu sierra sürümü ile yüklü olarak geldi) lakin ben 3 üzeri bir versiyon kullanmak istiyorum. Bu nedenle homebrew paket yönetim aracından yararlanarak yeni bir kurulum gerçekleştirdim(Tabii homebrew de sistemde yoktu. Nasıl kurulacağının keşfini size bırakıyorum)

brew update
brew install python

Azure CLI Kurulumu

Paket yöneticisini kurduktan sonra Azure tarafındaki işlemler için yararlanılan CLI(command-line interface) aracını da kurmak gerekiyor. Onu kurmak için ilk terminal komutunu kullanabiliriz. Pek tabi kurulum yeterli değil. Azure tarafı ile konuşabilmek için login işlemini de yapmamız lazım. İkinci terminal komutu bunun için(Bu aşamada Azure'da bir hesabınız olduğunu varsayıyorum)

brew install azure-cli
az login

Azure Deployment Hazırlıkları

Uygulamayı Azure tarafına deploy edebilmek için de yapılması gerekenler var. Sırasıyla deployment user, resource group, service plan ve son olaraktan da bir web app oluşturmalıyız. Bunlar için aşağıdaki terminal komutlarını kullanarak ilerleyebiliz.

as webapp deployment user set --user-name dpyl-usr-buraks --password <azure kurallarına uyan bir şifre>
az group create --name rg-todoshero --location westeurope
az appservice plan create --name plan-todoshero --resource-group rg-todoshero --sku B1 --is-linux
az webapp create --resource-group rg-todoshero --plan plan-todoshero --name todosherowebapp --runtime "PYTHON|3.7" --deployment-local-git

İlk satırda dply-usr-buraks isimli bir kullanıcı tanımlıyoruz. Deployment işlemlerini bu kullanıcı yapacak. İkinci satırda veri tabanı, servis planlaması, kurulumu yapılacak uygulamalar gibi bu işimizle ilgili kaynakları grupladığımız bir tanımlama bulunuyor(Örneğin Resource Group'u platformdan kaldırdığımızda bu grup altında hazırladığımız ne kadar Azure enstrümanı varsa silinecektir)İşimizle ilgili kaynakları tek noktadan yönetmek için açtığımızı düşünebilirsiniz. Üçüncü komutta bir servis planı oluşturmaktayız. Burada basic ödeme şartlarına göre oluşturulan ve linux tabanlı docker container kullanılan bir plan söz konusu. Son terminal komutu ile todosherowebapp isimli bir web uygulaması oluşturuyoruz. Bu servis uygulaması biraz önce oluşturlan servis planına göre hazırlanacak ve Python 3.7 sürümü ile çalışacak. Sonda yer alan --deployment-local-git parametresi ile dağıtım planımızı(git ile yapacağımızı) belirtiyoruz.

Terminalden çalıştırdığım komutlar başarılı olunca aşağıdaki sonuçla karşılaştım.

Uygulamanın web adresi todosherowebapp.azurewebsites.net olarak belirlenirken, github repository adresi de https://dply-usr-buraks@todosherowebapp.scm.azurewebsites.net/todosherowebapp.git şeklinde oluştu. Görüldüğü üzere varsayılan bir hoş geldin sayfamız bile var. Hatta doğrudan dokümantasyonlarına ulaşıp ilk geliştirmelerimizi yapabiliriz de (Şu an aktif değil. Malum kullanılmayacak bir servis olacağından sildim)

Uygulamada Yapılanlar

Öncelikle local ortamımızda neler yaptığımız bir bakalım. Python tarafındaki örneğimiz son derece basit. Klasör ve dosya ağacı aşağıdaki gibi oluşturulabilir. Kritik noktalardan birisi requirements.txt dosya içeriği.

cd src
mkdir todayshero
touch todayshero/app.py
touch todayshero/requirements.txt
touch todayshero/.gitignore

Python kodumuzu içeren app.py aşağıdaki gibi yazılabilir. Uygulama, web tarafından route adrese gelen taleplere karşılık içindeki heros isimli listeden rastgele isim döndürmek üzere tasarlanmış durumda.

from flask import Flask # basit web özelliklerini kazandırmak için
from random import seed
from random import randint  # Rastgele sayı üretmek için

app = Flask(__name__)

# bir kahraman listemiz var
heros = ["thor", "wolverine", "iron man", "hulk", "doctor strane"
         "kira", "superman", "batman", "wonder woman"]

# kök adrese talep geldiğinde devreye giren metodumuz
@app.route("/")
def getRandomHero():
    randomIndex = randint(0, len(heros)-1) #0 ile listedeki eleman sayısı aralığında rastgele bir tam sayı üretiyoruz
    return '<h2>'+heros[randomIndex]+'</h2>' # sonucu html olarak dönüyoruz

Requirements.txt, Azure platformunun Python ortamlı deploy işlemi için kullanacağı bir doküman. Bu dosya içerisine yazılan paketler, azure deploy işlemi sırasında pip ile uzak sunucu ortamına yüklenmeye çalışılır. Örneğe göre son derece sade bir içeriğimiz var (:

Flask==1.0.2

Bu arada ahch-to üzerindeki denemeler için flask paketini geliştirme ortamına da yüklememiz gerekiyor.

pip3 install flask

Çalışma Zamanı(Local ortamda)

Kod tarafı hazır olduğuna göre uygulamayı çalıştırıp sonuçlarını değerlendirebiliriz. İlk olarak local makine üzerinden aşağıdaki terminal komutu ile ilerleyelim.

FLASK_APP=app.py flask run

Sayfaya yeni talepler gönderdikçe farklı kahramanlar ile karşılaşmamız gerekiyor. Bu basit uygulama kodu list içeriğinden seçtiği rastgele bir karakterin adını ekrana yazdırmakla görevli(git ve azure ikilisinin bir arada kullanılması üzerine yoğunlaştığımızdan mümkün mertebe basit bir örnek kullanıyoruz)

Çalışma Zamanı(Git Deploy)

Artık programın çalıştığından eminiz. Dolayısıyla asıl dağıtım operasyonuna başlayabiliriz. Bunu git aracılığıyla yapmak için aşağıdaki terminal komutlarını çalıştırmamız yeterli(todoshero klasörü altında)

git init
git remote add azure https://dply-usr-buraks@todosherowebapp.scm.azurewebsites.net/todosherowebapp.git
git add .
git commit -m "Application has been added"
git push azure master

Standart git komutları ile uygulamayı azure reposuna deploy ettik. İlk olarak initialize işlemi var. Sonra uzak repo adresini ekliyoruz. Tüm kod dosyalarını . ile alıp commit ettikten sonra push çağrısı ile değişikliklerimizi yolluyoruz.

push işlemini takiben azure sitesine tekrar gittiğimde python uygulamasının başarılı bir şekilde etkinleştiğini görmemiz lazım. Aşağıdakine benzer bir durum olmalı.

Hatta Azure portaline baktığımızda hem oluşturulan resource group içeriğini hem de yaptığımız son push işlemlerini de görebilmeliyiz.

Hepsi bu! :) Siz de farklı uygulama geliştirme ortamları için(ruby, .net core, node.js vb) aynı kurguyu gerçekleştirmeyi deneyebilirsiniz.

Ben Neler Öğrendim?

Yazılım ürünlerinin resmi sitelerinde yer alan bu tip adım adım serileri konuyu artık çok iyi öğretiyor. Mesleğe ilk başladığım zamanlarda neredeyse yok denecek kadar azdı. Hatta MSDN'in bile ilk yıllarındaki içerik kalitesi oldukça karışıktı. Ancak yeni nesil bu konuda epey şanslı. Peki saturday-night-worksçalışmalarının birinci fazının bu son örneğinde ben neler öğrendim dersiniz.

  • brew ile macOS platformuna paket yüklemeyi
  • azure CLI ile deployment user, resource group, service plan ve web app oluşturmayı
  • git komutları ile kodu azure'a atmayı
  • requirements.txt dosya içeriğinin ne işe yaradığını

Böylece geldik bir maceramızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Hey Raspi! Gerçekten Çok Güçlü Bir Bilgisayara İhtiyacım Var mı?

$
0
0

1985 Yazı - Hikayenin Başladığı Yer

Almanya’daki kuzenlerimden biri olan Haluk abi orta öğrenimi için dayımlar tarafından İstanbul’a gönderilmişti. Şu anda 94 yaşında olan anneannem ve rahmetli Hasan dedemin Barbaros mahallesindeki bahçeli evlerinin üst katında sokak tarafına bakan oturma odasında yaşıyordu. Benim ilkokul sıralarında olduğum zamanlardı. Rahmetli Ali dayım orta okul sınıf geçme hediyesi olarak Haluk abime üstünde Commodore 64 yazan bir bilgisayar almıştı. Vaktinin çoğunu resim yaparak ve 37 ekran renkli televizyona bağlanmış Commodore’unda oyun oynayarak geçiriyordu.

Çılgın pilot Murdock’lı A takımının, John Jay Rambo’nun, He-Man’in, “bir alışveriş bir fiş” ile KDV’nin, siyah okul önlüklerinin, şeytan Rıdvan gol kralı Tanju’nun olduğu dönemlerdi. Mahallenin bir kaç sokak ötesindeki toprak futbol sahasının yanındaki pasajda müzik kasetleri, plaklar ve VHS’ler satan küçük bir dükkan vardı. Commodore’un popülerleşmesiyle birlikte oyun kasetleri doldurmaya da başlamıştı. Bir kaset içinde onlarca oyun olabiliyordu. Summer Games, Green Beret, River Raid, 1942, PaperBoy, Hawkeye, Emlyn Hughes Soccer ve aklıma gelmeyen niceleri…

Hangi gün olduğunu hatırlamıyorum lakin akşam üstü vakitleriydi. Haluk abiyi camda bekliyordum. Beni pek oynatmazdı ama yanında oturup oynadığı oyunları izlememe izin verirdi. Yokuştan aşağıya doğru yürüdüğünü gördüm. Anneannemin lezzetini hiç unutamadığım o nefis reçellerine malzeme yaptığı rengarenk güllerle süslediği bahçenin kapısını açıp hızlı adımlarla girişe doğru ilerledi. Merdivenlerden üst kata ikişer ikişer çıktığını anlayabiliyordum. Nefes nefese kalmış bir şekilde odaya girdi ve “Yeni oyunlar çıkmış Burak!” dedi gülümseyerek.

Ama hemen oynayamazdık. İlk başta cihazın önceki oyunlar nedeniyle bozulan kafasının düzeltilmesi gerekiyordu. Commodore oyunları okumak için bir kasetçalar kullanıyordu. İçindeki kasetin bantına yazılı bitleri kullanarak bizlerin saatlerce Tv başında vakit geçirmesine neden olan bir eğlence makinesiydi.

Haluk abim saatçi tornavidasını kullanarak kasetçaların üstündeki vidayı sağa sola doğru birkaç kez oynattı. Ekranda sarhoş birisi gibi dolanan eğri büğrü çizgiler bu hareketlerle düz bir doğruya dönüşmeye başladı. Bu, kafa ayarının tamamlandığı anlamına geliyordu. Haluk abim oyun kasetini taktı ve Play tuşuna bastı. İlk oyun başladı. Ekranın alt tarafındaki uçak gemisinden kalkan ikinci dünya savaşına ait bir tayyare yukarı doğru ilerliyordu. Karşısına çıkan düşman uçaklarına ateş açıyor, aşağıdaki hedeflere bomba atıyor, Fuel yazan yerlerden geçerek yakıt takviyesi yapıyordu. Sonra vuruldu. Haluk abi tekrar başladı. Yanında çıt çıkarmadan heyecanla ekranı izliyordum. Joystick kolunu bir o tarafa bir bu tarafa sürükleyip duruyordu. Bu kez biraz daha ilerlemeyi başarmıştı ama yine vuruldu ve tekrar başladı. Tekrar, tekrar ve tekrar…

Dakikalar saatleri tamamlamaya başladı. Sırtımız sokağa dönük olduğundan kararan havanın farkına bile varamamıştık. Arada akşam ezanı ya da yatsı okunmuştu belki ama fark etmemiştik. Sokak sakinleri zaten çoktan evlerine çekilmişti. Zifiri karanlıkta TV ekranına bakıyorduk sadece. Derken kulağımda kısa süreli bir acı hissettim. Bir kaç yıl sonra bir atari salonunda oyun oynarken aynı acıyı tekrar hissedecektim :) Başımı şöyle bir geriye doğru kaldırırken onun tebessüm eden yüzüyle karşılaştığımda Haluk abi sanki oyunun içindeymiş gibi tam konsantre devam ediyordu. İkimizde rahmetli babamın ne odanın kapsını açışını ne arkamıza geçişini fark etmiştik. Belki bir kaç dakikadır tepemizde dikilmiş bekliyordu hatta. Meğer saatler gece yarısını çoktan geçmişti. Ufak bir çocuğun o saatte bilgisayar oyunu başında ne işi vardı!?

Nedense yıllarca o cihazın sadece 64 kilobyte kapasiteli bir bellek ile bizleri ekran başına saatlerce bağlayan oyunlarının nasıl geliştirildiğini düşünmemiştim. Günümüzde sahip olduğumuz bilgisayarları bir düşünün. Gigabyte’larca ram’i olan, çok çekirdekli, müthiş ekran kartlı makinelerimiz var. Quantum’un sınırlarında gezip yüksek hızlara ulaşarak bilimde çığır açacak gelişmelere imza atıyoruz. Peki gerçekten bir programcının ultra mega süper kuponlu bir bilgisayara sahip olması şart mı? Neden hep en iyisini arıyoruz?

1993 - Üniversite 1nci Sınıf

Y.T.Ü. Matematik Mühendisliği bölümüne girdiğim yıl programcı olmak istediğime karar vermiştim. İlk yılın birinci ve ikinci dönemi grafik özellikleri arttırılmış GWBasic dilini ders olarak alıyorduk. Kişisel bilgisayarım olmadığı için bir çok çalışmayı sadece belli saatlerde açık olan okul labaratuvarında yapmak zorundaydım. O yıl 286 işlemcili bilgisayarların matematiksel işlem birimi arttırılmış 386 modelleri ile değiştirilmesi söz konusuydu. Beş çeyreklikler yerini 1.44Mb’lık disketlere bırakıyordu. Açık gri renkteki bilgisayar kasalarında hard disk bulunmuyordu. Bu nedenle MS-DOS işletim sistemlerini hocamızın verdiği disketlerden yükleyerek sistemi açabiliyorduk(Bu zamanın bootable USB’leri gibi düşünebilirsiniz) Siyah beyaz tüplü monitörler üzerinde zaman içerisinde Pascal, Cobol, C, C++ gibi lisanları da denedik ama olmuyordu. Kişisel bilgisayarımın olması şarttı. Ne var ki dönemin fiyatları en az bugünkü kadar el yakıyordu.

İmdadıma Siemens’te çalışan eniştem yetişti. Geçici bir süre de olsa çalışmam için Siemens Nixdorf marka, elektronik daktilodan hallice çevrilmiş epey ağır bir laptop getirdi. Ağırlığı nedeniyle dambıl almama bile gerek yoktu. Siyah beyaz ekranı vardı ve üzerinde Windows 3.1 işletim sistemi koşuyordu. Tabii programlama bir yana üzerinde denediğim şeylerden birisi de Duke Nukem oynamaktı. Windows‘un üstünde kabuk olarak koştuğu çekirdek işletim sistemi MS-DOS ile çalışan bir oyundu. Siyah beyaz ekran o kadar geç tazeleme hızına sahipti ki karakteri koştururken arkada bıraktığı gölgeleri gri tonlu kareler halinde görebiliyordunuz. Biraz ağır olsa da bir laptop’un seyyarlığını tatma hissi en azından o yıllarda muhteşemdi.

Tabii bir süre sonra kendi kişisel bilgisayarıma kavuştum. 14 inç monitörü olan 486DX-33mhz işlemcili bir bilgisayar. 8Mb RAM’inin olduğunu hatırlıyorum. Ekranı şenlendiren 2 Mb’lık Diamond Stealth marka bir kartı ve muhteşem ses veren Creative 32bit işlemcisi vardı. Tabii kısa bir süre geçmesine rağmen disket sürücüleri hala kullanılıyor bununla birlikte CD’ler de yüksek kapasiteli depolama birimleri olarak boy gösteriyordu. Artık kasete oyun çeken pek yoktu ama disketlere programlar koyuluyordu. Meşhur Yazıcıoğlu işhanına gidip 21 disketten oluşan Delphi 2.0 kurulum paketini aldığımı daha dün gibi hatırlarım.

Delphi’nin bende ayır bir yeri vardır. İlk kez onunla yazdığımız ve oluklu mukavva üreticisi bir matbaa için geliştirdiğimiz programla para kazanmıştık. 90ların ikinci yarısında Amerika’dan Java isimi bir kitap getirtip bana “Burak! Gelecek bu. Web!” diyen sevgili Orkun ile birlikte 250 dolar kazanmıştık. Ben Taksim’deki Elit kitabevinden Delphi 2 Unleashed isimli 1400 sayfalık bir programlama kitabı alırken ileriyi gören Orkun 14.4 Kbps hızında bir modem alarak Ataköy’deki evinin odasından internete açılmıştı.

1997 Şubatı - Compex Fuarı

O yıllar ne kadar bilgisayar dergisi varsa alıp okumaktaydım. Yeni çıkan işlemcileri, Visual Basic ve Delphi tarafındaki gelişmeleri, Java’nın yükselişini, web teknolojilerinin hayatımıza girişini izliyordum. Üniversitedeki yakın arkadaşlarım bilgisayarlara olan merakımı biliyordu. Bu nedenle yeni bir bilgisayar toplayacakları zaman veya format atmaları gerektiğinde kapımı çalarlardı.

Efes’in alt yapısında yıllarca oynayıp Matematik Mühendisi olmaya karar vermiş 1.96lık sevgili Serkan’da en yakın dostlarımdandı. Abdi İpekçi’de birlikte maç izlediğimiz çok olmuştu ama senede bir bilgisayar fuarına da giderdik. Böylece gelişmeleri canlı canlı görebilirdik. Üstelik bazı firmalar bu fuarlarda indirimler uygulardı. Pentium işlemcilerin yeni serilerinin geldiği heyecanlı günlerdi. Moore yasası sürekli olarak işliyor işlemciler iki seneden daha kısa sürede transistor sayılarını katlayarak hızlanıyordu.

Compex o yıl ocak sonu şubat başı açılmıştı. Serkan, babası ve ben üçümüz birlikte Tepebaşı’nda açılan fuarın yolunu tuttuk. Davetiyelerimizi Pc World dergisinden kapmıştık. Serkan yeni bir bilgisayar almak istiyordu. Tabii ki baş danışman bendim :) Bir stand’tan diğerine gidiyor, broşürleri alıp modelleri inceliyorduk. Sonunda bir tanesinde karar kıldık. Doğruyu söylemek gerekirse fiyatlara da bakıyorduk. Modelin iki versiyonu vardı. Birisi 32 diğeri 64Mb Ram kapasiteliydi. Pek tabii bu devirde olduğu gibi iki katına çıkan Ram farkı bilgisayar fiyatını da epeyce etkliyordu. Ben Serkan’a 64Mb olanını almasını tavsiye ettim. Biraz çekingen halde ve tereddüt ederek babasına doğru döndü ve “Baba…32 Mb yerine 64 Mb olanı alalım” dedi. On yaşında oğlu olan bir baba olarak benim bugün vereceğim tepkinin bir benzerini 1997 kışında Serkan’ın babası verdi; “Sen önce bir 32liği doldur sonra 64lüğüne bakarız” :D

Bu hatıram beni hep güldürür ve durup düşünürüm :) Neden en yüksek konfigurasyona sahip bir bilgisayar toplama çabasındaydık. Bir programcı için ortam şartları büyük bir engel oluşturmamalıydı. Optimize edilmesi gereken bir şeyler varsa bunun yolunu kendisi bulmalıydı.

2006 Sonbaharı

O zamanlar büyük bir hevesle ve gönüllü olarak katıldığım topluluk çalışmalarım nedeniyle bir vakit benden savunma istemiş yazılım firmasından ayrıldığım günler. Çalıştığım süre boyunca şirketin bana verdiği IBM Thinkpad T41 model bilgisayarı kullanmıştım. Tabii geri vermek durumunda kalınca bir anda bilgisayarsız kaldım. Evdeyse hiç aşina olmadığım yeşil renkli, dış kapağının saydamlığı nedeniyle içi görünen 1998 model bir iMac G3 vardı. İnanılmaz estetik bir tasarımdı. Tüm Apple’lar da olduğu gibi.

Derken başına oturup onu açtım ve bir kaç arkadaşıma iş aradığıma dair mail atmak üzere harekete geçtim. Ne yazık ki orjinal mac klavyesinde @ sembolünü bir türlü bulamıyordum. Tarayıcı epey tuhaf gelmişti. Başka bir tarayıcıyı nasıl yükleyeceğimi bile bilmiyordum. Gerçekten yabancı topraklarda kalmıştım. Bir kaç gün öncesine kadar Microsoft’un .Net Framework 2.0 sürümü üzerinde C# 2.0 ile geliştirmeler yapmaya çalışan ben sudan çıkmış balık misali çaresiz hissediyordum.

Ancak bir kaç yıl önce yaptığım keskin Linux geçişine keşke o yıllarda cesaret etseydim demeden edemiyorum. Gerçekten güçlü bir Laptop, PC ve Windows işlerimi yapabilmem için şart mıydı?

2019 Eylül - Günümüz

Yaz başına kadar yaklaşık 8 yıl önce aldığım Dell marka bir laptop kullanıyordum. WestWorld olarak isimlendirdiğim alet Ubuntu’da koşuyordu. Son olarak 18.04 sürümüne çıkmıştım. 4 çekirdekli intel işlemci, 8 Gb RAM ve 250GB disk kapasitesi hayli hayli yetiyordu. Ekranı bir kaç yıl önce bozulduğundan harici monitör kullanıyordum. Performansı ile ilgili bir sorunum yoktu. Nitekim artık eskisi gibi her şeyi makineye yüklemeye çalışmıyordum. SQL Server’a mı ihtiyacım var? Neden ki? Pekala PostgreSQL’de de denemelerimi yapabilirim. Peki onu kurmak zorunda mıyım? Elbette hayır! Docker ne güne duruyor :) İlle de SQL gerekiyorsa peki? O zaman Azure’a uğrayabilirim. Ya IDE!? Her yeni laptop alındığında mutlak suretle Visual Studio’nun en kallavi sürümünü yüklemiyor muyuz? Haydi itiraf edin. Ultimate sürümlerini torrent’lerden bulup yüklüyorsunuz değil mi? Yanına SQL Server Management Studio…Office’in tam sürümü…Oysa ki Visual Studio Code açık kaynak platformlar için harika bir IDE. Office’i gerçekten kurmak gerekli mi? Üstelik Office 365 varken ve dokümanlarımıza online olarak her yerden ulaşabiliyorken?

Ama işte WestWorld’ün o pancar motoru misali gürültülü fan sesi yok muydu? Müzik dinlemeden çalıştığım sessiz gecelerimin keyfini kaçırıyordu. Yıllar geçtikçe daha fazla zorlanmaya başlamıştı. Soğutucular deniyor, ara ara salon süpürgesi ile tozlarını çekiyor, Ubuntu’nun process’lerini kontrol edip sistemi yoranları ayıklamaya çalışıyordum. Yine de olmuyordu. Elim yeni bir bilgisayar almaya da gitmiyordu. Fiyatlar gerçekten yüksek. Her zaman böyleydi belki de ama dövizdeki artış, okul taksidi, basketbol ayakkabısı derken hep geriye atmak zorunda kaldığım bir maliyetti.
Sonra günlerden bir gün bütçeyi bir nebze olsun ayarlamayı başardım. İyi bir indirime giren Macbook Mini modelini bir süredir kovalıyordum. Ahch-to adını vereceğim aletin fiyatı 3K’nın altına düşmüştü. Monitorüm zaten olduğu için Mini PC tadında isteklerimi karşılayacaktı. Üstelik uzun zamandır üzerinde geliştirme yapmayı merak ettiğim macOS işletim sistemini de deneyimleme fırsatı bulacaktım. Bu kez Ubuntu çalışmalarından dolayı terminal penceresine biraz daha aşinaydım ve cesaretim vardı.

Yine de ortada ufak bir pürüz vardı. 4Gb Ram…Gözüme epey az gelmişti. Şirketin verdiği 16Gb RAM’li intel Core i7 Pro işlemcili alet bile bazı durumlarda takılabiliyordu. Baktığım mininin 8Gb olan modelindeki intel işlemcisi de daha iyiydi. Ama fiyat neredeyse %60 oranında fazlalaşıyordu. Sonunda bilgisayarı aldım. İlk kurulumlarımı yaptım. Visual Studio Code, xCode, git, Node.Js, docker ve daha bir sürü şeyi üzerinde denemeye başladım. Ancak korktuğum başıma gelmişti. Ubuntu’dan bile yavaştı sistem. Reaksiyon süresi çok düşüktü. Chrome tab’ları can çekiştiği için terk edip Safari’ye dönmüştüm ama pek bir şey değişmemişti. Her nedense makine açıldıktan çok uzun süre sonra kendini toplamaya başlıyor, tabir yerinde ise t anında sadece bir işe odaklanarak nefes alabiliyordu. “Keşke” dedim yine. Keşke diğer modeli alsaydım. Güçlü olanı. Daha pahalıydı belki de ama performansı iyi olacaktı.

Sonra düşünmeye başladım. Neden en iyisi gerekiyordu programlama yapmam için. Merak ettiğim şeyler arasında kullanacaklarımı pekala bulutta konumlandırabilirdim. Performans maliyeti yüksek IDE’lere gereksinimim yoktu gerçekten de. Ama içime bir huzursuzluk düşmüştü.

Bunun üzerine ucuz maliyetli Raspberry Pi 3B+ almaya karar verdim. 1 Gb Ram’i olan cihazın üzerinde Debian tabanlı Raspbian sürümünü kullanacaktım. 16 Gb’lık bir MicroSD kartı bootable disk olarak hazırladım. Ufacık entegrenin hard diski yoktu ama WiFi ile internete bağlanabiliyordu. Daha ilk gece üstünde bir çok şey yapmış, sadeliğine, sessizliğine ve minimal gereksinimlerine hayran kalmıştım. Şimdilik…

Şu adresten öğrendiğim kadarıyla Ubuntu Mate sürümünün en azından beta olarak Raspberry Pi 2,3 modellerine kurulumuna ait bir paylaşım var. Diğer yandan 2019 tarihli bu yazıya göre Ubuntu ile sınırlı değiliz. Bir çok işletim sistemini Raspberry Pi üzerinde kullanmamız mümkün. Windows IoT Core ve Ubuntu Core’da bunlara dahil.

Forbes’un bir haberine göre Ubuntu Mate’in Raspberry Pi 4'ü destekleyeceği ifade edilse de resmi olarak buna dair bir doğrulama en azından yazıyı hazırladığım tarih itibariyle yoktu. Sadece Estimated Time of Arrival konsepti ile üzerinde çalışıldığına dair söylemler var. Sanırım bu desteğin gelip gelmeyeceğini zaman içerisinde öğreneceğiz.

Raspbian’da Bir Gece…

Artık bir şeyleri kurmaya çalışıp, programlama ortamını hazırlamak üzere harekete geçebilirdim. Saat 22yi geçmişti. S(h)arp Efe ve üst kattaki gürültücü velet çoktan uykuya dalmıştı. Mahalle sakinleşmiş ve çalışma odam tam olarak istediğim sessizliğe gelmişti. Arkamdaki kütüphanenin üst rafına kaldırdığım emektar WestWorld’e göz ucuyla şöyle bir baktım ve tebessümle “Umarım tatilin iyi geçiyordur” diyerek klavyeme döndüm. Planladığım belli bir yol haritası yoktu. Doğaçlama gidecektim. Öncelikle Raspbian diyarında neler var ne yok bakayım dedim. Aklıma ilk önce Raspberry Pi’ler ile özdeşleşen Python ortamı geldi.

Raspbian üzerinde Python 2 ve 3 sürümleri ile pip paket yöneticisi zaten yüklü olarak geliyor ama benim Node.js ve npm ile yapacağım çalışmalar da var. Lakin bazı ürünlerin doğru sürümlerini yükleyebilmek ya da desteklerinin olup olmadığını görmek için ARM işlemcisinin versiyonunu öğrenmem gerekmişti. Bunun için şu terminal komutunu kullanarak çalışmalarıma başladım.

uname -m

Sonra aşağıdaki komutlarla nodejs’i sisteme yükledim. Beraberinde npm paket yöneticisi de geldi tabi.

curl -sL https://deb.nodesource.com/setup_10.x | sudo bash -
sudo apt install nodejs
node — version
npm — version

İşte elimin altında nodejs. Rastgele aklıma gelen enstrümaları toplamaya devam ettim. Mesela şunu merak ediyordum; Raspbian üzerinde PostgreSQL’in Docker Container’ını çalıştıramaz mıydım? Hani kendi başına çalışan hafif bir microservice konuşlandıracağım bir sürü Raspberry cihazımız olur mu hayalinden yola çıkarak(Hatta çalıştığım firmadaki bazı yeni nesil ürünlerin SQL Server’dan PostgreSQL platformuna göçü için yapılan çalışmaları düşünerek)Öncesinde Docker bu sistemde çalışır mı bunu öğrenmem gerekiyordu. Bunun üzerine terminali aşağıdaki komutlarla şenlendirdim.

sudo curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo docker container run hello-world

Tabii container’ın başarılı bir şekilde çalıştığını görünce SD kartın çabucak dolmasından endişe edip başlatılan container’lar ile imajı silmeyi de ihmal etmedim. Savurganlığın lüzumu yoktu :)

sudo docker container ls -a
sudo docker container rm f7db1b 354818
sudo docker image ls
sudo docker image rm 618e43

Pek tabii bir Container’ı ayağa kaldırmış olmak iştahımın daha da artmasına neden oldu. Bunun üzerine PostgreSQL imajını yükleyeyim ve iki SQL sorgusu çalıştırayım dedim.

sudo docker run -d — name c_postgres -v London:/var/lib/postgresql/data -p 54321:5432 postgres:11
sudo docker exec -it c_postgres psql -U postgres
CREATE DATABASE AdventureWorks;
CREATE TABLE IF NOT EXISTS Category (CategoryId SERIAL PRIMARY KEY,Name varchar);
INSERT INTO Category (Name) VALUES (‘Book’);
INSERT INTO Category (Name) VALUES (‘Movie’);
SELECT * FROM Category;

Veritabanı oluşturabildiğimi, kayıt atıp çekebildiğimi görünce epey mutlu oldum. Çalışmalarım sırasında ihtiyacım olabilecek veri depoları için PostgreSQL’i burada deneyimleyebilirim(Hatta SQLite gibi sürümleri de denesem gayet iyi olur)

Daha neleri kontrol edebilirim diye düşünürken asıl göz ağrım .Net Core’a ayrı bir sayfa açmam gerektiğini fark ettim. Nitekim C# programlama dilini ve .Net Core platformunu deneyimleyeceğim bir ortam olması önemliydi. Biraz araştırma yaptıktan sonra ARM sürümü için Microsoft’un şu adresindeki kaynaktan yararlanarak Raspbian üzerine .Net Core SDK 2.2 versiyonunu yükleyebildim. Tabi güncel .Net Core sürümlerinden hangilerinin hayatta kaldığına ara ara bakmakta yarar var. Özellikle ürünleştirilmiş sürümlerimiz varsa bu önemli bir nokta. Microsoft’un şu adresindeki görsel imgeler iyi birer yardımcı.

Gerekli kurulumun ardından basit bir Console uygulaması oluşturup denemem yeterliydi. Henüz Visual Studio Code editörü yüklenebiliyor mu bilmiyordum ama Program.cs içeriğini düzenlemek için Raspbian ile gelen Geany pekala kafiydi.

Gerçi Visual Studio Code’un açık kaynak olarak Raspberry Pi için genişletilmiş code-oss isimli bir versiyonunu şu adresten indirip kısa süre kullandım. Ancak yorumlarda da belirtilen blank screen probleminin çözümü için update’leri kapatmak zorunda kalmak beni biraz rahatsız etti. Belki ilerde işlemcinin armhf sürümü için tam Visual Studio Code desteği gelir.

dotnet new console -o hello-from-pi

Şu an için tek sıkıntı uygulamanın çalışmasının çok yavaş olması. Basit bir Hello World uygulaması için dotnet run komutunun epey beklettiğini fark ettim. Henüz bunun sebebini anlayamadım. Belki ve muhtemelen ARM işlemcisinden kaynaklı lakin bu canımı sıkan bir durum değil. Çünkü çıt çıkartmayan ve ceket cebime koyup istediğim yere götürebileceğim bir bilgisayarım var :) (Gittiğim yerde HDMI girişli bir monitor/tv, klavye ve mouse olması şartıyla tabii)

Tipik bir desktop kullanıcısının başlangıçta yadırgayacağı tek şey varsayılan olarak Raspberry Pi’nin Açma/Kapama düğmesine sahip olmayışıdır. İlk gece işletim sistemini bir kaç sefer kitleyince güç kablosunu çıkartıp tekrar takarak ilkel bir şey yaptığımı da düşündüm aslında. Fakat buna çok takılmadım. Yine de şu adresteki yönergeleri takip ederek güç düğmesi entegre edilebileceğini öğrendim. Yanlışlıkla mavi kabloyu keserek devrenin patlamasından korkanlar dükkandan hazır anahtar modülü alıp bağlayabilir de ;)

Pek tabii merak ettiğim ve denemek istediğim bir çok konu var. Git(Raspbian ile yüklü geliyor) komut satırı aracı ile GitHub projelerine bağlanmak, cihazı NGinX veya Apache sunucusu olarak kurgulayıp web servisi hizmeti sunacak şekilde ayağa kaldırmak, Erlang dilini öğrenmek(ki sorunsuz bir şekilde sisteme kuruldu ve hello world uygulaması çalıştırılabildi — bknz aşağıdaki resim), Flutter çatısının platform üzerinde kullanılıp kullanılamayacağına bakmak, Google Cloud Platform öğretilerini takip ederek bulut tabanlı temel geliştirici operasyonlarını Raspi ile birlikte denemek(gCloud CLI) ve benzerleri…

Aslında Raspberry Pi’yi bir IoT cihazı gibi kullanmaktansa düşük maliyetli ve programlama deneyimi yaşayabileceğim bir araç olarak ele almak şu anki hedeflerimden birisi. En nihayetinde üzerinde gelen Scratch ile çocuklara temel programlama deneyimini de yaşatıyor ki ben hala büyümedim.

Bir Öğle Arası…Şekerpınar

Gerçekten de programlama yapmak için çok güçlü bir makineye ihtiyacım var mı? Yeni bir maceraya atılmış gibi hissediyorum. Bazen teknolojinin baş döndürücü gelişmesine kızsam da yeni ufuklara yelken açmamıza olanak sağladığı için hevesleniyorum. Oldukça minimal bir cihaz üzerinde programlama öğrenmeye çalışmak…Motivasyonum tam olarak bu. Piyasadaki bilgisayarlara nazaran çok düşük maliyetli bu minion pc için elde edeceğim deneyime göre 128 Gb Micro SD karta geçmeyi bile düşünebilirim. Hatta 4Gb Ram kapasiteli Raspberry Pi 4 sürümü maliyet bazında ciddi olarak düşünülebilir ki PC’lere hafif bir alternatif olarak ön plana çıkmakta(Şuradaki karşılaştırma konu hakkında fikir verebilir) Aradığım sorunun cevabını ancak bu şekilde bulabileceğime inanıyorum. Tabii şu önemli parametreyi de unutmamak lazım; Bu karışımı üretim ortamları haricinde yazılım geliştirme adına bir şeyleri öğrenmek için kurcalayacağım deneysel bir çalışma sahası olarak görüyorum. ARM işlemcisi bazı geliştirme platformlarınca desteklenmiyor olsa dahi…

Azure SignalR Servisini Kullanmak

$
0
0

Basketbolu neden bu kadar çok seviyorum diye düşündüm geçenlerde. Oturduğumuz sitenin basket sahasını futbol oynamak için kullanan onca çocuk ve genç gibi bir gerçek varken ben neden bu spora böylesine sevdalıydım. İnanılmaz enerjisi ve sürekli değiştirdiği NBA şapkaları ile rahmetli İsmet Badem mi sevdirmişti? Yoksa final serisi maçları sabahın kaçında olursa olsun uyanamayıp okula geç gitmeme neden olan majestelerinin maçları mı? Basketbolun tüm efsanelerini kendi kardeşiymiş gibi tanıyan ve maçları kendine has heyecanı ile anlatan Murat Murathanoğlu muydu yoksa?

Belki de Koraç kupasını alarak Avrupa'da bir ilke imza atan Efes'in Abdi İpekçi salonundaki Stefanel Milano maçına girmek için kuyrukta beklerken arabasından bizi seyreden yaşıtım Mirsad Türkcan'ın onca seyirciyi coşkuyla selamlamasıydı. Kim bilir belki de hücum süresi henüz otuz saniyeyken Peter Naumovski'nin eliyle tshirt'ünün sağ yakasını ağzına götürerek verdiği setin adıydı. Belki de zamanında her gün büyük bir iç motivasyonla gittiğim turuncu bankanın CBL(corporate basketball league) seçmelerinde koçun bana gelip "abi kusura bakma" dedikten sonra yaşımı öğrenip "sen ciddi misin abi? Ben bu kadar büyük olduğunu bilmiyordum. Çok daha genç duruyorsun. Basketbol sevgine hayran kaldım" söylemine rağmen takıma almayışı ve Bill Murray'ın Space Jam'de Larry Bird ile olan konuşmasında ona "You can't play" demesini hatırlayışım mıydı? İnanın hiç bilmiyorum. Ama çok sevip de hiç bir zaman beceremediğim bu oyunu mesleki çalışmalarımda kullanmaya bayılıyorum. İşte öyle bir çalışmanın girizgahındasın şu anda sevgili okur :)

cumartesi gecesi çalışmasındaki amacım Azure platformundaki SignalR hizmetini kullanarak abone programlara çeşitli tipte bildirimlerde bulunabilmekti. Normal SignalR senaryosundan farklı olarak istemciler ve tetikleyici arasındaki eş zamanlı iletişimi(Real Time Communications) Azure platformundaki bir SignalR servisi ile gerçekleştirmek istemiştim. Senaryoda bildirimleri gören en az bir istemci(ki n tane olması daha anlamlı), local ortamda çalışan ve bildirim yayan bir Azure Function uygulaması ve Azure platformunda konuşlandırılan bir SignalR servisi olmasını planlamıştım. Ayrıca Azure üzerinde koşan bu SignalR servisini Serverless modda çalışacak şekilde ayarlamayı planlıyordum. Bir takım sonuçlara ulaşmayı başardım. Şimdi çalışmaya ait notları derleme zamanı. Öyleyse ne duruyoruz. Haydi başlayalım.

SignalR servisi tüm Azure fonskiyonları ile kullanılabilir. Örneğin Azure Cosmos DB'deki değişiklikleri SignalR servisi ile istemcilere yollayabiliriz. Benzer şeyi kuyruk mesajlarını veya HTTP taleplerini işleyen Azure fonksiyonları için de sağlayabiliriz. Kısacası Azure fonksiyonlarından yapılan tetiklemeler sonrasında SignalR servislerinden yararlanarak bağlı olan aboneleri bilgilendirebiliriz. Şimdi WestWorld'ün gereksinimlerini tamamlayaraktan örneğimizi geliştirmeye başlayalım.

Ön Gereksinimler

Azure platformunda SignalR servisini oluşturmadan önce WestWorld(Ubuntu 18.04, 64bit) tarafında Azure Function geliştirebilmek için gerekli kurulumları yapmam gerekiyordu. İlk olarak Azure Functions Core Tools'un yüklenmesi lazım. Aşağıdaki terminal komutları ile bunu gerçekleştirmek mümkün. Önce Microsoft ürün anahtarını Ubuntu ortamına kaydediyor ve sonrasında bir güncelleme yapıp devamında azure-functions-core-tools paketini yüklüyoruz.

curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg

sudo apt-get update

sudo apt-get install azure-functions-core-tools

Kurulumdan sonra terminalden Azure Function projeleri oluşturmaya başlanabilir. Lakin bu işin Visual Studio Code tarafında daha kolay bir yolu var. O da Azure Functions isimli aracı kullanmak.

Visual Studio Code'a gelen bu araçla kolayca Azure Function projeleri oluşturabiliriz.

Azure SignalR Servisinin Hazırlanması

Adım adım ilerlemeye çalışalım. Öncelikle Azure platformunda bir SignalR servisi oluşturmamız gerekiyor. Ben Azure Portal adresinden SignalR Service öğesini aratarak işe başladım. Sonrasında aşağıdaki ekran görüntüsünde yer alan bilgiler ile servisi oluşturdum.

Free Tier planında, learning-rg Resource Group altında, basketcini.service.signalr.net isimli bir SignalR servisimiz var. Bu servisinin oluşması biraz zaman alabilir ki ben bir süre beklediğimi hatırlıyorum. Servis etkinleştikten sonra özelliklerine giderek Serverless modda çalışacak şekilde ayarlayabiliriz. Bunun için Service Mode özelliğini Serverless'a çekmek yeterli. Tabii ekran görüntüsünden de fark edeceğiniz üzere PREVIEW modunda. Kuvvetle muhtemel sizin denemelerinizi yapacağınız durumda son halini almış olabilir.

Bu SignalR servisi ile local makinede çalışacak ve tetikleyici görevini üstlenecek Azure Function uygulamasının haberleşebilmesi için, Key değerlerine ihtiyacımız olacak. Bu değerleri Azure Function uygulamasının local.settings.json dosyasında kullanmamız gerekiyor. O nedenle aşağıdaki ekran görüntüsündeki gibi ilgili değerleri kopyalayıp güvenli bir yerlerde saklayın.

Azure Functions Projesinin Oluşturulması

Yüklenen Azure Functions aracından Create New Project seçimini yaparak ilerleyebiliriz. Proje için bir klasör belirleyip(Ben NotifierApp isimli klasörü kullandım) dil olarak C#'ı tercih ederek devam edelim. Sonrasında Create Function seçeneği ile projeye Scorer isimli bir fonksiyon ekleyelim. Ben bu işlem sırasında sorulan sorulara aşağıdaki cevapları verdim. Siz kendi projenize özgün hareket ederseniz daha iyi olabilir. Özetle HTTP metodları ile tetiklenen bir fonksiyon söz konusu diyebiliriz.

Fonksiyon Adı : Scorer
Klasör : NotifierApp
Tipi : Http Trigger
Namespace : Basketcini.Function
Erişim Yetkisi : Anonymous

Örnekte Table Storage seçeneği değerlendirilmiştir. Bunun için öncelikle Azure Portal üzerinde learningsignalrstorage isimli bir Storage Account oluşturdum ve Access Keys kısmında verilen Connection Strings bilgisini kullandım. Yani bildirimlerin depolanacağı Storage alanını sevgili Azure'a devrettim. Çünkü WestWorld'ün disk kapasitesi epeyce azalmış durumdaydı :P

Azure Functions Projesinde Yapılanlar

Azure fonksiyonu oluşturulduktan sonra elbette biraz kodlama yapmamız gerekecek. Ama öncesinde bizim için gerekli nuget paketlerini yüklemeliyiz. Aşağıdaki terminal komutlarını NotifierApp klasöründe çalıştırarak devam edelim.

dotnet add package Microsoft.Azure.WebJobs.Extensions.EventGrid 
dotnet add package Microsoft.Azure.WebJobs.Extensions.SignalRService 
dotnet add package Microsoft.Azure.WebJobs.Extensions.Storage

Önemli değişikliklerden birisi local.settings.json dosyasında yer alıyor. Burada Azure SignalR servisine ait Connection String bilgisi ve CORS tanımı(Senaryoya göre isimsiz tüm istemciler Azure Function Api'sini kullanabilecek) eklemek lazım. Nasıl yapıldığını söylemek isterdim ama gitignore dosyasında bu json içeriğini dışarıda bırakmışım. Yani hatırlamıyorum :) Yani sizin keşfetmeniz gerekecek ;)

Bunun haricinde skor durumunu ve anlık olarak meydana gelen olay bilgisini tutan Timeline ve Action isimli sınıfları da aşağıdaki gibi kodlayabiliriz. Biliyorum henüz senaryo tam olarak şekillenmiş değil. Ama çalışma zamanına geldiğimizde ne olduğunu gayet iyi anlayacaksınız. Action sınıfı ile başlayalım.

namespace Basketcini.Function
{
    /*
        Table Storage'e yazılacak veri içeriğini temsil eden sınıftır.
        Azure Table Storage'a aşağıdaki özellikler birer alan olarak açılacaktır.
     */
    public class Action
    {
        public string PartitionKey { get; set; }
        public string RowKey { get; set; }
        public string Player { get; set; }
        public string Summary { get; set; }
    }
}

ve Timeline sınıfımız;

namespace Basketcini.Function
{
    /*
        Abonelere döndürülecek veri içeriğini taşıyacan temsili sınıftır.
        Kim, hangi olayı gerçekleştirdi bilgisini tutar.
    */
    public class Timeline
    {
        public string Who { get; set; }
        public string WhatHappend { get; set; }
    }
}

Scorer isimli Function sınıfında da üç metod bulunuyor. Birisi tetikleyici olarak yeni bir olay gerçekleştirmek için, birisi istemcinin kendisini SignalR Hub'ına bağlaması için(negotiation aşaması), birisi de servisin istemciye olay bildirimlerini basması için(push message aşaması) Her zaman ki gibi kod içerisindeki yorum satırlarında anladıklarımı basitçe anlatmaya çalıştım.

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace Basketcini.Function
{
    public static class Scorer
    {
        /*
        Scorer fonskiyonu HTTP Post tipinden tetiklemeleri karşılar.
        Oluşan aksiyonları saklamak için Table Storage kullanılır. Actions isimli tablo Table niteliği ile bildirilmiştir.
        Ayrıca gerçekleşen olaylar bir kuyruğa atılır(Queue niteliğinin olduğu kısım)
        Console'a log yazdırmak için ILogger türevli log değişkeni kullanılır.
        */
        [FunctionName("Scorer")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "post")] Timeline timelineEvent,
            [Table("Actions")]IAsyncCollector<Action> actions,
            [Queue("new-action-notification")]IAsyncCollector<Timeline> actionNotifications,
            ILogger log)
        {
            log.LogInformation("HTTP tetikleme gerçekleşti");
            log.LogInformation($"{timelineEvent.Who} için {timelineEvent.WhatHappend} olayı");

            /* HTTP Post metodu ile gelen timeline bilgilerini de kullanarak bir Action nesnesi 
            oluşturuyor ve bunu Table Storage'e atıyoruz.
            Amaç, meydana gelen olaylarla ilgili gelen bilgileri bir tabloda kalıcı olarak saklamak.
            Pek tabii bunun yerine farklı repository'ler de tercih edilebilir. Cosmos Db gibi örneğin.
            */
            await actions.AddAsync(new Action
            {
                PartitionKey = "US",
                RowKey = Guid.NewGuid().ToString(),
                Player = timelineEvent.Who,
                Summary = timelineEvent.WhatHappend
            });

            /* 
                new-action-notification ile ilintili olan kuyruğa gerçekleşen olay bilgilerini atıyoruz.
                İstemci tarafını bu kuyruk içeriği ile besleyebiliriz.
            */
            await actionNotifications.AddAsync(timelineEvent);

            return new OkResult();
        }

        /*
        Azure SignalR servisine bağlanmak için kullanılan metodumuz. 
        HTTP Post ile tetiklenir.
        Fonksiyon bir SignalRConnectionInfo nesnesini döndürür.
        Bu nesne Azure SignalR'a bağlanırken gerekli benzersiz id ve access token bilgisini içerir.
        SignalR Hub-Name olarak notifications ismi kullanılır.
         */
        [FunctionName("negotiate")]
        public static SignalRConnectionInfo GetNotificationSignal(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post")]HttpRequest request,
            [SignalRConnectionInfo(HubName = "notifications")]SignalRConnectionInfo connection,
            ILogger log
        )
        {
            log.LogInformation("Negotiating...");
            return connection;
        }

        /*
        Abone olan tarafa veri göndermek (push) için kullanılan fonksiyondur.
        QueueTrigger niteliğindeki isimlendirme ve tipin Scorer fonksiyonundaki ile aynı olduğuna dikkat edelim.
        İstemciye mesaj taşıyan nesne bir SignalRMessage örneğidir. 
        Bu nesnenin Arguments özelliğinde timeline içeriği (yani gerçekleşen maç olayları) taşınır.
        Peki aboneler buradaki olayları nasıl dinleyecek dersiniz? Bunun içinde Target özelliğine atanan içerik önem kazanır. 
        Örneğimizide aboneler 'actionHappend' isimli olayı dinleyerek mesajları yakalayacaktır.
         */
        [FunctionName("PushTimelineNotification")]
        public static async Task PushNofitication(
            [QueueTrigger("new-action-notification")]Timeline timeline,
            [SignalR(HubName = "notifications")]IAsyncCollector<SignalRMessage> message,
            ILogger log
        )
        {
            log.LogInformation($"{timeline.Who} için gerçekleşen olay bildirimi");

            await message.AddAsync(
                new SignalRMessage
                {
                    Target = "actionHappend",
                    Arguments = new[] { timeline }
                }
            );
        }
    }
}

İstemci Uygulama Tarafı

İstemci tarafı Node.js tabanlı basit bir Console uygulaması. Aslında web tabanlı bir arayüzü takip etmem gerekiyordu ancak amacım kısa yoldan SignalR servisinden akan verileri görmek olduğundan Node.js kullanmayı tercih ettim. Siz istemci tarafında tamamen özgünsünüz. SignalR tarafı ile rahat konuşabilmek için @aspnet/signalr isimli npm paketini kullanabiliriz. Terminalden aşağıdaki komutları kullanarak kobay istemcimizi oluşturalım.

mkdir FollowerApp
cd FollowerApp
npm init
touch index.js
npm install @aspnet/signalr

İstemci tarafında index.js ve package.json dosyalarını kodlayacağız. Aşağıda index sınıfına ait kod içeriğini bulabilirsiniz. Uygulama Hub'a bağlandıktan sonra bildirimleri dinler modda yaşamını sürdürecek diyebiliriz.

const signalR = require("@aspnet/signalr"); // signalR istemci modülünü bildirdik

/* 
    Hub bağlantı bilgisini inşa ediyoruz.
    withUrl parametresi Azure Function uygulamasının yayın yaptığı adrestir
*/
const connection = new signalR.HubConnectionBuilder()
    .withUrl('http://localhost:4503/api')
    .build();

console.log('Bağlantı sağlanıyor...');

/*
    Bağlantıyı başlatıyoruz. Başarılı ise then metodunun içeriği,
    bir hata oluşursa da catch metodunun içeriği çalışır.
*/
connection.start()
    .then(() => console.log('Bağlantı sağlandı...'))
    .catch(console.error);

/*
    actionHappend olayını dinlemeye başladık.
    Eğer SignalR servisi üzerinden bir push mesajı söz konusu olursa
    bu olay üzerinden geçeceği için istemci tarafından yakalanıp
    doSomething metodu çağırılacaktır.
    doSomething'e gelen parametre Azure Function'daki
    PushTimelineNotification fonksiyonundan dönen mesajın Arguments içeriğini taşır.

*/
connection.on("actionHappend", doSomething);

function doSomething(action) {
    console.log(action);
}

connection.onclose(() => console.log('Bağlantı koparılıyor...'));

ve package.json

{
  "name": "followerapp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "node index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@aspnet/signalr": "^1.1.2"
  }
}

Çalışma Zamanı(NotifierApp Uygulaması)

Bu adam neler anlattı, neler yazdı diyor gibisiniz biliyorum. O nedenle çalışma zamanına geçmeden önce senaryodan bahsetmem çok doğru olacaktır. WestWorld üzerinde NotifierApp isimli Azure Function uygulaması ayağa kalkar. Bu, Azure SignalR servisi ile haberleşen programımız. Postman ile hayali olarak o anda oynanan bir basketbol maçından çeşitli bilgiler göndereceğiz. Sayı oldu, blok yapıldı vs gibi. Bu bilgiler Azure tarafındaki SignalR servisimiz tarafından karşılanacak ve Table Storage üstünde kuyruğa yazılacak. Yine WestWorld üzerinde çalışan bir başka uygulama(Etkili bir görsellik için bir web sayfası ya da konuyu anlamak için bir console uygulaması olabilir) Local ortamda çalışan Azure Function servisine bağlanıp actionHappend olaylarını dinleyecek. Postman üzerinden maça ait bir basketbol olayı gönderildikçe bu bilgilerin tamamının yer aldığı kuyruk içeriği abone olan istemcilere otomatik olarak dağıtılacak. Sonuçta canlı bir maçın gerçekleşen anlık olayları bu haber kanalını dinleyen istemcilerine eş zamanlı olarak basılmış olacak(en azından senaryonun bu şekilde çalışmasını bekliyoruz)

Yazılan Azure Function uygulamasını çalıştırmak için terminalden aşağıdaki komutu vermek yeterli. Tabii bu komutu Azure Function projesinin olduğu klasörde icra etmeliyiz ;)

func host start

Function uygulamamız şu anda local ortamda çalışır durumda olmalı ve Azure SignalR ile haberleşmesi gerekli. En azından WestWorld üzerinde bu şekilde işledi. Şimdi Postman aracını kullanarak api/Scorer adresine bir HTTP Post talebi gönderebiliriz. Örneğin aşağıdaki gibi.

Url : http://localhost:4503/api/Scorer
Method : HTTP Post
Body : {
"Who":"Mitsiç",
"WhatHappend":"3 sayılık basket. Skor 33-21 Anadolu Efes önde"
}

Bir şeyleri doğru yazmış olmalıyım ki log mesajlarında istediğim hareketliliği gördüm. Hatta Azure Storage tarafında bir tablonun oluşturulduğunu ve gönderdiğim bilginin içerisine yazıldığını da fark ettim(Tekrar eden bilgileri nasıl normalize etmek gerekir bunun yolunu bulmak lazım) Şu aşamaya gelen okurlarım, umarım sizler de benzer sonuçları görmüşsünüzdür.

Çalışma Zamanı(İstemci/Abone olan taraf)

Bildirim yapmayı başardık. Bildirimlerin kuyruğa gittiğini de gördük. Peki ya abonelerden ne haber? Senaryonun tam işlerliğini görmek için her iki uygulamayı da birlikte çalıştırmak lazım elbette. Node.js tabanlı FollowerApp için terminalden aşağıdaki komutu vermek yeterli.

npm run dev

İlk ekran görüntüsü istemci ile Azure SignalR servisinin, Azure Function uygulaması aracılığıyla el sıkışmasını gösteriyor.

Alt ekran görüntüsünde dikkat edileceği üzere Negotiation başarıyla sağlandıktan sonra bir id ve token bilgisinin üretildiği görülmekte. Buradaki çıktı, Azure Function uygulamasındaki negotiate sonrası döndürdüğümüz connection bilgisine ait. Dikkat çekici noktalardan birisi de Web Socket adresi. Görebildiniz mi?

İkinci ekran görüntüsünde http://localhost:4503/api/Scorer adresine HTTP Post talebi ile örnek bir olay bilgisi gönderilmekte. Bu talep sonrası uygulamalardaki log hareketliliklerine dikkat etmek lazım. Oluşan içerik bağlı olan istemciye yansımış olmalıdır. Bu yılın flaş takımı Anadolu Efes'ten 4 ve 5 numara pozisyonlarında oynayabilen ve üçlük yüzdesi de fena olmayan Moaerman epey ribaund toplamış sanki.

Üçüncü çalışma zamanı görüntüsünde ekrana ikinci bir istemci dahil etmekteyiz. Bu durumda push edilen bilgiler bağlı olan tüm abonelere gönderilecektir ki istediğimiz senaryolardan birisi de bu(Bırayn Danstın mı? Yok artık Babi diksın mı? :D )

Eğer bu senaryoda yaptığımız gibi bir maçın canlı anlatımını çevrimiçi tüm abonelere göndermek istiyorsak, sonradan dahil olanların maçın başından itibaren kaçırdıkları olayları da görmesini isteyebiliriz. Burada Table Storage veya benzeri bir depoda maç bazlı tutulacak verileri, istemci ilk bağlandığında ona nasıl yollayabiliriz doğrusu çok merak ediyorum. İşte size güzel bir TODO ;)

Ben Neler Öğrendim?

Aslında hepsi bu. Temel bir kurgu ile Azure tarafındaki SignalR servisimizi kullanarak bir push notification sürecini deneyimledik diyebilirim. Her cumartesi gecesi çalışmasında olduğu gibi bu uygulamadan da bir şeyler öğrendim elbette. Bunları aşağıdaki gibi sıralayabilirim. Unutana kadar bendeler :)

  • Azure tarafında bir SignalR Servisinin nasıl oluşturulacağını
  • Geliştirme ortamında bir Azure Function projesinin nasıl inşa edilebileceğini
  • SignalR üzerinden Hub dinleyicisi istemcilerde @aspnet/signalr npm paketinin nasıl kullanılabileceğini
  • Azure Storage oluşturmadan Function projesindeki Table Storage'ın kullanılamayacağını
  • SignalR servisini kullanan Azure Function projesinin herhangi bir istemci tarafından kullanılabilmesi için CORS tarafında '*' kullanılması gerektiğini(Bunu makalede bulamayacaksınız sizin keşfetmeniz gerekebilir:( )
  • Azure Function tarafında abonelerin SignalR ile el sıkıştığı fonksiyon adının 'negotiate' olması gerektiğini(Farklı bir isim kullanınca istemci tarafında HTTP 404 NotFound hatası aldım)
  • Benzer şekilde SignalR Hubname olarak notifications kullanılması gerektiğini(Farklı bir isimlendirme kullanınca oluşan bilgilerin SignalR servisi tarafından yorumlandığını ama abonelere akmadığına şahit oldum)

Böylece geldik doğduğum, yaşadığım ve asla kopamayacağım İstanbul plakalı cumartesi gecesi derlemesinin sonuna. Umarım sizler için de yararlı bir çalışma olmuştur. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Bu Sefer Bir React Uygulamasını Heroku Üzerine Alalım

$
0
0

Sir Ken Robinson, çocukların hayal güçlerini sınırlayan eğitim sistemini eleştirdiği TED'in en çok izlenen sunumunda William Shakespeare ile ilgili güzel bir anektod paylaşır. Konuşmasının ilgili bölümünde profesör onun bir zamanlar yedi yaşında bir çocuk olduğunu dile getirir. Kısa bir an için duraksar ve ne diyeceğini merak eden seyirciye "...Shakespeare'i hiç çocuk olarak düşünmemiştiniz, değil mi?" der :)

Hepimiz onu ünlü İngiliz şair ve yazar olarak bilir Romeo Juliet, Macbeth, Othello ve diğer trajedileri ile hatırlarız. Hatta "olmak yada olmamak, işte bütün mesele bu" sözleri hafızalarımıza kazınmıştır. Ancak çoğumuz onun da bir zamanlar çocuk olduğunu ve bir öğretmenin edebiyat dersine girdiğini düşünmeyiz(Bunu Ken Robinson gayet güzel bir şekilde düşündürtüyor) Onun da hepimiz gibi çocukken kurduğu hayaller olduğunu bu cümleleri duyana kadar da fark etmeyiz. Çok şükür ki sekiz numaralı çalışmamın içerisinde geçen bir kelime benim onu, onun yardımıyla Sir Ken Robinson'u ve sonrasında da bu güzel anekdotu hatırlamamı sağladı. Nihayetinde Shakespeare'in ölümsüz eserlerinden olan Hamlet'in Heroku tarafından bana önerilmesi işte bu kısa girizgahın hayat bulmasına vesile oldu. 

Sekiz numaralı örnekteki amacım node.js ile çalıştırılan basit bir React uygulamasını Heroku üzerine taşımaktı. React ile node haberleşmesinde express paketini kullanmıştım. Bu paket deneyimlediğim kadarıyla HTTP yönlendiricisi olarak kullanılmaktaydı. React tarafına gelen HTTP taleplerini karşılarken kullanılabilmekte. Diğer yandan React tarafında çok fazla tecrübem olmadığından benim için hala kapalı kutu olma özelliğini taşıyor. Bir nevi ona da merhaba demek istediğim bir çalışma olduğunu ifade edebilirim.

Heroku 2007 yılında işe başladığında sadece Ruby on Rails bazlı web uygulamalarına destek veren bir bulut bilişim sistemiydi ancak Platform as a Service(PaaS) olarak olgunlaştıktan sonra Java, Node.js, Scala, Python, Go, Closure ve benzeri bir çok dil ile geliştirilen uygulmalar için de hizmet vermeye başladı. Aslında heroku üzerindeki ilk denememi 2018 yılında yapmış ve şöyle bir yazı yazmıştım. Teknolojinin gelişimi düşünüldüğünde aradan yadsınamayacak kadar çok zaman geçmiş diyebilirim. O yüzden bu tip platformlara ara ara dönüş yaparak farklı enstrümanlarla kullanmayı denemek güncel kalmamız açısından önemli. Öyleyse vakit kaybetmeden sekiz numaralı Cumartesi gecesi çalışmasını derlemeye başlayalım.

Gerekli Hazırlıklar

Tabii öncelikle Heroku üzerinde bir hesap açmak gerekiyor. Ben gerekli hesabı açtıktan sonra WestWorld(Ubuntu 18.04, 64bit) üzerinde Heroku CLI(command-line interface) kurulumunu da yaptım. Böylece heroku ile ilgili işlemlerimizi terminal komutlarını kullanarak kolayca gerçekleştirebiliriz.

sudo snap install --classic heroku

Kurulum sonrası login olmamız gerekecektir.

heroku login -i

Yukarıdaki terminal komutunu çalıştırdıktan sonra credential bilgileri sorulur(-i parametresini heroku login bilgilerinin kalıcı olması için kullanabiliriz) Heroku tarafı ile iletişimi kurduğumuza göre uygulamanın çatısını oluşturmaya başlayabiliriz. Öncelikle app isimli bir klasör açıp aşağıdaki terminal komutu ile node tarafını başlatalım. Biraz sonra kuracağımız React uygulamamız ile konuşacağı basit node sunucusu bu klasörde yer alacak.

npm init

Bazı yardımcı paketlerimiz var. Bunları şu terminal komutu ile yükleyebiliriz.

npm i --save-dev express nodemon concurrently

express, servis tarafını daha kolay kullanabilmemiz için gerekli özellikleri sunan bir paket. nodemon ile de node.js tarafında yapılan değişikliklerin otomatik olarak algılanması sağlanıyor. Yani uygulamayı tekrar başlatmaya gerek kalmadan kod tarafındaki değişikliklerin çalışma zamanına yansıtılması sağlanabilir. concurrently paketi hem express hem react uygulamalarının aynı anda başlatılması için kullanılmakta. Paket yüklemeleri tamamlandıktan sonra app kök klasörü altında server.js isimli bir dosya oluşturup kodlamasını sonradan tamamlamak üzere çalışmamıza devam edebiliriz.

React Uygulamasının Oluşturulması

React uygulmasını standart bir hello-world şablonu şeklinde açacağız. Bunun için aşağıdaki terminal komutunu kullanmamız yeterli.

npm i -g create-react-app
create-react-app fromwestworld

Bu arada create-react-app komutu ile uygulamayı oluştururken şunu fark ettim ki proje adında sadece küçük harf kullanılabiliyor. İlerde değişir mi, siz bunu okurken değişmiş midir, neden böyledir tam bilemiyorum ama bir unix isimlendirme standardından kaynaklıdır diye düşünüyorum(Fikri olan?)

Bir süre geçtikten sonra fromwestworld klasörü içerisinde React için gerekli ne varsa oluşturulduğunu görürüz. Oluşturulan bu şablonu çok fazla bozmadan kullanabiliriz. 

Şimdi geliştirme ortamı için fromwestworld klasöründeki package.json içerisine bir proxy tanımı ekleyerek devam edelim. Biraz sonra kodlayacağımız node sunucumuz 5005 numaralı porttan yayın yapacak(geliştirme ortamı için farklı bir portta tercih edilebilir) Bu bildirim ile React uygulmasının geliştirme ortamında konuşacağı servis adresi ifade edilir. Bir başka deyişle React'a gelen HTTP taleplerinin yönlendirileceği adresi belirtiyoruz.

"proxy": "http://localhost:5005",

Şablonla gelen app.js içeriğini aşağıdaki gibi değiştirebiliriz. İçeriğin şu anda çok fazla önemi yok. Hedefimiz olan Heroku deployment için mümkün mertebe basit kodlar kullanmakta yarar var.

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App"><header className="App-header"><img src={logo} className="App-logo" alt="logo" /><p>
            Edit <code>src/App.js</code> and save to reload.</p><a
            className="App-link"
            href="https://www.buraksenyurt.com"
            target="_blank"
            rel="noopener noreferrer">
            Bu benim blog sayfamdır :)
          </a></header></div>
    );
  }
}

export default App;

React uygulamasının iletişimde olacağı sunucuya ait server.js dosyasını da aşağıdaki gibi yazabiliriz. 

var express = require('express');
var app = express();
var path = require('path');
var port = process.env.PORT || 5005; //heroku'nun portu veya local geliştirme için belirlediğimiz 5005 nolu port

// statik klasör bildirimini yapıyoruz
app.use(express.static(path.join(__dirname, 'fromwestworld/build')));

//canlı ortamdaysak yani uygulamamız Heroku'ya alınmışsa
if (process.env.NODE_ENV === 'production') {
    // build klasöründen index.html dosyasını alıp get talebinin karşılığı olarak istemciye sunuyoruz
    app.use(express.static(path.join(__dirname, 'fromwestworld/build')));
    app.get('*', (req, res) => {
        res.sendfile(path.join(__dirname = 'fromwestworld/build/index.html'));
    })
}

// Eğer canlı ortamda(heroku'da) değilsek ve amacımız sadece localhost'ta test ise
// index.html'i public klasöründen sunuyoruz
app.get('*', (req, res) => {
    res.sendFile(path.join(__dirname + '/fromwestworld/public/index.html'));
})

// express sunucusunu çalıştırıyoruz
app.listen(port, (req, res) => {
    console.log(`sunucumuz ${port} nolu porttan yayındadır`);
})

Temel olarak bulunulan ortama(development veya production) göre public klasöründe yer alan(ve benim üzerinde hiçbir değişiklik yapmadığım) index.html sayfasının sunulması söz konusu. Kod tarafındaki bu değişiklilere ilaveten root klasör olarak düşüneceğimiz app altındaki package.json dosyasındaki scripts kısmını da kurcalamalıyız. Güncel hali aşağıdaki gibi.

"scripts": {
"client-install": "npm install --prefix fromwestworld",
"start": "node index.js",
"server": "nodemon index.js",
"client": "npm start --prefix fromwestworld",
"dev": "concurrently \"npm run server\" \"npm run fromwestworld\""
}

Bunlardan start haricindekiler npm run arkasına eklenen komutlardır. Örneğin npm start ile node index.js komutu ve dolayısıyla uygulama çalışır. Diğer yandan npm run server ile nodemon devreye alınır ve kodda yapılan değişiklik anında çalışma zamanına yansır. npm run client, sunucuyu başlatmadan react uygulamasını çalıştırma görevini üstlenir. npm run client-install sayesinde ise React uygulaması için gerekli tüm bağımlılıklar ilgili ortama(örnekte Heroku olacaktır) yüklenir. npm run dev ile development ortamı ayağa kalkar ve hem node sunucusu hem de react uygulaması aynı anda başlatılır.

Uygulamayı komple çalıştırmak için app klasöründeyken

npm run dev

terminal komutunu işletebiliriz. concurrently paketi bu noktada bize avantaj sağlamaktadır. Eş zamanlı olarak "npm run server" ve "npm run client" komutlarının işletilmesinde rol alır.

Bu işlem sonrasında önce node sunucusu yüklenir. Sunucu çalışmaya başladıktan sonra React uygulaması tetiklenir ve localhost:3000 nolu porttan ilgili içeriğe ulaşılır. Hatırlayacağınız üzere node sunucusu 5005 numaralı porttan hizmet veriyordu. React uygulamasında yapılan proxy bildirimi, 3000 nolu adrese gelen HTTP taleplerinin arkadaki node sunucusuna yönlendirilmesinde rol alır. Dolayısıyla React uygulaması çalışmaya başladığında oluşan HTTP Get talebi server.js dosyasındaki get metodlarına düşer. Buradaki bildirimlere göre fromwestworld içerisindeki index.html dosyasının sunulması söz konusudur. index.html içeriğinde dikkat edileceği üzere root id'li bir div elementi vardır. Yine dikkat edilecek olursa index.js tarafı da aşağıdaki gibidir (Hiç değiştirmedim)

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.unregister();

ReactDOM.render metoduna dikkat edelim. root isimli DOM elementini yakalayıp buraya app isimli bileşenin basılması sağlanmaktadır. Özetlemek gerekirse React uygulaması kendi içeriklerini sunarken arka plandaki node sunucusu ile anlaşmaktadır. Buna göre server.js tarafında sunucu bazlı materyalleri(veri tabanı, asenkron operasyonlar vb) kullanarak bunların react bileşenlerince ele alınması sağlanabilir.

Çalışmayı gerçekleştiğimde West-World tarafında uygulamanın açılması biraz zaman almıştı. Sizin de benim gibi sebat edip panik yapmadan beklemeniz gerekebilir. İşte çalışma zamanı görüntüleri.

Her şey yolunda giderse localhost:3000 adresinden aşağıdaki içeriğe ulaşabiliriz.

Uygulamanın Heroku Platformuna Alınması

Öncelikle Heroku üzerinde bir uygulama oluşturmamız gerekiyor. Bunu aşağıdaki terminal komutuyla yapabiliriz.

heroku create

Bana proje adı olarak Heroku'nun otomatik olarak ürettiği frozen-hamlet-75426 denk geldi. Ayrıca uygulama kodlarını atabilmek için github ve ulaşacağım web adresi bilgisi de iletildi.

Uygulamanın web adresi https://frozen-hamlet-75426.herokuapp.com/ şeklinde. github adresi ise https://git.heroku.com/frozen-hamlet-75426.git. Hatta sonuçları Heroku Dashboard üzerinden de görebiliriz(Tabii siz örneği denerken güncel hali Heroku üzerinde olmayabilir. Kendiniz için bir tane yapsanız daha iyi olur)

Uygulama klasöründeki json dosyasında yer alan heroku-postbuild script'i bu aşamada önem kazanıyor. Kodlar git ortamına taşındıktan sonra bir build işlemi gerekiyor. Heroku bu script kısmını ele alıyor.

"heroku-postbuild":"NPM_CONFIG_PRODUCTION=false npm install --prefix fromwestworld && npm run build --prefix fromwestworld"

Bu düzenlemenin ardından yazılan kodların geliştirme ortamından github üzerine atılması gerekiyor. Yani Heroku'nun kod deposu olarak github'ı kullandığını ifade edebiliriz. Aşağıdaki komutlarla devam edelim öyleyse.

heroku git:remote -a frozen-hamlet-75426
git add .
git commit -am 'Heroku React Express örneği eklendi'
git push heroku master

Yapılanları aşağıdaki gibi özetleyebiliriz.

  1. Heroku için git remote adresini belirle
  2. Tüm değişiklikleri stage'e al
  3. Değişiklikleri onayla(commit)
  4. ve kodun son halini master branch'e push'la

Kodun github'a alınması aynı zamanda heroku'nun da ilgili uygulamayı gerekli build betiklerini çalıştırarak devreye alması anlamına gelmekte. Dolayısıyla bir süre sonra https://frozen-hamlet-75426.herokuapp.com/ adresine gitmek sonuçları görmemiz açısından yeterli olacaktır.

Bazı Hatalarım da Olmadı Değil

Çalışma sırasında bu basit hello-world uygulamasını tek seferde Heroku'ya taşıyamadığımı ifade etmek isterim. Eğer taşıma sırasında sorunlarla karşılaşırsanız bunları görmek için terminalden

heroku logs --tail

komutunu kullanabilirsiniz. Yaşadığım sorunları ve çözümlerini aşağıdaki maddelerde bulabilirsiniz.

  • İlk hatam server.js dosyasında process.env.PORT yerine process.env.port kullanmış olmamdı. Heroku ortamı bu port'u anlamadığı için 5005 nolu porttan yayın yapmaya çalıştı ki bu mümkün değildi.
  • İkinci hatam package.json içerisinde ortam için gerekli node engine versiyonunu söylememiş olmamdı. Nitekim Heroku tarafı node'un hangi sürümünü kullanacağını bilmek ister.
  • Diğer problemse bağımlı olunan npm paketleri için package.json dosyasında dependencies yerine devDependencies sektörünü bırakmış olmamdı. Üretim ortamı için dependencies kısmına bakılıyor.
  • Ayrıca .gitignore dosyasını koymayıp node_modules ve package-log.json öğelerini hariç tutmadığım için bu klasörleri de komple push'lamış oldum(Sonraki versiyonda düzelttim tabii)

Bu hususlara dikkat ettiğimiz takdirde ürünü başarılı bir şekilde yayına almış oluruz. Tabii uygulamanın şu an için yaptığı hiçbir şey yok. Oysa ki PostgreSQL kullanaraktan veri odaklı basit bir Hello World uygulaması pekala geliştirilebilir. Daha önceden sıkça dile getirdiğim üzere bu kutsal görevi siz değerli okurlarıma bırakıyorum :)

Ben Neler Öğrendim?

Uzun süre sonra derlemek üzere ele aldığım bu çalışmada Heroku üzerine bir React uygulamasının nasıl alındığını hatırlayıp bilgilerimi tazeleme fırsatı buldum. Kabaca yaptıklarımın üstünden geçtikten sonra öğrendiklerimi aşağıdaki maddeler halinde ifade edebileceğimi düşünüyorum.

  • Heroku'da hesap açma ve uygulama oluşturma adımlarını
  • Heroku CLI üzerinden dağıtım işlemlerinin nasıl yapıldığını
  • Bir node sunucusu üzerinden bir React uygulamasının ayağa kaldırılmasını
  • En temel düzeyde app react bileşeninin nerede nasıl kullanıldığını
  • Deployment sırasında veya çalışma zamanındaki hatalara ait loglara nasıl bakıldığını

Böylece geldik bir cumartesi gecesi derlemesinin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Hasura GraphQL Engine ile geliştirilmiş bir API Servisini Vue.js ile Kullanmak

$
0
0

Yıl 2015. Hindistan'ın Bengaluru şehrinde doğan bir Startup(Sonradan San Fransico'da da bir ofis sahibi olacaklar), Microsoft'un BizSpark programından destek buluyor. Kurucuları Rajoshi Ghosh(Aslen bioinformatik araştırmacısı) ve Tanmai Gopal(Bulut sistemleri, fonksiyonel programlama ve GraphQL konusunda uzman) isimli iki Hintli. Şirketlerine şeytanın sanskritçedeki adını veriyorlar; Hasura! Aslında O, fonksiyonel dillerin kralı Haskell ile yazılmış bir platform ve şimdilerde Heroku ile daha yakın arkadaş.

Ekibin amacı geliştiricilerin hayatını kolaylaştıracak, yüksek hızlı, kolayca ölçeklenebilir, sade ve Kubernetes ile dost PaaS(Platform as a Service) ile BaaS(Back-end as a Service) ortamları sunmak.

İsimlendirmenin gerçek hikayesi tam olarak nedir bilemiyorum ama eğlenceli bir logoları olduğu kesin :) Startup'ların en sevdiğim yanlarından birisi de bu. Özgün, tabulara takılmadan, etki bırakacak şekilde düşünülen isimleri, renkleri, logoları...Belki de arka planda sessiz sedasız süreçler çalıştıran back-end servislerini birer iblis olarak düşündüklerinden bu ismi kullanmışlardır. Hatta Heroku üzerinde koştuğunu öğrenince Japonca bir kelime olduğunu bile düşünmüştüm. Hu novs?! Ama işin özü verdikleri önemli hizmetler olduğu. Bunlardan birisi de GraphQL motorları.

API'ler için türlendirilmiş(typed) sorgulama dillerinden birisi olarak öne çıkan GraphQL'e bir süredir uğramıyordum. Daha doğrusu GraphQL sorgusu çalıştırılabilecek şekilde API servis hazırlıklarını yapmaya üşeniyordum. Bu nedenle işi kolaylaştıran ve Heroku üzerinden sunulan Hasura GraphQL Engine hizmetine bakmaya karar vermiştim. Hasura, veriyi PostgreSQL kullanarak saklıyor ve ayrıca API'yi bir Docker Container içerisinden sunuyor. Amacım Hasura tarafında hazırlayacağım iki kobay veri setini, Vue.js tabanlı bir istemcisinden tüketmekti. Basitçe listeleme ve veri ekleme işlerini yapabilsem başlangıç için yeterli olacaktı. Öyleyse ne duruyoruz. 37nci saturday-night-works çalışmasının derlemesine başlayalım. İlk olarak Hasura servisini hazırlayacağız.

Hasura GraphQL Engine Tarafının Geliştirilmesi

Pek tabii Heroku üzerinde bir hesabımızın olması gerekiyor. Sonrasında şu adrese gidip elements kısmından Hasura GraphQL Engine'i seçmek yeterli.

Gelinen yerden Deploy to Heroku diyerek projeyi oluşturabiliriz.

Ben aşağıdaki bilgileri kullanarak bir proje oluşturdum.

Deploy başarılı bir şekilde tamamlandıktan sonra,

View seçeneği ile yönetim paneline geçebiliriz.

Dikkat edileceği üzere GraphQL sorgularını çalıştırabileceğimiz bir arayüz otomatik olarak sunuluyor. Ancak öncesinde örnek veri setleri hazırlamalıyız. Bunun için Data sekmesinden yararlanabiliriz.

Arabirimin kullanımı oldukça kolay. Ben aşağıdaki özelliklere sahip tabloları oluşturdum.

categories isimli tablomuzda unique tipte, get_random_uuid() fonksiyonu ile eklenen satır için rastgele üretilen categoryId ve text tipinden title isimli alanlar bulunuyor. categoryId, aynı zamanda primary key türünden bir alan.

products tablosunda da UUID tipinden productId, text tipinden description, number tipinden listPrice ve yine UUID tipinden categoryId isimli alanlar mevcut. categoryId alanını, ürünleri kategoriye bağlamak için(foreign key relations) kullanıyoruz. Ama bu alanı foreign key yapmak için Modify penceresine geçmeliyiz. 

İlişkinin geçerlilik kazanması içinse, categories tablosunun Relationships penceresine gidip önerilen bağlantıyı eklemek gerekiyor. 

Bu durumda categories üzerinden products'a gidebiliriz. Ters ilişkiyi de kurabiliriz ve bir ürünle birlikte bağlı olduğu kategorinin bilgisini de yansıtabiliriz ki ürünleri çektiğimizde hangi kategoride olduğunu da göstermek güzel olur. Bunu nasıl yapabileceğinizi bir deneyin isterim.

Hasura'nın Postgresql tarafındaki örnek tablolarımız hazır. İstersek Insert Row penceresinden tablolara örnek veri girişleri yapabilir ve GraphiQL pencresinden sorgular çalıştırabiliriz. Ben yaptığım denemelerle alakalı bir kaç örnek ekran görüntüsü paylaşayım. Arabirimin sağ tarafında yer alan Docs menüsüne de bakabilirsiniz. Burada query ve mutation örnekleri, hazırladığımız veri setleri için otomatik olarak oluşturuluyorlar.

Örnek Sorgular

Veri setimizi oluşturduktan sonra arabirim üzerinden bazı GraphQL sorgularını deneyebiliriz. Ben aşağıdaki örnekleri denedim.

Kategorilerin başlıklarını almak.

query{
  categories{
    title
  }
}

Kategorilere bağlı ürünleri çekmek.

query{
  categories{
    title
    products{
      description
      listPrice
    }
  }
}

Ürünlerin tam listesi ve bağlı olduğu kategori adlarını çekmek.

query{
  products{
    description
    listPrice
    category{
      title
    }
  }
}

Listeleme işlemleri dışında veri girişi de yapabiliriz. Bunun için mutation kullanıldığını daha önceden öğrenmiştim. Örneğin yeni bir kategoriyi aşağıdaki gibi ekleyebiliriz.

mutation {
  insert_categories(objects: [{
    title: "Çorap",
  }]) {
    returning {
      categoryId
    }
  }

Hasura, GraphQL API’si arkasında PostgreSQL veri tabanını kullanırken SQLden aşina olduğumuz bir çok sorgulama metodunu da hazır olarak sunar. Örneğin fiyatı 300 birimin üstünde olan ürünleri aşağıdaki sorgu ile çekebiliriz.

{
  products(where: {listPrice: {_gt: 300}}) {
    description
    listPrice
    category {
      title
    }
  }
}

Where metodu sorgu şemasına otomatik olarak eklenmiştir. _gt tahmin edileceği üzere greater than anlamındadır. Yukarıdaki sorguya fiyata göre tersten sıralama opsiyonunu da koyabiliriz. Sadece where koşulu arkasından order_by çağrısı yapmamız yeterlidir.

{
  products(where: {listPrice: {_gt: 300}}, order_by: {listPrice: desc}) {
    description
    listPrice
    category {
      title
    }
  }
}

Çok büyük veri setleri düşünüldüğünde ön yüzler için sayfalama önemlidir. Bunun için limit ve offset değerlerini kullanabiliriz. Örneğin 5nci üründen itibaren 5 ürünün getirilmesi için aşağıdaki sorgu kullanılabilir.

{
  products(limit: 5, offset: 5) {
    description
    listPrice
    category {
      title
    }
  }
}

Hasura Query Engine’in sorgu seçenekleri ile ilgili olarak buradaki dokümanı takip edebilirsiniz.

İstemci(Vue) Tarafı

Gelelim ilgili servisi tüketecek olan istemci uygulamamıza. İstemci tarafını basit bir Vue projesi olarak geliştirmeye karar vermiştim. Aşağıdaki terminal komutunu kullanıp varsayılan ayarları ile projeyi oluşturabiliriz. Ayrıca GraphQL tarafı ile konuşabilmek için gerekli npm paketlerini de yüklememiz gerekiyor. Apollo(ilerleyen ünitelerde ondan bir GraphQL Server yazmayı denemiştim), GraphQL servisimiz ile kolay bir şekilde iletişim kurmamızı sağlayacak. Görsel taraf içinse bootstrap kullanabiliriz.

sudo vue create nba-client
sudo npm install vue-apollo apollo-client apollo-cache-inmemory apollo-link-http graphql-tag graphql bootstrap --save

Kod Tarafı

Vue uygulaması tarafında yapacaklarımız kabaca şöyle(Kod dosyalarındaki yorum bloklarında daha detaylı bilgiler mevcut)

Components klasörüne tek ürün için kullanılabilecek ProductItem isimli bir bileşen ekliyoruz. Anasayfa listelemesinde tekrarlanacak türden bir bileşen olacak bu. Bileşende product özelliği üzerinden içerideki elementlere veri bağlama işlemini gerçekleştirmekteyiz. {{nesne.özellik}} notasyonlarının nasıl kullanıldığına dikkat edelim.

<template><div :key="product.productId" class="card w-75"><div class="card-header"><p class="card-text">{{product.description}}</p></div><div class="card-body text-left"><h6 class="card-subtitle mb-2 text-muted">{{product.listPrice}} Lira</h6><h6 class="card-subtitle mb-2">'{{product.category.title}}' kategorisinden</h6></div><div class="card-footer text-right"><a href="#" class="btn btn-primary">Sepete Ekle</a></div><hr/></div></template><script>
export default {
  name: "ProductItem",
  props: ["product"]
};</script>

Ürünlerin listesini gösterebilmek içinse ProductList isimli bir bileşen kullanacağız. Bunu da components altında aşağıdaki gibi yazabiliriz.

<template><div><!--
      products dizisindeki her bir ürün için product-item öğesi ekliyoruz.
      Bu öğe bir ProductItem bileşeni esasında
      --><product-item v-for="product in products" :key="product.productId" :product="product"></product-item></div></template><script>
/*
  div içerisinde kullandığımız product-item elementi için ProductItem bileşenini eklememi gerekiyor.
  gql ise GraphQL sorgularını çalıştırabilmemiz için gerekli
*/
import ProductItem from "./ProductItem";
import gql from "graphql-tag";

/*
  GraphQL sorgumuz.
  Tüm ürün listeini, kategori adları ile birlikte getirecek
*/
const selectAllProducts = gql`
  query getProducts{
  products{
    productId
    description
    listPrice
    category{
      title
    }
  }
}
`;

/*
  Sorguyu GraphQL API'sine gönderebilmek için apollo'ya ihtiyacımız var.
  products dizisini query parametresine verilen değişken ile çekiyoruz.
*/
export default {
  name: "ProductList",
  components: { ProductItem }, // Sayfada bu bileşeni kullandığımız için eklendi
  data() {
    return {
      products: []
    };
  },
  apollo: {
    products: {
      query: selectAllProducts
    }
  }
};</script>

Ürün ekleme işini ProductAdd isimli bileşen üstleniyor. Yine components sekmesinde konuşlandıracağımız tipin kod içeriği aşağıdaki gibi olmalı.

<template><!-- Veri girişi için basit bir formumuz var. Input değerlerini v-model niteliklerine verilen isimlerle bileşene bağlıyoruz --><form @submit="submit"><fieldset><div class="form-group w-75"><input
          class="form-control"
          aria-describedby="descriptionHelp"
          type="text"
          placeholder="Ürün bilgisi"
          v-model="description"><small
          id="descriptionHelp"
          class="form-text text-muted">Satışı olan basketbol malzemesi hakkında kısa bir bilgi...</small></div><div class="form-group w-75"><input class="form-control" type="number" v-model="listPrice"><small id="listPriceHelp" class="form-text text-muted">Ürünün mağaza satış fiyatı</small></div><div class="form-group w-75"><input
          class="form-control"
          type="text"
          placeholder="Halledene kadar kategorinin UUID bilgisi :D"
          v-model="categoryId"></div><!-- Kategoriyi drop down olarak nasıl ekleyebiliriz? --></fieldset><div class="form-group w-75 text-right"><button class="btn btn-success" type="submit">Dükkana Yolla</button></div></form></template><script>
import gql from "graphql-tag";
//import { InMemoryCache } from "apollo-cache-inmemory";

/*
  Bu veri girişi yapmak için kullanacağımız mutation sorgumuz.
  insert_products'u Hasura tarafında kullanmıştık hatırlarsanız.

  mutation parametrelerini belirlerken veri türlerine dikkat etmemiz lazım.
  Söz gelimi listPrice, Hasura tarafında Numeric tanımlandı. CategoryId değeri
  ise UUID formatında. Buna göre case-sensitive olarak veri tiplerini söylüyoruz.
  Aslında bunu anlamak için numeric! yerine Numeric! yazıp deneyin. HTTP 400
  Bad Request alıyor olmalısınız.
*/
const addNewProduct = gql`
  mutation addProduct(
    $description: String!
    $listPrice: numeric!
    $categoryId: uuid!
  ) {
    insert_products(
      objects: [
        {
          description: $description
          listPrice: $listPrice
          categoryId: $categoryId
        }
      ]
    ) {
      returning {
        productId
      }
    }
  }
`;

export default {
  name: "ProductAdd",
  data() {
    return {
      description: "",
      listPrice: 0,
      categoryId: ""
    };
  },
  apollo: {},
  methods: {
    /*
    form submit edildiği zaman devreye giren metodumuz.
    $data ile formdaki veri içeriğini description, listPrice ve categoryId olarak yakalıyoruz
    */
    submit(e) {
      e.preventDefault();
      const { description, listPrice, categoryId } = this.$data;

      /*
      apollo'nun mutate metodu ile addNewProduct isimli mutation sorgusunu çalıştırıyoruz.
      Sorgunun beklediği değişkenler this.$data ile zaten yakalanmışlardı.
      */
      this.$apollo.mutate({
        mutation: addNewProduct,
        variables: {
          description,
          listPrice,
          categoryId
        },
        refetchQueries: ["ProductList"] // Insert işlemini takiben ürün lstesini tekrardan talep ediyoruz
      });
    }
  }
};</script>

Uygulamanın ana bileşeni olan App.Vue'da product-add ve product-list isimli nesnelerimizi aşağıdaki gibi yerleştirebiliriz.

<template><div id="app"><h2 class="text-left">Yeni Ürün</h2><!-- Bileşenleri altalta dizdik --><product-add/><h2 class="text-left">Basketbol Ürünleri</h2><product-list/></div></template><script>
/*
  Ana bileşen içerisinde kullanılan alt bileşenlerin import edilmesi
*/
import ProductList from "./components/ProductList.vue";
import ProductAdd from "./components/ProductAdd.vue";

export default {
  name: "app",
  components: {
    ProductList,
    ProductAdd
  }
};
</script>

Main.js içerisinde de önemli kodlamalarımız var. Amaç Hasura'yı ve GraphQL'i kullanabilir hale getirmek. Kodlarını aşağıdaki gibi geliştirebiliriz.

import Vue from 'vue';
import App from './App.vue';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import 'bootstrap/dist/css/bootstrap.min.css';
import VueApollo from 'vue-apollo';

Vue.config.productionTip = false;

// Hasura GraphQL Api adresimiz
const hasuraLink = new HttpLink({ uri: 'https://basketin-cepte.herokuapp.com/v1alpha1/graphql' });

/* 
  Servis iletişimini sağlayan nesne
  GraphQL istemcileri veriyi genellikle cache'de tutar.
  Tarayıcı ilk olarak cache'ten okuma yapar. 
  Performans ve network trafiğini azaltmış oluruz bu şekilde.
*/
const apolloClient = new ApolloClient({
  link: hasuraLink, // Kullanacağı servis adresini veriyoruz
  connectToDevTools: true, // Chrome'da dev tools üzerinde Apollo tab'ının çıkmasını sağlar. Debug işlerimiz kolaylaşır
  cache: new InMemoryCache() // ApolloClient'ın varsayılan Cache uyarlaması için InMemoryCache kullanılıyor. 
});

// Vue ortamının GraphQL ile entegre edebilmek için VueApollo kütüphanesini entegre ediyoruz. (https://akryum.github.io/vue-apollo/)
Vue.use(VueApollo);

/* 
  Vue tarafında GraphQL sorguları oluşturabilmek ve veri girişleri(mutations)
  yapabilmek için ApolloProvider örneği kullanmamız gerekiyor.
  VueApollo'den üretilen bu nesnenin Hasura tarafına işlemleri commit
  edebilmesi içinse yukarıdaki apolloClient'ı parametre olarak atıyoruz
*/
const apolloProvider = new VueApollo({
  defaultClient: apolloClient
});

new Vue({
  apolloProvider,// Vue uygulamamızın ApolloProvider'ı kullanabilmesi için eklendi
  render: h => h(App),
}).$mount('#app');

TODO (Benim tembelliğimden size düşen)

Bu servisi JWT Authentication bünyesine almak lazım. İşte size güzel bir araştırma konusu. Başlangıç noktası olarak Auth0'ın şu dokümanına bakılabilir. Ben şu an için sadece HASURA_GRAPHQL_ADMIN_SECRET kullanarak servis adresine erişimi kısıtlamış durumdayım. Zaten büyük ihtimalle yazıyı okuduğunuzda onun yerinde yeller estiğine şahit olacaksınız.

Çalışma Zamanı

Hasura servisimiz ve istemci taraftaki uygulamamız hazır. Artık çalışma zamanına geçip sonuçları irdeleyebiliriz. Programı başlatmak için

npm run serve

terminal komutunu vermemiz yeterli. Sonrasında http://localhost:8080 adresine giderek ana sayfaya ulaşabiliriz. Aynen aşağıdakine benzer bir görüntü elde etmemiz gerekiyor.

Yeni ürün ekleme bileşeni konulduktan sonrasına ait örnek bir ekran görüntüsünü de buraya iliştirelim.

Hatta yeni bir forma eklediğimizde gönderilen Graphql Mutation sorgusundan dönen değer, F12 sonrası Network sekmesinden yakalayabiliriz.

throw new UnDoneException("Yeni ürün ekleme sayfasında kategori seçiminde combobox kullanımı yapılmalı");

Ben Neler Öğrendim?

Doğruyu söylemek gerekirse bu çalışma benim için oldukça keyifliydi. Heroku platformunu oldukça beğeniyorum. Şirkette Vue tabanlı ürünlerimiz var ama onlar üzerinden çok iyi değilim. Dolayısıyla Vue tarafında bir şeyler yapmış olmak beni mutlu ediyor. Peki bu çalışma kapsamında neler mi öğrendim. İşte listem...

  • Heroku'da Docker Container içerisinde çalışan ve PostgreSQL verilerini GraphQL ile sorgulanabilir olarak sunan Hasura isimli bir motor olduğunu
  • Hasura arabirimden tablolar arası ilişkileri nasıl kuracağımı
  • Bir kaç basit GraphQL sorgusunu(sorgularda sayfalama yapmak, ilişkili veri getirmek, where koşulları kullanmak)
  • Vue tarafında GraphQL sorgularının nasıl gönderilebileceğini
  • Component içinde Component kullanımlarını(App bileşeni dışında product-list içinde product-item kullanımı)
  • Temel component tasarlama adımlarını
  • Vue tarafından bir Mutation sorgusunun nasıl gönderilebileceğini ve schema veri tiplerine dikkat etmem gerektiğini

Böylece geldik bir cumartesi gecesi derlemesinin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Bir React Uygulamasında En Ala SQL Veritabanını Kullanmak

$
0
0

İngilizcede bazen gemi kaptanlarına Captain yerine Skipper dendiğini biliyor muydunuz? Aslında Hollandalıların schipper, schip en nihayetinde de ship kelimelerinden türeyerek gelmiş bir ifade. Her ikisi de kaptanı ifade etmekte ama Skipper daha çok bir hitap şekli. Hatta yer yer takım kaptanları veya uçak pilotları için de kullanılıyor. Skipper kelimesinin kullanıldığı yerleri düşününce aklıma The Hunt For Red October filminde USS Dallas kaptanı Mancuso'nun CIA'den Jack Ryan'a "That's right? Skipper's Ramius?" demesi geliyor.

Esasında bu hitap şeklinin bana anımsattığı daha güzel şeyler var. Blizzard geliştiricilerinin Warcraft II'sini oynadığım zamanlarda insan kuvvetlerindeki gemilere Skipper diye sesleniliyordu. Karakterlerin o müthiş ses efektleri hala aklımda. "Ay ay sör", "Ayy keptın", "Set seyıl", "Sıkipp?", "Andır veyy!" :D Yazılı olarak seslendirmeye çalıştım ama dinleseniz çok daha iyi olabilir. Diğer pek çok karakterin sesi de harikaydı. Mesela köylülerin "Yeş mi lord" diyişindeki şirinlik ya da okçuların tonlamasındaki keskinlik. Youtube'dan silinene kadar şu adresten dinleyebilir veya aratabilirsiniz. Bugünkü konumuza gündem olmasının sebebi ise skipper isimli bir nesne dizisini kullanacak olmamız. Öyleyse başlayalım.

Öğrenecek bir çok şeyler araştırırken(ki samimi olmak gerekirse 24 saat uykusuz kalıp bir şeyleri öğrensek bile zamanın yetmeyeceği ve güneşe daha uzak bir gezegende yaşamamız gerektiği ortaya çıkıyor) AlaSQL isimli bir çalışma ile karşılaştım. Tarayıcı üzerinde çalışabilen istemci taraflı bir In-Memory veritabanı olarak geçiyor. Tamamen saf Javascript ile yazılmış. Geleneksel ilişkisel veritabanı özelliklerinin çoğunu barındırıyor. Group, join, union gibi fonksiyonellikleri karşılıyor. In-Memory tutulan veriyi kalıcı olarak saklamakta mümkün. Hatta bu noktada localStorage olarak ifade edilen yerel depolama alanlarından veri okunup tekrar yazılabiliyor. IndexedDB veya Excel gibi ürünleri fiziki repository olarak kullanabiliyor. Ayrıca JSON nesnelerle çalışabiliyoruz ki bu da NoSQL desteği anlamına gelmekte. Bu nedenle SQL ve NoSQL rahatlığını bir arada sunan hafif bir veritabanı gibi düşünülebilir.

Açık kaynak kodlu, dokümantasyonu oldukça zengin bir proje. Yine de endüstriyel anlamda olgunlaştığına dair emareler görülmeden canlı ortamlarda kullanmak riskli olabilir diye düşünürken github projesinden çıkıp org alan adına geçerek biraz daha ciddiye alınmaya başladığını fark ettim. Yine de deneysel çalışmalarda ele almakta yarar var. Benim 25nci cumartesi gecesi çalışmasındaki amacım onu yalın bir React uygulamasında deneyimlemeye çalışmaktı. Öyleyse gelin notlarımızı toparlamaya başlayalım.

Kurulum ve Hazırlıklar

Ben her zaman olduğu gibi örneğimi WestWorld(Ubuntu 18.04, 64bit)üzerinde deneyimledim. Ancak komutlar plaform bağımsız olarak ele alınabilir. Ah bu arada sisteminizde node'un yüklü olduğunu varsayıyorum. React uygulamasını kolayca oluşturabilmek için aşağıdaki terminal komutunu kullanabiliriz.

npx create-react-app submarine

AlaSQL'i kullanabilmek içinse uygulama klasöründe gerekli npm paketinin yüklenmesi yeterli olacaktır. Ayrıca görselliği zenginleştirmek için ben Bootstrap'i kullanmayı tercih ettim.

cd submarine
npm install --save-dev alasql bootstrap

React şablonu aslında senaryomuzdan bağımsız bir çok gereksiz dosya içerebilir. Bunları silip manifest.json içeriğini bir parça değiştirebiliriz. Örneğin uygulamanın tanımlayıcısı olan short_name ve name değerlerini aşağıdaki hale getirebiliriz.

{
  "short_name": "Tac-War-Mag",
  "name": "Tactical World Magazine",
// diğer kısımlar

Pek tabii en önemli kısım App.js dosyasında yapılanlar. Burası uygulamanın ayağa kalktıktan sonra oluşturulan ana bileşeni(component) Ana sayfanın HTML içeriği ile birlikte SQL ilişkili kodlarını barındırmakta. Ben mümkün mertebe içeriği yorum satırları ile zenginleştirerek açıklamaya çalıştım.

import React, { Component } from 'react';
import 'bootstrap/dist/css/bootstrap.css'; // az biraz bootstrap ile görselliği düzeltelim
import * as alasql from 'alasql'; // alasql ile konuşmamızı sağlayacak modül bildirimimiz

class App extends Component {

  /* yapıcı metod gibi düşünebiliriz sanırım
  genellikle local state değişkenlerini başlatmak ve onlara değer atamak
  için kullanılır
  */
  constructor(props) {
    super(props);

    /* 
    state'i değişebilir veriler için kullanırız. state değişikliğinde
    bileşenin otomatik olarak yeniden render edilmesi söz konusu olur
    */
    this.state = { skippers: [] };
  }

  /*
  componentWillMount metodu, ilgili bileşen Document Object Model'e bağlanmadan
  önce çalışır.
  Bizim örneğimizde veritabanını ve tablo kontrolünün yapılması ve
  yoklarsa yaratılmaları için ideal bir yerdir
  */
  componentWillMount() {
    /*
    Klasik SQL ifadeleri ile TacticalWorldDb isimli bir veritabanı olup
    olmadığını kontrol ediyor ve eğer yoksa oluşturup onu kullanacağımızı belirtiyoruz.
    SQL ifadelerini çalıştırmak için alasql metodunu çağırmak yeterli.
    */
    alasql(`
            CREATE LOCALSTORAGE DATABASE IF NOT EXISTS TacticalWorldDb;
            ATTACH LOCALSTORAGE DATABASE TacticalWorldDb;
            USE TacticalWorldDb;            
            `);

    /*  
      Şimdi tablomuzu ele alalım. Submarine isimli tablomuzda
      id, name, displacement ve country alanları yer alıyor. 
      Id alanı için otomatik artan bir primary key'de kullandık.
      Örneği abartmamak adına alan dozajını belli bir seviyede tuttuk.
    */
    alasql(`
            CREATE TABLE IF NOT EXISTS Submarine (
              id INT AUTOINCREMENT PRIMARY KEY,
              name VARCHAR(25) NOT NULL,
              displacement NUMBER NOT NULL,
              country VARCHAR(25) NOT NULL
            );
          `);
  }
  /*
  İlk satırda yer alan alasql komutu ile Submarine tablosundaki verileri displacement değerine
  göre büyükten küçüğe sıralı olacak şekilde çekiyoruz.
  Ardından state içeriğini bu tablo verisiyle ilişkilendiriyoruz.
  */
  getAll() {
    const submarineTable = alasql('SELECT * FROM Submarine ORDER BY displacement DESC');
    this.setState({ skippers: submarineTable });
    // console.log(submarineTable); // Kontrol amaçlı açıp F12 ile geçilecek kısımda Console sekmesinden takip edebiliriz. Bir JSON array olmasını bekliyoruz
  }

  /*
  Bileşen DOM nesnesine bağlandıktan sonra çalışan metodumuzdur.
  Burası örneğin tablo içeriğini çekip state nesnesine almak için
  son derece ideal bir yerdir.
  */
  componentDidMount() {
    this.getAll();
  }

  /*
  Yeni bir satır eklemek için aşağıdaki metodu kullanacağız.
  denizaltının adı, tonajı ve menşeği gibi bilgileri
  this.refs özelliği üzerinden yakalyabiliriz. this.refs DOM
  elemanlarına erişmek için kullanılmakta. Bu şekilde 
  formdaki input kontrollerini yakalayıp value niteliklerini
  okuyarak gerekli veriyi çekebiliriz

  Insert sorgusu için yine alasaql nesnesinden yararlanıyoruz.
  Bu sefer parametre içeriğini tek ? içerisinde yollamaktayız.
  Parametre değerleri aslında bir json nesnesi içinden yollanıyor.
  key olarak kolon adını, value olarak da refs üzerinden gelen bileşene ait value özelliğini veriyoruz.
  Id alanının otomatik arttırmak içinse autoval fonksiyonu devreye girmekte.

  Pek tabii yeni eklenen kayıt nedeniyle bileşeni güncellemek lazım.
  getAll metodu burada devreye girmekte
  */
  addSkipper() {
    const { name, displacement, country } = this.refs;
    if (!name.value) return;
    // console.log(dicplacement.value); // Kontrol amaçlı. Browser'dan F12 ile değerlere bakılabilir
    alasql('INSERT INTO Submarine VALUES ?',
      [{
        id: alasql.autoval('Submarine', 'id', true),
        name: name.value,
        displacement: displacement.value,
        country: country.value
      }]
    );
    this.getAll();
  }

  /*
  Silme operasyonunu yapan metodumuz.
  Parametre olarak gelen id değerine göre bir DELETE ifadesi çağırılı
  ve tüm liste tekrardan çekilir.
  */
  deleteSkipper(id) {
    alasql('DELETE FROM Submarine WHERE id = ?', id);
    this.getAll();
  }

  /* 
    State değişikliği gibi durumlarda bileşen güncellenmiş demektir. 
    Bu durumda render fonkisyonu devreye girer.

    render metodu bir HTML içeriği döndürmektedir.
    form sınıfındaki input kontrollerinin ref niteliklerine dikkat edelim.
    Bunları addSkipper metodunda this.refs ile alıyoruz.

    iki button bileşenimiz var ve her ikisinin onClick metodları ilgili fonksiyonları
    işaret ediyor.

    HTML sayfası iki kısımdan oluşuyor. Yeni bir veri girişi yaptığımız form ve tablo verisini
    gösteren bölüm. Tablo içeriğini birer satır olarak ekrana basmak için map fonksiyonundan
    yararlanıyoruz. map fonksiyonu lambda görünümlü blok içerisine sırası gelen satır bilgisini
    atıyor. Örnekte ship isimli değişken bu taşıyıcı rolünü üstlenmekte. ship değişkeni üzerinden
    tablo kolon adlarını kullanarak asıl verilere ulaşıyoruz.
  */
  render() {

    const { skippers } = this.state;

    return (
      <main className="container"><h2 className="mt-4">En Büyük Denizlatılar</h2><div className="row mt-4"><form><div className="form-group mx-sm-3 mb-2"><input type="text" ref="name" className="form-control" id="inputName" placeholder="Sınıfı" /></div><div className="form-group mx-sm-3 mb-2"><input type="text" ref="displacement" className="form-control" id="inputDisplacement" placeholder="Tonajı" /></div><div className="form-group mx-sm-3 mb-2"><input type="text" ref="country" className="form-control" id="inputCountry" placeholder="Sahibi..." /></div><div className="form-group mx-sm-3 mb-2"><button type="button" className="bnt btn-primary mb-2" onClick={e => this.addSkipper()}>Ekle</button></div></form></div><div><table className="table table-primary table-striped"><thead><tr><th scope="col">Sınıfı</th><th scope="col">Tonajı</th><th scope="col">Ülkesi</th><th></th></tr></thead><tbody>
              {
                skippers.length === 0 && <tr><td colSpan="5">Henüz veri yok</td></tr>
              }
              {
                skippers.length > 0 && skippers.map(ship => (<tr><td>{ship.name}</td><td>{ship.displacement}</td><td>{ship.country}</td><td><button className="btn btn-danger" onClick={e => this.deleteSkipper(ship.id)}>Sil</button></td></tr>
                ))
              }</tbody></table></div></main >
    );
  }
}

export default App;

Aslında CRUD(Create Read Update Delete) operasyonlarını sunmaya çalıştığımız bir arayüz var. Senaryomuzda dünyanın en büyük denizaltılarını listelediğimiz, ekleyip çıkarttığımız bir web bileşeni söz konusu. Tahmin edeceğiniz üzere benim hep atladığım bir şey daha var...Güncelleme eksik :| Artık o kısmını da siz değerli okurlarıma bırakıyorum.

Öyleyse aşağıdaki terminal komutunu vererek uygulamamızı çalıştıralım (Bu arada react uygulamasını şablondan oluşturduğumuz için package.json içerisindeki scripts kısmı otomatik olarak ayarlanmıştır. start anahtar kelimesini kullanmamızın sebebi bu)

npm run start

İşte çalışma zamanına ait bir kaç görüntüsü.

Her şey yeni başlarken ve hiç veri yokken ana sayfa aşağıdaki gibi açılmalıdır.

Bir kaç satır ekledikten sonraki durum ise şöyle olacaktır.

Local Storage Nerede?

Peki veriyi tarayıcımız nerede tutuyor? Sonuçta In-Memory bir veritabanı olduğundan bahsediyoruz. Lakin içerik tarayıcı tarafında bir alanda konumlanıyor. Varsayılan senaryoda veri Local Storage bölümünde depolanmakta. Uygulamayı çalıştırdıktan sonra Chrome DevTools'a geçip Application sekmesine giderek içeriğini görebiliriz. Dikkat edileceği üzere TacticalWorldDb.Submarine isimli bir tablo bulunuyor ve verilerimiz içerisinde JSON nesneler olarak tutuluyor (Diğer yandan depolama alanı olarak componentWillAmount metodu içerisindeki SQL komutumuzda LocalStorage'ı ifade ettiğimizi hatırlayalım)

Storage sekmesine bakarsak Local Storage dışında IndexedDB seçeneği de bulunmaktadır. Eğer bu alanı kullanmak istersek

ATTACH INDEXEDDB DATABASE TacticalWorldDB

gibi bir SQL ifadesinden yararlanmamız gerekiyor. Tabii bu arada önemli bir soru da gündeme geliyor. Uygulamayı kapattığımızda veriye ne olacak? Bunu cevaplayabilmeniz için kodu buraya kadar geliştirmiş olmanız gerekiyor :)

Ben Neler Öğrendim?

Bazen sıfırdan başlanacak bir ürün için ya da lisans maliyetleri ve diğer sebepler nedeniyle modernize edilecek bir proje için verinin nerede neyle tutulacağını araştırmak isteyebiliriz. Böyle durumlarda alternatifleri POC(Proof of Concept) tadındaki deneysel programlarda denemek faydalıdır. Bende buna istinaden AlaSQL'i incelemiştim. Yanıma kar olarak kalanlarsa şöyle.

  • Hazır bir react uygulama iskeletinin nasıl oluşturulduğunu
  • React sayfası içerisindeki yaşam döngüsüne dahil olan componentWillMount, componentDidMount ve render metodlarının hangi aşamalarda devreye girdiğini
  • Alasql paketinin react uygulamasına nasıl dahil edildiğini ve temel SQL ifadelerini(veritabanı nesnelerini oluşturmak, insert ve delete sorgularını çalıştırmak vb)
  • state özelliğini ne amaçla kullanabileceğimi
  • Veritabanından çekilen JSON dizisinin map fonksiyonu ile nasıl etkileştiğini
  • refs özelliği ile kontrollerin metotlarda nasıl ele alınabildiğini

Böylece geldik bir saturday-night-works notu derlemesinin daha sonuna. Yolun açık olsun Skipper :) Tekrardan görüşünceye dek hepinize mutlu günler dilerim.


Ruby Tarafından Redis(Docker Bazlı) Veritabanı ile Konuşmak

$
0
0

Geçtiğimiz yıl kendi kendimi eğitmek üzere 41 bölümden oluşan Saturday-Night-Works isimli bir çalışma yapmıştım. Github üzerine aldığım notlar ve kod örneklerini blog üzerinde derleyip toparlamak onları pekiştirmek için önemli ve gerekliydi. Ne var ki yetişemediğimiz yazılım teknolojilerindeki gelişmeler için sürekli antrenman yapmam gerekiyor. Bu sebepten araya uzun bir tatil koyup tekrardan enerjimi depolamaya çalıştım. Nadas süresi boyunca neler yapabileceğimi düşündüm. Yeni maceramın felsefesi bir öncekisi ile aynı olmalıydı. Rastgele ve pek deneyimli olmadığım konularla ilgili araştırma yapıp bunları basit örneklerle çalışmalı ve dokümanlaştırmalıydım. Ha geldi gelecek derken nihayet ilham perim beni buldu ve artık tatilimi sonlandırmam gerektiğine karar verdim. Yeni serüvenime SkyNet evrenindeki Ahch-To(MacOS Mojave - Intel Core i5 1.4Ghz, 4 Gb 1600 Mhz DDR3) gezegeninde başlıyorum. Orada geçirdiğim bir saat dünya zamanında birkaç güne denk geliyor.

Öyleyse gelin size ilk gün başımdan geçenleri anlatayım. Bu ilk macerada epeydir eğilmediğim Ruby kodları ile tekrar bir aradayım. Amacım Ruby ile docker container üzerinde host edilmiş bir Redis veritabanında basit işlemler yapabilmek. Daha önce birkaç NoSQL sistemini deneyimle fırsatım olmuştu. Ancak MacOS üzerinde docker kullanımı konusunda tecrübeli değildim. Ruby kodlamayı ve Redis'in temel veri tipleriniyse çoktan unuttum. Dolayısıyla benim için bilgi pekiştirmek açısından iyi bir gün olacağını düşünüyorum.

Redis, In-Memory NoSQL veritabanı sistemlerinden birisi olarak karşımıza çıkıyor. Tüm veriyi bellekte saklayıp sorguladığından epeyce hızlı(Metrik değerlere bakmak lazım) Key-Value(Tuple Store) tipinden bir veritabanı olup hash, list, set, sorted-set ve string tiplerinden oluşan zengin bir veri yapısına sahip. Dağıtılabilir caching stratejilerinde, otomatik tamamlama özelliklerine ait önerilerin hızla getirilmesinde, aktif kullanıcı oturumlarının takibinde, iş kuyruklarının modellenmesinde(publisher/subscriber türevli) etkili bir NoSQL çözümü olarak tercih ediliyor. 

Bu temel bilgilere ek olarak CAP(Consistency, Availability, Partition Tolerance)üçgeninin CP kenarında yer aldığını söylemeliyiz. CAP teoremindeki harflerin anlamlarını kısaca hatırlayalım mı? Dağıtık bir sistemde Consistency ilkesine göre tüm istemciler verinin her zaman aynı görünümüne ulaşır. Pi'nin değeri bir istemci tarafından 3.14 olarak belirlenmişse diğer istemciler Pi'ye baktıklarında bu sayıyı görür. Availability ilkesi tüm istemcilerin dağıtık sistem üzerinde her an okuma ve yazma yapabiliyor olmasını öngörür. Partition ilkesine göre node'larda fiziki olarak kopma meydana gelse bile sistem kullanılabilir durumda kalmalıdır. Tabii en önemli nokta şudur ki CAP teoremine göre dağıtık bir sistem bu üç unsuru aynı anda karşılayamaz. Sürprizzzz :) Çok sık gördüğümüz CAP üçgenini aklımızda kaldığı kadarıyla çizmeye çalışırsak bilgileri biraz daha pekiştirmiş oluruz. Örneğin aşağıdaki gibi.

Bu kısa bilgilerden sonra örneklerimizi geliştirmeye başlamaya ne dersiniz!?

İlk Adımlar

Öncelikle Redis Docker imajını ayağa kaldırıp ping atmamız gerekiyor. Sistemde docker yüklüyse aşağıdaki komutlardan yararlanılabilir.

docker pull redis
docker run --name london --network host -d redis redis-server --appendonly yes
docker run -it --network host --rm redis redis-cli -h localhost
ping

docker stop london
docker container rm 0793a

İlk komut ile redis imajının son sürümünü indiriyoruz. Buradaki appendonly anahtarına verilen değer sebebiyle dataset üzerinde yapılan her değişiklik fiziki diskte kalıcı hale getirilecektir(Persistance detayları için şuraya bakabiliriz)İkinci komutla container'ı başlatıp ardından gelen ile redis-cli terminal aracını devreye alarak redis ortamına bağlanıyoruz. ping karşılığında PONG mesajını görmemiz redis'in çalıştığının işaretidir. Dilerseniz buradayken de Redis ortamını deneyimleyebilirsiniz. Son komutlar opsiyonel olmakla birlikte Docker container'ını durdurmak ve kaldırmak için kullanılır.

Docker container'ı varsayılan olarak localhost:6379 adresinden hizmet vermektedir. Proje iskeletini oluşturmak için ben kendi sistemimde aşağıdaki terminal komutlarını kullandım. Diğer yandan örnek kodlarımızı ruby ile geliştireceğiz ancak Redis ile konuşabilmemizi kolaylaştıracak bir pakete de(gem diyoruz) ihtiyacımız olacak.

mkdir src
cd src
touch main.rb publisher.rb subscriber.rb dessert.rb

sudo gem install redis

Son satırda yer alan install komutu ile redis isimli gem paketini sisteme dahil etmiş oluyoruz.

Kod Tarafı

Öğretide üç farklı uygulama söz konusu. İlk örnek ruby kodlarından Redis'e bağlanıp çok basit bir kaç işlemin nasıl icra edildiğini göstermekte. Ağırlıklı olarak Redis veri tiplerinin genel kullanımları söz konusu. Kodlarda yer alan yorum satırlarında mümkün mertebe ne yaptığımızı açıklamaya çalıştım.

main.rb içeriği

require 'redis' # redis gem'ini kullanacağımızı belirttik

redis=Redis.new(host:"localhost")
#redis.ping()

redis.set("aloha",1001) # örnek key:value ekledik
word=redis.get("aloha") # eklediğimizi alıp 

puts word # ekrana bastırdık

# List kullanımına örnekler

redis.del('user_actions') # önce user_actions ve tutorial_list listelerini temizleyelim
redis.del('tutorial_list')

# Right Push
redis.rpush('user_actions','Naycıl login olmayı deniyor')
redis.rpush('user_actions','şifre hatalı girildi')
redis.rpush('user_actions','Naycıl giriş yaptı')
redis.rpush('user_actions','Naycıl alışveriş sepetine bakıyor')
redis.rpush('user_actions','Naycıl sepetten 19235123A kodlu ürünü çıkarttı')

p redis.lrange('user_actions',0,-1) # tüm listeyi ilk girişten itibaren getirir

redis.ltrim('user_actions',-1,-1) # son elemana kadar olan liste elemanlarını çıkarttık
puts ''
p redis.lrange('user_actions',0,-1)

puts ''

#Left Push
redis.lpush('tutorial_list','redis')
redis.lpush('tutorial_list','mongodb')
redis.lpush('tutorial_list','ruby on rails')
redis.lpush('tutorial_list','golang')

p redis.lrange('tutorial_list',0,-1)

# Set Kullanımına Örnekler

redis.del('cenifer-friends')
redis.del('melinda-friends')

redis.sadd('cenifer-friends','semuel')
redis.sadd('cenifer-friends','nora')
redis.sadd('cenifer-friends','mayki')
redis.sadd('cenifer-friends','lorel')
redis.sadd('cenifer-friends','bill')

redis.sadd('melinda-friends','mayki')
redis.sadd('melinda-friends','ozi')
redis.sadd('melinda-friends','bill')
redis.sadd('melinda-friends','törnır')
redis.sadd('melinda-friends','sementa')
redis.sadd('melinda-friends','kıris')

puts ''
p redis.smembers('cenifer-friends')
puts ''
p redis.smembers('melinda-friends')
puts ''
p redis.sinter('cenifer-friends','melinda-friends') # Yukarıdaki iki kümenin kesiştiği elemanları verir
puts ''
p redis.srandmember('melinda-friends') # her srandmember çağrısı kümeden rastgele bir elemanı döndürür
p redis.srandmember('melinda-friends')

# Sorted Set örnekleri

redis.del('best-players-of-the-week')
# haftanın oyuncularını ağırlık puanlarına göre ekledik
redis.zadd('best-players-of-the-week',32,'maykıl cordın')
redis.zadd('best-players-of-the-week',24,'skati pipın')
redis.zadd('best-players-of-the-week',32,'leri börd')
redis.zadd('best-players-of-the-week',21,'con staktın')

puts ''
puts redis.zrevrange('best-players-of-the-week',0,-1) # en yüksek skor puanından en küçüne doğru getirir (rev hecesine dikkat)
puts '' 
puts redis.zrevrange('best-players-of-the-week',0,0) # en iyi skora sahip olanı getirir (rev hecesine dikkat)
puts ''
puts redis.zrangebyscore('best-players-of-the-week',20,30) # skorları 20 ile 30 arasında olanlar

İkinci örnek biraz daha kapsamlı olup publish/subscribe modelinin uyarlamasını ele alıyor. Publisher görevini üstlenen uygulama sembolik olarak belirli aralıklarla broadcast yayını yapar ve game-info-101 ve game-info-102 kodlu kannalar üzerinden mesajlar yollar. Bu kanalları dinleyen aboneler yayınlanan mesajları görebilir. 

publisher.rb içeriği

require 'redis'

puts 'Maç bilgileri gönderiliyor...'
redis=Redis.new(host:"localhost") #Redis sunucusuna bağlan

redis.publish 'game-info-101','Harden üç sayılık basket. Skor 92-92' # bir mesaj fırlat
redis.publish 'game-info-102','Furkan top çalma, hızlı hücum ve basket. Skor 2-0'
sleep 16
redis.publish 'game-info-102','Joel Embit blog. Skor 2-0'
sleep 14 # 14 saniye bekle
redis.publish 'game-info-101','Donçiç harika bir assist yapıyor ve Maksi Kleber smacı vuruyor. Skor 92-94'
sleep 22 # 22 saniye bekle
redis.publish 'game-info-101','Dallas son bir molaya gidiyor'
sleep 60 # 1 dakika bekle
redis.publish 'game-info-101','exit' # aboneler, bu kanal için aboneliklerini sonlandırabilir
redis.publish 'game-info-102','exit'

puts 'Program sonu'

subscriber.rb içeriği

require 'redis'

channelName=ARGV[0] # komut satırında kanal bilgisini al

redis=Redis.new(host:"localhost") #Redis sunucusuna bağlan

begin
    # game-info-101 isimli olaya abone ol
    redis.subscribe channelName do |on|

    on.subscribe do |channel,msg| # abonelik gerçekleşince çalışır
        puts "#{channel} kanalına abone olundu"
    end
    
    on.message do |channel, msg| # mesaj gelince çalışır
      puts "#{channel} -> #{msg}" # mesajı ekrana bastır
      redis.unsubscribe if msg=="exit" # eğer mesaj bilgisi exit olarak geldiyse aboneliği sonlandır
    end

    on.unsubscribe do |channel,msg| # abonelik sonlandığında çalışır
        puts "#{channel} kanalına abonelik sonlandırıldı"
    end
end
rescue redis::BaseConnectionError => err # bir bağlantı hatası sorunu olursa 3 saniye içinde tekra bağlanmaya çalışılır
    puts "#{err}, 3 saniye içinde tekrar deneyeceğim"
    sleep 3
    retry
end

Üçüncü ve son örnek ise bir SQL tablosunun Redis dünyasında kabaca nasıl tarif edilebileceği ile alakalıdır. Olayı karakterize etmek için aşağıdaki görseli ele alabiliriz.

Player isimli bir tablomuz olduğunu düşünelim. Bu tip bir veri yığınını Redis'in deneysel dokümanlarına göre sağ taraftaki gibi tariflemek mümkün. Hash ve Sorted Set'ler tablo ve Select sorgusunu karşılayabilir niteliktedir. Örneğin oyuncuların skorlarına göre sıralanacağı bir listeyi Sorted Set nesnesi gibi düşünebilir ve belli bir puan aralığındakilerin listesinin sıralı olarak çekilmesini sağlayabiliriz. Bu deneyselliği ele aldığımız Dessert.rb dosyasının içeriği aşağıdaki gibidir.

require "redis"

redis=Redis.new(host:"localhost")

# Hash içerisine birkaç player örneği ekliyoruz
redis.hmset("player:1","fullname","Baz Layt Yiır","country","moon","score",339)
redis.hmset("player:2","fullname","Mega maynd","country","mars","score",317)
redis.hmset("player:5","fullname","Payn payn laki lu","country","wild west","score",405)
redis.hmset("player:3","fullname","Aeyrın Vaykovski","country","poland","score",322)
redis.hmset("player:4","fullname","Bileyd Rut","country","saturn","score",185)

# hgetall metoduna verdiğimiz parametre ile player:3 veri setini çekip ekrana bastırıyoruz
# Select'in where koşulu gibi düşünelim
player3=redis.hgetall("player:3")
puts "#{player3}\n\n"

# hmget ile player:2 key içeriğinden sadece country ve fullname değerlerini yazdırıyoruz
puts "#{redis.hmget('player:2','country','fullname')}\n\n"

# bir sorted list oluşturuyoruz
# listeyi yukarudaki hash üzerinden oluşturmak için player:* desenine uygun olan key içeriklerini çekmemiz lazım
# scan_each metodunun match parametresi bunu sağlıyor
redis.scan_each(:match=>"player:*"){|key|
    currentScore=redis.hmget(key,"score") #hmget metoduna iterasyondaki key değerini verip score bilgisini yakalıyoruz
    redis.zadd("score_list",currentScore,key) # score bilgisini o anki key ile ilişkilendirerek sorted list nesnesine ekliyoruz
}

# sorted list için skoru 330 puanın altına olanların listesini çektik (Select'in where koşulu gibi düşünelim)
scores_under_330=redis.zrangebyscore("score_list",0,330)

puts "Skoru 0 ile 330 arasında olanlar\n\n"
# bulduğumuz listede dolaşıp sadece fullname bilgilerini ekrana bastırdık
scores_under_330.each do |plyr|
    info=redis.hgetall(plyr)
    puts "#{info['fullname']} için güncel skor değeri #{info['score']}"
end

Çalışma Zamanı

Kod tarafını tamamladıysak çalışma zamanına geçebiliriz. Main.rb ile birlikte diğer örnekleri de incelemeye başlayalım.

Başlangıç

Öncelikle Redis container'ını başlatmalıyız. Bunun için aşağıdaki terminal komutları ile ilerleyebiliriz. İlk komut docker container'ını çalıştırıyor. İkinci komut kontrol amaçlı olarak container listesine bakmak için. Son terminal komutumuz tahmin edileceği üzere main.rb isimli ruby dosyasını yürütüyor.

docker run -d -p 6379:6379 redis
docker container ps -a
ruby main.rb

Ana Yemek

Yemek olarak publisher soslu subscriber'ımız var. Üstelik en ünlü İtalyan şeflerinden Rizotta Galliani tarafından özenle hazırlandı :P Sizi ciddiyete davet ediyorum Burak Bey. En az 3 terminal ekranı açıp birisinde publisher.rb diğer ikisinde de subscriber.rb dosyalarını dinleyecekleri kanalları parametre olarak verip çalıştırmak sonuçları irdelememiz için yeterlidir. Aynen aşağıdaki gibi.

ruby subscriber.rb 'game-info-1'
ruby subscriber.rb 'game-info-2'
ruby publisher.rb

Dikkat edileceği üzere aboneler program başlatılırken bağlanacakları kanalın adını belirtiyor. Bu nedenle her ikisi de farklı maç akışlarını dinlemekteler. Pek tabii bir abonenin birden fazla kanalı dinlemesi de mümkün. Kurgumuz burada oldukça basit ve senkron kaldı. N sayıda maça ait haber akışını paylaşabileceğiniz bir publisher uygulamasını nasıl yazarsınız bir düşünün.

Tatlı

Tatlı olarak bir SQL tablosunun Redisce yorumlanması var. Aşağıdaki terminal komutu ile örneğimizi çalıştıralım ve sonuçları sınıfça irdeleyelim.

ruby dessert.rb

Pek tabii SQL'in Domain'e özgü yazılmış lisanında SELECT, WHERE gibi ifadeleri kullanarak istenilen sorguları çalıştırmak çok daha kolay. Lakin yine de Redis dünyasında bu tip bir yaklaşımı nasıl ele alabiliriz sonuçlardan görebiliyoruz. Bunun farklı varyasyonlarını LINQ(LanguageINtegratedQuery) gibi yapılarda da görmemiz mümkün. Elimizde bir liste ve içerisinde veri tutan nesnelerimiz varken filtrelemeler için koddakine benzer yaklaşımları kullanıyoruz. 

Tamamlarken

Eğer kullandığımız Redis Container'ı ile işimiz bittiyse önce durdurup sonrasında sistemden kaldırmak isteyebiliriz. Bunun için aşağıdaki terminal komutlarını çalıştırmak yeterli olacaktır(Muhtemelen sizdeki Names değeri farklı olur. Ben çalışırken rastgele interesting_nobel ismi atanmıştı)

docker container ps -a
docker stop interesting_nobel
docker container rm interesting_nobel
docker container ps -a

Neler Öğrendim?

Saturday-Night-Worksçalışmalarında olduğu gibi yeni başladığım SkyNet serüveni de bana birçok şey öğreteceğe/hatırlatacağa benziyor. Bu ilk macerada aklımda kalanları şöyle özetleyebilirim.

  • Redis docker imajını MacOS üzerinde kullanmayı
  • Redis'in CAP teoreminde hangi ikiliye yakın olduğunu
  • Redis gem'ini kullanarak yapılabilecek temel işlemleri
  • Temel redis veri tiplerini
  • Basit bir pub/sub kurgusunu işletmeyi
  • SQL stilinde bir tablonun Redisce oluşturulmasını ve verinin filtrelenmesini
  • Bir hash içindeki tüm key değerlerini nasıl dolaşabileceğimi

Eksikliği Hissedilen Konular

SkyNet'in ilk gününden kalan ve merak ettiğim şeyler de var. Bunlar siz değerli okurlarımın araştırabileceği konular arasında yer alıyor. Bölüm sonu soruları gibi düşünebilirsiniz :)

  • Publisher tarafında farklı kanalların birbirinden bağımsız ve asenkron olarak yayın yapmasını nasıl sağlayabiliriz?
  • Subscriber olarak birden fazla kanala abone olabilir miyim?
  • Olmayan bir kanala abone olmak istediğimde Ruby çalışma zamanı nasıl tepki verir?
  • Peki pub/sub senaryosunu Ruby on Rails tabanlı bir web projesinde nasıl kullanırım?

Böylece geldik bir SkyNet gününün sonuna. Zaman farkından dolayı tekrar Dünya'ya dönmek zorundayım. Bir sonraki gidişimde bakalım bizleri ne tür maceralar karşılayacak. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Örnek kodlara github adresinden de ulaşabilirsiniz.

MongoDB ile Bir GO Uygulamasını Konuşturmak

$
0
0
Teknoloji baş döndüren bir hızla ilerlerken beynimizin tembelleştiğini de kabul etmemiz gerekiyor. Artık pek çok işimiz otonomlaştırıldığından zihnimiz eski egzersizleri yapmıyor. Yıllar önce İngiltere'de yapılan bir araştırmada çocukların hesap makinesi kullanması sebebiyle temel dört işlem matematiğinde sorunlar yaşadığı tespit edilmişti. Yine Kanada'da yapılan bir araştırma insanların dikkat dağılma sürelerinin 8 saniyelere kadar indiğini gösterdi. Hafızamızı dinç tutma noktasında Japon balıkları ile yarışır bir konumda olduğumuz da aşikar. Kaçımız aklından ezbere 4 telefon numarasını sayabilir(Üç haneliler yasak) Otonomlaşan dünya sebebiyle tembelleşen ve dış uyarıcılar yüzünden sürekli dikkati dağılan zihnimiz...Gerçekten de dikkatimizi dağıtan, odaklanmamızı bozan o kadar çok şey var ki. Dolayısıyla kendimizi yetiştirmek istediğimiz konulara çalışırken ne kadar verimli olabiliyoruz bir bakmak gerekiyor. Tekrar satın alınamayacak olan zamanın ne kadar kıymetli olduğunu düşünürsek verimli çalışmanın ilerleyen yaşlarda çok çok önemli bir mesele haline geldiğini vurgulamak isterim.
 
İşte bu sebepten birkaç haftadır Saturday-Night-Worksçalışmalarım sırasında pomodoro tekniğini neden kullanmadığımı karar kara düşünmekteyim. Oysa ki çok verimli bir çalışma pratiği. Atladığım bu önemli detayı SkyNet çalışmalarımın ilk gününden itibaren uygulamaya karar verdim. Genellikle gece 22:00 sularında başlayarak 4X25 dakikalık seanslar halinde ilerliyorum. Her seans arasında 5er dakikalık standart molalar var. Tabii bu tekniği uygularken en önemli kural çalışmayı bölecek unsurları mutlak suretle dışarıda bırakmak. Cep/ev telefonu, televizyon, radyo, e-mail programı ve benzeri odak dağıtıcı ne kadar şey varsa kapatmak gerekiyor. Bunun faydasını epeyce gördüğümü ve 25 dakikalık zaman dilimlerindeki çalışmalardan iyi seviyede verim aldığımı ifade edebilirim. Aranızda uygulamayanlar varsa bir göz atsınlar derim ;) 
 
Pomodoro tekniğini uygularken size akıllı bir kronometre gerekecek. Tarayıcıda çalışan Tomato-Timer tam size göre. Hatta Visual Studio Code için eklentisi bile var ;)
Gelelim SkyNet'te geçirdiğim ikinci güne. Elimizdeki malzemeleri sayalım. MongoDB için bir docker imajı, gRPC ve GoLang. Bu üçünü kullanarak CRUD(Create Read Update Delete) operasyonlarını icra eden basit bir uygulama geliştirmek niyetindeyim. Bir önceki öğretide Redis docker container'dan yararlanmıştım. Kaynakları kıymetli olan Ahch-To sistemini kirletmemek adına MongoDB için de benzer şekilde hareket edeceğim. Açıkçası GoLang bilgim epey paslanmış durumda ve sistemde yüklü olup olmadığını dahi bilmiyorum.
go version
terminal komutu da bana yüklü olmadığını söylüyor. Dolayısıyla ilk adım onu MacOS üzerine kurmak.

İlk Hazırlıklar(Go Kurulumu ve MongoDB)

GoLang'i Ahch-To adasına yüklemek için şu adrese gidip Apple macOS sürümünü indirmem gerekti. Ben öğretiyi hazırlarken go1.13.4.darwin-amd64.pkg dosyasını kullandım. Kurulum işlemini tamamladıktan sonra komut satırından go versiyonunu sorgulattım ve aşağıdaki çıktıyı elde ettim.
 
 
Pek tabii içim rahat değildi. Versiyon bilgisi gelmişti ama bir "hello world" uygulamasını çalışır halde görmeliydim ki kurulumun sorunsuz olduğundan emin olayım. Hemen resmi dokümanı takip ederek $HOME\go\src\ altında helloworld isimli bir klasör açıp aşağıdaki kod parçasını içeren helloworld.go dosyasını oluşturdum(Visual Studio Code kullandığım için editörün önerdiği go ile ilgili extension'ları yüklemeyi de ihmal etmedim)
package main

import "fmt"

func main() {
	fmt.Printf("Artık go çalışmaya hazırım :) \n")
}
Terminalden aşağıdaki komutları işlettikten sonra çıktıyı görebildim. 
go build
./helloworld
Go ile kod yazabildiğime göre MongoDB docker imajını indirip bir deneme turuna çıkabilirim. İşte terminal komutları.
docker pull mongo
docker run -d -p 27017-27019:27017-27019 --name gondor mongo
docker container ps -a
docker exec -it gondor bash

mongo
show dbs
use AdventureWorks
db.category.save({title:"Book"})
db.category.save({title:"Movie"})
db.category.find().pretty()

exit
exit
 
İlk komutla mongo imajı çekiliyor. İzleyen komut docker container'ını varsayılan portları ile sistemin kullanımına açmak için. Container listesinde göründüğüne göre sorun yok. MongoDB veritabanını container üzerinden test etmek amacıyla içine girmek lazım. 4ncü komutu bu işe yarıyor. Ardından mongo shell'e geçip bir kaç işlem gerçekleştirilebilir. Önce var olan veritabanlarını listeliyor sonra AdventureWorks isimli yeni bir tane oluşturuyoruz. Devam eden kısımda category isimli bir koleksiyona iki doküman ekleniyor ve tümünü güzel bir formatta listeliyoruz. Arka arkaya gelen iki exit komutunu fark etmişsinizdir. İlki mongo shell'den, ikincisi de container içinden çıkmak için.

Ah çok önemli bir detayı unuttum! Örnekte gRPC protokolünü kullanacağız. Bu da bir proto dosyamız olacağı ve Golang için gerekli stub içeriğine derleyeceğimiz anlamına geliyor. Dolayısıyla sistemde protobuf ve go için gerekli derleyici eklentisine ihtiyacım var. brew ile bunları sisteme yüklemek oldukça kolay.
brew install protobuf
protoc --version
brew install protoc-gen-go
Kod tarafına geçmeye hazırız ama öncesinde ufak bir bilgi.

gRPC Hakkında Azıcık Bilgi

gRPC, HTTP2 bazlı modern bir iletişim protokolü ve JSON yerine ProtoBuffers olarak isimlendirilen kuvvetle türlendirilmiş bir ikili veri formatını kullanmakta(strongly-typed binary data format) JSON özellikle REST tabanlı servislerde popüler bir format olmasına rağmen serileştirme sırasında CPU'yu yoran bir performans sergiliyor. HTTP/2 özelliklerini iyi kullanan gRPC ise 5 ile 25 kata kadar daha hızlı. Bu noktada hatırlamak için bile olsa gRPC ile REST'i kıyaslamakta yarar var. İşte karşılaştırma tablosu.
 
 REST TarafıgRPC Tarafı 
 HTTP 1.1 nedeniyle gecikme yüksek   HTTP/2 sebebiyle daha düşük gecikme 
 Sadece Request/Response Stream desteği(Örneğimizde bir kullanımı var)
 CRUD odaklı servisler için  API odaklı(Burada CRUD odaklı yapacağız çaktırmayın)
 HTTP Get,Post,Put,Delete gibi fiil tabanlı RPC tabanlı, sunucu üzerinden fonksiyon çağırabilme özelliği
 Sadece Client->Server yönlü talepler Çift yönlü ve asenkron iletişim
 JSON kullanıyor(serileşme yavaş, boyut büyük) Protobuffer kullanıyor(veri daha küçük boyutta ve serileşme hızlı)

Örnek Uygulama

Gelelim kod tarafına... Uygulamanın temel klasör yapısını aşağıdaki gibi oluşturabiliriz. Ben bu işlemleri $HOME\go\src\ altında gerçekleştirdim.

mkdir gRPC-sample
cd gRPC-sample
mkdir playerserver
mkdir clientapp
mkdir proto
playerserver ve clientapp tahmin edileceği üzere sunucu ve istemci uygulama görevini üstleniyorlar. proto klasöründe yer alan player.proto, gRPC mesaj sözleşmesine ait tanımlamaları içermekte. Servis metodları, parametre tipleri ve içerikleri bu dosyada aşağıdaki gibi bildiriliyor. 
syntax="proto3"; //protobuffers v3 versiyonu kullaniliyor

package player; // proto paketinin adi
option go_package="playerpb"; // generate edilecek go paketinin adi

// Player mesaj tipinin tanimi
message Player{
    string id=1;
    string player_id=2;
    string fullname=3;
    string position=4;
    string bio=5;
}

// Operasyonlarin kullandigi request ve response mesajlarina ait tanimlamalar
message AddPlayerReq{
    Player plyr=1;
}

message AddPlayerRes{
    Player plyr=1;
}

message EditPlayerReq{
    Player plyr=1;
}

message EditPlayerRes{
    Player plyr=1;
}

message RemovePlayerReq{
    string player_id=1;
}

message RemovePlayerRes{
    bool removed=1;
}

message GetPlayerReq{
    string player_id=1;
}

message GetPlayerRes{
    Player plyr=1;
}

message GetPlayerListReq{}

message GetPlayerListRes{
    Player plyr=1;
}

// servis ve operasyon tanimlari
service PlayerService{
    rpc GetPlayer(GetPlayerReq) returns (GetPlayerRes);
    rpc GetPlayerList(GetPlayerListReq) returns (stream GetPlayerListRes); //server bazlı streaming kullanacağımız için dönüş parametresi stream tipinden
    rpc AddPlayer(AddPlayerReq) returns (AddPlayerRes);
    rpc EditPlayer(EditPlayerReq) returns (EditPlayerRes);
    rpc RemovePlayer(RemovePlayerReq) returns (RemovePlayerRes);
}
Bu içeriği Go tarafında kullanabilmek için derlememiz lazım. Derlemeyi aşağıdaki terminal komutu ile gerçekleştirebiliriz(proto dosyasını VS Code tarafında daha kolay düzenlemek için vscode-proto3 isimli extension'ı kullandım)
protoc player.proto --go_out=plugins=grpc:.
Proto dosyasının tamamlanmasını takiben playerserver klasöründeki main.go dosyasını yazmaya başlayabiliriz. Biraz uzun bir kod dosyası ama sabırla yazıp, yorum satırlarını da okuyarak neler yaptığımızı anlamaya çalışmakta yarar var.
package main

import (
	"context"
	"fmt"
	"net"
	"os"
	"os/signal"
	"strings"

	"go.mongodb.org/mongo-driver/bson/primitive"

	playerpb "gRPC-sample/proto"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

/* proto'dan otomatik üretilen player.pb.go içerisindeki RegisterPlayerServiceServer metoduna bir bakın.
Pointer olarak gelen grpc server nesnesi ikinci parametre olarak gelen tipi register etmek için kullanılır.
Bir nevi interface üzerinden enjekte işlemi yaptığımızı düşünebilir miyiz?
*/
type PlayerServiceServer struct{}

var db *mongo.Client
var playerCollection *mongo.Collection
var mongoContext context.Context

func main() {
	// TCP üzerinden 5555 nolu portu dinleyecek olan nesne oluşturuluyor
	server, err := net.Listen("tcp", ":5555")
	// Olası bir hata durumunu kontrol ediyoruz
	if err != nil {
		fmt.Printf("5555 dinlenemiyor: %v", err)
	}

	// gPRC sunucusu için kayıt(register) işlemleri
	grpcOptions := []grpc.ServerOption{}
	// yeni bir grpc server oluşturulur
	grpcServer := grpc.NewServer(grpcOptions...)
	// Bir PlayerService tipi oluşturulur
	playerServiceType := &PlayerServiceServer{}
	// servis sunucu ile birlikte kayıt edilir
	playerpb.RegisterPlayerServiceServer(grpcServer, playerServiceType)

	// mongoDB bağlantı işlemleri
	fmt.Println("MongoDB sunucusuna bağlanılıyor")
	mongoContext = context.Background()
	// bağlantı deneniyor
	db, err = mongo.Connect(mongoContext, options.Client().ApplyURI("mongodb://localhost:27017"))
	// olası bir bağlantı hatası varsa
	if err != nil {
		fmt.Println(err)
	}
	// Klasik ping metodunu çağırıyoruz
	err = db.Ping(mongoContext, nil)
	if err != nil {
		fmt.Println(err)
	} else {
		// çağrı başarılı olursa bağlandık demektir
		fmt.Println("MongoDB ile bağlantı sağlandı")
	}
	// nba isimli veritabanındaki player koleksiyonuna ait bir nesne örnekliyoruz
	// veritabanı ve koleksiyon yoksa oluşturulacaktır
	playerCollection = db.Database("nba").Collection("player")

	// gRPC sunucusunu aktif olan TCP sunucusu içerisinde bir child routine olarak başlatıyoruz
	go func() {
		if err := grpcServer.Serve(server); err != nil {
			fmt.Println(err)
		}
	}()
	fmt.Println("Sunucu 5555 nolu porttan gPRC tabanlı iletişime hazır.\nDurdurmak için CTRL+C.")

	// CTRL+C ile başlayan kapatma operasyonu
	cnl := make(chan os.Signal)      // işletim sisteminde sinyal alabilmek için bir kanal oluşturduk
	signal.Notify(cnl, os.Interrupt) // CTRL+C mesajı gelene kadar ana rutin açık kalacak
	<-cnl

	fmt.Println("Sunucu kapatılıyor...")
	grpcServer.Stop() // gRPC sunucusunu durdur
	server.Close()    // TCP dinleyicisini kapat
	fmt.Println("GoodBye Crow")
}

/* Protobuf mesajlarında taşınan serileşmiş içeriği nesnel olarak ele alacağımı struct */
type Player struct {
	ID       primitive.ObjectID `bson:"_id,omitempty"` // MongoDB tarafındaki ObjectId değerini taşır
	PlayerID string             `bson:"player_id"`
	Fullname string             `bson:"fullname"`
	Position string             `bson:"position"`
	Bio      string             `bson:"bio"`
}

/* PlayerServiceServer'ın uygulanması gereken metodlarını. Yani servis sözleşmesinin tüm operasyonları
 */

// Yeni bir oyuncu eklemek için kullanacağımız fonksiyon
func (srv *PlayerServiceServer) AddPlayer(ctx context.Context, req *playerpb.AddPlayerReq) (*playerpb.AddPlayerRes, error) {
	payload := req.GetPlyr() // GetPlyr (GetPlayer değil o servis metodumuz) fonksiyonu ile request üzerinden gelen player içeriği çekilir

	// İçerik ile gelen alan değerleri player struct nesnesini oluşturmak için kullanılır
	player := Player{
		PlayerID: payload.GetPlayerId(),
		Fullname: payload.GetFullname(),
		Position: payload.GetPosition(),
		Bio:      payload.GetBio(),
	}

	// player nesnesi mongodb veritabanındaki koleksiyona kayıt edilir
	result, err := playerCollection.InsertOne(mongoContext, player)

	// bir problem oluştuysa
	if err != nil {
		// gRPC hatası döndürülür
		return nil, status.Errorf(
			codes.Internal,
			fmt.Sprintf("Bir hata oluştu : %v", err),
		)
	}

	// Hata oluşmadıysa koleksiyona eklenen yeni doküman
	// üretilen ObjectID değeri de atanarak geri döndürülür
	objectID := result.InsertedID.(primitive.ObjectID)
	payload.Id = objectID.Hex()
	return &playerpb.AddPlayerRes{Plyr: payload}, nil
}

func (srv *PlayerServiceServer) EditPlayer(ctx context.Context, req *playerpb.EditPlayerReq) (*playerpb.EditPlayerRes, error) {
	return nil, nil
}

func (srv *PlayerServiceServer) RemovePlayer(ctx context.Context, req *playerpb.RemovePlayerReq) (*playerpb.RemovePlayerRes, error) {
	// önce silinmek istenen playerId bilgisi alınır
	id := strings.Trim(req.GetPlayerId(), "\t \n")
	fmt.Println(id)
	// DeleteOne metodu ile silme operasyonu gerçekleştirilir
	_, err := playerCollection.DeleteOne(ctx, bson.M{"player_id": id})

	// hata kontrolü yapılıyor
	if err != nil {
		return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Silinmek istenen oyuncu bulunamadı. %s", err))
	}

	// hata yoksa işlemin başarılı olduğuna dair sonuç dönülür
	return &playerpb.RemovePlayerRes{
		Removed: true,
	}, nil
}

// MongoDB'deki ID bazlı olarak oyuncu verisi döndüren metodumuz
func (srv *PlayerServiceServer) GetPlayer(ctx context.Context, req *playerpb.GetPlayerReq) (*playerpb.GetPlayerRes, error) {
	// request ile gelen player_id bilgisini alıyoruz
	// Trim işlemi önemli. İstemci terminalden değer girdiğinde alt satıra geçme işlemi söz konusu.
	// Veri bu şekilde gelirse kayıt bulunamaz. Dolayısıyla bir Trim işlemi yapıyoruz
	id := strings.Trim(req.GetPlayerId(), "\t \n")
	// bson.M metoduna ilgili sorguyu ekleyerek oyuncuyu koleksiyonda arıyoruz
	result := playerCollection.FindOne(ctx, bson.M{"player_id": id})

	player := Player{}
	// bulunan oyuncu decode metodu ile ters serileştirilip player değişkenine alınır
	if err := result.Decode(&player); err != nil {
		return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("Sanırım aranan oyuncu bulunamadı %v", err))
	}

	// Decode işlemi başarılı olur ve koleksiyondan bulunan içerik player isimli değişkene ters serileşebilirse
	// artık dönecek response nesne içeriğini hazırlayabiliriz
	res := &playerpb.GetPlayerRes{
		Plyr: &playerpb.Player{
			Id:       player.ID.Hex(),
			PlayerId: player.PlayerID,
			Fullname: player.Fullname,
			Position: player.Position,
			Bio:      player.Bio,
		},
	}
	return res, nil
}

// Tüm oyuncu listesini stream olarak dönen metod
func (srv *PlayerServiceServer) GetPlayerList(req *playerpb.GetPlayerListReq, stream playerpb.PlayerService_GetPlayerListServer) error {

	currentPlayer := &Player{}

	// Find metodu veri üzerinden hareket edebileceğimiz bir Cursor nesnesi döndürür
	// bu cursor nesnesi sayesinde istemciye tüm oyuncu listesini bir seferde göndermek yerine
	// birer birer gönderme şansına sahip olacağız
	// Bu nedenle sunucu bazlı bir streamin stratejimiz var
	cursor, err := playerCollection.Find(context.Background(), bson.M{})
	if err != nil {
		return status.Errorf(codes.Internal, fmt.Sprint("Bilinmeyen hata oluştu"))
	}

	// metod işleyişini tamamladığında cursor nesnesini kapatacak çağrıyı tanımlıyoruz
	defer cursor.Close(context.Background())

	// iterasyona başlanır ve Next true döndüğü sürece devam eder
	// yani okunacak mongodb dokümana kalmayana dek
	for cursor.Next(context.Background()) {
		// cursor verisini currentPlayer nesnesine açıyoruz
		cursor.Decode(currentPlayer)
		// istemciye mongodb'den gelen güncel oyuncu bilgisinden yararlanarak cevap dönüyoruz
		stream.Send(&playerpb.GetPlayerListRes{
			Plyr: &playerpb.Player{
				Id:       currentPlayer.ID.Hex(),
				PlayerId: currentPlayer.PlayerID,
				Fullname: currentPlayer.Fullname,
				Position: currentPlayer.Position,
				Bio:      currentPlayer.Bio,
			},
		})
	}

	return nil
}
Sunucu tarafındaki kodlama tamamlandıktan sonra istemci tarafı için clientapp altında tester.go isimli bir başka dosya oluşturarak ilerleyelim. Burada komut satırından temel CRUD operasyonlarını icra edeceğiz. Yeni bir oyuncunun eklenmesi, bir oyuncu bilgisinin çekilmesi, tüm oyuncuların listesinin alınması vb
package main

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"os"
	"strings"

	playerpb "gRPC-sample/proto"

	"google.golang.org/grpc"
)

var client playerpb.PlayerServiceClient
var reqOptions grpc.DialOption

func main() {

	// HTTPS ayarları ile uğraşmak istemedim
	reqOptions = grpc.WithInsecure()
	// gRPC servisi ile el sıkışmaya çalışıyoruz
	connection, err := grpc.Dial("localhost:5555", reqOptions)
	if err != nil {
		fmt.Println(err)
		return
	}
	// proxy nesnesini ilgili bağlantıyı kullanacak şekilde örnekliyoruz
	client = playerpb.NewPlayerServiceClient(connection)

	// Oyuncu ekleyelim
	insertPlayer()
	// tüm oyuncu listesini çekelim
	getAllPlayerList()

	// sembolik olarak ID bazlı 3 oyuncu aratalım
	for i := 0; i < 3; i++ {
		reader := bufio.NewReader(os.Stdin)
		fmt.Println("Oyuncu IDsini gir.")
		playerID, _ := reader.ReadString('\n')
		getByPlayerID(playerID)
	}

	// Silme operasyonunu deniyoruz
	reader := bufio.NewReader(os.Stdin)
	fmt.Println("Silmek istediğiniz oyuncunun IDsini girin.")
	playerID, _ := reader.ReadString('\n')
	removePlayerByID(playerID)

	// tüm oyuncu listesini çekelim
	getAllPlayerList()
}

func insertPlayer() {
	// Yeni oyuncu eklenmesi için deneme kodu
	// Veri ihlalleri örneğin basitliği açısından göz ardı edilmiştir
	reader := bufio.NewReader(os.Stdin)
	fmt.Println("Yeni oyuncu girişi")
	fmt.Println("Id->")
	id, _ := reader.ReadString('\n')
	id = strings.Replace(id, "\n", "", -1)
	fmt.Println("Adı->")
	fullname, _ := reader.ReadString('\n')
	fullname = strings.Replace(fullname, "\n", "", -1)
	fmt.Println("Pozisyon->")
	position, _ := reader.ReadString('\n')
	position = strings.Replace(position, "\n", "", -1)
	fmt.Println("Kısa biografisi->")
	bio, _ := reader.ReadString('\n')
	bio = strings.Replace(bio, "\n", "", -1)

	// protobuf dosyasındaki şemayı kullanarak örnek bir oyuncu nesnesi örnekliyoruz
	newPlayer := &playerpb.Player{
		PlayerId: id,
		Fullname: fullname,
		Position: position,
		Bio:      bio,
	}

	// servisin AddPlayer metodunu o anki context üzerinden çalıştırıp
	// request payload içerisinde yeni oluşturduğumuz nesneyi gönderiyoruz
	res, err := client.AddPlayer(
		context.TODO(),&playerpb.AddPlayerReq{
			Plyr: newPlayer,
		},
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	// Eğer bir hata oluşmamışsa MongoDB tarafından üretilen ID değerini ekranda görmemiz lazım
	fmt.Printf("%s ile yeni oyuncu eklendi \n", res.Plyr.Id)
}

// Tüm oyuncu listesini çektiğimiz metod
func getAllPlayerList() {

	// önce request oluşturulur
	req := &playerpb.GetPlayerListReq{}

	// proxy nesnesi üzerinden servis metodu çağrılır
	s, err := client.GetPlayerList(context.Background(), req)
	if err != nil {
		fmt.Println(err)
		return
	}

	// sunucu tarafından stream bazlı dönüş söz konusu
	// yani kaç tane oyuncu varsa herbirisi için sunucudan istemciye
	// cevap dönecek
	for {
		res, err := s.Recv() // Recv metodu player.pb.go içerisine otomatik üretilmiştir. İnceleyin ;)
		if err != io.EOF {   // döngü sonlanmadığı sürece gelen cevaptaki oyuncu bilgisini ekrana yazdırır
			fmt.Printf("[%s] %s - %s \n\n", res.Plyr.PlayerId, res.Plyr.Fullname, res.Plyr.Bio)
		} else {
			break
		}
	}
}

// Oyuncuyu PlayerID değerinden bulan metodumuz
func getByPlayerID(playerID string) {
	// parametre olarak gelen playerID değerinden bir request oluşturulur
	req := &playerpb.GetPlayerReq{
		PlayerId: playerID,
	}
	// GetPlayer servis metoduna talep gönderilir
	res, err := client.GetPlayer(context.Background(), req)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(res.Plyr.Fullname)
}

// Oyuncu silme fonksiyonumuz
func removePlayerByID(playerID string) {
	// RemovePlayer servis çağrısı için gerekli Request tipi hazırlanır
	req := &playerpb.RemovePlayerReq{
		PlayerId: playerID,
	}
	// servisi çağrısı yapılıp sonucu kontrol edilir
	_, err := client.RemovePlayer(context.Background(), req)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("Oyuncu silindi")
}
Piuvvv!!! Uzun bir yol oldu. Öyleyse çalışma zamanı sonuçlarımıza bakalım mı?

Çalışma Zamanı

İlk gün çalışmasının meyveleri pek fena değil. server ve client tarafa ait go dosyalarını kendi klasörlerinde aşağıdaki terminal komutları ile derledikten sonra
go build main.go
go build tester.go
önce sunucu ardından istemci programlarını çalıştırıp kodlaması ilk önce biten AddPlayer fonksiyonunu deneme şansı buldum. Birkaç oyuncu verisini girdikten sonra mongodb container'ına ait shell'e bağlanıp gerçekten de yeni dokümanların player koleksiyonuna eklenip eklenmediğine baktım. Sonuç tebessüm ettiriciydi :) İstemci uygulama gRPC üzerinden sunucuya mesaj göndermiş, sunucuya gelen içerik docker container üzerinde duran mongodb veritabanına yazılmıştı.
 
 
İkinci gün tüm oyuncu listesini gRPC üzerinden istemciye döndüren süreci yazmaya çalıştım. İlk başta yaptığım bir hata nedeniyle epey vakit kaybettim. GetPlayerList metodunu protobuffer dosyasında stream döndürecek şekilde tasarlamamıştım. Büyük bir veri kümesini filtresiz çektiğimizde bu ağ trafiğinin sağlıklı çalışması açısından sorun olabilir. Oyuncuları sunucudan istemciye doğru bir stream üzerinden tek tek göndermek çok daha mantıklı(Burada REST ile gRPC arasındaki farkları hatırlayalım) Sonunda servis sözleşmesini değiştirip gerekli düzenlemeleri yaptıktan sonra aşağıdaki ekran görüntüsünde yer alan mutlu sona ulaşmayı başardım.
 
 
Devam eden gün bir öncekine göre daha zorlu geçti. FindOne metodunu player_id değerine göre çalıştırmayı bir türlü başaramadım. Neredeyse 4 pomodoro periyodu uğraştım. Hatta pomodoro süreci bittikten sonra farkında olmadan saatlerce bilgisayar başında kaldım. Sorunu araştırırken vakit nasıl geçti anlamamışım. Sonuçta işe 3 saatlik uykuyla gittim. Ertesi gün Ahch-To'nun tuşuna bile basmadım. Bir günlük ara, problemi çözmem için beni sakinleştirmeye yeterdi. Nihayetinde sorunu buldum. İstemci aradığı ID değerini girip sunucuya çağrı yaptığında, servis metoduna gelen ID bilgisinin sonunda boşluk ve alt satıra geçme karakterleri de geliyordu. Trim fonksiyonu ile bu durumun oluşmasını engelledikten sonra silme operasyonunu da işin içerisine dahil ettim ve güncelleme operasyonu hariç komple bir test yaptım. Sonuçlar ekran görüntüsünde olduğu gibi tatmin ediciydi.
 
Silme operasyonuna ilişkin çalışmaya ait örnek bir ekran görüntüsü de aşağıdaki gibi.

Neler Öğrendim?

Elbette SkyNet'te geçirdiğim bugünün de bana öğrettiği bir sürü şey oldu. Bunları aşağıda yer alan maddelerle ifade etmeye çalıştım.
  • Bir protobuf dosyası nasıl hazırlanır ve Go tarafında kullanılabilmesi için nasıl derlenir,
  • Go tarafından MongoDB ile nasıl haberleşilir,
  • MongoDB docker container'ına ait shell üstünde nasıl çalışılır,
  • Temel mongodb komutları nelerdir,
  • Sunucudan istemciye stream açarak tek tek mongo db dokümanı nasıl döndürülür(main.go'daki GetPlayerList metoduna bakın)

Eksikliği Hissedilen Konular

Her ne kadar pomodoro tekniği ile çalışmalarımı olabildiğince verimli hale getirsem de ister istemez yaşlı zihnim yoruluyor. Dolayısıyla şunları da yapabilsem iyi olurdu dediğim şeyler var. Bunları da şu iki madde ile sıralayabilirim.

  • İstemci tarafını Go tabanlı bir web client olarak geliştirmeyi deneyebiliriz. Terminalden hallice daha iyidir. En azından çalışma sırasında yaşadığım Trim ihlali oluşmaz.
  • Bir çok sunucu metodunda hata kontrolü var ancak bunların çalışıp çalışmadığı test etmek gerekiyor. Yani Code Coverage değerimizi neredeyse 0. Yazıyla sıfır :) Bir Go uygulamasındaki fonksiyonlar için Unit Test'ler nasıl yazılır öğrenmem lazım.

Görev Listeniz

Ve tabii kabul ederseniz sizin için iki güzel görevim var :)

  • Select * from players where fullname like 'A%' gibi bir sorguya karşılık gelecek mongodb fonksiyonunu geliştirip uygulamaya ekleyin.
  • Güncelleme fonksiyonunu tamamlayın.

Böylece geldik SkyNet'te bir günün daha sonuna. Sonraki çalışmada Wails paketini kullanarak Go ile yazılmış bir masaüstü programı geliştirmek niyetindeyim. O zaman dek hepinize mutlu günler dilerim.

Switch Case Kullanmadan Kod Yazılabilir mi?

$
0
0

İnsanoğlu yağmurlu bir pazar günü evden çıkıp ne yapacağını bilemezken ne hakla ölümsüzlükten bahseder. Bir yazara ait olan bu cümleyi sevgili Serdar Kuzuloğlu'nun yakın zamanda izlediğim söyleşisinden not almışım. İnsanlığın ömrünü uzatmaya çalışması ile ilgili bir konuya atıfta bulunurken ifade etmişti. Oysa karşımızda duran ekolojik denge ve iklim problemleri, yakın gelecekte(2025 deniyor) dünya nüfusunun 1 milyar 250 milyon kadarının içilebilir su kaynaklarına erişemeyeceğini işaret etmekte. Lakin bundan etkilenmeyecek olan ve asıl ömrünü uzatmak isteyen dünya nüfusunun en zengin %1i, söz konusu kıtlığın yaratacağı sorunlardan ve başka felaketlerden korunmak için kendisine dev sığınaklar inşa ediyor, adalar satın alıyormuş. Gerçekten anlaşılması çok zor ve bir o kadar da karmaşık bir durum değil mi? Bu distopik senaryo bir kenara dursun biz geleceğin iyi şeyler getireceğini ümit ederek gelişmeye devam edelim.

Bazen üzerinde çalıştığımız Legacy sistemlerin biriktirdiği teknik borçlar da aynen bu distopik senaryoda olduğu gibi bizi kara kara düşündürür. Bunun önemli sebeplerinden birisi teknik borçlanma konusundaki bilinçsizliğimizdir. Ancak benim farklı bir teorim var. İtiraf etmeliyim ki programlama dillerini bazen çok yanlış bir biçimde öğreniyoruz. Özellikle nesne yönelimli dillerde bu daha fazla öne çıkıyor.

İlk öğrendiğiniz nesne yönelimli programlama dilini düşünün. Çoğunlukla değişken tanımlamaları ile başlayan, karar yapıları ve döngülerle devam eden bir öğretiyi takip ederiz. Bende genellikle bir programlama dilini tanırken bu yolu tercih ediyorum. Lakin tecrübemiz artıp nesne yönelimli dil bilgimizin yanına tasarım kalıpları ile SOLID(Single Responsibility, Open Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) gibi ilkeleri de ekleyince başka bir dili öğrenirken karar yapılarının üzerinde durmalı mıyız tartışabiliriz.

Öyle ki temiz kodlama ilkeleri bir çok halde switch-case/if-else gibi karar yapılarını kullanmadan kod yazmamız gerektiğini öğütlüyor. Şimdi kısa bir süre için ilgilendiğiniz koda veya projeye bakın. Bir switch-case/if-else bloğu arayın. Şöyle kallavi olanlardan bulursanız harika. Mesela case bloklarının içerisinde bol miktarda if-else ifadeleri de olsun. Tam bir kararsızlıklar abidesinin ekranın üst kısmından aşağıya doğru ilerlediğini ama tüm bu hengameyi içinde barındıran metodun gerçekten işe yarar bir şeyler yaptığını farz edin. Şimdi derin bir nefes alın ve o kod parçasını switch-case olmadan nasıl yazabileceğinizi bulmaya çalışın.

Bu noktaya nasıl mı geldim? Pek tabii bir süredir hayatımızda önemli bir yere sahip olan statik kod analiz aracı Sonarqube sayesinde. Son günlerde Cognitive Complexity (Kavramsal Karmaşıklık desek yeridir) değeri ne yazık ki tavan yapmış sınıflar ve üyeleri arasında gezinmekteyiz. Kullandığımız Sonarqube metriklerine göre bir metodun Cognitive Complexity değerinin 15 puanı aşmaması bekleniyor ancak 200lü değerleri geçen fonksiyonlar var. Complexity değerinin yüksek olması kodun okunurluğu, anlaşılması, yönetimi, bakımı ve test edilebilirliği noktasında çok zayıf olduğu olduğu anlamına gelir. Bu değeri arttıran bir çok bulgu var. Meşhur olanlarından bir tanesi de fazla sayıda switch-case ifadesinin kullanılması. Aslında Sonarqube'a ait aşağıdaki ekran görüntüsü konu hakkında biraz daha fazla fikir verebilir.

Kavramsal karmaşıklık değeri, temel programlama yapılarının kod içerisindeki kullanımları için belirlenmiş ağırlık değerleri baz alınarak hesaplanmaktadır. Söz gelimi if-then-else için 2, for döngüsü için 3, eş zamanlı çalışan paralel kod parçaları için 4 ağırlık puanı ele alınır. Bu değerlere metodlara aktarılan ve çıkan parametre sayıları gibi kriterler de eklenerek fonksiyonun karmaşıklığı hakkında sayısal bir bilgi elde edilir. Cognitive Complexity puanlamasına etki eden kriterler için Sonarqube'un şu adresinden yararlanabilirsiniz ancak olay sanıldığı kadar basit değil. İşin içerisinde Matematik formüller de var ;) IEEE'nin 2018 basımı şu dokümanındaçok daha fazlasını bulabilirsiniz.

Buradaki en önemli sorun metodun bulunduğu sınıfın SOLID ilkelerindeki Open-Closed prensibini ihlal etmesi. Bu ilkeye göre bir nesnenin genişletilmeye açık değiştirilmeye kapalı olması istenir. "Bunu bir örnekle anlamaya çalışsak nasıl olur?" diyorum içimden :) Öyleyse gelin aşağıdaki kod parçasını mercek altına alalım.

using System;
using System.Collections.Generic;

namespace Sonarqube.Tests
{
    public enum ProviderDirection
    {
        Kafka,
        Rabbit,
        Redis
    }

    class Program
    {
        static void Main()
        {
            #region Klasik kullanım

            RoleProvider.Ping(ProviderDirection.Kafka,"Birr bilmecem var çocuklar...");

            #endregion
        }
    }

    #region Birinci Durum (Complexity değerini yükselten switch-case kullanımı -Temsili)
	
    public static class RoleProvider
    {
        public static void Ping(ProviderDirection target,string message)
        {
            switch (target)
            {
                case ProviderDirection.Kafka:
                    Console.WriteLine("Kafka->{0}",message);
                    break;
                case ProviderDirection.Rabbit:
                    Console.WriteLine("Rabbit MQ->{0}",message);
                    break;
                case ProviderDirection.Redis:
                    Console.WriteLine("Redis->{0}",message);
                    break;
                default:
                    break;
            }
        }
    }

    #endregion
}

Olayı basit bir şekilde analiz etmek için az sayıda case ifadesi kurguladık. Aslında tertemiz bir kod parçamız var. RoleProvider sınıfı içerisindeki Ping metodu bir enum sabitini kullanarak farklı sistemler üzerinden mesaj gönderme işlemini temsil ediyor. Main metodu içerisinde yaptığımız çağrıda nasıl davranması gerektiğini belirliyoruz. Sorun RoleProvider sınıfı içerisine yeni bir provider durumu eklemek istediğimizde ortaya çıkıyor. Bunun için RoleProvider sınıfının kodunu değiştirmek zorundayız. Yani yeni durumu da eklememiz gerekmekte. Eğer RoleProvider sınıfını devasa bir uygulamanın ortak kütüphanelerince kullanılan bir tipi olarak düşünürsek işimiz daha da zorlaşıyor. İşte hem bu ilkenin ihlalini engellemek hem de Sonarqube aracını memnun etmek için başvurabileceğimiz güzel bir yol var. O da strateji tasarım kalıbının bir uyarlaması. Şimdi yukarıdaki kod parçasını aşağıdaki hale getirelim.

using System;
using System.Collections.Generic;

namespace Sonarqube.Tests
{
    public enum ProviderDirection
    {
        Kafka,
        Rabbit,
        Redis
    }

    class Program
    {
        static void Main()
        {
            #region Strategy Pattern uygulanan çözüm

            ProviderContext strategyContext = new ProviderContext();
            string message = "Acaba nedir nedir? Bisküvi denince akla...";
            strategyContext.Ping(ProviderDirection.Kafka,message);

            #endregion
        }
    }

    #region Strateji kalıbının uygulandığı çözüm
    interface IProviderStrategy
    {
        void Send(string message);
    }

    class KafkaProvider
        : IProviderStrategy
    {
        public void Send(string message)
        {
            Console.WriteLine("Mesaj Kafka'ya gönderilir. {0}", message);
        }
    }

    class RabbitMqProvider
        : IProviderStrategy
    {
        public void Send(string message)
        {
            Console.WriteLine("Mesaj RabbitMQ'ya gönderilir. {0}", message);
        }
    }

    class RedisProvider
        : IProviderStrategy
    {
        public void Send(string message)
        {
            Console.WriteLine("Mesaj Redis'e gönderilir. {0}",message);
        }
    }

    class ProviderContext
    {
        private static Dictionary<ProviderDirection, IProviderStrategy> _providers = new Dictionary<ProviderDirection, IProviderStrategy>();

        //// Belki basit bir Injection kurgusu ile provider'lar içeri alınabilir
        //public void AddProvider(ProviderDirection direction, IProviderStrategy provider)
        //{
        //    _providers.Add(direction, provider);
        //}
        static ProviderContext()
        {
            _providers.Add(ProviderDirection.Kafka, new KafkaProvider());
            _providers.Add(ProviderDirection.Rabbit, new RabbitMqProvider());
            _providers.Add(ProviderDirection.Redis, new RedisProvider());
        }
        public void Ping(ProviderDirection direction,string message)
        {
            _providers[direction].Send(message);
        }
    }

    #endregion
}

Evet evet biliyorum. Üç tanecik case bloğu için tonlarca kod yazdık ama duruma böyle bakmamak gerekiyor değil mi? ;) Strateji kalıbının bu kullanımı sayesinde kodun daha okunabilir hale geldiğini söyleyebiliriz. Özellikle aşağıya doğru uzayıp giden case bazlı kod bloklarını çevirdiğinizde farkı çok daha net anlayacaksınız. Şimdi kod tarafında neler yaptığımızı kısaca inceleyelim. İstemci(Client) olarak niteleyebileceğimiz program sınıfı belli bir Provider tipine göre mesaj göndermek istiyor. Bunu yaparken enum sabitinden yararlanıyor. Enum sabitinin her elemanı farklı bir davranış biçiminin sergilenmesi anlamına da gelmekte. Eğer Kafka seçiliyse mesaj gönderme ona göre yapılmalı. RabbitMQ seçildiyse de ona göre. İşte bu hal değişikliğinin uyarlanması bizi ister istemez davranışsal tasarım kalıplarına(Behavioral Design Patterns) götürüyor. Bu tip switch-case kullanımlarının önüne geçilmesinde yukarıdaki kod parçasında yer verilen strateji tasarım kalıbının uyarlanması yeterli. 

Uyarlamada dikkat edileceği üzere case durumuna giren asıl tipler(KafkaProvider, RabbitMqProvider, RedisProvider) ile uyguladıkları bir interface(IProviderStrategy) söz konusu. Context tipi olarak ProviderContext sınıfı kullanılıyor. Bu sınıf dikkat edileceği üzere enum sabiti ile karşılığı olan provider tiplerinin eşleştirildiği bir koleksiyon barındırmakta. Dolayısıyla sisteme yeni bir case bloğu eklemek demek aslında yeni bir IProviderStrategy türevini tasarlayıp buraya koymak demek. Tabii burada kafa karıştıracak bir durum var. switch-case yapısından kurtulduk ancak Context tipi halen open-closed ilkesini ihlal ediyor gibi. Bir başka deyişle generic Dictionary koleksiyonuna eklenecek bağımlılıkları içeriye enjekte etmeyi de düşünebiliriz. Yorum satırı haline getirilen AddProvider metodunda bunu bir nebze olsun ifade etmeye çalıştım ancak çözümü çok daha şık hale getirebiliriz. Bunu bir düşünün ve nasıl yapacağınıza karar verin.

Ah sonlandırmadan çalışma zamanına ait bir ekran görüntüsü de paylaşırsam çok iyi olacak.

Tahminlerime göre aklınıza gelen bir başka soru daha var. "Peki ya if-else kullandığımız senaryolar varsa...Hah haaa" Elbette Cognitive Complexity değerini yükselten durumlardan birisi de if bloklarının çokluğu. Hatta ternary operatörü kullansak bile Sonarqube bunu yutmayabilir:) Strateji kalıbını if-else yapıları için de kurgulayabiliriz elbette ama farklı yaklaşımlar da söz konusu. Bunlardan birisi Chain of Responsbility kalıbının uygulanmasıdır. Buna benzer bir kalıp olup normalde GOF(Gangs of Four) listesinde yer almayan ancak Steve Smith'in bir Pluralsight eğitiminde anlattığı ve tesafüden de olsa çok geç bulduğum Rules tasarım kalıbı da var. Bir başka yazıda farklı ilkeler ile bu kez if-else karar yapısını daha doğru kurgulayarak nasıl ilerleyebileceğimizi incelemeye çalışacağım. Şimdilik bana müsade. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Bakınız : switch-case/if-else gibi yapıları kullanmadan geliştirme yaparken tek seçeneğimiz Strategy veya Rules deseni midir? Örneğin Command Pattern kullanılabilir mi? ;)

Sekiz Saatlik Sonsuz Döngü

$
0
0

Uygulama geliştirme yaşam döngümüzün henüz otuzuncu Sprint başındaydı. İki haftalık koşu görevlerini Sprint Planning toplantısında zaten belirlemiştik. Takım olarak 13 Story Point’e sahip Production Support Buffer mecburen her sprint içerisine dahil ettiğimiz bir maliyet. 17 yaşındaki Microsoft .Net tabanlı devasa ERP(Enterprise Resource Planning)ürünümüz ek geliştirmeler veya önceki yıllardan kalan teknik borçlar sebebiyle bazen üretim ortamı sorunları ile karşımıza gelmekte. Büyüklüğüne nazaran Code Coverage oranının düşük olması yeni ilavelerin var olan yapılara olan etkisini anlamamızı zorlaştırıyor. Ben, Mali İşler ve Ortak Modüller(Kimsenin bilmediği bir modül varsa böyle gelin) ekibindeyim. Lakin ERP’nin diğer modüllerinde de benzer sorunlar olabiliyor. 

Bazen kod kalite standartlarını korumak, bakım maliyetlerini düşürmek, minimum sorunla sürümleştirmek, hızla ölçekleyebilmek ve çabuk adaptasyon için geliştirilmiş yeni kültür, ilke, mimari model veya araçları gözden kaçırırız(farkına varmayız) Bu, uzun ömürlü ürünlerin ilerleyen zamanlarda ki modernizasyonunu oldukça güçleştirebilir. Tabii her şeyi de sorgusuz sualsiz benimsememeliyiz. Değişime açık ama sakin davranmalıyız.

O gün her zaman ki gibi Madelein yanıma oturdu ve acil çözüm bekleyen müşteri sorunlarının en önemli olanlarından birisinden bahsetmeye başladı. Tüm öncelikleri bir kenara bırakıp bu üretim problemine yoğunlaşmam ve en kısa sürede çözüme ulaştırmam gerekiyordu. Bu, bazı stand-up meeting toplantıları sonrası yaşadığımız standart bir durum. Gün geçtikçe azalan ama bir süre daha hayatımızın parçası olacak bir gerilim hatta. Her ne kadar müşterinin de dahil olduğu kurumsal çaptaki Dijital Board ile işler önceliklendirilse de, bayilerden ve ana yüklenici firmadan gelen üretim ortamı sorunları göz ardı edilemiyor.

Bir bayinin kısa süreliğine de olsa fatura kesememesi ya da stok sahasındaki mobil cihaz süreçlerinde meydana gelen yavaşlık nedeniyle durma noktasına gelen sevkiyat, elbette ortaya çıkan zararlar düşünülünce kriz ortamının bir mantar bulutu gibi geliştiricinin masasında patlamasına neden olmakta(Ve kimse radyoaktif maddeyle kaplanmış bir geliştiricinin yakınlarında olmayı tercih etmez)

Bu nedenle müşteri isteklerinin doğru şekilde parçalanması, ortak paydaşlarda el sıkışılması, önceliklendirmelerin ve iş değerlerinin üst kademeden itibaren şeffaf bir şekilde görülebilmesi elzem derecede önemli. Bunu aşmak için modüllerdeki iş sahipleri ve scrum master’lar belirli aralıklarda müşteri ve üst yönetim ile bir araya gelerek kabul kriterleri üzerinde müzakerelerde bulunup vaziyet hakkında bilgi paylaşımında bulunuyorlar. Bu sayede her şeyin şeffaklıkla tüm birimlere akması sağlanıyor.

Shelby’nin Monolitik Dünyası

Tamamiyle monolitik karakteristikte bir yazılım olarak niteleyebileceğimiz ERP ürünü gerçek bir Legacy sistem. 2002 yılında .Net Framework’ün ilk sürümüyle birlikte geliştirilmeye başlanmış, N-Tier mimariye göre yazılmış, zaman içerisinde SOA(Service Oriented Architecture) evrimini gerçekleştirmiş, tek Microsoft SQL Server örneği ile çalışan, 8500 ekran, 4500 tablo, 27binden fazla Stored Procedure’den oluşan ve sahada 10bin personel tarafından kullanılan bir Asp.Net Web Forms uygulaması(Şimdi pek çoğunuz “ne kadar da demode bir teknoloji kullanıyorlar” diyebilirsiniz ama unutmayın; Bu monolitik mimari yıllardır ve halen müşteri için olan görevini başarıyla yerine getiriyor)

Shelby, çevresel olarak entegre olduğu çeşitli tipte sayısız servise de sahip. Eski nesil SSIS(Sql Server Integration Services) paketlerinden yeni nesil REST servislerine, kurum dışı WCF uç noktalarından SOAP bazlı Web hizmetlerine kadar oldukça geniş bir dal budak yığını söz konusu. Sektörün bir gereği olarak yurt dışı firmalarla yapılan bir çok entegrasyon noktası bulunuyor. Bazılarıyla TCP gibi protokollerle iletişim kuruluyor. Regülasyonlar sebebiyle devlet kurumları ile entegre olunan noktalar da mevcut. Kocaman bir ekosistem işte :)

Monolitik bir sistem her zaman kötü çocuk değildir. Shelby sahip olduğu dezavantajlara nazaran tek bir paket olması nedeniyle üretime alma, performans ve hata izleme ile bağımlılık yönetimi gibi konularda dağıtık mimariye göre daha avantajlı durumda. 
Nitekim dağıtık sistemlerin yönetilmesi ve yaşatılması zordur. Öyle ki dağıtık sistemlerde üretime alma sürecinin otomatikleştirilmesi, test süreçlerine ait otomasyonun her aşamasında uygulanması(Unit Test, Functional Test, Regression Test, User Acceptance Test), yazılım ve operasyon ekiplerinin daha yakın çalışması(DevOps), her servisin çalışma zamanı performansının izlenebilmesi, takibinin yapılması, versiyonunun yönetilmesi vb bir çok şey gereklidir.

Güncel .Net Framework sürümüne evrilmiş olsa da SonarQube ile başlanan teknik borç azaltma çalışmaları öncesi yaklaşık 2 milyon satır koda sahip olan bir üründen söz ediyoruz. Big Bang öncesi %17 kadarı tekrarlı koddan oluşuyordu (Doğruyu söylemek gerekirse turuncu bankada çalışırken gördüğüm C ile yazılmış devasa muhasebe paketinden sonra Tanrı Nesneler-God Object içeren bir uygulama daha görmek şaşırtıcı değil) SonarQube, çeşitli seviyelerden gelen toplam teknik borcun temizlenmesi için 786 adam günlük bir tahminlemede bulundu.

Teknik borçların azaltılması şarttı çünkü bir sene öncesinde başlanmış olan dönüşüm süreci kapsamında yeni araç ve kültürlere adapte olunurken Shelby’nin modernize edilmesi gerekiyordu. Terk edilemeyecek kadar önemli bir role sahip olan ürünün baştan yazılması maliyeti ciddi anlamda yüksekti. Bu nedenle hedeflerden birisi sonraki yıl onun teknik borcunun sıfırlanması olarak belirlenmişti. Bunu yapmazsak CI/CD hattındaki Quality Gate noktasına takılacağımız ve Gandalf’ın o meşhur ‘you shall not pass’ sözüyle karşılaşacağımız aşikar.

İşin başında ve sonrasında,

ThoughtWorks firmasının zamanında verdiği danışmanlık sonrası sunduğu 171 sayfalık rapor, dev monolitik ERP uygulamasının mikro servis dönüşümüne tabii olmasının zorunlu olduğunu belirtmişti. Bunun için bir çok Anti-Pattern’i bünyesinde barındıran ürünün öncelikle teknik borçlarından arındırılması önemli.

SonarQube’nin pek çok çıktısı benzer kural ihlallerini verdiğinden genç arkadaşlarımızdan oluşan bir ekip(deneyimli yazılımcıların yanına eklenen stajyerlerden oluşan bir grup parlak beyin) Roslyn ile kokan kısımları hızlıca bertaraf etti. İşin başında teknik borcun farkında olup ürünleşme yolundaki engelleri de bilen ve CEO’ya doğrudan raporlama yapan yazılım grup müdürü dahi vardı. Bizzat kodlamaya katıldığını ve teknik borcun temizlenmesi için elinden geleni yaptığını gözlerimle gördüm. Kısa sürede teknik borcun akıllı kod parçaları ile 133 güne kadar indiği görülmüştü. Asıl sorun Complexity değerleri yüksek olan God Object damgalı sınıfların nasıl ayıklanacağıydı. İşte bu noktada daha önceden öğrenip de ne zaman kullanırız dediğimiz bazı kavramlar değer kazanıyor (Çok fazla if bloğu veya switch koşulu barındıran ve bu nedenle Complexity değeri yüksek çıkan bir fonksiyonda hangi tasarım kalıbını kullanarak çözüm üretirsiniz?)

Burası konfor alanınız dışına çıkmanızı gerektiren, iletişimin ön plana çıktığı, mücadelesi yüksek bir yer. Bu bence iyi bir şey. Çünkü daha fazlasını kaldırabileceğinizi görüyorsunuz.

Dönüşmeye Çalışmak ve Sıkıntılar (Dijital Dönüşüm)

Çeşitli ürün gruplarından farklı teknolojilerle geliştirilmiş bir çok sistemin bir arada yer aldığı kaotikleşmiş, bakım maliyetlerinin yüksek olduğu, sirkülasyon nedeniyle bilgi kaybının yaşandığı, basit bir kod düzeltmesinin(hotfix gibi)çıkılması için dahi rutin geçiş gününün beklendiği bir ortam söz konusuydu. ERP bir yana yeni nesil ürünler ile de haşır neşir olunuyor, kaçınılmaz modernizasyon çalışmaları pek çok ekipçe planlamalara dahil ediliyordu. 

Kabaca bir yanda yeni nesil savaş uçağının geliştirilmesi, diğer yanda kırk yıllık tankların modernize edilmesi öteki tarafta amfibik yetenekleri ile diğer unsurları taşıyacak uçak gemisinin yapımı noktasında çalışan bir çok insan olduğunu hayal edin. Akıllı ve dikkatlice hareket edilmediği takdirde 135mm tank topu taşıyan ve uçacağına kesin gözüyle bakılan bir mühendislik harikası icat etmeniz içten bile değil.

Uzun yıllarını şirket bünyesinde geçirirken pek çok konuda bilgi sahibi olan bir geliştirici, özellikle o şirket kültürünün şartları düşünüldüğünde değerlidir. Bu tip çalışanların kaybı ister istemez bazı kritik müdahale bilgilerinin de uçmasına sebebiyet verebilir. 

Çalışanlar arası bilgi transferinin daha iyi yapılabileceği çevik iletişim teknikleri ve farklı modüllerdeki kişilerin çapraz yetkinlikler kazanabileceği Tribe usülü yapılar bu kayıplara belki çözüm olabilir. 

Yine de işin can alıcı kısmı insanların buna ne kadar istekli olduğuyla da alakalıdır. 

Bu bağlamda çevik metodolojilere adapte olunup Scrum ile yürünmesine, DevOps kültürünün benimsenmesine, ürün taşıma sisteminin yani CI/CD(Continuous Integration/Continuous Delivery-Deployment) hattının kurgulanıp TFS yerine git bazlı Azure DevOps platformuna geçilmesine karar verildi. Feature bazlı geliştirmeyi desteklemek adına Git Flow stratejisi tercih edildi. Artık develop isimli branch tabanlı olarak açılan Feature setleri üzerinde yapılan geliştirmelerin test sonuçlarından gelen bilgilere göre tekrar develop hattına, oradan master’a merge edilmesi söz konusu. Hatta kod yamaları için de Git Flow stratejisi tercih ediliyor. Üretim ortamında aldığımız bir hata için rutin geçiş gününü beklememiz şart değil. Acil geçiş mi? Hiç sorun değil!

git extension tarafından bir görüntü

Her feature, Scrum Board üzerinde açılmış bir User Story veya Work Item ile ilişkili. Derlenmesi neredeyse bir saati bulan(şimdilik) ERP her öğle vakti teste, iki haftada bir her pazartesi dondurularak(freeze) pre-prod ortamına ve o haftanın çarşamba gecesi üretim ortamına çıkıyor(Şu vakitler haftada bir üretim ortamına çıkılması da gündemde) Benim için heyecan uyandıran tüm bu otomatikleştirilmiş etkileşim Azure DevOps üzerinden yönetiliyor. Hedef bu periyotların dışına çıkıp istenildiği zaman üretim ortamına özellik eklenebilmesi. Lakin bunun için biraz daha yolumuz var.

Araya renk katacak bir ekran görüntüsü de koyalım. Fotoğrafta buradaki belki de en renkli takımlardan olan lTunes klanının board’u yer alıyor. Bu renkli oluşumda onları motive eden yöneticileri Ismail KIRTILLI nın rolü çok büyük(Şekerpınar’daki bu board hiç bozulmadan Maslak’taki yeni ofise de taşındı)

Her ne kadar süreç otomatik hale getirilmiş olsa da üretim geçişleri çoğunlukla CAB(change advisory board) süreci üzerinden ilerletiliyor. Acil geçiş ihtiyaçları mutlaka direktör onayından geçiriliyor. Geçişin etkisi, kritikliği ve geri alma planları sürece dahil edilmeye çalışılıp sorun olduğunda bir önceki doğrulanmış versiyona dönebilmek için ne gerekirse yapılıyor.

Tüm bu çetrefilli işlerin yanında şirketin kültür dönüşümünün sancıları da olmadı değil. Bizi en çok zor anlayan her zaman olduğu gibi müşteri tarafıydı. ERP, onların göbekten bağlı olduğu bir sistemdi ve şimdi isteklerini sprint’ler başlamadan önce ürün sahipliği rolünü üstlenerek ilk kez temas ettikleri yazılım ekipleri ile birlikte belirlemeye çalışmak başlarda onlara da zor gelmişti. Binalar arası bir otoban olmasıydı belki de bizi birbirimizden uzaklaştıran ama sonunda alıştılar…

Aslında biz yazılım ekipleri çoğunlukla SCRUM, KANBAN gibi metodolojiler ile uğraşıyoruz. Ancak üst yönetim kademisine baktığımızda benzer stratejilerin dijital board gibi terimlerle uygulandığını da görmekteyiz. SAFE(Scaled Agile Framework) aslında tam da bu kademenin uygulayacağı türden bir pratik.

Yeni Nesil Ürünler ve Gelecek Vizyonu

İlk başladığım zamanlarda bu yeni nesil ürünlere biraz temkinli yaklaştığımı ifade etmek isterim. Henüz deneysel sürümlerde olan platformların ürünleştirilip müşteriye sunulmasına her zaman karşıyım. Deneyimlemek daima tavsiye ettiğim heyecanlı bir çalışma ama müşterinin bakış açısı çok daha farklı oluyor. Onlar sorunsuz ürünlere yeni isteklerini ekletmek istiyorlar.

“Ayıkla pirincin taşını” dememek yerine gerçekten emin olup ürünleştirmek bu tip kurumsal firmalar için çok daha iyi. Sonuçta Start-Up kültüründen oldukça farklı dinamikler söz konusu. Bu nedenle hype teknolojileri ürünleştirmeden önce belki de Technology Radar gibi bir kaynağa bakarak ilerlemekte yarar var. 

Neler geliyor, kimi mercek altına alalım, ne için hazırlık yapalım, hangisine adapte olalım ve benzeri soruların cevapları stratejik olarak önemli. Pek tabii yazılımcıları da bu noktada teşvik etmek, cesaretlendirmek ve hata yapmaktan korkmamalarını sağlamak lazım. Elbette hata yapma toleransının kabul edilebilir sınırlarını da iyi belirlemek gerekiyor.

Yeni nesil ürünler çok daha şanslı. Microsoft’un açık kaynak ve platform bağımsız .Net Core tabanlı Web API servisleri ile konuşan Vue/Angular tabanlı önyüz sistemlerinden oluşan bu çözümlerde deneyselliğe de izin veriliyor. 

Yazılımcıların hata yapmalarına müsaade etmek gelişimi destekleyen en önemli unsurdur. Bu hatalardan ders çıkartıldığı müddetçe.

Javascript yerine daha çok Typescript tercih ediliyor, webpack gibi modern paketleyiciler, Node.js gibi sunucular kullanılıyor. Tamamen Dockerize edilerek ölçeklenebilirliği kolaylaşan uygulamalar, Azure üzerindeki CI/CD (Continuous Integration/Delivery-Deployment) hattında Test standartlarına uyulmaya çalışılarak yaşıyor. Yaygınlaştırılmaya başlanan Selenium entegrasyonları ile fonksiyonelliklerin davranış odaklı test senaryoları üzerinden kontrolü yapılıyor. Günün herhangi bir anında kolayca sürüm çıkılması ve container çoklaması ile performans iyileştirilmesi mümkün.

KONG arkasında yönetilen bu ürünler çoğunlukla kendi veritabanları ile çalışırken pek çoğunda PostgreSQL gibi farklı alternatifler de değerlendiriliyor(Hatta Shelby’nin veritabanının domain bazlı olarak parçalanmasından önce PostgreSQL’e geçişi ile ilgili deneysel çalışmalar yapılmakta. Üzgünüm Microsoft ama topyekün bakıldığında lisanslama maliyetlerin çok pahalıya gelebiliyor) Yeni nesil ürünler kendi Bounded Context’leri içinde konumlandırıldıklarından mikro servis yapısına daha uygunlar. Henüz mezun olmuş ya da birkaç yıllık iş tecrübesi bulunan geliştiricilerin aşina olduğu ve kolayca adapte olabileceği ortamlar.

Son zamanlarda geniş bir stajyer programı uygulanıyor. Özellikle uzun dönem stajyer arkadaşlar doğrudan okyanusa bırakılıyor ve aktif olarak müşteri isteklerinin karşılanması ile ilgili görevlerde çalışıyor. 

İnsan Kaynakları ekibinin üniversitelere bizzat giderek iyi bir eleme programı uyguladığına inanmaya başladım gördüklerimden sonra. Şirket, çekirdekten yetiştirmenin meyvelerini günden güne alıyor. 

Her Şeyin Farkında Olmalıyız (İzleme, Erken Tedbir ve Otomatikleştirme)

İster onyedi yaşındaki ERP ürünü olsun ister yeni doğmuş, emekleyen, yürümeye henüz başlamış yeni nesil ürünler, izleme(monitoring) her şeydir.

Her ne kadar erken uyarı sistemlerini tam olarak robotlaştıramasak da çalışma zamanında oluşan sıkışmaları görmek, hataları koda girmeden yakalayabilmek adına çeşitli araçlardan yararlanıyoruz. Shelby’nin dünyasını genellikle Riverbed üzerinden izliyoruz(Open APM ve HP Diagnostics kullanan ekipler de var) Yavaşlayan bir ekran varsa, ön yüz sayfa taleplerinden Data Access Layer metotlarına ve arka plandaki SQL çağrılarına giderek sorunu tespit etmemiz ve çözmemiz kolaylaşıyor. Yavaşlamaya başlayanlar genellikle yoğun iş kuralı içeren stored procedure’ler veya indeksleri bozulan tablolar oluyor(Veritabanı ekibiyle yapılan koordineli çalışma ile sorunlar bertaraf edilebiliyor ama kalıcı çözümler için bazı bazı kaynak sıkıntısı da yaşanıyor)

Rutin olarak yapılan işlerden birisi de sistemin yavaşladığı noktaları Riverbed veya muadili bir araç üzerinden yakalayıp nasıl çözebiliriz sorusuna cevap bulmaya çalışmak. Böylece uygulamanın gizli saklı tüm sırlarını görüyor, MSMQ yerine Apache Kafka veya RabbitMQ’ya geçmeliyiz gibi yorumlarda bulunabiliyoruz.

RiverBed ürününden bir ekran görüntüsü. Temsili.

Vakit ve belki de yeterli kaynak olsa bu ve benzer sorunları problem olarak sınıflayıp ITIL(Information Technology Infrastructure Library) felsefesinde geçen ilkelere göre kalıcı olarak çözmek çok da güzel olur. 

Yeni nesil ürünlerin Elasticsearch tarafına atılan sayısız log verisini Kibana üstünden takip ediyoruz. Şu meşhur ELK üçlemesini ele aldığımızı ifade edebilirim. Bu log gerçekten çok büyük önem arz ediyor. Üretim ortamına ait sıkıntılı durumlarda kayıtlardaki izlere bakarak yazılıma müdahale bile etmeden çözümler üretebiliyoruz.

ElasticSearch içeriği daha güzel bir şekilde izlediğimiz Kibana'dan bir görüntü

Yeni şeyler ekledikçe ürünlerin kalitesini korumamız da lazım. Bunun için SonarQube’ye, güvenlik noktasındaki açıkların tespiti için Fortify’a başvuruyor, OWASP(Open Web Application Security Project) gibi standartlara bağlı kalmaya çalışıyoruz. 

Yeni nesil ürünlerdeki en yakın dostumuz ise Chrome’un F12 tuşu. Ön yüzün arka taraftaki REST servislerine hangi tip çağrı ve payload bilgisi ile eriştiğini görmek, problem yaşanan durumlar için ilgili senaryoları local geliştirme ortamında kolayca tatbik edip çözüm bulmamızı kolaylaştırıyor. Senaryoları gerçekleştirirken ağırlıklı olarak Postman ve SOAP UI gibi araçlardan yararlanıyoruz. 

REST Api tipindeki servisler her ne kadar popüler olarak kullanılsa da bazı ürünlerde yeni nesil gRPC protokollü versiyonları kullanmak ve hatta Event-Driven yaklaşımlara geçmek çok daha iyi olabilir. 

Bazı işleri otomatik hale getirebiliyoruz. Kullanıcıların sıklıkla tekrarladığı aynı şeyleri RPA(Robotic Process Automation) süreçleri ile kontrol altına alıyoruz. Bu da şirket bünyesinde yaygınlaştırılmaya çalışan bir alan. Sonuçta önemli bir zaman ve maliyet kazancı olacağı ortada.

Vizyon

Kurumsal boyuttaki uygulamaların yenilenmek üzere tekrardan masaya yatırılması sıklıkla gündeme gelir. Sadece yeni isteklerin karşılanması değil baş ağrılarının giderilmesi ve modern çağa uymak için bu gereklidir. Bu nedenle somut ve cesaret isteyen adımlar atılmalıdır. Bu kararlılığı bir veya iki kişinin ya da bir takımın göstermesi yetmez. Konunun müşteri ile tartışılabilecek ve sayısal veriler ile desteklenebilecek şekilde en üst yönetim kadrosu tarafından da benimsenmiş olması beklenir. İnsiyatif alma isteği uyandıracak iç motivasyon unsurları önemlidir. Ancak hepsi bir yana bir teknoloji vizyonunun olması şarttır. Hangi durumdayız, nereye gitmek istiyoruz, bu yolda ilerlemek için hangi teknolojileri mercek altına almalıyız, kısa vadede hangi kararları uygulamalı uzun vadeye ne şekilde yaymalıyız…

Şahsen bulunduğum mavi teknoloji firmasının gerek bir önceki gerek şu anki yazılım grup müdürleri bir eylem planı oluşturmak, radikal kararlar almak ve korkmadan bunları sahiplenmek konusunda oldukça cesaretliler. Karar verici mecradakilerin sahip olduğu vizyon ile değişimi desteklemeleri önemli bir mesele.

Geçtiğimiz yaz ayında yapılan bir toplantıda Shelby’nin modernizasyonu kapsamında aşağıdaki maddelerdekine benzer işlerin planlandığını ifade edebilirim.

  • Web Site’ların ilk etapta Web Application’a çevrilmesi ve ilerleyen dönemlerde Blazor çatısına taşınması(Sıcak bir noktada)
  • En kısa sürede DevOps ekibinin hazırız dediği Blue/Green dağıtım stratejisine geçilmesi.
  • MSSQL veritabanının PostgreSQL dönüşümüne başlanması. Kısa vadede yoğun iş kuralları içeren SP’lerin EF tarafında karşılıklarının oluşturulma maliyetlerinin çıkartılması.
  • Orta vadede bir ORM(Object Relational Mapping) kullanımına geçilmesi.
  • Teknik borç hedeflerinin yerine getirilmesi hususunda devam eden Fortify, Sonarqube, Testinium çalışmalarının tamamlanması (Testinium üzerinde modül bazlı olarak given-when-then senaryoları yazılmaya başlandı)
  • Platformda .Net Framework 4.8 versiyonuna geçilmesi ve kod içerisinde C# 8.0 destekli yeni kalite standartlarının adaptasyonu(Yazıyı tamamladığım tarih itibariyle bitmişti)
  • N-Tier yapının 3-Tier formuna indirgenmesi.
  • Performans ölçümlemede OpenAPM ürününe geçilmesi için gerekli çalışmalarının yapılması(ki geçildi)
  • Servis bağımlılıkları sebebiyle duran WS-* transaction yapılarının terk edilerek farklı bir stratejinin kullanılmasına başlanması.

Buradaki bazı kararları değil almak düşünmenin bile çok zor olduğu kurumsal dünyalar olduğu aşikar.

Sonuç

Şüphesiz ki pek çok büyük oyuncunun dönüşmeye çalıştığı/dönüştürdüğü platformlar ile haşır neşir olduğumuz bir zaman dilimindeyiz. Yirmi yıl önceki dönüşümler tekrarlanıyor ve ileride de tekrarlanacak. Hatta daha kısa periyotlarla değişime adapte kalmamız gerekecek. Eğer şirketiniz aşağıdakileri yapıyorsa ciddi anlamda değişimi düşünmek gerekebilir.

  • Programa eklenen ve müşteri testi yapılmış yeni bir özellik için üretime çıkış gününü bekliyoruz.
  • Geliştirdiğimiz ve üretime aldığımız özelliklerin müşteri için oluşturduğu katma değeri bilmiyor/ölçümleyemiyoruz.
  • Az zamanımız olduğu için bazı testleri atlamak zorunda kalıyoruz.
  • Üretim ortamında oluşan bir sorunu çözmek için kullandığımız tekniğe onu düzeltmek için tekrar dönme fırsatı bulamıyoruz.
  • Temel kod metriklerini aşan devasa sınıflar ve veritabanı nesneleri üzerinde hata ayıklama işlemleri yapıyoruz.
  • Sahip olduğumuz ürünün ne kadar büyük bir teknik borcu olduğunu bilmiyoruz.

Bu dünyada birbirinden farklı eski-yeni bir çok teknolojiyi bir arada görüyoruz. Yeni nesil araçlar sayesinde önceden önlemlerimizi almak, teknik borçlanma gibi konuları bertaraf etmek, her noktasını test ettiğimizden emin olduğumuz ürünleri sürümlemek artık çok daha kolay. Hazırlıklarımızı buna göre yapmalı ve kendimizi sürekli geliştirmeliyiz.

Yazıda bahsettiğim ne kadar şey varsa sadece bulunduğum ERP uygulaması ve çevresindeki gelişmelerden ibaret. Robotik süreç otomasyonu(RPA-Robotic Process Automation), yapay zeka, makine öğrenimi, IoT(Eşyanın Interneti), finansman, sigorta, filo, raporlama vb işlerle uğraşan daha pek çok ekip var ve hepsi bu büyük ekosistemin bir parçası olarak gelişimini sürdürüyor.

Bahsedilen Terimler

Yazı boyunca aşağıdaki listede yer alan terimlerden bahsettim. Eminim ki bir çoğuna aşinasınız ama çoğunu da bilmiyorsunuz. Bildiklerinizi de iyi bilip bilmediğiniz konusunda tereddütleriniz var. Bu yüzden bilmedikleriniz dahil var olanları da araştırıp pekiştirmenizi, üzerlerine eklenecek yeni daha neler olduğunu sürekli araştırmaya devam ederek canlı kalmanızı tavsiye ederim. Benim sekiz saatlik sonsuz döngüm neredeyse her gün bu şekilde işliyor. Zamansal paradox yaratarak oturduğum yerde kara delik açan bu cümle ile yazıma son veriyorum. Hepinize mutlu günler dilerim.

#agile #scrum #sprint #storyPoint #safe #workItem #dotNet #dotNetCore #nodeJs #typescript #vue #angular #erp #monolithic #legacySystem #nTier #SOA #SOAP #REST #WCF #sonarQube #codeCoverage #technichalDepth #qualityAssurance #mssqlServer #postgreSql #gitFlow #vsts #tfs #azureDevOps #branch #fortify #owasp #roslyn #CI/CD #continuousIntegration #continuousDelivery #continuousDeployment #dockerize #container #microService #kong #riverBed #ITIL #CAB #antiPattern #godObject #boundedContext #dataAccessLayer #chromeDevTools #thoughtWorks #microService #hypeTech #techRadar #SSIS #IoT #yapayZeka #robotik #postman #soapUI #rpa

The Internet Computer (Internetin Yeniden Keşfi) ve Motoko'yu Duyunca Ben

$
0
0

Herkese açık olan interneti genişletip kendi yazılım sistemlerimizi, kurumsal IT çözümlerimizi, web sitelerimizi, dağıtık bir ortamda firewall'lara ve yedekleme sistemlerine ihtiyacı duymadan güvenli bir şekilde konuşlandırabildiğimizi düşünelim. Hatta bunu sağlayan altyapı ile internete konan bu sistemler arasında fonksiyon çağrımı yapar gibi kolayca haberleşebildiğimizi(ve tabii ki güvenli bir ortamda) hayal edelim. Biraz blockchain benzeri bir dağıtık sistem kurugusu gibi değil mi? Tam olarak olmasa da oradaki teorileri baz almışlar gibi görünüyor. The Internet Computer adlı bu proje ICP(Internet Computer Protocol) adı verilen ve herhangi bir merkezi olmayan bir protokolü baz alarak, küresel ortamdaki bağımsız veri merkezlerinin, web sitelerinin, backend hizmetlerinin vb yazılımların aynı güvenlik garantileriyle çalıştığı kapatılamaz bir alt evren vaat ediyor.

Aslında ilk okumalarımda şunu anladığımı ifade edebilirim: Internete alacağımız bir hizmeti geliştirirken kodun güvenliği ve ürünün açıklarının kapatılması için çaba sarf ediliyor. Bu durum referans ettiğimiz paketler güncellendiğinde benzer kontrollerin tekrar yapılmasını gerektiriyor, lakin hacker'lar bu açıkları çok seviyor. Bağımlı olduğumuz sistemlerle belki de yeterince özgür bir ortama da sahip olamıyoruz. İşte The Internet Computer fikri, geliştirdiğimiz sistemlerin standart bir güvenlik sözleşmesi ile ayağa kalkabildiği, asla kapatılamayacak ve kurcalanamayacak bir ortamın üstünde çalışmasını garanti etme felsefesini öne sürüyor.

Birde Big Tech denilen şirketlerin internetteki neredeyse her tür SaaS(Software as as Services)'ın altından çıkmasının, topladıkları müşteri verilerini sürekli birbirleriyle paylaşmasının ve interneti sahiplenmesinin de bu projenin başlatılmasında önemi büyük(Sahibi olmayan bir internet ortamında güvenilir, kesintiye uğramayan uygulamaların geliştirilmesini sağlamak amaçlardan birisi) Proje çok yeni de değil. DFINITY adı verilen kar amacı gütmeyen bir kuruluşun 2016 yılında başlattığı bir çalışma.

Motoko

Konu esasında çok çok derin görünüyor. Detaylar için şu adrese bir uğrayın derim. Pek tabii benim derdim nasıl geliştirme yapıldığı. Bu platformun da bir SDK'sı(Canister Software Development Kit olarak geçiyor :) ) ve programlama dili var. Motoko, bahsedilen uygulamaları geliştirmek için kullanılan yazılım dili. Aslında benimde bu merak çalışmasındaki amacım Motoko'yu Heimdall(Ubuntu-20.04) ile tanıştırmak.

Ön Gereksinim ve Kurulumlar

Öyleyse hiç vakit kaybetmeden maceramıza başlayalım. Sistemde eğer önyüz geliştirmeleri de yapacaksak Node.js'in yüklü olması bekleniyor. CSDK'yi yüklemek içinse aşağıdaki terminal komutu yeterli.

sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"

# SDK'in doğru yüklenip yüklenmediğini anlamak için versiyona bakmak yeterli
dfx --version

# Yeni bir hello world projesi oluşturmak için
dfx new freedom

Motoko için bir Visual Studio Extension'da mevcut. Üstelik new ile yeni proje oluşturduktan sonraki terminal görüntüsü de çok tatlı.

Gelelim freedom içerisindeki kodlarımıza. Burada iki dosyaya dokunduğumu söyleyebilirim. Birisi index.js.

import freedom from 'ic:canisters/freedom';

freedom.sayHello(window.prompt("En sevdiğin renk")).then(lovelyColor => {
  window.alert(lovelyColor);
});

ve diğeri de main.mo

actor {
    public func sayHello(color : Text) : async Text {
        return "Hımmm...Demek en sevdiğin renk " # color # "!";
    };
};

İlk Örneğin Çalışma Zamanı

Yine terminal penceresinden ilerlemek lazım. Ben src altındaki main.mo ve asset'lerdeki index.js'i biraz kurcaladım ve ilişkilerini anlamaya çalıştım. Aslında motoko kodları main.mo içerisinde yer alıyor. Asset dediğimiz örneğin önyüz tarafı da public altındaki index.js. Index.js içinden main.mo'daki bir fonksiyonu(sayHello)çağırabiliyoruz.

# Önce makinedeki veya uzaktan erişilebilen bir Internet Computer ağına bağlanmak gerekiyor
# Bunu projenin package.json dosyasının olduğu klasörde yapmak lazım
dfx start

# Network oluşturulduktan sonra uygulamamız için buraya benzersiz bir Canister Id ile kayıt olmamız gerekiyor
# Bunu da yine package.json'ın olduğu yerde aşağıdaki komutla yapabiliriz 
dfx canister create --all

# Şimdi gerekli npm paketlerinin yüklenmesi lazım
npm install

# Ve ardından bizim uygulamamızın build edilmesi
dfx build

# Build işlemi başarılı bir şekilde tamamlandıysa bunu az önce oluşturduğumuz
# Local Internet Network'üne dağıtmamız gerekiyor
dfx canister install --all

# Bu işlemlerin arından yazılan program fonksiyonunu terminalden anından test edebiliriz
# freedom uygulamasındaki sayHello fonksiyonunu Black parametresi ile çalıştır
dfx canister call freedom sayHello Black

# İşlerimiz bittikten sonra Network'ü kapatmak içinse;
dfx stop

Ama birde node.js ön yüzümüz vardı ;) Onu da tarayıcıya gidip localhost:8080 arkasına, uygulama için üretilen Canister ID bilgisini dahil ederek test edebiliriz.

http://127.0.0.1:8000/?canisterId=cxeji-wacaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-q

Canister register, build ve deploy işlemlerine ait bir görüntüyü paylaşarak devam edelim.

ve çalışma zamanına ait iki görüntüyü de buraya bırakalım. 

Bazı Tespitler

Bu ilk örneği geliştirirken bir takım tespitlerim oldu. Onları şu maddelerle ifade edebilirim;

  • Önyüz için Assets klasörü altındaki index.js'i kurcalamak lazım.
  • Önyüzün kullandığı fonksiyonlar main.mo altında tutuluyor ancak bu şart değil. İkinci örnekte başka bir mo kaynağını kullanıyoruz.
  • Local Network adres ve port için tanımlama dfx.json altında bulunuyor.
  • Frontend tarafının entrypoint bilgisi ile main programlarının bildirimleri de dfx.json içerisinde.
  • Dağıtım tarafında webpack kullanılmış. Paket bağımlılıkları ise package.json üstünde tutuluyor.
  • Proje geliştirildikten sonra bir Local Network başlattık ve bu ortam için benzersiz bir Canister ID ürettik. Üretilen bu değeri başlatılan ağa kaydettik, projeyi build ettik ve sonrasında build olan projeyi bu ağa install ettik(ki burada WebAssembly içerisinde deploy oluyormuş) En nihayetinde örneği test edip çalıştırdık.

Bu arada build işlemi sonrası eğer terminalden aşağıdaki komutu girersek,

ls -l .dfx/local/canisters/freedom/

WebAssembly oluşumlarını da(wasm uzantılı dosya) görebiliriz.

Diğer yandan örnekleri çoğaltmaya başlayıp Internet Computer Network ortamına yeni Canister'lar eklendikçe şöyle bir arabirimle de karşılaştım.

İkinci Örnek

Derken Motoko'yu tanımak için bir örnek daha yapayım dedim. Bu sefer algebra isimli bir proje oluşturdum. main.mo yanına einstein isimli yeni bir actor ekledim ve dfx.json'dan main.mo yerine bunu kullanacağımı belirttim. einstein.mo içerisinde tek bir fonksiyon bulunuyor. İki integer değer aralığındaki sayıların toplamını buluyor.

actor einstein {
    public func gauss_sum(x:Int,y:Int) : async Int {
        var total:Int=0;
        var counter=x;
        while(counter<=y)
        {
            total+=counter;
            counter+=1;
        };
        return total;
  };
};

dfx.json'daki kısım;

{
  "canisters": {
    "algebra": {
      "main": "src/algebra/einstein.mo",
      "type": "motoko"
    },
// Kod devam ediyor

Sonrasında uygulamayı aşağıdaki adımlardan geçirerek test ettim.

# Birinc terminalde (hepsi algebra klasörü altında yapılmalı)
dfx start

# Ağ başlatıldıktan sonra ikinci terminalde sırasıyla aşağıdaki işlemleri yaptım
dfx canister create --all
dfx build algebra
dfx canister install algebra

# ve komut satırından denememi yaptım
dfx canister call algebra gauss_sum '(1,100)'

İşte 1 ile 100 arasındaki sayıların toplamının bağımsız internet bilgisayarındaki ağda bulunması :)

CanisterId'yi kullanarak aynı uygulamayı otomatik olarak üretilen web sayfasıyla da test edebiliriz. Benim örneğimde bu adres http://127.0.0.1:8000/candid?canisterId=75hes-oqbaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-q şeklindeydi.

Motoko ve The Internet Computer'un geleceği ne olur bilinmez ama Umut Özel ile bir sohbetimiz sırasında ortaya çıkan bu kavramı şöyle bir kurcalama fırsatı bulduğum için kendi adıma memnunum. Önümüzdeki yıl bu alandaki gelişmeleri takip etmek istiyorum. Bu ilginç araştırmaya ait kodları skynet github reposunda bulabilirsiniz. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Viewing all 351 articles
Browse latest View live