Quantcast
Channel: Burak Selim ŞenyurtBurak Selim Şenyurt0.0000000.000000
Viewing all 351 articles
Browse latest View live

Ocelot - .Net Core Tarafında Bir API Gateway Denemesi

$
0
0

Uzun süre önce bankada çalışırken nereye baksam servis görüyordum. Bir süre sonra ana bankacılık uygulaması dahil pek çok ürünün kullandığı bu sayısız servisler ağının yönetimi zorlaşmaya başladı. Bir takım ortak işlerin daha kolay ve etkili yönetilmesi gerekiyordu. Müşterek bir kullanıcı doğrulama ve yetkilendirme kontrolü(authentication & authorization), yük dengesi dağıtımı (load balancing), birkaç servis talebinin birleştirilmesi ve hatta birkaç servis verisinin birleştirilerek döndürülmesi(aggregation), servis verisinin örneğin XML'den JSON gibi farklı formata evrilmesi, servis geliş gidişlerinin loglanması, yönlendirmeler yapılması(routing), performans için önbellek kullanılması(caching), servis hareketliliklerini izlenmesi(tracing), servislerin kolayca keşfedilmesi(discovery), çağrı sayılarına sınırlandırma getirilmesi, bir takım güvenlik politikalarının entegre edilmesi, özelleştirilmiş delegeler yazılması(custom handler/middleware), tüm uygulamalar için ortak bir servis geçiş kanalının konuşlandırılması ve benzerleri. Yazarken yoruldum, daha ne olsun :D Sonunda Java tabanlı WSO2 isimli bir API Gateway kullanılmasına karar verildi.

Geçtiğimiz günlerde de yine konuşma sırasında Ocelot isimli C# ile yazılmış açık kaynak bir ürünün adı geçti ve tabii ki bende bir merak uyandı. Kanımca hafif sıklet mikroservis ortamlarında veya servis odaklı mimari çözümlerinde düşünülebilir. Ama önce denemek ve nasıl işlediğini görmek gerekiyor, öyle değil mi? ;) Bu arada Ocelot'un oldukça doyurucu bir dokümantasyonu olduğunu da belirteyim. Haydi gelin SkyNet derlememize başlayalım.

Senaryo

Örnekte şöyle bir senaryoyu icra etmeye çalışacağız; Oyuncu detaylarını getiren, ona öneri oyunları ürün olarak sunan, kazandığı bir promosyonu sisteme kaydetmesini sağlayan üç kobay servis tasarlayacağız. İstemci uygulama(Postman bile yeterli olur) bu birkaç servis çağrısı için API Gateway'e gelecek. Yani istemciler bu servisler için aslında tek bir noktaya gelip API Gateway üzerinden konuşacaklar. İlk etapta ocelot paketini kullanan gateway uygulaması basit bir router olacak. Hatta iki servis çıktısını birleştirip döndüren bir aggregation fonksiyonelliği de katacağız. Sonrasında Load Balancing işlevselliğini entegre edeceğiz.

Hazırlıklar ve Kodlama

Çok doğal olarak birkaç kobay servise ihtiyacımız var. Tamamını .net core web api olarak tasarlamak doğrusu işime geldi :) Ancak gerçek hayat senaryolarında farklı programlama dilleri ve çatıları ile geliştirilmiş servisler kullanmak daha mantıklı olacaktır.

mkdir services
cd services
# İlk olarak kobay servislerimizi ekleyelim
# Fonksiyon başına bir servis gibi oldu ama
# amacımız bilindiği üzere Ocelot'un kurgusunu anlamak

# Oyuncu bilgilerini getireceğimiz bir servis
dotnet new webapi -o GamerService

# Oyuncuya önerilecek promosyonların çekileceği bir servis
dotnet new webapi -o PromotionService

# Oyuncunun daha önce satın almış olduğu ürünleri getirecek bir servis
dotnet new webapi -o ProductService

# ve Ocelot Servis Uygulamasının oluşturulup gerekli Nuget paketinin eklenmesi
cd ..
dotnet new web -o Bosphorus
dotnet add package ocelot
# Bu uygulamada kritik olan nokta ocelot konfigurasyonunun durduğu json dosya içerikleri
cd Bosphorus
touch ocelot.json

Servislerimiz kobay niteliği taşıdıklarından birşeyler döndürseler yeterli. Yine de sayfanın dışına çıkmadan devam edebilmeniz için aşağıya gerekli kod parçalarını bırakıyorum(İsteyenler SkyNet github reposuna uğrayıp indirebilirler de)

GamerService içindeki PlayerController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace GamerService.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class PlayerController : ControllerBase
    {
        private readonly ILogger<PlayerController> _logger;

        public PlayerController(ILogger<PlayerController> logger)
        {
            _logger = logger;
        }

        /*
            HTTP Get taleplerine karşılık verecek metodumuzdan geriye sembolik olarak bir Player nesnesi döndürüyoruz.
            Player/19 gibi gelen taleplere cevap verecek
        */
        [HttpGet("{id}")]
        public Player Get(string id)
        {
            return new Player
            {
                Id = id,
                Fullname = "Megen Enever",
                Level = 58,
                Location = "Dublin"
            };
        }
    }

    public class Player
    {
        public string Id { get; set; }
        public string Fullname { get; set; }
        public int Level { get; set; }
        public string Location { get; set; }
    }
}

ve aynı servisi farklı port ile ayağa kaldıracağımızdan Program sınıfındaki UseUrls kullanımı.

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    // Farklı bir porttan yayın yapsın
                    webBuilder.UseStartup<Startup>().UseUrls("http://localhost:6501");
                });

ProductService içindeki ProductController sınıfı(7501 Numaralı porttan kaldıracak şekilde Program sınıfını değiştirmeyi unutmayın)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace ProductService.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ProductController : ControllerBase
    {
        private readonly ILogger<ProductController> _logger;

        public ProductController(ILogger<ProductController> logger)
        {
            _logger = logger;
        }

        /*
            Oyuncu için önerilecek oyunları döndüren bir operasyonmuş gibi hayal edelim.
            api/product/suggestions/1234 gibi HTTP Get taleplerine cevap verecek.
        */
        [HttpGet("suggestions/{id}")]
        public IEnumerable<Product> Get(string id)
        {
            var products = new List<Product>{
                new Product{Id=1,Title="Commandos III",Price=34.50},
                new Product{Id=2,Title="Table Child",Price=23.67},
                new Product{Id=3,Title="League of Heros 2022",Price=145.99},
            };

            return products;
        }
    }

    public class Product
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public double Price { get; set; }
    }
}

PromotionService içerisindeki ApplierController sınıfı(Bunu da 8501 nolu porttan ayağa kaldırmayı ihmal etmeyin)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace PromotionService.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class ApplierController : ControllerBase
    {
        private readonly ILogger<ApplierController> _logger;

        public ApplierController(ILogger<ApplierController> logger)
        {
            _logger = logger;
        }

        /*
            Birde HTTP Post deneyelim bari.
            Sembolik olarak promosyon uygulayan bir metot olduğunu varsayalım.
        */
        [HttpPost]
        public IActionResult SetPromotoion(Code promoCode)
        {
            return Ok($"{promoCode.No} için {promoCode.Duration} gün süreli promosyon kullanıcı hesabına tanımlanmıştır");
        }
    }

    public class Code
    {
        public string No { get; set; }
        public int Duration { get; set; }
        public int PlayerId { get; set; }
        public int GameId { get; set; }
    }
}

İlk kobay servislerimiz hazır. Şimdi yapmamız gereken Ocelot paketini kullanan uygulamamızı geliştirmek. Basit bir Console olarak geliştirebiliriz. Program sınıfının kodunu aşağıdaki gibi yazarak devam edelim.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
// Ocelot için gerekli bildirimler
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using System.Net.Http;
using System.Threading;

namespace Bosphorus
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args).ConfigureServices(services =>
            {
                services
                    .AddOcelot() // Ocelot'u bildirdik
                    .AddDelegatingHandler<RequestInspector>(); // HttpClient isteklerinde araya girecek delegeyi bildirdik
            }).ConfigureAppConfiguration((host, config) =>
            {
                config.AddJsonFile("ocelot.json"); // Ocelot ayarlarının alınacağı konfigurasyon dosyasını belirttik
            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>().Configure(async app => await app.UseOcelot());
            });
    }

    /*
        Aşağıdaki sınıfın yardımıyla Ocelot'taki belirttiğimiz bir Route'a gelen HTTP istekleri işlenmeden önce araya girebiliriz.
        Request içeriğine bakıp akışı değiştirebiliriz.
        Bu temsilci sınıfını kullanacağımızı yukarıdaki AddDelegatingHandler metodunda belirttik.
        Ayrıca ocelot.json içerisinde, örnek olması açısından /eagames/player/{id} adresine gelen taleplerde araya gireceğimizi belirttik.
    */
    public class RequestInspector : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            Console.WriteLine($"\nDevam etmeden önce şu gelen Request içeriğini bir inceyelim\n{request.ToString()}\n");
            return await base.SendAsync(request, cancellationToken);
        }
    }
}

İlk Deneme(Aggregation ve Standart Routing)

Öncelikle kobay servislerin ayağa kaldırılması lazım. GamerService, ProductService ve PromotionService isimli servisleri kendi klasörlerinde dotnet run ile çalıştırabiliriz. Kobay servisler aşağıdaki adreslerden devreye girecektir.

GamerService -> http://localhost:6501 
ProductService -> http://localhost:7501 
PromotoionService -> http://localhost:8501

Ocelot için çalışma zamanı ayarları bildiğiniz üzere json türünden konfigurasyon dosyasında tutulmaktadır. İlk versiyonunu aşağıdaki gibi yazıp ilerleyelim.

{
  "Routes": [
    {
      "UpstreamPathTemplate": "/eagames/player/{id}",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "DownstreamPathTemplate": "/player/{id}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 6501
        }
      ],
      "Key": "Player"
    },
    {
      "UpstreamPathTemplate": "/eagames/product/{id}",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "DownstreamPathTemplate": "/api/product/suggestions/{id}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 7501
        }
      ],
      "Key": "Product"
    },
    {
      "UpstreamPathTemplate": "/eagames/applypromo",
      "UpstreamHttpMethod": [
        "Post"
      ],
      "DownstreamPathTemplate": "/applier",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 8501
        }
      ]
    }
  ],
  "Aggregates": [
    {
      "RouteKeys": [
        "Player",
        "Product"
      ],
      "UpstreamPathTemplate": "/{id}"
    }
  ]
}

Artık Bosphorus uygulamasını çalıştırıp localhost:5000/19 şeklinde bir talep gönderebiliriz. İlk örnek Aggregation durumunu taklit etmekte ve promosyon ekleme için yönlendirme yapmaktadır. Ayrıca GamerService ve ProductService'e ortak çağrı yapıp arka planda çağırılan servis çıktılarını tek bir JSON paketinde birleştirip geriye döndürür ;)

İlk örnekteki UpstreamPathTemplate tanımlarına göre http://localhost:5000/eagames/player/23 adresine yapılan çağrı esasında http://localhost:6501/player/23 adresine yönlendirilir.

Benzer şekilde http://localhost:5000/eagames/product/23 şeklinde yapılacak çağrıda http://localhost:7501/api/product/suggestions/23 adresine yönlendirilir.

PromotionService içerisinde bir de POST metodumuz var. Ocelot.JSON için yaptığımız tanıma göre http://localhost:5000/eagames/applypromo adresine gelen talebi, http://localhost:8501/applier adresine yönlendiriyor olmalı. İşte örnek POST içeriği ve sonuç...

{
	"No":"PROMO-12345",
	"Duration":30,
	"GameId":102935,
	"PlayerId":1
}

İkinci Deneme(Load Balancer)

Bu kez Dockerize edilmiş bir Web API hizmetinden üç tanesini farklı portlarda ayağa kaldırıp Ocelot'un gelen talepleri bu adreslere dağıtmasını sağlamayı deneyelim. Temel amacımız ocelot konfigurasyonunda bunun nasıl ele alınacağını öğrenmek.

# Yine Services klasöründe RewardService isimli bir .Net Core Web API var
dotnet new webapi -o RewardService

cd RewardSercice

# Dockerize edeceğimiz
touch Dockerfile

# bin ve obj klasörlerini dışarıda bırakmak için
touch .dockerignore

# Dockerize için
docker build -t rewards .

# Dockerize ettiğimiz servisi çalıştırırken de aşağıdaki komutu kullanabiliriz
# Aynı servisin 3 farklı porttan çalışacak birer örneğini ayağa kaldırıyoruz
docker run -d -p 5555:80 -p 5556:80 -p 5557:80 rewards

Servisin RewardController sınıfını ve Dockerfile içeriklerini aşağıdaki gibi yazabiliriz.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace RewardService.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class CalculatorController : ControllerBase
    {
        private static readonly string[] topics = new[]
        {
            "1000 Free Spell"
            , "10 Free Coin"
            , "30 Days Free Trail"
            , "Gold Ticket"
            ,"Legendary Tournemant Pass"
            ,"1000 Free Retro Game"
            ,"One Day All Games Free"
        };

        private readonly ILogger<CalculatorController> _logger;

        public CalculatorController(ILogger<CalculatorController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<Reward> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 3).Select(index => new Reward
            {
                Duration = rng.Next(7, 60),
                Description = topics[rng.Next(topics.Length)]
            })
            .ToArray();
        }
    }

    public class Reward{
        public int Duration { get; set; }
        public string Description { get; set; }
    }
}

ve Dockerfile;

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "RewardService.dll"]

Bu sefer http://localhost:5555/Calculator , http://localhost:5556/Calculator ve http://localhost:5557/Calculator adreslerinden talep alan bir Web API servisimiz var. Load Balancer ayarlarını ocelot.json'a aşağıdaki gibi ekleyelim ve denemelerimize geçelim.

{
  "DownstreamPathTemplate": "/calculator",
  "DownstreamScheme": "http",
  "DownstreamHostAndPorts": [
    {
      "Host": "localhost",
      "Port": 5555
    },
    {
      "Host": "localhost",
      "Port": 5556
    },
    {
      "Host": "localhost",
      "Port": 5557
    }
  ],
  "UpstreamPathTemplate": "/eagames/rewards",
  "LoadBalancerOptions": {
    "Type": "LeastConnection"
  },
  "UpstreamHttpMethod": [
    "Get"
  ]
}

Artık http://localhost:5000/eagames/rewards adresine geldiğimizde

Talepler LeastConnection seçimi nedeniyle her seferinde bir sonraki backend servisine yönlendirilecektir.

Diğer yandan hatırlayacağınız gibi gelen talepler sırasında araya girebileceğimizden bahsetmiştik. Bu sayede Ocelot'a gelen bir Http isteğine cevap dönmeden önce bir takım iş kurallarını işletmek mümkün olabilir.

Gelelim bu SkyNet derlemesinin bomba sorularına :)

  • Gateway arkasında XML içerik döndüren bir servis metodu olduğunu düşünelim. Gateway'e bu servis için gelen çağrı karşılığında XML yerine JSON döndürmemiz mümkün olur mu? Bunu Ocelot üzerinde nasıl tanımlarız?
  • Dockerize ettiğimiz servisi üç farklı porttan ayağa kaldırdığımız bir container başlattık. Ocelot'un Load Balancer ayarları gereği eagames/rewards'a gelen talepler arkadaki portlara seçilen stratejiye göre dağıtılıyor. Üç portta esas itibariyle aynı container'a(80 portuna) iniyor. Sizce gerçek anlamda bir Load Balancing oldu mu? Arkadaşlarınızla tartışınız.
  • Load Balancer senaryolarında Sticky Session dikkat edilmesi gereken bir konudur. Ocelot'ta Sticky Session desteği var mıdır araştırınız?

Soruları düşünerkene örneği geliştirmeye de devam edebilirsiniz. Mesela en az iki servisi daha farklı programlama dilleri ile senaryoya dahil edebilir(NodeJs, Java, Rust, GO) ya da RewardService'in geriye döndürdüğü bedava ödüller listesindeki tekrar eden bilgileri tekleştirmek için gerekli kod düzenlemesini yapabilirsiniz. Bunlara ek olarak ürünü şirketinizde bir POC(Proof of Concept)çalışması olarak değerlendirip yük testi altında nasıl davranış sergileyeceğini araştırabilirsiniz.

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


Vue için Bebek Adımları

$
0
0

Yazılım işine girdiğimden beri en çok zorlandığım konu Frontend tarafında kodlama yapmak. Ne yazık ki sadece Backend tarafta kalma lüksümüz de pek bulunmuyor. Örneğin hali hazırda çalışmakta olduğum firmada yeni nesil birçok uygulama önyüz tarafında çeşitli Javascript çatıları(Framework) kullanıyor.

Pratikte bakınca oldukça iyi bir kurgu aslında. Önyüzü Vue, React, Angular vb yapılarla geliştirip, asıl iş kuralları için arka planda yer alan .Net Core Web API servislerine gelmek. C# ve .Net Core tarafına aşina olduğum için arka planı rahatça kodluyorum, önyüz tarafında ise önceden geliştirilmiş sayfalara bakarak bir şeyler yapabiliyorum. Yani işin özü Vue çatısının temellerinde sorunlarım var. Bu amaçla SkyNet'e uğradığım bir gün oturdum ekran başına en basit adımlarıyla bu işi nasıl öğrenirim bir kurcalayayım dedim.

Tabii öncesinde Vue.Js ile ilgili bir bilgi vermek de lazım. Açık kaynak olarak sunulan ilk Commit'i 2013 ve ilk Release'i de 2014 olan Vue, temel olarak Singe Page Application geliştirmek için Model View ViewModel(MVVM) desenini baz alan bir Javascript Framework olarak düşünülebilir. İşi gücü arayüz geliştirilmesini MVVM'in nimetlerinden yararlanarak kolaylaştırmak. Reactive olması, HTML'i direktifler ile genişletmesi ve DOM elementleri ile veriyi, olayları kolayca bağlaması öne çıkan özellikleri arasında sayılabilir.

Bu arada bugüne kadar çıkan sürümlere Manga dizilerinin adları verilmiş. Son sürüm One Piece, 2016 - Ghost in the Shell, 2015 - Dragon Ball, 2014 - Blade Runner, Cowboy Bebop ve Animatrix gibi isimlendirmeler kullanılmış(Peki Animatrix ile başlayan bu Manga adlarının alfabetik sırada gittiğini biliyor muydunuz?)İşin magazin kısmı bir yana kalsın gelin biz basit adımlarla Vue'nun temel kabiliyetlerini tanımaya çalışalım. Bunun için Javascript kütüphaneleri veya CLI araçlarını indirmemize de gerek yok. Temel konular için Vue'nun CDN(Content Delivery Network) kaynağından(https://unpkg.com/vue) yararlanmamız yeterli.

İlk adımımızda onun ne kadar duyarlı olduğunu(Reaktif) anlamaya çalışalım(Yorum satırlarını okumayı ihmal etmeyin)

touch vue_is_reactive.html

Kodlarımızı aşağıdaki gibi geliştirelim.

<html><head><title>VueJs Bebek Adımları - 01</title><!-- CDN adresinden Vue.js'i kullanacağımızı söyledik--><script src="https://unpkg.com/vue"></script></head><body><!--
        Vue'da DOM nesneleri ile veri(data) birbirine bağlıdır ve sürekli etkileşim halindedir.
    --><div id="appComponent" style="text-align:center;"><h1>Şu An Çalıştığım Kitap</h1><p>{{bookName}}</p><p>{{startDate}}</p><!-- Javascript expression içerisinde aşağıdaki gibi fonksiyon çağrıları da yapılabilir--><p>{{bookName.split('').reverse().join('')}}</p></div><script type="text/javascript">
        /*
            app bir Vue uygulama nesnesidir. 
            Parametre olarak çeşitli seçenekleri ihtiva eden bir JSON değişkeni alır.
            el(element) niteliği uygulama nesnesini appComponent ismiyle div elementine bağlar .
            Vue yapıcı fonksiyonundaki JSON nesnesinin data özelliği içerisinde koyduğumuz alanlar, DOM içerisinde {{ }} ifadelerinin olduğu yerlerde kullanılır.
            Mustache stilindeki {{ }} yerler javascript expression olarak adlandırılır. Vue bu ifadeleri gördüğünde, data özelliğindeki karşılıkları ile değiştirir.
            Vue'nun nesne verileri(instance data) HTML'de referans edildikleri heryere bağlanır. 
            Bunu daha iyi anlamak için HTML sayfasını açtıktan sonra F12 ile Developer bölgesine geçin ve Console'da 
                app.bookName="Learning Vue"
            yazın. HTML içeriğinde bookName olan heryerin anında değiştiğini göreceksiniz. İşte bu Reactive olma özelliğidir.
        */
        var app = new Vue(
            {
                el: '#appComponent',
                data: {
                    bookName: "Rust Programming Cookbook",
                    startDate: "Today"
                }
            }
        )

    </script></body></html>

Oluşturduğumuz HTML sayfasını bir tarayıcıda açtıktan sonra özellikle F12 ile Debug moduna geçip Vue uygulama nesnesi olan app değişkeninin data özelliğindeki bookName içeriğini Console üstünden değiştirmeyi deneyin. Bu değişiklik sayfada bookName'i kullanan tüm elementlere yansıyacaktır. Buradan Vue ana bileşeninin(Component) DOM ile etkileşim halinde olduğunu söyleyebiliriz. İşte bu reaktif olmanın bir sonucudur.

ve F12 - Console sonrası. 

İkinci adımımızda Attribute Binding konusunu ele alacağız. HTML elementlerindeki nitelikleri(Örneğin img elementinin src niteliğini) direktifler(Örnekte v-bind) ile Vue verisine(data özelliğinin değerleri) nasıl bağlayacağımızı göreceğiz.

touch vue_attribute_binding.html

HTML sayfa kodlarını aşağıdaki gibi yazarak devam edelim.

<html><head><title>VueJs Bebek Adımları - 02</title><!-- CDN adresinden Vue.js'i kullanacağımızı söyledik--><script src="https://unpkg.com/vue"></script></head><body><!--
        HTML elementlerinin niteliklerini(attribute)'de veriye bağlamak isteyebiliriz.
        Örneğin aşağıdaki HTML yapısından yer alan src ve altText niteliklerini, data nesnesinin sırasıyla coverPhoto ve alternativeText alanlarına bağlamak istediğimizi düşünelim.
        Bu durumda v-bind isimli yönergeyi(directive) kullanmamız gerekir.
        01 nolu örnekte olduğu gibi bu HTML'i açtıktan sonra tarayıcının Console penceresinde
            app.coverPhoto="./images/book_2.jpeg"
        yazın. Fotoğrafın hemen değiştiğini göreceksiniz. Yani v-bind direktifi ile bağlanan yerlerde veri değişikliğini anında yansıtır.
    --><div id="app" style="text-align:center;"><div id="book"><h1>{{title}}</h1><p>{{description}}</p></div><div id="book-photo"><!-- 
                v-bind direktifini : operatörü ile daha kısa şekilde de kullanabiliriz.
                Yani v-bind:src yerine :src yazılabilir.
            --><img :src="coverPhoto" v-bind:altText="alternativeText" /></div></div><script type="text/javascript">

        var app = new Vue(
            {
                el: '#app',
                data: {
                    title: "Rust Programming Cookbook",
                    description: "Perfect book about programming with rust",
                    coverPhoto: "./images/book_1.jpeg",
                    alternativeText: "Rust Programming Cookbook Cover Photo"
                }
            }
        )

    </script></body></html>

Sayfadaki img elementinin kullandığı resmi kaynağı ve açıklama kısmı Vue bileşeninin data özelliğinden beslenir. Yine F12 Debug moddayken bu içeriklerin değişmesi anında elementlere de yansıyacaktır. Aşağıdaki ekran görüntülerinde olduğu gibi ;)

ve F12 Debug mod durumu.

Buraya kadar az çok bir Vue bileşeninin HTML DOM nesneleri ile nasıl konuştuğunu anladık diyebiliriz. Üçüncü adımımızda akış kontrol ifadelerinden if...else kullanımına bakalım.

touch vue_conditional_render.html

Kodlarımızı da aşağıdaki gibi geliştirelim.

<html><head><title>VueJs Bebek Adımları - 03</title><!-- CDN adresinden Vue.js'i kullanacağımızı söyledik--><script src="https://unpkg.com/vue"></script></head><body><!--
        02nci örneğin aynısı ancak bu kez kitabın stoktaki miktarına göre HTML elementlerinin Render edilip edilmeyeceklerini belirliyoruz.
        v-if v-else, v-else-if, v-show gibi direktiflerle HTML elementlerinin Render operasyonları koşula bağlanabilir.
        stock-state isimli div içerisinde p elementleri, quantity değerine göre görüntülenmektedir.
        onDiscount, true veya false değer almaktadır. Bu gibi sıklıkla kapalı veya açık konuma geçecek bir element söz konusu olduğunda v-show direktifinin kullanılması önerilir.
        Pek tabii Vue tarafında da switch yapısı mevcuttur.
        Yine tarayıcı Console'unda onDiscount ve quantity değerleri ile oynayarak sayfanın nasıl değişikliklere uğradığını inceleyebilirsiniz.
    --><div id="app" style="text-align:center;"><div id="book"><h1>{{title}}</h1><p>{{description}}</p><div id="stock-state" style="font-weight:bold;"><p v-if="quantity>100">Depoda yeterli miktarda var. Tam {{quantity}} adet. Sakin!</p><p v-else-if="quantity>50 && quantity<100">İdare ederizzzz... {{quantity}}</p><p v-else-if="quantity>0 && quantity<50">Imm..Şey. Sipariş etsek mi? Sadece {{quantity}} adet kalmış</p><p v-else>Ovv yooo!!! Bu da ne? {{quantity}}</p></div><p v-show="onDiscount">İndirimde</p></div><div id="book-photo"><img :src="coverPhoto" v-bind:altText="alternativeText" /></div></div><script type="text/javascript">

        var app = new Vue(
            {
                el: '#app',
                data: {
                    title: "Rust Programming Cookbook",
                    description: "Perfect book about programming with rust",
                    coverPhoto: "./images/book_1.jpeg",
                    alternativeText: "Rust Programming Cookbook Cover Photo",
                    quantity: 120,
                    onDiscount: true,
                    level:"Small"
                }
            }
        )

    </script></body></html>

Sayfada ürünün miktarına göre stock-state altındaki paragraflardan hangisinin gösterileceğine karar veriliyor. Yani verinin durumuna göre bir elementin görünümü, içeriği vs değiştirilebiliyor. İşte örneğe ait çalışma zamanı çıktıları.

ve F12 Console'dan quantity ile onDiscount değerlerini değiştirdikten sonraki durum.

Çok doğal olarak bu tip bir Vue sayfasında bileşenin kullandığı veri önemlidir. Data özelliğinin içeriği bir servisten çekilmiş bir liste olabilir. Bu durumda veriyi sayfada gösterirken basit for döngülerine ihtiyaç duyarız. Dördüncü adımda bu döngüyü bir JSON dizisi için nasıl kullanacağımızı ele alıyoruz.

touch vue_for_loop.html

Kodlarımızı aşağıdaki gibi geliştirelim.

<html><head><title>VueJs Bebek Adımları - 04</title><!-- CDN adresinden Vue.js'i kullanacağımızı söyledik--><script src="https://unpkg.com/vue"></script></head><body><!--
       app nesnesinin data özelliği ile gelen JSON içeriğinde bir liste olduğunu düşünelim. Örnekte kitap kategorisindeki birkaç ürün bilgisine yer veriliyor.
       Bu listeyi sıralı bir şekilde HTML' e yazdırmak için v-for direktifinden yararlanılabilir.
       Çalışma zamanında yine Chrome Console'a girilmiş ve anlık olarak books array'indeki değerlerle oynanmıştır.
    --><div id="app" style="text-align:center;"><div id="book"><h1>'{{category}}' Kategorisindeki Ürünler</h1><!--
                data özelliğindeki books dizisinin herbir elemanı book olarak isimlendirilmiştir.
                {{ }} notasyonu ile book üstünden id, title, publisher ve level özelliklerine erişililir.
                Döngülerde Render edilen elementlerin tekil bir anahtar ile işaretlenmesi önerilir. 
                Bunun için :key direktifi kullanlır.
                Örnekte <p> elementlerinin id isimli özellik değeri ile tekil(unique) olması sağlanır.
            --><div v-for="book in books" :key="book.id"><p>{{book.title}} <i>({{book.publisher}}) - ${{book.listPrice}}</i></p></div></div></div><script type="text/javascript">

        /*
            data özelliği bir JSON nesnesi alabildiğinden içerisinde n elemanlı array'ler de barındırabilir.
        */
        var app = new Vue(
            {
                el: '#app',
                data: {
                    category: "Kitap",
                    books: [
                        { id: 1, title: "Programming C# for Beginners", publisher: "Wrox", listPrice: 19.95, level: 100 },
                        { id: 2, title: "Patterns of Enterprise Application Architecture", publisher: "Addison Wesley", listPrice: 34.50, level: 300 },
                        { id: 3, title: "Game Engine Architecture", publisher: "Gregory", listPrice: 45.50, level: 300 },
                    ]
                }
            }
        )

    </script></body></html>

Bu adımdan sonraki çalışma zamanı çıktıları ise aşağıdaki gibi olacaktır.

ve F12 ile Console'a geçip dizinin elemanlarında değişiklik yaptıktan sonrası.

Bir Web sayfası mutlaka kullanıcı ile etkileşim halindedir. Dolayısıyla sayfa üstünde gerçekleştireceği bazı olayların Vue bileşeni tarafında da ele alınması gerekir. Beşinci adımda bunu anlamaya çalışacağız.

touch vue_event_handling.html

HTML sayfasının kodları da şöyle.

<html><head><title>VueJs Bebek Adımları - 05</title><!-- CDN adresinden Vue.js'i kullanacağımızı söyledik--><script src="https://unpkg.com/vue"></script></head><body><!--
        Sayfadaki olaylar v-on direktifi kontrollere bağlanabilir.
        Örnekte kitap fiyatını artırmak ve azaltmak için iki button ve click olayları kullanılmaktadır.
    --><div id="app" style="text-align:center;"><h1>'{{category}}' Kategorisindeki Ürünler</h1><div v-for="book in books" :key="book.id"><p><!-- 
                    CSS stillerini de veriye bağlayabiliriz.
                    span elementinin arkaplan rengini belirleyen backgrounColor değeri o anki book nesnesinin color özelliğine bağlanmıştır.
                --><span :style="{backgroundColor: book.color}">     </span>
                {{book.title}}<i>({{book.publisher}}) - ${{book.listPrice}}</i><!--
                    Fiyat artırma işini üstlenen click olayı gerçekleştiğinde ifade içerisindeki kod çalışır. Bulunulan book nesnesinin listPrice değeri 1 artar.
                --><button v-on:click="book.listPrice+=1">+</button><!--
                    Ancak olayları aşağıdaki gibi fonksiyonlara devrederek kullanmak daha doğrudur.
                    Bu kez button click olayı gerçekleştiğinde book.id değerini alan decreasePrice metodu çağrılır.
                    Bu metod Vue nesnesinin opsiyonel parametrelerinden olan methods içerisinde tanımlanır.
                    v-on direktifi aşağıdaki gibi @ ifadesi ile daha kısa şekilde yazılabilir.
                    Örnekte disabled niteliği de indirim yapılıp yapılmayacağını belirten incAvailable düğmesine bağlanmıştır.
                    Mesela 2 numaralı ürün için indirim uygulanamaz.
                --><button @click="decreasePrice(book.id)" :disabled="!book.incAvailable">-</button></p></div></div><script type="text/javascript">

        var app = new Vue(
            {
                el: '#app',
                data: {
                    category: "Kitap",
                    books: [
                        { id: 0, title: "Programming C# for Beginners", publisher: "Wrox", listPrice: 19.95, level: 100, incAvailable: true, color: "blue" },
                        { id: 1, title: "Patterns of Enterprise Application Architecture", publisher: "Addison Wesley", listPrice: 34.50, level: 300, incAvailable: true, color: "red" },
                        { id: 2, title: "Game Engine Architecture", publisher: "Gregory", listPrice: 45.50, level: 300, incAvailable: false, color: "red" },
                    ]
                },
                methods: {
                    /*
                        button click olayı gerçekleştiğinde çalıştırılan metot.
                    */
                    decreasePrice(id) {
                        console.log(id, ". eleman için 1 dolar indirim");
                        this.books[id].listPrice -= 1;
                    }
                }
            }
        )</script></body></html>

Kullanıcı kitap fiyatlarını artırıp azaltabilir. Her iki aksiyon için olay bildirimlerinin nasıl yapıldığına dikkat edin. Olayın gerçekleşmesi sonucu çalışacak kod bir direktif ile birlikte yazılabileceği gibi Vue bileşeninin methods özelliği içerisinde de konuşlandırılabilir.

Yine F12 - Console penceresinde CSS rengini değiştirecek şekilde veriyle oynayabiliriz.

Altıncı adımda verinin HTML elementlerinin içeriğine göre bir hesaplamaya dahil edilmesine bakacağız. Burada Vue bileşeninin computed özelliğindeki fonksiyonlar devreye giriyor. Listelenen kitaplardan herhangi birinin üstüne gelindiğinde o kitabın fiyatı güncel döviz kuru değerine göre hesaplatılıp alt tarafta yazılıyor. Burada senaroyu biraz daha zengileştirebilirsiniz. Örneğin fare imleci kitap adının üstüne geldiğinde bir popup içinde resmini ve açıklamasını gösterebilirsiniz.

touch vue_computed_props.html

Kodlarımızı aşağıdaki gibi geliştirelim.

<html><head><title>VueJs Bebek Adımları - 06</title><!-- CDN adresinden Vue.js'i kullanacağımızı söyledik--><script src="https://unpkg.com/vue"></script></head><body><div id="app" style="text-align:center;"><h1>'{{category}}' Kategorisindeki Ürünler</h1><!--
            book_count, bir computed property'dir.
        --><p><i>{{book_count}} adedi satışta</i></p><!--
            div üzerinde mouse bir kitaba denk geldiğinde bu kitabın indis değeri markIndex isimli fonksiyona gönderilir.
            markIndex fonksiyonu gelen bu indis değerini data elementindeki selectedBookIndex'e set eder.
            Buna göre computed içerisinde hangi kitap için işlem yapacağımızı anlayabiliriz.
        --><div v-for="(book,index) in books" :key="book.id" @mouseover="markIndex(index)"><p>
                {{book.title}} <i>${{book.listPrice}}</i></p></div><h3>Güncel kurdan fiyatı {{local_price}} liradır.</h3></div><script type="text/javascript">

        /*
            Computed Properties.
            app nesnesinin computed özelliğinde local_price isimli bir fonksiyon bulunmaktadır.
            Bu fonksiyon selectedBookIndex değerine işaret eden ürünün liste fiyatını güncel döviz kuru ile çarpıp geriye döndürür.
            Döndürülen değer HTML içerisinde for döngüsünün dışındaki bir h3 elementine basılır.
        */
        var app = new Vue(
            {
                el: '#app',
                data: {
                    category: "Kitap",
                    books: [
                        { id: 0, title: "Programming C# for Beginners", publisher: "Wrox", listPrice: 19.95, onSale: true },
                        { id: 1, title: "Patterns of Enterprise Application Architecture", publisher: "Addison Wesley", listPrice: 34.50, onSale: true },
                        { id: 2, title: "Game Engine Architecture", publisher: "Gregory", listPrice: 45.50, onSale: false },
                    ],
                    selectedBookIndex: 0,
                },
                methods: {
                    markIndex(index) {
                        this.selectedBookIndex = index;
                        //console.log(index);
                    }
                },
                computed: {
                    /*
                        Computed Property'ler hesaplamaya dahil ettikleri veriler değişmediği sürece cache üzerinde tutulurlar.
                        local_price, mouseover hareketine göre üzerinde durulan kitabı alıp liste fiyatını bir işlemden geçirir ve geriye bir değer döner.
                        book_count ise satışta olan kitapların adedini bulur ve geriye döner. 
                    */
                    local_price() {
                        //console.log(this.selectedBookIndex);
                        return this.books[this.selectedBookIndex].listPrice * 8.15;
                    },
                    book_count() {
                        count = 0;
                        for (i = 0; i < this.books.length; i++) {
                            if (this.books[i].onSale)
                                count++;
                        }
                        return count;
                    }
                }
            }
        )</script></body></html>

Bu örneğin çalışma zamanı çıktısı ise aşağıdaki gibi olacaktır.

Buraya kadarki adımlarda hep ana Vue bileşeni ile çalıştık. Çok doğal olarak HTML DOM yapısının birden fazla Vue bileşeni ile çalışması da istenebilir. Nitekim bir süre sonra ana bileşen çok fazla kalabalıklaşır. Yedinci adımda bu durumu anlamaya çalışacağız. 

touch vue_components.html

Örnekte yer alan sportnews ve book iki ayrı Vue bileşeni olarak tasarlanmıştır. Template olarak birer div döndürdüklerine dikkat edelim. HTML'de konuşlandırılan aynı isimli elementler içerisine bu şablonlar basılır. book bileşenindeki iLikeIt olayının kullanımı da önemli. book bir alt bileşen olarak üzerinde gerçekleşen olay sonrası app isimli ana bileşeni de uyarmaktadır. Yani alt bileşene ait bir olay tetiklendiğinde üst bileşende de bir olay tetiklenmesini sağlayabiliriz. Dolayısıyla bileşenler birbirleriyle olaylar üzerinden de iletişim kurabilirler.

<html><head><title>VueJs Bebek Adımları - 07</title><!-- CDN adresinden Vue.js'i kullanacağımızı söyledik--><script src="https://unpkg.com/vue"></script></head><body><div id="app" style="text-align:left;color:white"><sportnews></sportnews><!--
            Bileşenleri aşağıdaki gibi de kullanabiliriz. book isimli bileşenin verisi app bileşenindeki data içerisinde yer alan json dizisidir.
            book elementi içerisinde :property_name şeklindeki tanımlamalar ile döngünün dolaştığı book nesnesinin değerleri bileşen içerisine aktarılır.
            :property_name bilgileri book bileşeninin props özelliğinde tanımlanmıştır.
            
            Ek: Alt bileşenden üst bileşene bildirim yollamak.
            Amaç bir kitabın "Beğendim" butonuna basıldığında app bileşenindeki(üst component) toplam beğeni sayısını artırmak.
            Bunun için book bileşeninde düğmeye basıldığında, üst bileşene bunu bildirecek şekilde bir mesaj göndermek gerekir.
            Bu mesaj $emit fonksiyonu ile yollanabilir(book içerisindeki iLikeIt metoduna bakın)
            @i-like-it niteliğinde belirtilen updateLikeCount fonksiyonu ise alt bileşenlerden birisi i-like-it mesajını yukarı fırlattığında çağırılır.
            Bu arada hangi book bileşenine basıldığını anlamak için (b,index) çiftindeki index değerini updateLikeCount metoduna parametre olarak verebiliriz.
        --><div v-for="(b,index) in books" :key="b.id"><book :book_title="b.title" :book_summary="b.description" :book_authors="b.authors"
                :book_list_price="b.listPrice" @i-like-it="updateLikeCount(index)"></book></div><p style="color: purple;">Toplamda {{likeCount}} kere beğen düğmelerine bastınız!</p></div><script type="text/javascript">
        /*
            Bu örneğe kadar dikkat edileceği üzere app nesnesinin data, computed, methods gibi özelliklerinin kalabalıklaşmaya başladığını gördük.
            Yönetilebilir ve modüler yapıdaki bir Vue sayfası için bileşenler(component) kullanmak doğru bir yaklaşımdır.
            Yani ana sayfadaki component'in alt bileşenlerden oluştuğunu düşünebiliriz.Örnekte iki component tanımlanmış ve app isimli div içerisinde kullanılmıştır.
            
            sportnews isimli bileşen oldukça sıradandır. Kendi verisini kullanır.
            book isimli bileşen ise app bileşenindeki data içeriğini kullanır.
            
            Bileşenler component fonksiyonu ile tanımlanır.
            Her bileşen bir template kullanmalıdır. 
            template özelliği bir container döndürmelidir(div gibi)
        */
        Vue.component('sportnews', {
            template:
                `<div class='sportnews' style='text-align:left;backgroundColor:purple;'><h2>Günün Öne Çıkan Spor Haberi</h2><p><h3>{{title}}</h3></p><p>{{summary}}</p></div>
            `,
            data() {
                return {
                    title: "Shane Larkin Milli Takıma Çağırıldı",
                    summary: "Bir süredir ülkesi ABD'de olan Shane Larkin, Anadolu Efes kampına döndükten sonra doğrudan milli takıma çağırıldı."
                }
            }
        });

        Vue.component('book', {
            template:
                `
                <div class='book' style='text-align:right;backgroundColor:gold;color:purple'><p><h3>{{book_title}}</h3></p><p>{{book_summary}}, {{book_authors}}<br/>{{book_list_price}} TL</p><button v-on:click="iLikeIt">Beğendim</button></div>
            `,
            methods: {
                iLikeIt() {
                    console.log('book bileşeninin iLikeIt olayı çağrıldı');
                    /*
                        $emit ile button click olayı tetiklendiğinde üst bileşene i-like-it olayı gerçekleşti şeklinde bir bilgi yollanır.
                    */
                    this.$emit('i-like-it');
                }
            },
            props: {
                book_title: {
                    type: String,
                    required: true
                },
                book_summary: {
                    type: String,
                    required: true
                },
                book_authors: {
                    type: String,
                    required: true
                },
                book_list_price: {
                    type: Number,
                    required: true
                },
            }
        });

        var app = new Vue(
            {
                el: '#app',
                data: {
                    likeCount: 0, // Alt bileşenlerdeki düğmeye basıldığında bu değeri artırıyoruz
                    books: [
                        {
                            id:1001,
                            title: "Veba",
                            description: "Camus adı çoğu okur için Yabancı romanıyla özdeşleşir. Ancak yazarın en önemli yapıtı aslında Veba'dır...",
                            authors: "Albert Camus",
                            listPrice: 34
                        },
                        {
                            id:1002,
                            title: "Mahur Beste",
                            description: "Mahur Beste'de Tanpınar'ın Huzur ve Sahnenin Dışındakiler adlı romanlarında önemli bir motif olan 'Mahur Beste' teması önemli yer tutar. Mahur Beste, acı bir aşk hikayesinin klasik musiki kalıplarıyla soyutlanmasıdır...",
                            authors: "Ahmet Hamdi Tanpınar",
                            listPrice: 23
                        },
                        {
                            id:1003,
                            title: "1Q84",
                            description: "Sarsıcı bir yolculuğa hazır mısınız? Öyleyse kemerlerinizi bağlayın. Erkekleri, titizlikle geliştirdiği bir yöntemle öteki dünyaya gönderen genç bir kadınla tanışacaksınız. Ve amansız bir takiple onun peşine düşen fanatik bir cemaatin müritleriyle…",
                            authors: "Haruki Murakami",
                            listPrice: 23
                        },
                        {
                            id:1004,
                            title: "Beden Kayıt Tutar",
                            description: "Ne yazık ki şimdiki psikiyatri anlayışı, yakınmalarınızı anlatmanız ve hekimin de bu yakınmaları düzeltecek bir ilaç önermesi üzerine kurulu. Ancak 'Hiç bir ilaç, kötü geçmiş bir çocukluğu düzeltmiyor'...",
                            authors: "Bessel A. van der Kolk",
                            listPrice: 41,
                        }
                    ]
                },
                methods: {
                    /*
                        Alt bileşenin emit ile gönderdiği mesaj sonrası tetiklenen metot
                    */
                    updateLikeCount(index) {
                        selected_title = this.books[index].title;
                        console.log("`", selected_title, "` isimli kitabı beğendin");
                        console.log('Üst bileşen(app) için updateLikeCount olayı çağırıldı');
                        this.likeCount += 1;
                    }
                }
            }
        );

    </script></body></html>

Bu örneğe ait çıktıları aşağıda görebilirsiniz. Gözleriniz kanayabilir o nedenle güneş gözlüğü kullanmanızı ya da monitörden beş metre kadar uzaklaşıp kısık gözle bakmanızı rica ederim :D 

Yine F12 - Console üstünde oynayıp veri değişimlerini izlemekte yarar var.

Bu tip kullanıcı etkileşimli sayfalarda bir diğer konu ise Form kullanımıdır. Yani Form verisi ile Vue tarafı nasıl haberleşebilir, POST edilen veri nasıl ele alınabilir sekizince ve son adımda bunu anlamaya çalışacağız.

touch vue_forms.html

Son sayfamıza ait kodları aşağıdaki gibi yazabiliriz.

<html><head><title>VueJs Bebek Adımları - 08</title><!-- CDN adresinden Vue.js'i kullanacağımızı söyledik--><script src="https://unpkg.com/vue"></script></head><body><div id="app" style="text-align:left"><!--
            @new-book-created, book-form bileşeninin onSubmit olayı içerisinden yapılan bildirimin adıdır.
            Bu bildirim gerçekleştiğinde üst bileşen yeni bir kitap üretildiğini anlayabilir ve bu nesneyi kendi data nesnesindeki books isimli diziye ekleyebilir.
            Bunun için addBookToList metodu kullanılır.
        --><book-form @new-book-created="addBookToList"></book-form><div v-for="(b,index) in books" :key="b.id"><p><h2>{{b.title}} ({{b.like}} beğeni)</h2></p><p>{{b.summary}}</p></div></div><script type="text/javascript">

        /*
            Örnekte bir form kullanılarak Submit işlemi ele alınıyor.
            input, textarea, select gibi girdi elemanları v-model direktifi yardımıyla data fonksiyonundan döndürülen alanlara bağlanırlar.
            select kontrolünde kullanılan .number, option içeriğinin integer olarak dönüştürülmesini sağlar.
            Submit işlemi gerçekleştiğinde @submit.prevent ile belirtilen onSubmit metodu tetiklenir.
            Form Validation için onSubmit metodunda bir takım tedbirler aldık. 
            Bu arada HTML 5 için required ifadesi ile elementlerin zorunlu hale getirilebileceğini de belirtelim.
        */
        Vue.component('book-form', {
            template:
                `
                <div class='book'><form class="new-book-form" @submit.prevent="onSubmit"><p><label for="title">Kitabın adı nedir?</label><input id="title" v-model="title" placeholder="title"></p>                    <p><label for="summary">Düşüncelerin neler?</label>      <textarea id="summary" v-model="summary"></textarea></p>                        <p><label for="like">Ne kadar beğendin?</label>                        <select id="like" v-model.number="like"><option>1</option><option>2</option><option>3</option><option>4</option><option>5</option></select></p>                            <p><input type="submit" value="Ekle">  </p>            <p v-if="errors.length" style="color:red;"><b>Hata : </b><ul><li v-for="err in errors">{{ err }}</li></ul></p>            </form>                    </div>
            `,
            data() {
                return {
                    title: null,
                    summary: null,
                    like: null,
                    errors: []  // Form doğrulama hatalarını tutmak için eklendi
                }
            },
            methods: {
                /* 
                    Submit düğmesine basılınca tetiklenir.
                    this ile bu bileşenden gelen name, summary, like gibi alanlar ele alınabilir.
                    Bu değerler kullanılarak yeni bir nesne oluşturulur.
                */
                onSubmit() {
                    this.errors = [];
                    if (this.title && this.summary) {
                        let newBook = {
                            title: this.title,
                            summary: this.summary,
                            like: this.like
                        }
                        /*Üst bileşene yeni bir girdi oluşturulduğuna dair bilgiyi yine $emit ile gönderebiliriz.İkinci parametre ile oluşturulan nesne örneği üst bileşene yollanır.
                        */
                        this.$emit('new-book-created', newBook)
                        /*
                            newBook örneklendikten sonra bu bileşenin verisi temizlenir ve yeni veri girişine uygun hale getirilir.
                        */
                        this.title = null
                        this.summary = null
                        this.like = null
                    } else {
                        /*
                            Doğrulama için koyduğumuz kısım.
                            Eğer başlık veya özet girilmemişse bunla ilgili olarak bu bileşenin errors dizisine bilgi ekliyoruz.
                        */
                        if (!this.title) this.errors.push("Kitap başlığı girilmeli.")
                        if (!this.summary) this.errors.push("Kitap için geri bildirim eklenmeli.")
                    }
                }
            }
        });

        var app = new Vue(
            {
                el: '#app',
                data: {
                    books: []
                },
                methods: {
                    addBookToList(book) {
                        /*
                            book-form bileşeninde Submit işlemi ile bir eleman eklendiğinde @new-book-created bildirimine göre bu metot çağrılır.
                            book parametresi ile gelen nesne, push fonksiyonu ile books dizisine eklenir.
                        */
                        this.books.push(book)
                    }
                }
            }
        );

    </script></body></html>

İlk denemelere ait bir ekran çıktısını aşağıda bulabilirsiniz.

Doğrulama ile ilgili kodların çıktısı da şöyle.

Örnekte ekrandan girilen kitap bilgileri books isimli JSON dizisine ekleniyor. Tahmin edileceği üzere bu veri erişimi bir servise doğru yapılmalı. Yani uygulamanın bir servis üzerinden bir veritabanı ile konuşması daha doğru olacaktır. Burada veritabanı hayal gücünüze kalmış. Senaryoya uygun bir NoSQL veya ilişkisel veritabanı kullanılabilir.

Sekizinci örnekle birlikte Vue'nun en temel parçalarını bebek adımları ile biraz olsun incelemiş olduk. Konuyu kendi kendinize çalışabileceğinizi düşünerek sizlere birkaç soru ve ödev bırakıyorum.

Bomba Sorular

  • Vue'da v-switch direktifi var mıdır? Yoksa bile kullanmanın bir yolu olabilir mi?
  • vue_event_handling örneğinde tek bir karakter ekleyerek oluşacak bug'ı bulun.
  • Vue.component ile bileşen tanımlanırken computed, methods özelliklerini kullanabilir miyiz?
  • vue_components.html örneğinde yer alan data neden bir fonksiyon şeklinde tanımlanmıştır?
  • "Props'lar üst bileşenden alt bileşene veri aktarımında kullanılırlar" ifadesi doğru mudur?

ve Ödevler

  • vue_attribute_binding.html örneğinde kitap fotoğrafına bir link bağlayın(a href) ve href niteliğinin data nesnesindeki url isimli bir özellikten beslenmesini sağlayın.
  • vue_conditional_render.html örneğinde, level değişkeninin Small, Medium, Large, XLarge olmasına göre sayfanın sağ üst köşesinde S,M,L,XL harflerinin şöyle janjanlı imajlar şeklinde görünmesini sağlayın.
  • vue_for_loop örneğinde yer alan level değerini kullanarak kitap fontlarını renklendirmeyi deneyin. 100 için farklı bir renk, 300 için farklı bir renk vb
  • vue_event_handling örneğinde fiyat azaltmada 0 ve eksi değere geçilmesini önleyin. Ayrıca her ürün fiyatı için bir üst artırma limiti olsun ve artışlar bu değeri geçemesin.
  • Vue antrenmanı yaptığınız herhangi bir sayfada yine ürünleri listeleyin ancak bir ürün adının üstüne geldiğinizde ürünün fotoğrafının olduğu bir div elementini aktif hale getirin. Yani ürün adı üstüne gelince fotoğraf gösterilmesini sağlayın(Popup ile uğraşmayın, sayfadaki bir div alanı görünür hale gelsin yeterli)
  • Okduğunuz son beş kitabın sadece başlıklarını listeleyen bir bileşen tasarlayın. Bu bileşende her başlık yanında "Detay" isimli bir Button olsun. Bu düğmeye basınca kitapla ilgili detayları içeren başka bir bileşen başlığın hemen altında görünür olsun.
  • Vue_forms.html örneğinde kitap ekledikçe bunu ekrana listeleyen bir bileşeni for döngüsü yardımıyla kullanmayı deneyiniz.
  • Vue_forms.html örneğinde summary için maksimum 250 karakter girilmesine izin veren bir doğrulama fonksiyonelliği geliştirin.

Örnek kodlara github reposu üzerinden erişebilirsiniz. Böylece geldik bir SkyNet derlememizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Docker Yerine Podman (Pod Manager) Kullanmak

$
0
0

Heimdall üstünden birşeyler kurcalamak istediğimde yolum genellikle bir Docker imajı ile kesişiyor. Bakmak istediğim bir NoSQL veritabanı mı var, ELK üçlüsü mü gerekli, bir NGinx server ortamımı lazım ya da yeni bir servis için çalışma zamanımı hazırlamam gerekiyor... Hemen Docker kardeşimizin kapısını çalıyorum. Aslında bakarsanız Container teknolojileri denince çoğumuzun aklına Docker'dan başka bir şey gelmiyordur belki de. "Gerçekten de böyle mi?" diye düşündüğüm bir ara Docker'ın güçlü bir alternatifi olan Podman isimli ürünle karşılaştım ve onu biraz tanımaya karar verdim.

Esasen Docker tek ve vazgeçilmez bir container aracı olarak düşünülmemeli. Sonuçta Open Container Initiative tarafından belirlenmiş standartlara uyan araçlar mevcut. Open Container Initiative, bu tip araçlarda üç temel özelliğin olmasını vurguluyor. Container çalışma zamanı(runtime), dağıtım stratejisi(distribution) ve baş aktör olarak da imagePodman bu standartlara uyan araçlardan birisi. Buna göre Podman ile hazırlanan imajlar Docker ile veya XYZ isimli başka bir Container ile de uyumlu oluyor (Zaten stadartların amacı da bu değil mi? Farklı ürünlerin birbirleri yerine tercih edilip kullanılabilmesi için ortak bir yöntemler kılavuzu sunmak)

Red Hat tarafından açık kaynak olarak geliştirilen Podman özünde Pod(Kubernetes'in en küçük işlem birimi olarak bahsi geçen lakin benim aklıma hep Anakin Skywalker'ın yarış aracını getiren) sistemine dayanıyor. Dolayısıyla Podman ortamından Kubernetes üzerine göç etmek(migration) oldukça kolay. Pod içerisinde birden fazla Container kullanılabiliyor ve ayrıca Docker da olduğu gibi daemon ihtiyacı bulunmuyor. Zaten temel fark her ikisinin farklı mimari kullanmaları. Docker, client-server temelli bir mimariyi baz alıyor. Client görevi üstlenen CLI arabirimi(ki biz onun komutlarını kullanıyoruz) arka planda(Server side) image nesnesi inşa etmek ve container çalıştırmak gibi işleri üstlenen daemon ile iletişim halinde. CLI'ın Daemon ile olan bu iletişimi Root kullanıcı yetkilerine ihtiyaç duymakta.

Podman ise bunun aksine Root kullanıcı şart koşmuyor çünkü Daemonless bir mimari kullanmakta. Standart bir kullanıcı söz konusu ise onun için açılan özel çalışma sahası(namespace) kullanılıyor. Buna göre her kullanıcı sadece kendi Container örnekleri ile çalışıyor. Root kullanıcı yetkisi gerekmemesinin başka bir avantajı daha olabilir. Container başka bir kullanıcı tarafında ele geçirilse bile Root kullanıcı yetkilerine sahip olmadığından sadece o workspace üzerindeki yetkilerle sınırlı kalacaktır.

Podman, Image oluşturmak için Buildah ve uzak repolara image kaydetmek(Register) için skopeo gibi araçları kullanır. Diğer yandan öğrendiğim kadarıyla sadece Linux sistemlerde çalışıyor(Windows Subsystem for Linux bir istisna olabilir mi bakmak lazım) Bununla birlikte Docker Compose'un Podman tarafında henüz bir karşılığı yok(Yazıyı hazırladığım tarih itibariyle doğrulanmamış bir bilgidir. Lütfen araştırınız) Haydi gelin bu Podman nasıl kurulur, temel terminal komutları nelerdir ve onunla bir Poderize işlemi nasıl gerçekleştirilir bir bakalım.

Önce Kurulum

Bu seneki pek çok SkyNet çalışmasında olduğu gibi ben denemelerimi Heimdall(Ubuntu-20.04) üzerinde yapmaktayım. Ancak diğer platformlar içinde aynı terminal komutları ve prensipler söz konusu olacaktır. Linux tarafı için şağıdaki şekilde konuya giriş yapabiliriz(Güncel kurulum bilgilerine resmi adresinden bakılabilir)

# Podman resmi dokümantasyonundaki adımları takip ederek kurulumu yaptım
. /etc/os-release
echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/Release.key | sudo apt-key add -
sudo apt-get update
sudo apt-get -y upgrade 
sudo apt-get -y install podman

# Sonrasında bir versiyon kontrolü de yaptım
podman -v

Kurulum sonrasında birkaç komutla Podman'i inceleyebiliriz. İlgi çekici kısım bir pod oluşturmak ve bu pod içerisine n sayıda container yerleştirmek olacak tabii ki ama önce temeller.

# Önce bir pod oluşturalım (adı pod_race olsun)
podman pod create --name pod_race

# Şimdi sistemdeki pod listesine bakalım
podman pod list

# Bu pod içinde bir tane alpine imajından container başlatalım
podman run -d --pod pod_race alpine

# Hadi bir tane de nginx imajından http server container'ı çalıştıralım (aynı pod içinde)
podman run -d --pod pod_race nginx

# Hatta bir tane de rabbitmq container'ı başlatalım. O da aynı pod içinde olsun.
podman run -d --pod pod_race rabbitmq

# Şimdi pod_race isimli pod'un içindeki container'lara bakabiliriz
podman ps -a --pod

# Aşağıdaki komutla yüklü olan image örneklerini de görebiliriz
podman images

# Bir podu aşağıdaki komutla durdurabiliriz. Bu, içindeki Container'ları da durduracaktır.
podman pod stop pod_race

# Pek tabi oluşturulan bir podu, içindeki tüm container örnekleri ile birlikte silebiliriz de
podman pod rm pod_race

Yukarıdaki temel çalışmaların kısa bir özeti aşağıdaki ekran çıktısındaki gibidir.

Podman ile uzak depolardaki imajları kolayca arayabiliriz de. Mesela sevgili MariaDB imajlarını aradığımızı ve 20 yıldız üstünde olup automated özellikli olanları bulmak istediğimizi düşünelim...

podman search mariadb --filter=stars=20 --filter=is-automated

# ya da resmi bir imaj arayıp açıklamaların da tamamını(--no-trunc) istersek şunu kullanabiliriz
podman search mariadb --no-trunc --filter=is-official

# Hatta çıktı tablosundaki kolonlardan sadece istediklerimizi de mustache stilindeki parametrelerle değiştirebiliriz
podman search --format "table {{.Name}} {{.Stars}}" mariadb --filter=stars=20

# Uzak repolardaki kendi imajlarımızı da aratmak isteyebiliriz.
# Mesela Quay.io'da ki imajlarımızı aratmak istediğimizi düşünelim.
# Aşağıdaki komutla bunu yapabiliriz?
# Lakin kuvvetle muhtemel öncesinde Quay.io için Login olmamız gerekebilir. Podman bunu da sağlar.
podman login quay.io
podman search quay.io/

Uzak diyarlardaki imajları terminalden nasıl arayacağımızı gördük. Bazen belli bir imajın özelliklerini onu sisteme indirmeden detayda da öğrenmek isteyebiliriz. Bunun için Skopeo aracından yararlanıyoruz. Örneğin alpine imajının son sürümü ile ilgili bir takım temel özellikleri şu komutla öğrenebiliriz.

skopeo inspect docker://docker.io/alpine:latest

Yukarıdaki terminal komutlarına ait Heimdall çıktıları ise aşağıdaki gibi oluşmuştur.

Podman varsayılan kurulumunda image registery adresleri olarak docker ve quay geldi. Başka adresler eklemek istersek(mesela private repo'lar) /etc/containers/registries.conf dosyasını düzenlemek gerekir.

Şimdi podman ile bir imaj hazırlayıp onu build etmeyi deneyelim. Tahmin edileceği üzere bize kobay bir uygulama lazım ve bunun en basit yolu aptal bir NodeJs servisi. Express web çatısını kullanan bu servisi aşağıdaki terminal komutları ile hazırlayıp kodlayarak devam edelim.

mkdir pingapi
cd pingapi
npm init -y
touch index.js
npm i express
touch Dockerfile

index.js içeriğini aşağıdaki gibi yazabiliriz. Sadece masa tenisinin güzel bir spor olduğunu ve bazen her şeye ara verip biraz masa tenisi oynamanın iyi olacağını söyleyeyen bir servis ;)

const express = require('express')
const app = express()

app.get('/ping', function (request, result) {
    result.send('Biraz ara verip Ping Pong oynayalım mı?')
})

app.listen(5555, "0.0.0.0", function () {
    console.log('Servisimiz http://localhost:5555/ping adresinden denenebilir.')
})

Tabii inşanın olmazsa olmazı Dockerfile dosyamız. Node11 sürümünü baz alarak uygulamamızı olduğu gibi kopyalayıp 5555 nolu port üstünden açacak bir çalışma zamanı ortamını tanımlıyor.

FROM node:11
WORKDIR /app
COPY package*.json  ./
RUN npm install
COPY . .
EXPOSE 5555
CMD ["node", "index.js"]

Kobay servisimiz Dockerize(Poderize) edilmeye hazır ;) İzleyen terminal komutları ile onu inşa edelim, sorasında çalıştırıp kullanmayı deneyelim.

# Önce Build işlemini yapalım
podman build -t ping-api .

# İmajın oluşup oluşmadığı kontrol ettikten sonra onu
# çalıştırıp api'den değer alıp alamadığımıza bakmak iyi olabilir ;)
podman images
podman run -p 5555:5555 ping-api
podman ps -a
curl http://localhost:5555/ping

# Çalışmakta olan container'ı durdurmak içinse aşağıdaki komutu kullanabiliriz
# (337f id değeri tabii ki siz denerken farklı olacaktır)
podman stop 337f

İşte çalışma zamanı çıktısı. Podman ile Container ayağa kalktıktan sonra servisi masa tenisi oynamaya götürebiliriz.

Tabii stop komutu ile ilgili container durdurulduğunda servise gönderilen talepler cevapsız kalır. Bu bir nevi Container tatile çıktığında servisin çalışmaması gerektiğinin de bir ispatıdır.

Skopeo

Podman ile ilgili bilgileri araştırırken yanında yardımcı başka araçlar da görebiliriz. OCI ilkelerine göre imaj oluşturmayı kolaylaştıran Buildah(Kanımca Build Yeaaa diye telafuz ediliyor) veya yukarıda bir image nesnesinin detay özelliklerini öğrenmek ve aynı zamanda depolar arası container transferlerini(kendi deponuzla docker.io veya quay.io gibi public registery noktaları arasında taşımak) kolaylaştıran skopeo gibi. Skopeo için Linux tarafında aşağıdaki adımları takip ederek kurulum yapabilirsiniz.

. /etc/os-release
sudo sh -c "echo 'deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/x${NAME}_${VERSION_ID}/ /' > /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list"
curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/x${NAME}_${VERSION_ID}/Release.key | sudo apt-key add -
sudo apt-get -y update
sudo apt-get -y install skopeo

Şimdilik bu kadar. Gelelim bu çalışma haricinde daha neler yapabileceğinize. Örneğin pod_a ve pod_b şeklinde iki ayrı pod oluşturup içlerindeki container'ların birbirlerini kullanmasını deneyebilirsiniz. Yani pod_a'da ki bir .net web api, pod_b'deki MongoDb container'ını kullanmaya çalışabilir mi sorusunun cevabını arayabilirsiniz. Diğer yandan Podman benzeri OCI standartlarına uyan başka container teknolojileri var mı araştırmakta ve hatta aralarındaki kıyaslamalara bakmakta yarar var. 

Böylece geldik bir SkyNet derlememizin daha sonuna. Bu çalışmamızda kobay bir NodeJs servisini Poderize(Dockerize yerine bunu kullanayım dedim) etmeyi ve Podman'in genel kullanımını öğrendik. Örnek kodlara github reposuüzerinden erişebilirsiniz. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Stackoverflow Sevgi Anketinde Yıllardır Bir Numara Çıkan Programlama Dili Rust Kimdir?

$
0
0

Hepimiz için berbat geçen bir yılı geride bırakıyoruz. Koca sene boyunca uğraştığımız salgının etkileri daha da sürecek gibi duruyor. 2021 bize neler getirir bilemiyorum ama yazılımcıların bilgisayarları başında daha çok vakit geçirdiği günlerin hayatımızda kalıcı hale geldiğini de ifade edebilirim. Geçen yılın bir bölümünde işlerden arta kalan vakitlerde kendimce yeni şeyler öğrenmeye gayret ettim. Bunlardan birisi de Mozilla Labs'ın gücünü arkasına almış olan Rust programlama diliydi.

Stackoverflow 2020 yılı geliştirici anketine göre en sevilen programlama dili olduğunu söylemeden geçmeyelim ki bu son birkaç yıldır da böyle. Geliştiricilerini Rustaceans olarak adlandırıldığı programlama dilini öğrenmekte zorlandığımı itiraf edeyim (Halen daha emekleme aşamasındayım)Özellikle ownership(sahiplenme), borrowing(borç alma), referans türlerin yaşam ömürlerinin kontrolü(lifetime,scope), mutex yapıları, reference counting, smart pointers vb

Uzun yıllar Garbage Collector gibi enstrümanlara sahip yönetimli ortamlarda geliştirme yapınca, birden tüm bellek operasyon maliyetlerinin ne olacağını bilerek kodlama yapmaya çalışmak çok kolay olmuyor. Üstelik Rust derleyicisi oldukça akıllı ve kodlama hatalarınızda sizi en iyi seçeneğe gitmeniz noktasında sürekli olarak uyarıyor. Kod gelişitirken özellikle derleyici açısından baktığımızda GO'nun katı kuralcılığını açıkça hissediyorsunuz.

Fonksiyonel programlama paradigmasına ait özellikler barındıran Rust daha çok sistem seviyesinde programlama için tasarlanmış bir dil ancak farklı kullanım alanları da var. Örneğin Deno platformu, Microsoft Azure IoT Edge'in çok büyük bir kısmı, Servo isimli yüksek hızlı tarayıcı motoru, TockOS, Tifflin, RustOS, QuiltOS, Redox gibi işletim sistemleri, Linux ls komutunun alternatifi olan exa bu dil kullanılarak geliştirilmiş. Bunların dışında oyun motorları, derleyiciler, container'lar, sanal makineler(VM), Linux dosya sistemleri ile gömülü cihaz sürücülerinin geliştirilmesinde de tercih ediliyor. Bir başka ifadeyle Rust diliyle iş odaklı uygulamalar harici yazılım ve yazılım platformları geliştirildiğini ifade edebiliriz. Bu nedenle Rust donanım dostu bir dil desek yeridir. 

Rust ortamında Garbage Collector gibi bir mekanizma yok ve bunun en büyük amacı tahmin edileceği üzere çalışma zamanı performansının artırılması. Koda eklenen her satırın parasını ödememizi isteyen bir ortam sunuyor diyelim. Cimri bir dil olduğunu ve bellek kullanımında aşırı masraftan kaçınmamızı istediğini belirteyim. Öyleki tüm değişkenler varsayılan olarak immutable(değiştirilemez) oluşuyor ve herhangi bir fonksiyon içerisinde kullanıldıktan sonra bulunduğu scope içerisinde tekrar kullanılamıyor(Diğer fonksiyonda kullanıldı ve artık işi bitti, Bellekten At!!! Tabii bu kuralı esnetebiliriz) Ayrıca immutable kullanım minik veri yapılarında(Data Structures) önemli bir performans kazanımı sunmaktadır. Yüklü veri yapılarında ise mutable kullanım daha uygun olabilir nitekim referans almak yığının bir kopyasını oluşturarak çalıştırmaktan daha mantıklıdır.

Bu dilin diğer önemli hedefleri arasında eş zamanlılık(Concurrency) ve güvenli bellek kullanımı yer almakta. Derlemeli bir dil olan Rust çıktı olarak Web Assembly'da üretebiliyor(Şuradaki dokümana bir bakın derim) Rust dilinde paket yöneticisi olarak Cargo isimli araç kullanılıyor.

Dilin henüz öğrenmeye çalıştığım daha pek çok özelliği var. Gelin bu uzun dokümantasyonla hangi konuları ele aldık inceleyelim. Ben ilgili örnekleri Heimdall(Ubuntu 20-04)üstünde ve Visual Studio Code kullanarak geliştirdim. Sistemde Rust ortamını hazırlamak ve rs uzantılı bir kod dosyası oluşturup derlemek oldukça basitti.

curl https://sh.rustup.rs -sSf | sh

# Dilin genel özelliklerini tanımak için bir dosya üstünde çalışalım
touch WhoAreYouRust.rs

Burada kullandığımız kod parçası ise şöyle.

// fonksiyon bildirimi anlamına gelir
fn main(){ // Tahmin edileceği üzere programın giriş noktası. Önceden tanımlı fonksiyondur    println!("I hate hello world!"); // Sondaki ünlem işareti println'in bir macro olduğu ifade eder.    /*        Makrolar fonksiyonların genişletilmiş hali olarak ifade ediliyor. Çalışma zamanındaki meta bilgileri ile konuşma olanakları varmış.
        Sanırım bunu ilerde daha net anlarım.
    */
}

Peki bu kodu nasıl çalıştıracağız? Öncelikle bir derleme işlemi yapmamız gerekiyor. Sonrasında platform için oluşan binary'yi çağırabiliriz.

# Rust kodlarını derlemek için 
rustc WhoAreYouRust.rs

# Çalıştırmak içinse
./WhoAreYouRust

Faktöryel ile Hello World

Biraz önce paket yöneticisi olarak Cargo isimli programın kullanıldığından bahsetmiştik. Cargo ile rust projeleri oluşturabilir, onları çalıştırabilir, paketleri yönetebilir ve testler başlatabiliriz. Bundan sonraki örneklerin tamamında cargo aracının kullanıldığını ifade edeyim. Örneğin factorial isimli biraz da ortamı koklayacağımız örneği oluşturmak için aşağıdaki terminal komutunu vermemiz yeterli.

cargo new factorial

main.rs içeriğini aşağıdaki gibi kodlayabiliriz. Yorum satırlarını dikkatlice okumaya gayret edin.

/*    isimlendirme standardı olarak snake_case kullanılıyor.
    Mesela input_x yerine inputX kullanınca cargo check sonrası uyarılar alıyoruz.

    Bu ilk kod parçası ekrandan bir sayı değeri alıp faktöryelini buluyor.
*/

use std::io; // IO modülünü kullanacağımızı belirttik. stdin fonksiyonunu kullanabilmek için gerekli

fn main() {    println!("Selam! Ben bir hesap makinesiyim :P");    println!("X değeri?");

    /*        Aşağıdaki iki değişken tanımı söz konusu.        Rust dilinde değişkenler varsayılan olarak immutable'dır. Yani atamadan sonra değerleri değiştirilmez.
    */    let mut input_x = String::new(); // bunun mutable olması gerektiğinden mut keyword'ü kullanıldı.
    let x: u32;    io::stdin().read_line(&mut input_x).expect("Bir hata oldu"); // ekrandan girilen bilgiyi input_x'e okuyoruz (& sanıyorum pointer. İleride netleştirelim)

    x = input_x        .trim()        .parse()        .expect("Dönüştürme işleminde hata"); // ekrandan alınan bilgi 32bit integer'a dönüştürüyoruz.    /*        expect fonksiyonları, bir önceki işlemde bir panic havası eserse ilgili mesajı veriyor. Panic'ler nasıl ele alınıyor ilerde öğrenelim.
    */    let y = calculate(x); // hesaplama fonksiyonunu çağırıyoruz
    println!("x! = {}", y); // Sonucu ekrana basıyoruz

    // x = 9; // Değişkenler varsayılan olarak immutable olduğundan burada derleme hatası oluşur. x'e ikinci kez değer atayamayız.
}

/*
    Recursive çalışan fonksiyonumuz.    Unsigned Integer 32 alıp aynı tipten sonuç dönüyor.
*/
fn calculate(num: u32) -> u32 {    match num {        // Pattern matching uygulanıyor
        0 | 1 => 1,                    // 0 veya 1 ise 1 döner        _ => calculate(num - 1) * num, // bunlardan farklı ise sayıyı bir azaltıp yine kendisini çağırır
    }
}

Önce kodu derleyelim ve sonrasında çalıştıralım.

# Cargo üstünde build için
cargo build
# ve çalıştırmak için
cargo run

Bu arada kodu derlemeden kontrol etmek için cargo check, sürüm çıkarmak içinse cargo build --release komutlarını kullanabiliriz.

Sayı Tahmin Oyunu

Sıradaki örneğimizde rastgele sayı üretimi için kullanacağımız rand isimli bir kütüphane var. Bu örnekte amaçlardan birisi de harici kütüphaneleri nasıl kullanabileceğimizi görmek. Rand kütüphanesini kullanabilmek için toml dosyasındaki [dependencies] kısmında bir bildirim yapmak gerekiyor.

[package]
name = "lucky_number"
version = "0.1.0"
authors = ["buraksenyurt <...@....com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.7.3" # random sayı üreten kütüphane

Dolayısıyla Rust uygulamalarının bu tip paket bağımlılıklarının toml dosyasında bildirildiğini ifade edebiliriz. İlgili paketler cargo build sonrası eğer sistemde yoksa indirileceklerdir. Ana program kodlarını aşağıdaki gibi yazarak devam edelim.

use rand::Rng; // rastgele sayı kütüphanesi
use std::cmp::Ordering; // Kıyaslama operasyonu için eklenen Enum. cmp(Compare) için match kullanılan yere dikkat
use std::io; // Standart kütüphane. Ekrandan girdi okumakta işimize yarayacak

/*
    Örnek kod parçasında sayı tahmin oyunu icra ediliyor.    Yarışmacının 5 tahmin hakkı var.
*/

fn main() {    println!("Sayı tahmin oyununa hoşgeldin...");
    println!("10 ile 30 arası bir sayı tuttum.\nBakalım bilebilecek misin?");    // 1 ile 50 arası sayı üretiyor    // thread_rng, rastgele sayı üretici nesnesini verir    // get_range metodu ile    let computer_number = rand::thread_rng().gen_range(10, 30);    let mut player_guess: u8; // yine mutable bir pozitif tam sayı tanımı. 8 bitlik.    println!("Hadi bir tahminde bulun");    // 5 iterasyonlu bir döngü kurduk    for i in 1..6 {        let mut screen_input = String::new(); // Değeri değiştirilebilir bir String değişken (Mutable)        println!("{}. hak", i);        // Ekran girilen veriyi screen_input değişkenine alıyoruz
        io::stdin()            .read_line(&mut screen_input)            .expect("Okuma sırasında hata"); // Olası hata durumu mesajımız

        /*            String değeri u8 tipine dönüştürüyoruz ama nasıl? :)            parse metodu bir sonuç döner. Bu sonuç Ok veya Err değerleri içeren bir Enum sabitidir.            Bu sabiti match ederek ne yapılacağına karar veriyoruz.            Ok ise sorun yok. Yani dönüştürme başarılı olmuş.
            Lakin dönüştürme başarısızsa parse dönüşü Err demektir. Bu durumda ekrana mesaj yazdırıp continue ile döngüyü devam ettiriyoruz.        */        player_guess = match screen_input            .trim() // neden trim'ledik            .parse()        {            Ok(n) => n,            Err(_) => {                println!("Girdiğin sayıyı dönüştüremedim. Lütfen tekrar dene.");                continue;            }        };        /*            cmp çağrısının sonucu Ordering sabitinin hangi durumu oluşuyorsa,
            ona göre bir kod parçası işletiliyor.

            match Arms Aşağıdaki şekilde bir kullanım söz konusu.            match value {                pattern => expression,                pattern => { expressions }, // blokta olabilir                pattern => expression,            }        */        match player_guess.cmp(&computer_number) {            Ordering::Less => println!("Tahminini yükselt"),            Ordering::Equal => {                // Doğru tahmin etmişse döngüden çıkartırız
                println!("Bingo!!!");                break;            }            Ordering::Greater => println!("Tahminini küçült."),        }    }    println!(        "Oyun tamamlandı ve benim tuttuğum sayı {} idi",        computer_number    );
}

İşte çalışma zamanına ait iki örnek çıktı.

ve

Sepeti Doldurmaya Devam

Şimdiki örnekte değişkenlerin immutable olma halini, sabitleri(constants), shadowing konusunu, temel veri türlerini, statik dil özelliklerini, tuple ile slice veri yapılarını, for döngülerinde match, iter ve rev kullanımları ile loop döngüsünü ele alıyoruz.

fn main() {    /*        Rust dilinde değişkenler varsayılan olarak immutable karekteristiktedir.    */    let point = 90;    println!("Sınav puanın {}", point);    // point += 10; // Yandaki atamaya izin verilmez. Derleme zamanı hatası alınır.

    // Ancak immutable bir tipin mutable yapılarak sonradan değerinin değiştirilebilmesi de mümkündür.    // fight değişkeni mutable olarak işaretlemiştir. Bu nedenle değeri değiştirilebilir.
    let mut fight = "Dı dıp dı dıp dı dıp dı dı dıp dı";
    println!("Mortal Combat {0}", fight);    fight = "dı dı dıı dı dı dı dı dıı dıı dı dı dı dıııd";
    println!("Mortal Comat(Remix) {}", fight);    /*        CONSTANT        const ile sabit değişkenler tamınlanabilir. Bunlar sadece tanımlandıkları gibi immutable değildir. Daima immutable'dır.
        Bir constant tipi tanımlanırken tür belirtilmelidir. İsimlendirme standardı da aşağıdaki gibidir    */    const ALWAYS_ON: bool = false;    println!(        "Always on mode is {}",        match ALWAYS_ON {            true => "Active",            false => "Passive",        } // Şu match ifadesinin kullanımını biraz daha anlayayım diye    );    /*        SHADOWING        let ile tanımladığımız immutable bir değişken(ki varsayılan olarak da öyle zaten)        tekrardan let kullanılarak yeni bir değer alabilir ve hatta değişken türü de değişime uğrayabilir.
        Buna shadowing deniyor. İkinci let kullanımı ile birlikte ilk değişkenin değeri gölgede bırakılıyor.
        shadowing immutable tipler için geçerli bir durum.    */    let value = 23.93;    let value = value + 0.58; // Burada shadowing söz konusu.    println!("Value = {}", value); // En azından buradaki gibi value değişkenini kullanmazsak derleme zamanında Warning mesajı görürüz    let value = true; // hatta burada shadowing olmakla kalmıyor veri türü de değişiyor
    println!("Value = {}", value);    /*        DATA TYPES        Rust statik tür kullanan bir dildir. Dolayısıyla derleme noktasına gelindiğine her değişkenin türü bellidir.        Veri tileri saysıla (Scaler) ve bileşik (Compound) olmak üzere ikiye ayrılır.

        Integer Tipi        Bit     Signed  Unsigned        8-bit   i8      u8        16-bit  i16     u16        32-bit  i32     u32        64-bit  i64     u64        128-bit i128    u128        arch    isize   usize İşlemcinin 32 bit veye 64 bit olma durumuna göre boyutlanır

        Floating Point Tipi        f32        f64        Bunlar haricinde bool, char (4 byte'lık Unicode Scalar türüdür ve neredeyse her dilden karakteri destekler)        COMPOUND(Bileşik Türler)        Rust dilinde önceden tanımlı iki bileşik tür vardır. Tuple ve Array        Tuple tipinde farklı türlerden değişkenleri bir liste olarak tutabiliriz.        Array tipi ise sadece aynı türden değişkenleri barındırabilir.
        Array'lerde eleman sayısı sabittir. Veriyi stack üzerinde tutmak istediğimizde idealdir. Aksi durumda Vector tipi tercih edilebilir.    */    let pi = 3.14; // tip belirtmesekte Rust eşitliğe bakarak varsayılan bir tür ataması yapar    let ageLimit: u8 = 12; // pek tabii veri türünü bilinçli olarak söyleyebiliriz de (u8 - 8 Bit Unsigned Integer oluyor)    let limit: u8 = "18".parse().expect("Dönüştürme hatası"); // Tipler arası dönüşüm de söz konusudur. Bu durumda da dönüştürülecek veri türü söylenmelidir    let eular: f32 = 2.76666666; // 32 bit floating point. Bir şey belirtmezsek f64 türünü alır

    let basket = ("Lamba", true, 1.90, 3.14, 10); // Burada basit bir tuple tanımı söz konusu.    println!("{} {}", basket.0, basket.3); // tuple içindeki farklı yerlerdeki elemanlara bu şekilde erişebiliriz.
    let (a, b, c, _, e) = basket; // pattern matching ile tuple içeriğini bu şekilde değişkenlere de alabiliriz. Hatta _ ile atlayabiliriz de. (Bu arada bu atamaya destructuring deniliyor)    println!("{},{},{},{}", a, b, c, e);    let numbers = [1, 5, 2, 6, 9, 3, 8, 15, 37, 99]; // Basit bir dizi tanımı
    println!("{}", numbers[2]);    let colors: [char; 3] = ['R', 'G', 'B']; // Diziyi tanımlarken veri türü ve eleman sayısı da verilebilir    println!("{}", colors[2]);    let columns = [1; 10]; // Bu da değişik bir kullanım. 10 tane 1 den oluşan bir dizi tanımladık
    println!("{}", columns[9]);    // let column = columns[11]; //11 numaralı indis olmadığı için derleme hatası oluşur. Hatta VS Code IDE'sinde bile altı kırmızı olarak çizilir    /*        SLICES        Veri yapılarından birisi de slice türüdür. Ownership özelliği yoktur.        Bir nesne dizisinden bir dilimin referans eden veri türü gibi düşünülebilir.    */    let song = String::from("Uzun ince bir yoldayım. Gidiyorum gündüz gece");    let slice1 = &song[..5]; // baştan itibaren 5 karakterlik bir dilimi işaret eden bir slice    println!("{}", song);    println!("{}", slice1);    let slice2 = &song[5..17]; // bu sefer 5nci karakterden itibaren 16ncıya kadarlık bir kısmı dilimleyip başlangıç adresini işaret eden bir slice değişkeni oluşturduk
    println!("{}", slice2);    let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16];    let slice3 = &numbers[10..]; //10ncu eleman sonrasında itibaren son eleman kadar olan kısmı dilimledik    for n in slice3 {        println!("{}", n);    }    /*        FUNCTIONS        Değer döndüren fonksiyonlarda eğer en son noktada işlem sonucu alınıyorsa return kelimesini kullanmak zorunlu değildir,
        fonksiyonun farklı noktalarında dönüş vermek istersek return kullanabiliriz    */    println!("4+7={}", sum_of_two(4, 7));    // fibonacci fonksiyonunu da çağıralım
    println!("13ncü sıradaki fibonacci sayısı {}", find_fibonacci(13));    /*        LOOPS        Birkaç döngü örneği de koyalım. Rust dilin üç döndü çeşidi var loops,for ve while    */    // iter fonksiyonu ile yukarıdaki numbers dizisi elemanlarında ileri yönlü hareket edebiliriz    for nmbr in numbers.iter() {        println!(            "{} {}",            nmbr,            if nmbr % 3 == 0 {                // burada satır içi if koşulu kullandım
                "Tek sayı"
            } else {                "Tek sayı değil"
            }        );    }    // for döngüsünü ters yönlü olarak da kullanabiliriz    // 1den 10a kadar tanımlı bir sayı aralığında geriye doğru gidiyoruz    for nmbr in (0..11).rev() {        print!("{}...", nmbr);    }    println!("On Live");    // loop örneği. Koşula bağlı tekrarlı kod parçaları için tercih edilebilir    // Sonsuz döngü örneği
    // loop {    //     println!("Aghh!!! Çıkarın beni burdan");    // }    let mut counter = 0;    let result = loop {        // loop içerisinde break ile çıktığımızda döndürdüğümüz değer bu değişkene atanır
        counter += 1;        if counter == 10 {            break "işlemler bitti";        }    };    println!("{}", result);
}

fn sum_of_two(x: i32, y: i32) -> i32 {    x + y // return dememize ve ; koyduğumuzda hata alırız
}

// n'nci sıradaki fibonacci sayısını bulan fonksiyon
fn find_fibonacci(n: u32) -> u32 {    match n {        // pattern matching kullandık
        0 => 1,                                             // n sayısının 0 olma hali        1 => 1,                                             // n sayısının 1 olma hali        _ => find_fibonacci(n - 1) + find_fibonacci(n - 2), // n sayısının bunlar 0 ve 1 dışında olma hali    }
}

Bu örnekte immutable olan point değişkenini değiştirmeye çalıştığımızda aşağıdaki ekran görüntüsünde yer alan derleme zamanı hatasını alırız.

Sahiplenme(Ownership)

Bu ilk etapta anlamakta oldukça zorlandığım bir kavramdı. Bir değişkenin kullanıcı operatör tarafından sahiplenmesi ve işini yaptıktan sonraki akibeti ile tüm bunların yönetimi aslında performans ve bellek kullanımı güvenliği için önemli. İzleyen örnekte String tipleri arasında yapılan atama sonrası atanan tipin scope dışında kalmasını(move), metot parametre ve dönüş tiplerinde sahipliğin değişmesi, &(referencing) ve *(deReferencing) operatörlerinin kullanımı, borç alma(borrowing), aynı scope içinde birden fazla mutable referans ataması hali(Data Race kısıtı) gibi biraz daha zorlayıcı konulara değinmeye çalışıyoruz. Yorum satırlarına dikkat!

/*    RUST dilinde Garbage Collector mekanizması yoktur.    Ownership (Sahiplik) dilin en önemli olgularındandır.
    Belleğin Stack ve Heap alanlarının ne olduğunu iyi bilmeyi gerektirir.
*/

fn main() {    /*        Önce scope kavramına bir değinmek lazım.
        Aşağıda {} arasında bir scope açtık. Bu scope içinde tanımlı değişkenler sadece bu scope içinde kullanılabilir.
    */    {        // greetings değişkeni henüz scope'a dahil değil
        let greetings = "It's raining..."; // scope'a dahil oldu        println!("{}", greetings); // scope içinde kullanıldı
    } //burası açtığımız scope'un sonlandığı yer      // println!("{}", greetings); // greetings artık scope dışı ve kullanılamaz

    /*        string demişken...
        Doğal olarak string literal ile tanımlanan değişkenler de diğer türler gibi varsayılan olarak immutable'dır.
        Diğer yandan string içeriği kullanıcı tarafından çalışma zamanında da girilebilir. Hatta bu belki bir dosyanın içeriğidir.
        Yani başlangıçta ne kadar alan kaplayacağı belli olmayabilir.        String veri tipinden yararlanarak içeriği çalışma zamanında belli olacak metinsel içerikler tanımlayabiliriz.
        Bilin bakalım String türü bellekte nerede durur (Heap)    */    {        // Yeni bir scope açtık
        let mut username = String::from("Jean"); //scope içinde geçerli        username.push_str("Van Damme"); // metne yeni bilgi ekledik. username mutable hale getirildi.        println!("{}", username); // scope içinde kullandık
    } //scope dışına çıktık. username kaynağa iade edildi      // Scope dışına çıkıldığında Rust çalışma zamanı drop isimli bir fonksiyon çağırır. C#'taki Destructor gibi düşünebilirim.    /*        Değişkenler arası atamalar, bellekte tutuldukları lokasyonlara göre farklı davranışlar gösterirler.        Stack'te tutulan sayısal değerler ile String'i karşılaştıralım.
        Özellikle String'lerin atamasında move adı verilen bir olay söz konusudur    */    let x = 10; // stack'de x için yer açıldı
    let mut y = x; // stack'de y için yer açıldı ve içine x'in değeri kopyalandı
    y += 5; // y değerini değiştirdim. Atayama rağmen bu x'in değerini bozmaz    println!("x={} y={}", x, y);    // Şimdi String tipinin durumuna bakalım
    // start_city değişkeni tanımlandığında stack'te bir işaretçi alan ve heap'te de içeriğin tutulduğu alanlar ayrılır
    // stack'te değişken heap'e referans ettiği adres bilgisi, içeriğin uzunluğu(Length) ve yine içeriğin byte cinsinden ne kadar alan tuttuğu(Capacity) bilgileri de yer alır
    let start_city = String::from("London");    let end_city = start_city; // x ve y arasındaki atamaya benzer bir atama yaptık. Farklı olarak stack bölgesinde end_city isimli bir değişken oluşturuldu ve start_city'deki adres, uzunluk ve kapasite bilgileri buraya kopyalandı
                               // yani end_city'de start_city'nin heap'te referans ettiği veriyi işaret etmekte    println!("{}", end_city); // Bu noktada start_city'nin ömrü dolar. Artık sadece end_city geçerlidir                              // println!("City is {}", start_city); // Burada derleme zamanı hatası alınır.
                              /*                                  start_city'yi end_city'ye almak scope dışına çıkıldığında bir hataya neden olur.                                  drop fonksiyonu her iki değişken içinde çalışacağından Double Free hatası oluşur ve bellek güvenliği(memory safety) kaybolur.                                  Bu nedenle Rust aslında start_city'nin stack'teki bilgilerini (adres, uzunluk, kapasite) end_city'ye alırken, start_city'yi de geçersiz kılar.
                                  Ancak yine de istersek heap bölgelerinin de birer kopyasını çıkartabiliriz. Deeply Copy                              */    let name = String::from("niklas");    let copy_of_name = name.clone(); // deeply copy. Artık stack ve heap'te iki ayrı kopya var. Ancak bunun maliyeti yüksektir. Hem temizleme sırasındaki ek operasyon yüzünden hem de programın çalıştığı bellek alanının büyümesi nedeniyle    println!("{} {}", name, copy_of_name);    /*        String gibi Heap kullananlar ile stack'i kullananların fonksiyonlara parametre olarak geçtikleri zamanki duruma bir bakalım.
        Sonrasında stack üzerinde duran ve dahili copy işlemine destek veren türlere(i32 mesela)    */    let words = String::from("blue,red,green,gold,pink");    process_word(words); // burada move işlemi söz konusu yani artık words oyun dışı kaldı
                         //println!("{}", words); // burada derleme zamanı hatası alınır

    let my_lucky_number = 32;    process_number(my_lucky_number); // my_luck_number, fonksiyona kopyalanarak geçti. Yani stack'teki konumu halen daha geçerli    println!("{}", my_lucky_number); // bu nedenle my_lucky_number scope'taki konumunu korumaya devam ediyor    /*        O zaman soru geliyor.        Örneğin bir String değişkeni bir metoda ille de referans olarak geçirmek istersem ne yapacağım?

        find_world_length metodundaki word, atama sonrası quote değişkeninin stack'teki adres alanını referans eden bir değere sahip olur.        sadece adres bilgisini taşır, quote üstünde bir sahipliği yoktur.    */    let quote =        String::from("Zaman su misali akıyor.Engel tanımadan, duraksamdan, geriye dönmeden");    let l = find_word_length("e);    println!("'{}' cümlesinin uzunluğu {} karekterdir", quote, l); // referans türünden taşıma nedeniyle quote hala oyunun içinde(scope dahilinde yani)    /*        Referanslı değişkenlerin mutable olarak kullanılmasında dikkat edilmesi gereken bir nokta var.        Bir referansı mut kelimesi ile mutable yapabiliyoruz ancak aynı scope içinde sadece bir kere yapılabiliyor.
        Yani aşağıdaki kor parçası geçersiz.        your_quote referansını aynı scope içinde mutable olarak iki değişkene almamız kısıtlanmıştır.
        Amaç çalışma zamanında birden fazla pointer'ın aynı bellek adresine erişmesine müsaade etmemektir.        Data Races adı verilen bu durum uygulamanın çalışma zamanında beklenmedik davranışlar sergilemesine neden olur.        Rust bunu henüz derleme aşamasında engellemek ister. O nedenle aşağıdaki kod build olmaz.        Elbette farklı scope'lar kullanarak bu durum aşılabilir.

        Diğer yandan aynı scope'da bir mutable ve n sayıda immutable referansa izin verilmektedir    */    let mut your_quote = String::from("Hımm...");
    let s1 = &mut your_quote;    let s2 = &mut your_quote;    println!("{} {}", s1, s2);
}

fn process_word(word: String) {    println!("{} üstünde işlemler...", word);
}

fn process_number(number: i32) {    println!("{}", number);
}

// parametrenin referans olarak taşınması
// word & bildirimi ile bir sahiplik değil referans beklediğini söyler
// Rust dilinde fonksiyonların referans tipinden parametre almasına Borrowing deniliyor
fn find_word_length(word: &String) -> usize {    // word.push_str(" - Anonim"); // borrowing durumlarında bu kullanıma izin verilmez. Derleme zamanı hatası alınır. Ancak bir istinsa var. word parametresi mutable hale getirilir. (word: &mut String) şeklinde
    word.len()
} // scope dışına çıktığımız yer. word bir sahiplik taşımadığı için metodun çağırıldığı yerdeki quote değişkeni oyunda kalmaya devam eder

Örnekteki borrow of moved hatasına ait ekran görüntüsü aşağıdaki gibidir.

Kendi Struct Veri Türümüz

Rust tarafında kendi veri türlerimizi tanımlarken başvurduğumuz önemli tiplerden birisi struct. Bu örnekte tuple görünümlü struct yazılması, impl blokları ile struct veri yapısına kendi çalışma zamanı örneği ile çalışacak metotlar eklenmesi gibi konulara odaklanıyoruz.

/*    OOP'taki gibi bir varlığı ve niteliklerini tanımlamanın yolu struct veri tipidir
*/

fn main() {    // Product tipinde bir struct nesnesi örnekledik    // Aksi belirtilmedikçe struct türleri de immutable'dır
    // Sonradan içeriklerinde değişiklik yapacaksak mut ile mutable hale getirilmelidir    let mouse = Product {        title: String::from("El Ci Kablosuz Mouse"),        company: String::from("Azon Manufacturing Company"),        unit_price: 44.50,        stock_level: 100,        is_usable: false,    };    write_to_console(mouse); //Ekrana bilgilerini yazıracağımı bir metot kullanayım dedim    // println!("{}", mouse.title);    // mouse.company = String::from("New Company"); // mouse değişkeni mutable tanımlanmadığı için mümkün değildir lakin mutable olsa da kod hata verecektir    let monitor = create_product(        String::from("Filips 24 inch monitor"),        String::from("Northwind Enterteintmant"),        340.50,        45,    );    // Bir struct'ı diğer bir struct içeriğinden yararlanarak oluşturmak da mümkün (struct update sytnax)    let monitor2 = Product {        title: String::from("Soni viewsonic monitor"),        ..monitor // Dikkat! Bu noktada monitor oyun dışı kalıyor(scope dışında). Neden?    };    write_to_console(monitor2);    // Burada da tuple struct kullanımı söz konusu    let persival = Player(String::from("Ready"), String::from("Player One"), 95);    println!("{} {} {}", persival.0, persival.1, persival.2);    /*        Bir struct için tanımlanan metot kullanım örneği.
        struct yapısından değişkenler tanımladıktan sonra o değişken kapsamına dahil olan ilgili metotları çağırabiliriz.
    */    let gudyonsen = Gamer {        play_count: 17,        penalty_point: 12,        ability_rate: 3,    };    println!("{}", gudyonsen.get_level());    println!("{}",gudyonsen.calc_reward());
}

struct Who {} // Yandaki gibi hiçbir alan içermeyen türden strcut ta tanımlanabiliyor. Trait konusunda önem kazanıyormuş. Henüz amacını anlayamadım
              /*                  Birde tuple struct diye bir mevzu var.                  Alan adları(field names) yok dikkat edileceği üzere.                  Bu nedenle alan adlarına 0,1,2 gibi isimler üzerinden erişebiliyoruz.
              */
struct Player(String, String, i16);

// Parametrelerden yararlanarak geriye Product örneği döndüren fonksiyonumuz
fn create_product(title: String, company: String, unit_price: f32, stock_level: i16) -> Product {    /*        metot parametre adları ile struct alan adları aynı olduğu için aşağıdaki gibi bir kullanım mümkün.        yani title:title, company:company gibi atamalar yapmak zorunda değiliz
    */    Product {        title,        company,        unit_price,        stock_level,        is_usable: false,    }
}

fn write_to_console(p: Product) {    println!(        "\n{} ({})\n{} dalır.\nStokta {} adet var.\nŞu an satışta mı? {}",        p.title,        p.company,        p.unit_price,        p.stock_level,        if p.is_usable { "evet" } else { "hayır" }    );
}

// Product isimli bir struct
struct Product {    title: String,    company: String,    unit_price: f32,    stock_level: i16,    is_usable: bool,
}

/*
    Struct veri yapısı için metotlarda tanımlanabilir.
    Ancak tanımlanma şekilleri fonksiyonlardan biraz farklıdır.
    Struct metotları, struct'ın kendi kapsamı içinde tanımlanır.
    Aşağıda Gamer struct'ı için iki metodun nasıl tanımlandığı gösterilmekte.
*/
struct Gamer {    play_count: i16,    ability_rate: i16,    penalty_point: i16,
}

impl Gamer {    fn get_level(&self) -> i16 {        // self ile metodu imlpemente ettiğimiz veri yapısının çalışma zamanındaki örneğini işaret ederiz ki struct metotları &self referansı ile başlamak zorundadır
        return ((self.play_count * 10) - self.penalty_point) + self.ability_rate;        // çalışma zamanındaki değişken değerlerine erişmek için de self. notasyonu üstünden ilerleriz.    }    fn calc_reward(&self) -> String {        return String::from("Müthiş bir ödül kazandın");
    }
}

Enum Veri Türü

Pek çok programlama dilinde enum tipi mevcut. Sayısal olarak ifade edilen sabitleri isimlendirerek kullandığımız tipler olarak düşünebiliriz. Rust dilinde de enum desteği var ama bazen struct'lar yerine de tercih edilebiliyorlar. Öyle ki enum içindeki değişkenler başka veri türlerini ele alarak kullanılabiliyorlar. Enteresan değil mi? Yani bir başka deyişle enum türünü sadece sayılara isimler veren bir tür olarak değil bir veri yapısı şeklinde tanımlayıp kullanabiliyoruz. Pattern Matching ifadelerinden de enum değişkenlerinde pek bir güzel yararlanılabiliyor(Option ile match kullanımı)

// Önce örnek bir enum nasıl tanımlanıyor bakalım
enum TaskSize {    Small,    Medium,    Large,    Xlarge,
}

// Şimdi de yukarıdaki enum sabitini de kullanan bir struct tanımladık
struct Task {    size: TaskSize,    title: String,
}

// Lakin yukarıdaki gibi bir kullanım yerine struct verisini içeren bir enum tipi de tanımlanabiliyor
enum Job {    Small(String, i32), // Parantez içerisindeki String kısımları Task struct'ı içerisindeki title yerine geçiyor. i32 ile de işin büyüklüğünü ifade edebiliriz    Medium(String),    Large(String),    Xlarge(String),
}

// Hatta enum veri yapısındaki değişkenler primitive türler gibi bir struct'ı da kullanabilirler
struct Detail {    title: String,    business_value: i32,
}
enum Action {    Small(Detail), //Action değişkenleri Detail isimli struct veri yapısını içerir    Medium(Detail),    Large(Detail),
}

// Enum veri yapısı her değişkeni farklı sayıda ve türle çalışacak şekilde de tanımlanabilir.
enum Status {    Done,                                      // Bir veri ile ilişkili değil. Standart enum sabiti.    Error { reason: String, impact_size: i8 }, // Error değişkeni anonymous bir struct içerir    Log(String),                               // Log değişkeni ise bir String içerecektir
}
// Yukarıdaki Status isimli veri yapısı struct'lar ile aşağıdaki şekilde de ifade edilebilirdi.
struct StatusDone;
struct StatusError {    reason: String,    impact_size: i8,
}
struct StatusLog(String); //Tuple Struct

/*
    Aynen struct veri yapısında olduğu gibi, enum veri yapısı da kendi metotlarına sahip olabilir.    Bunun için de impl bloğu kullanılır. Örneğin,
*/
impl Action {    fn write_detail(&self) {}
}

/*
    Pek tabii struct veri yapısını kullanırken büyük ihtimalle ortada bir duruma uyan vakalar vardır.
    Hangi enum durumunda neler yapılacağına karar verirken pattern matching'den yardım alabiliriz.    Aşağıdaki enum yapısını ve process fonksiyonunu ele alıp main içerisinde nasıl kullanıldığına bakalım.
*/
enum VehicleEvent {    StartEngine,    StopEngine,    Fire { x: i32, y: i32 }, // Buna C stilinde veri yapısı deniyor (C-Style Structure)
}
fn process(event: VehicleEvent) {    // pattern matchin ile VehicleEvent'in tüm durumlarını ele alıyoruz
    match event {        VehicleEvent::StartEngine => println!("Motor çalıştı"),
        VehicleEvent::StopEngine => println!("Motor durdu"),        VehicleEvent::Fire { x, y } => println!("Araç {}:{} konumuna ateş etti", x, y),    }
}

/*
    Option<T> enum veri yapısı ile etkili pattern matching kodları yazabiliriz.    Aşağıdaki fonksiyon i16 türünden Option değişkeni alıyor. Option enum veri yapısı için değer vardır veya yoktur(None) durumu söz konusu.    Buna göre herhangibir i16 için karesini alacak.
*/
fn square(number: Option<i16>) -> Option<i16> {    match number {        Some(n) => Some(n * n),        None => None,    }
}

fn main() {    // Enum içindeki bir değişken aşağıdaki gibi atanabilir    let small = TaskSize::Small;    // Bir görevi büyüklüğü ile tanımladığımız struct değişkeninin örnek tanımı
    let install_git = Task {        size: TaskSize::Medium,        title: String::from("Ubuntu ortamına git kurulacak"),    };    // Job enum tipinden bir değişkeni de aşağıdaki gibi oluşturabiliriz
    let install_docker = Job::Small(        String::from("Heimdall üstünde Docker kurulumu yapılmalı."),
        5,    );    // Action veri yapısı(ki enum tipidir) değişklenleri Task isimli struct'ı kullanıyor.
    let micro_service = Action::Large(Detail {        title: String::from("Müşteri modülünün mikro servise dönüşümü."),        business_value: 13,    });    /*        Rust dilinde null yoktur. Ancak bazı hallerde verinin o an geçersiz olduğu ifade edilmek de istenebilir.        Rust standart kütüphanesinde yer alan Option<T> isimli enum yapısı bir değerin var olduğunu veya olmadığını belirtmek için kullanılır.
        Standart kütüphanedeki tanımlanma şekli şöyledir.(T, generic türdür)        enum Option<T> {            Some(T),            None,        }        Some herhangi bir türde veri tutabilir. None kullanacağımız zaman tür belirtmemiz gerekir.    */    let one = Some(1);    let not_yet_valid: Option<f32> = None; // None kullanırken (yani null bir şeyler olduğunu ifade ederken) Option<T> ile henüz olmayan ama beklediğimiz verinin türünü de ifade etmemiz gerekir    /*        Yukarıda tanımlı VehicleEvent struct yapısının kullanımına ait örnek kodlar.        process fonksiyonu pattern matchin ile parametre olarak gelen enum değişkenine göre bir aksiyon alınmasını sağlar(Örnekte basit olarak ekrana yazdırdık)
    */    let engine_on = VehicleEvent::StartEngine;    process(engine_on);    let fire_somewhere = VehicleEvent::Fire { x: 10, y: 16 };    process(fire_somewhere);    let engine_of = VehicleEvent::StopEngine;    process(engine_of);    /*        Option<T> ile enum sabiti kullanımı örnekleri.    */    let result = square(Some(10)); // Option<i16> türünden bir değer gönderdik    let none_result = square(None); // Bu durumda square fonksiyonundaki match bloğundaki none koşulu icra olur    let myNum = Some(5);    is_your_luck_day(myNum);    is_your_luck_day(Some(23));    is_your_luck_day(None);
}
/*    Mesela kullanıcı 23 girerse şanslı günündedir. Diğer sayılar içinse değildir.
    23 olma haline Some(23) ile kontrol edebiliriz. Diğer haller içinse _ operatörü kullanılır
*/
fn is_your_luck_day(number: Option<i16>) {    // match number {    //     Some(23) => println!("Şanslı günündesin"),    //     _ => println!("{:?} Büyük talihsizlik", number), // Option ile gelen değeri yazdırmak için :? söz dizimini kullandım
    // }    // Bu arada yukarıdaki ifade şu şekilde de yazılabilir
    if let Some(23) = number {        println!("Şanslı günündesin")    } else {        println!("{:?} Büyük talihsizlik", number)    }
}

Koleksiyonlar

Her ne kadar Rust'ın built-in pek çok veri tipi stack bellek bölgesini kullanıyor olsa da koleksiyonlardaki gibi heap'de tutulan, dolayısıyla derleme zamanında ne kadar yer tutacağının bilinmesine gerek duyulmayan veri tipleri de mevcuttur. Koleksiyon türlerinin kabiliyetleri farklılık göstermekle birlikte duruma göre tercih edilirler.  Rust dilinde en sık kullanılan koleksiyonlar belli türden değişkenlerden oluşan vector(minions'daki vector mü acaba :D ), karakter katarı koleksiyonu olan string ve key-value düzeninde çalışan hash map'tir.

use std::collections::HashMap; // HashMap kullanabilmek için eklendi

fn main() {    /*        vector tipi ile başlayalım.
        İlk satırdaki tanımlanma şeklinden de anlaşılacağı üzere vector generic bir koleksiyondur.        Sadece <T> ile belirtilen türde elemanlar barındırır.

        Bir vector'ü new ile tanımlayabileceğimiz gibi macro ile de tanımlayabiliriz (! işareti olan fonksiyonlar)        Tahmin edileceği üzere vector türü de varsayılan olarak immutable'dır.
        Bu nedenle colors isimli vector'e push metodu ile yeni elemanlar ekleyebilmek için,        mut ile mutable olarak işaretlenmesi gerekmiştir.
    */    let points: Vec<i32> = Vec::new(); // Şu anda i32 türünden elemanlar taşıyacak bir vector koleksiyonu tanımladık

    {        // Elbette scope kanunları vector türü için de geçerlidir        let mut colors = vec!["red", "green", "blue"]; // bu durumda vector'ün kullandığı tip sağ tarafa göre tahminlenir(infer)        colors.push("purple"); //push sona eleman ekler        colors.push("yellow");        colors.push("pink");        let last = colors.pop(); // pop ile son eklenen eleman elde edilir. aynı zamanda koleksiyondan da çıkartılır
        println!("{:?}", last);    } // şu andan itibaren colors ve içeriğindeki tüm veriler bellekten atılmıştır (drop)    // iterator dizileri kolayca bir vector'e alınabilirler
    let mut numbers: Vec<i32> = (10..20).collect();    let x = numbers[5]; // vector içindeki herhangi bir elemana indis değeri üstünden erişebiliriz
    println!("{}\n", x);    // iter fonksiyonundan yararlanarak vector elemanları kolayca dolaşılabilir
    // for n in numbers.iter() {    for n in &numbers {        // & operatörü ile vector referansını elde edip for ile ilerleyebiliriz        print!("{},", n);    }    println!("\n");    /*        Eğer iterasyon sırasıdan koleksiyon elemanlarında değişiklik yapmak isterse iter_mut fonksiyonundan yararlanabiliriz        Tabii aşağıdaki kodun çalışabilmesi için numbers isimli vector'ün değişikliğe izin vermesi de gerekir.        Bu nedenle numbers mut ile mutable hale getirilmiştir
    */    for n in numbers.iter_mut() {        *n += 10; // vector'de o an referans edilen değeri değiştirmek için *(dereference) operatörünü kullanıyoruz
    }    println!("{:?}", numbers);    /*        vector'leri pattern matching tadından aşağıdaki gibi değerlendirebiliriz.
        get ile 1nci indisi ele alıyoruz.
        get fonksiyonu Option<T> döndürdüğü için Some, None durumlarını ele alabiliriz.    */    match numbers.get(1) {        Some(21) => println!("1 indisine denk gelen eleman {}", numbers[1]),        None => println!("Hayır değil"),
        _ => println!("Diğerleri için bir şey yapmaya gerek yok"), // 21 olma ve olmama durumu haricinde diğer durumları da kontrol etmemiz beklenir. Buraya yazmazsak derleme zamanı hatası alırız.
    };    /*        vector türü tek tiple çalışacak şekilde tanımlanmıştır.
        Eğer farklı veri türlerinden bir nesne koleksiyonu olarak kullanmak istersek enum veri yapısını kullanabiliriz.        Product enum veri yapısını bu amaçlar ele alabiliriz.        Eğer çalışma zamanında vector'ün tutacağı veri türleri belli değilse enum yerine trait nesneleri kullanabiliriz.    */    let data_row = vec![        Product::Id(1001),        Product::Title(String::from("12li Su Bardağı")),
        Product::ListPrice(12.90),    ];    /*        Gelelim Rust standart kütüphanesi ile birlikte gelen diğer bir koleksiyon olan String'e.        String'i aslında byte'lar koleksiyonu olarak düşünmek daha doğru olabilir.        String'in birkaç oluşturulma şekli var. Örneğin new ile tanımlanıp literal bir string üstünden to_string çağrısı ile        ya da from fonksiyonu ile üretilebililir.        String veri türü UTF-8 formatında kodlanmış içerikleri kullanabilir. Bu sebepten whatsup değişkeninde olduğu gibi pek çok dili destekler.        String'leri birleştirmek veya bir String'e başka bir String parçası eklemek için push_str ve        tek bir karakter eklemek için push fonksiyonlarını kullanabiliriz.        Tabii + operatörü de String'leri birleştirmek için kullanılabilir.
        Çok fazla birleştirilecek String varsa + operatörü (ki add fonksiyonuna karşılık gelir) yerine,        format! isimli macro'yu kullanmak daha uygundur.    */    let mut aloha = String::new(); // aşağıda değerini değiştireceğimiz için mutable yaptık
    let incoming_data = "Alofortanfane";    aloha = incoming_data.to_string();    println!("{}", aloha);    let raining_day = String::from("Một ngày mưa.");
    println!("{}", raining_day);    let mut quote = String::from("Siz ne kadar veri üretirseniz");    quote.push_str(", organize suç örgütleri de o kadar tüketir");    quote.push('!');    println!("{}", quote);    quote.push_str(" Marc Goodman - Geleceğin Suçları");
    println!("{}", quote);    /*        + operatörünü kullandığımızda & ile referans adreslerine ulaşmamız gerekir.        Bunun sebebi aslında + operatörünün işaret ettiği add metodunun (fn add(self, s: &str) -> String şeklinde yazılmıştır)
        &str şeklinde referans istemesidir.    */    let s1 = "Ne".to_string();    let s2 = String::from("güzel");    let s3 = String::from("bir");    let s4 = String::from("gün!");    let last_word = s1 + " " + &s2 + " " + &s3 + " " + &s4; //s1'e sırasıyla s2, s3 ve s4 değişkenlerinin referans adresleri eklendi    println!("{}", last_word);    let black = String::from("black");    let white = String::from("white");    let black_and_white = format!("{} {} {}", black, "or", white);    println!("{}", black_and_white);    /*        String veri türünde uzunluk aslında kullanılan karakterlerin byte olarak kapladıkları yere göre değişir.
        Eğer Unicode karakter varsa bu UTF-8 kodlaması sonucu 2 byte olarak ele alınır ve uzunluk değişir.
        Belki de bu sebepten ötürü String türünde indis operatörü kullanılamaz.
    */    let siyah = "đen";
    println!(        "Siyah vietnamca `{}` olarak yazılır. Rust için uzunluğu {}. Halbu ki sen 3 karakter saydın :)",        siyah,        siyah.len()    );    // let second = siyah[1]; // the type `str` cannot be indexed by `{integer}` hatası döner    /*        Bir String içinden belli bir dilimi almak (slice) mümkündür ancak dikkat etmek gerekir.        Çünkü denk gelen byte bir karakter olarak ifade edilemeyebilir.        Aşağıdaki kod parçası derlenecektir ama çalışma zamanında bir panic oluşacaktır.
        thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'đ' (bytes 0..2) of `đen`'
    */    // let a_bit_off_word = &siyah[0..1];    // println!("{}", a_bit_off_word);    /*        String içerisindeki karakterleri veya byte'ları dolaşmanın en güzel yolu chars ve bytes fonksiyonlarından yararlanmaktır
    */    println!();    let rusca_bir_seyler = String::from("Добрый день умереть.");

    for c in rusca_bir_seyler.chars() {        print!("{} ", c);    }    println!();    for b in rusca_bir_seyler.bytes() {        print!("{} ", b);    }    println!();    /*        String ile başka neler yapabiliriz bakalım.
        Mesela String içindeki karakterleri bir vector'e indirebiliriz.    */    let char_vector: Vec<char> = rusca_bir_seyler.chars().collect();    for c in char_vector {        println!("`{}` ", c);    }    /*        Biraz da key:value mantığında çalışan Hash Maps türüne bakalım.
        HashMap<Key,Value> şeklinde bir generic tip olarak düşünebiliriz sanırım.

        Yeni elemanlar eklemek için insert fonksiyonunu kullanabiliriz.        Bir key'in karşılığı olan value içeriğini değiştirmek için de yine insert fonksiyonu kullanılabilir.
    */    let mut agent_codes = HashMap::new();    agent_codes.insert(7, String::from("James Bond"));    agent_codes.insert(23, String::from("Jean Claude Van Damme"));    agent_codes.insert(66, String::from("Lord Vather"));    agent_codes.insert(32, String::from("Larry Bird"));    agent_codes.insert(32, String::from("Ervin Magic Jhonson")); // Aynı key üstüne yeni değeri yazdık

    // agent_codes.remove(&7); // key:7 olan satırı HashMap'ten çıkartmış olduk    // key değeri 7 olan value içeriğini almak için aşağıdaki gibi ilerleyebiliriz    let key7 = 7;    let bond = agent_codes.get(&key7); // Option<T> döndürür    println!("{:?}", bond);    // HashMap içindeki key:value çiftlerine aşağıdaki gibi erişebiliriz
    // Bu arada liste hashcode değerlerine göre sıralanır
    for (k, v) in agent_codes {        println!("{} {}", k, v);    }    /*        Bir HashMap'in key:value değerleri vector'lerden de oluşturulabilir.
        Aşağıdaki stat ve beğeni oranlarının tutulduğu HashMap nesnesi,        iki farklı vector ile oluşturulmuştur.
    */    let stads = vec![        String::from("Jüseeppe Meyaza"),        String::from("Vodafon Park"),        String::from("Noy Kamp"),        String::from("Stat dö fırans"),
    ];    let fun_scores = vec![58, 90, 95, 72];    let stad_fun_scores: HashMap<_, _> = stads.into_iter().zip(fun_scores.into_iter()).collect();    for (stad, score) in stad_fun_scores {        println!("{}:{}", stad, score);    }    // println!("{:?}", stad_fun_scores); // Yukarıda for döngüsünde kullandığımız için stad_fun_scores artık scope dışında kaldı. Dolayısıyla bu satır derleme zamanı hatası verir
}

enum Product {    Id(i32),    Title(String),    ListPrice(f32),
}

Hata Yönetimi(Error Handling)

Rust yönetimli bir dil olmadığından aşina olduğumuz gibi bir Exception Manager sistemi bulunmuyor. Bir nevi tek başınayız diyelim. Ancak bu bir dezavantaj olarak görülmemeli. Lakin daha titiz ve dikkatli(defansif diyebiliriz belki) olmaya zorluyor.

use std::fs::File;
use std::io;
use std::io::Read;

/*
    Hata yönetimi.    Rust dili hataları iki kategoriye ayırıyor. Kurtarılabilir olanlar(recoverable) ve tabii ki kurtarılabilir olmayanlar(unrecoverable)    Managed bir ortam olmadığını da biliyoruz. Dolayısıyla bir exception yönetim sistemi de bulunmuyor.    Kurtarılabilir hatalarda kullanıcının uyarılması ve yeniden deneme yapılması mümkün.    Kurtarılamayan hatalar ise tipik olarak çalışma zamanı bug'ları gibi düşünülüyor.    Rust, kurtarılabilir hataların kontrolü için Result<T,E> tipini değerlendirmekte.
    Kurtarılamayan hatalar ise aslında ortamda bir panik havasının esmesi ve programın çalışmasının durması demek. Bu noktada,    panic! makrosu ile karşılaşıyoruz. Hiç beklenmeyen ve geliştiricinin öngöremediği bir hata oluştuğunda çalışan panic! makrosu    stack'i de temizleyip programın bir hata mesajı ile sonlanmasını sağlıyor.

    Winding: panic! makrosu çalıştığında rust ortamı çağırılan ne kadar fonksiyon varsa bunları takip ederek stack üzerinde bellek temizleme operasyonu icra eder.    Tahmin edileceği üzere bu operasyon maliyetlidir. Eğer üretim ortamı dosyası hafifse winding devre dışı bırakılabilir ki buna Unwinding deniyor.    Geliştirici olarak hangi durumda tekrar deneme yaptırılması yani hatadan dönülmeye çalışılması ve hangi durumda sürecin durdurulmasının kararını verebilmek gerekiyor.

*/
fn main() {    // #1 Kendimiz de panik havası estirebiliriz    // analyse_nickname(String::from("bam-bam"));    // analyse_nickname(String::from("fck"));    // #2    // a_little_bit_panic(); // Yukarıdaki ikinci çağrım nedeniyle zaten bir panic oluştu ve program sonlandı. Dolayısıyla bu satır işletilmez

    // #3    // Aşağıdaki çağrı Propagating Error senaryosu için örnektir.    let r = load_file(String::from("./Crgo.toml")); //./Cargo.toml ile de deneyin. Yani var olan metinsel bir dosyanın da okunabiliyor olması lazım
    match r {        Ok(content) => println!("{}", content),        Err(e) => println!("Dosya okumada hata -> {}", e),    }    println!("\nYine biz işimize bakalım...\n");

    // Şimdi burada ? operatörünün kullanıldığı çok daha kısa kod bloğu içeren fonksiyonu kullandık
    let ct = load_file_content(String::from("nowhere.txt"));    match ct {        // Yine dönen içeriği ele aldık
        Ok(s) => println!("{}", s), //Hata yoksa dosya içini ekrana basıyor
        Err(e) => println!("Hata Bilgisi -> {}", e), // hata varsa error bilgisini yazdırıyoruz
    }    println!("\nHatayı kontrol altında tutuyoruz\n");    /*        #4 Result<T,E> tipinin kullanışlı iki fonksiyonu vardır (unwrap ve except)        unwrap, işlem başarılı ise Ok içinde ne dönmesi gerekiyorsa onu döner ve bir hata durumunda otomatik panic! makrosunu tetikletir.        match deseni ile uğraşmaya gerek kalmaz.    */    // let cargo_file = File::open("./Cargo.toml").unwrap(); // eğer dosya varsa File nesnesini döndürür.    // let unknown_file =    //     File::open("olmayan.txt").expect("Bu dosya sistemde yok veya bozulmuş olabilir.");    // // panic! makrosu çalışması halinde burada yazdığımız mesaj trace içeriğine alınacaktır.

    // #5 Minik kod kırıntıları
    let number = String::from("123"); // string değerin kullanıcıdan geldiğini düşünelim    let numeric = number.parse::<i32>().unwrap(); // number i32'ye dönüştürülebiliyorsa numeric'e gelir, aksi durumda panic! çalıştırılır
    println!("{}", numeric * 3);    let levels = vec!["100", "200", "300", "Dörtyüz", "500", "Altıyüz"]; // Şimdi bu vector içeriğini i32'ye parse etmek istediğimizi düşünelim (Dörtyüze ve Altıyüze dikkat)                                                                         // hataya neden olan kısımları dışarıda bırakıyoruz
    let numeric_levels: Vec<_> = levels        .into_iter() //vector için bir iterasyon çektik        .map(|s| s.parse::<i32>()) // değerlerin her biri parser fonksiyonu ile i32'ye dönüştürülmeye çalışıyor
        .filter_map(Result::ok) // bazı dönüşümler Error verecektir. Sadece Result<T,E> den Ok dönenleri        .collect(); // topluyoruz    println!("Results: {:?}", numeric_levels); // ve ekrana basıyoruz
}

/*
    Fonksiyon, parametre olarak gelen dosyası açıp içeriğini geri döndürmek istemekte.    Ancak sistemde olmayan bir dosya da söz konusu olabilir.    Burada early return adı verilen hata kontrol senaryosu ele alınıyor. Yani bir hata oluştuğunda bunun bilgisi çağıran yere döndürülüyor.    panic! çağrısı yerine hata mesajını object user'a veriyoruz.
*/
fn load_file(file_name: String) -> Result<String, io::Error> {    let f = File::open(file_name); // dosyayı açmaya çalışıyoruz

    // Pattern Matching ile Result<T,E> sonucuna bakıyoruz.
    let mut f = match f {        Ok(file) => file, // Dosya açılabildi, her şey yolunda. Aşağıda içeriğini okuyacağız
        Err(error) => return Err(error), // Error oluştu ve bunu fonksiyonu çağırdığımız yerde ele alabiliriz    };    let mut content = String::new();    // şimdi dosya içeriğini okumaya çalışıyoruz ve yine hata olma durumunu ele alıyoruz
    match f.read_to_string(&mut content) {        Ok(_) => Ok(content), // sorun yok ve Ok ile dosya içeriğini geriye dönüyoruz        Err(error) => return Err(error), // sorun var geriye hata bilgisini verelim    }
}

/*
    Yukarıdaki dosya okuma ve içeriğini döndürme fonksiyonunun çok daha kısa hali aşağıdaki gibi.    Ama tabii burada olmayan veya içeriği okunamayacak dosyalar Error dönecektir    ? operatörünün kullanımına dikkat.
*/
fn load_file_content(file_name: String) -> Result<String, io::Error> {    let mut content = String::new();    File::open(file_name)?.read_to_string(&mut content)?;    Ok(content)
}

fn analyse_nickname(message: String) {    if message == "fck" {        panic!("Hey dostum, ne dediğinin farkında mısın?"); // Programı burada sonlandırıyoruz.
    } else {        println!("Bana `{}` dedin", message);    }
}

fn a_little_bit_panic() {    let points = vec![0..10]; // burada bir vector dizisi oluşturduk
    println!("{:?}", points[11]); //ve burada da 11nci elemana ulaşmak istedik ki yok. Bu satırda panic! makrosu devreye girecektir
}

ve çalışma zamanı. Önce bir kuple panic! havası,

ardından Result<T,E> ile olayı kontrol altında tutma çabası.

Generics

<T> .Net dünyasından aşina olduğumuz bir konu. Generic veri türleri özellikle kod tekrarının azaltılması noktasında çok işe yarıyor. Rust dilinde ağırlıklı olarak bu amaçla kullanılmakta.

/*    sum_of_two isimli fonksiyonu ele alarak konuyu irdeleyelim.    fonksiyon i16 tipinden iki sayıyı alıp toplamını geriye döndürüyor.
*/

use std::ops::Add;

fn sum_of_two(x: i16, y: i16) -> i16 {    return x + y;
}

fn main() {    // #1    let r1 = sum_of_two(1, 6);    println!("{}", r1);    /*        Şimdi bu fonksiyonu aşağıdaki gibi çağırmayı denersek i16 türünden parametre beklediğine dair derleme zamanı hatası ile karşılaşırız.
        Çözüm olarak sum_of_two'nun f32 türü ile çalışacak bir versiyonunu yazabiliriz ama bu kod tekrarının en canlı örneği olur.        Bunun yerine generic bir fonksiyon da geliştirebiliriz (yani sum fonksiyonu)    */    //let r2 = sum_of_two(1.2, 6.4);    let r3 = sum(19, 4);    let r4 = sum(3.14, 2.56);    println!("{}\n{}", r3, r4);    // Generic strcut kullanımı örneği
    let cmp1 = Complex { r: 18, v: 1.56 };    println!("{}+{}i", cmp1.r, cmp1.v);    let cmp1 = cmp1.change(); // let ile yapılan atamayı kaldırdığınızda aşağıdaki satır için bir hata alacaksınız. Sizce sebebi ne olabilir?    println!("{}+{}i", cmp1.r, cmp1.v);
}

/*
    Generic fonksiyon örneği.
    sum fonksiyonu T türünden parametreler ile çalışıp yine T türünden sonuç döndürecek şekilde yazıldı.
    Ancak dikkat edilmesi gereken bir nokta var.    T'nin tanımlanmasında Add şeklinde başka bir ifade daha yer almaktadır. Buradaki Add bir Trait'tir.    T tipinin sahip olması gereken bir davranışı(iki T nin toplanabilmesi özelliğini) belirtiyoruz.    Eğer Add Trait'ini kullanmazsak T'nin T'ye eklenemeyeceğine dair bir hata mesajı alırız.
        Trait'leri traits isimli örnekte ele alıyoruz.
*/
fn sum<T: Add<Output = T>>(x: T, y: T) -> T {    return x + y;
}

/*
    #2    Pek tabii bir struct içinde de ve hatta struct'a ait metotlarda da generic yaklaşımı kullanılabilir.
    Aşağıdaki Complex isimli struct'ın alanları T ve U türündendir. Ne atarsak o.    Complex sınıfına entegre edilen change metodu kompleks sayının gerçel ve sanal köklerinin yerini değiştirip yeni bir Complex türünü geriye döndürmektedir
*/
struct Complex<T, K> {    r: T,    v: K,
}

impl<T, K> Complex<T, K> {    fn change(self) -> Complex<K, T> {        Complex {            r: self.v,            v: self.r,        }    }
}

Görüldüğü üzere generic türler kod tekrarının önüne geçmekte sıklıkla kullanılabilir. Struct ve ona uygulanan metotlar generic tasarlanabilir. Örnekte kompleks sayıların toplamı için bir trait bildirimine yer verilmiştir. Aslında var olan Add isimli trait(ki bir sözleşme tanımlar) generic Complex veri türü için yeniden programlanmıştır. Bir trait ile struct türleri için ortak davranış sözleşmeleri bildirebiliriz(tam olarak interface değil, tam olarak abstract sınıf da değil. Değişik bir şey :D ) 

Trait

Yeri gelmişken trait konusuna da kısaca bir deyinelim. Nesne yönelimli programlama tarafından gelen birisi için interface tipine benzetilebilir. Esasında struct türlerinin sahip olması istenen davranışları belirten metotların tanımlandığı bir sözleşmedir. Yani metotların neye benzeyeceğini tanımlar ve ortak bir deklarasyon sunar. Diğer yandan iş yapan fonksiyonlar da içerebilir. Bu açıdan da abstract sınıflarla benzerlik gösterir. Rust standart kütüphanesi birçok trait tanımı içerir. Add, Copy, Clone, Eq vb Bu davranışlar tahmin edileceği üzere kendi veri yapılarımız için yeniden programlanabilir(Üstteki kompleks sayı aritmetiğini hatırlayın) Konuyu aşağıdaki kod parçası ile biraz daha detaylı analiz edebiliriz. 

use std::ops; // + operatörünü tekrardan programlamak için eklendi (#4ncü örnek)

/*
    #1    Action isimli bir trait.    İçinde iki fonksiyon tanımı yer alıyor.

    Takip eden iki struct bu trait içerisindeki fonksiyonları kendilerine göre uyarlıyorlar.
*/

trait Action {    fn initialize(&self, x: i32, y: i32); // Trait fonksiyonları &self parametresine sahip olmalıdırlar. Elbette, başka parametreler de içerebilirler ve geriye döndürebilirler.    fn click(&self) {        println!("varsayılan bir click davranışı olsun diyelim"); // Varsayılan bir davranış icra ettik. Eğer click ezilirse(override) burası devreye girmez    }
}

struct Button {    name: String,
}
struct Hyperlink {    url: String,
}

impl Action for Button {    // Button struct'ı için Action trait'inin uygulanacağını söylüyoruz ancak sadece initialize metodunu ezdik.    // Tabii click fonksiyonunun varsayılan bir kod bloğu olmasaydı onu da burada ezmek zorundaydık
    fn initialize(&self, x: i32, y: i32) {        println!(            "{} isimli düğme {}:{} noktasında oluşturuldu",
            &self.name, x, y        );    }
}

impl Action for Hyperlink {    // Benzer şekilde Hyperlink struct'ı için de Action trait'inde belirtilen metotların uygulanacağının söylüyoruz    fn initialize(&self, x: i32, y: i32) {        println!("{} link kontrolü {}:{} noktasına eklendi", &self.url, x, y);    }    fn click(&self) {        println!("Linke basılırsa {} adresine gidilir", &self.url);    }
}

fn main() {    let submit = Button {        name: String::from("btnSubmit"),    };    let go_home = Hyperlink {        url: String::from("https://www.buraksenyurt.com"),    };    submit.initialize(10, 20);    submit.click();    go_home.initialize(15, 30);    go_home.click();    /*        #2        Şimdi gelelim trait'lerin güzel kullanımlarından birine.        Yukarıdaki kullanım çok anlam ifade etmiyor çünkü.        Bu nedenle on_load fonksiyonuna odaklanalım. Parametre olarak Action trait'ini uygulayan tipleri kabul etmekte.        Dolayısıyla Action trait'ini implement eden struct değişkenlerini aynı fonksiyonu içinde ele almamız mümkün.    */    on_load(&submit, 10, 20);    on_load(&go_home, 20, 20);    /*        #3        Tabii bunun üzerine akla, "e o zaman trait türünü kullanan vector tanımlayıp n adet struct için aynı operasyonu tetikleyelim" düşüncesi gelir        Lakin trait'lerin boyutu yoktur ve bu nedenle bellekte ne kadar yer tutacakları bilinemez. Dolayısıyla düşündüğümüzü yapmak biraz beyin yakar.    */    println!("");    let main_page = Hyperlink {        url: String::from("azondot.com"),    };    let controls: Vec<Box<dyn Action>> = vec![        Box::new(Button {            name: String::from("help_me"),        }),        Box::new(main_page),        Box::new(Button {            name: String::from("next_page"),        }),    ]; // Box struct'ı heap'teki yer ayırımları için bir referans sunar.    prepare(controls);    /*        #4 Operator Overloading        C# taki gibi Rust dilinde de bilinen operatörleri yeniden programlayabiliriz.        Örneğin kompleks sayıları temsil eden bir struct için + operatörünü yeniden programlamak istediğimizi düşünelim.        + operatörünün karşılığı olan trait'i (Add) bu struct için yeniden programlamak yeterli olacaktır.
    */    let cx1 = Complex { x: 1.23, y: 2.56 };    let cx2 = Complex { x: 0.45, y: -4.89 };    let cx3 = cx1 + cx2;    println!("{} + ({})i", cx3.x, cx3.y);    /*        #5 Operator Overloading(drop)        Bu arada değişkenlerin scope dışına çıktıları zaman devreye giren ve bellek boşaltma işini üstlenen drop'da bir trait'tir ve yeniden programlanabilir.    */    let london = MongoConnection {        server: String::from("localhost"),        port: String::from("3001"),    };    println!("{}:{}...", london.server, london.port); // london değişkenini kullandık ve scope dışında kaldı. Yazdığımız drop metodu devreye girecek
}

/*
    prepare fonksiyonu Action trait'ini uyarlayan yapılardan oluşan bir vector kabul eder.    Bu sebeple Button ve Hyperlink nesnelerini içeren bir vector dizisini parametre olarak verip herbiri için aynı fonksiyonun çalıştırılmasını sağlayabiliriz.
    (Polymorphsym olabilir mi? Bir düşünelim)
*/
fn prepare(controls: Vec<Box<dyn Action>>) {    let mut x = 5;    let y = 10;    for c in controls.iter() {        // parametre ile gelen nesnelerin initialize fonksiyonu çalışır. Override edilmiş sürümleri        c.initialize(x, y);        x += 5;    }
}

fn on_load<T: Action>(control: &T, x: i32, y: i32) {    control.initialize(x, y);
}
/*    Aşağıda on_load'un ilk versiyonu var.    Yukarıdaki ise Trait Bound Syntax adı verilen sürümü. Bu versiyon tercih edilirse on_load'u çağırdığımız yerlerde Action değişkenleri için & kullanmamız gerekir.
*/
// fn on_load(control: impl Action, x: i32, y: i32) {
//     control.initialize(x, y);
// }

struct Complex {    x: f32,    y: f32,
}

// Complex struct'ı için Add operatörünü yeniden programlıyoruz
impl ops::Add for Complex {    type Output = Self; // Kendi türünü döndüreceğini söylüyoruz ki bu Complex tip oluyor    // add operasyonunu yeniden tanımlıyoruz
    fn add(self, c2: Complex) -> Self {        Self {            x: self.x + c2.x,            y: self.y + c2.y,        }    }
}

/*
    #5 için kullanılan kobay struct ve drop uyarlaması.
    Mesela oluşturduğumuz MongoConnection nesnesi scope dışına çıktığında yapılmasını istediğimiz özel bir şeyler varsa,    drop trait'inin yeniden programlayarak gerçekleştirebiliriz.
*/
struct MongoConnection {    server: String,    port: String,
}

impl Drop for MongoConnection {    fn drop(&mut self) {        println!(            "{}:{} için belki bağlantı sonlandırma işini üstlenebiliriz.",            self.server, self.port        );    }
}

İşte çalışma zamanından bir görüntü.

Lifetimes

Rust dilinde tüm referans türlerinin bir yaşam ömrü(lifetime) vardır. Değişkenlerde sıklıkla gündeme gelen scope kavramı ile lifetime birbirlerine benzer ama aynı şey değildirler. Bir fonksiyon lifetime referans ile dönüyorsa parametrelerinden en az birisinin de lifetime referans olması gerekir ve struct yapılarında referans türlü alanlar varsa lifetime annotation kullanmak gerekir. Konuyu aşağıdaki üç farklı örnekle inceleyeceğiz.

fn main() {    /*        #1         Önce lifetime nerede devreye girer anlamak lazım.
        Aşağıdaki kod parçasını ele alalım. İç içe iki scope var.        Bu kod derlenmeyecektir.    */    {        // ana scope        let number; // henüz hiçbir şey atamadığımız bir değişken

        {            // iç scope            let stranger_thing = 1;            number = &stranger_thing; // ve number değişkenine iç scope'daki stranger_thing değişkeninin referansını atadık
        } // sorun şu ki tam bu noktada stranger_thing'in ömrü doldu.        println!("{}", number); // ve bu nedenle number'ı kullanmak istediğimizde(ki halen ana scope içinde olduğu için kullanılabilir) `stranger_thing` does not live long enough şeklinde derleme zamanı hatası alırız
        // bu derleme hatasının sebebi basittir. number, artık serbest kalmış bir bellek adresini kullanmaya çalışmaktadır

        // Rust derleyicisi yukarıdaki senaryoda kapsamları kontrol ederken Borrow Checker isimli bir tekniğe başvurur
    }
}

Yukarıdaki örnek derlenmeyecektir ve aşağıdaki görüntüde yer alan hata mesajını verecektir.

Lifetime noktalarını daha iyi anlamak için aşağıdaki düzeni göz önüne alabiliriz.

fn main() {    /*        lifetime noktalarını daha iyi anlamak için şu kod parçasına bakalım.
        x ve y, en fazla yaşam ömrü olan number değişkeninin referansını kendi yaşam süreleri boyunca ödünç alıp kullanıyorlar.
    */    let number = 3.14; //------------------------------------> number lifetime start    {        let x=&number; //--------------------> x lifetime start        println!("{}",x);    }//--------------------------------------> x lifetime end    {        let y=&number; //--------------------> y lifetime start        println!("{}",y);    }//--------------------------------------> y lifetime end
} //----------------------------------------------------------> number lifetime end

Şimdi biraz daha zihnimizi yakalım ve generic lifetime parametreleri konusuna bakalım. İzleyen kod parçasında yer alan find_winner isimli fonksiyon Player tipinden iki referansı parametre olarak alır ve geriye yine bir Player referansı döndürür.
find_winner fonksiyonunun parametre olarak gelen Player değişkenlerini sahiplenmesini istemediğimizi düşünelim. Bu nedenle referans olarak geçmekteyiz. Lakin Rust derleyicisi ve özellikle Borrow Checker mekanizması bir kafa karışıklığı yaşayacaktır. p1'in mi yoksa p2'nin mi geriye döneceği belli değildir. Bu durumda find_winner'dan dönecek Player referansının(p1 veya p2 olabilir) ne kadar süre yaşaması gerektiği de belli değildir. p1'inki kadar mı ömrü olmalıdır, yoksa p2'ninki kadar mı? Bu durum derleyicinin "explicit lifetime required in the type of `p2`" benzeri bir hata uyarısı vermesi ile devam eder. Olayın önüne geçmek için generic lifetime parametrelerini kullanmak gereki. Böylece referanslar arası yaşam süreleri için bir ilişki kurulabilir.

struct Player {    nick_name: String,    total_point: i32,
}

// // lifetime hatası veren versiyon
// fn find_winner(p1: &Player, p2: &Player) -> &Player {
//     if p1.total_point > p2.total_point {
//         return p1;
//     } else {
//         return p2;
//     }
// }

/*
    'l lifetime'ın adıdır ve &'l Player, Player referansı için 'l kadarlık bir yaşam ömrü belirttiğimizi ifade eder.    Bir başka deyişle referansın yaşam ömrünü açık bir şekilde belirtmiş oluruz.    Bu yeni sürümde p1, p2 ve geriye dönen Player dahil olmak üzere 3 referansta aynı yaşam sürelerine sahiptir.
*/
fn find_winner<'l>(p1: &'l Player, p2: &'l Player) -> &'l Player {    if p1.total_point > p2.total_point {        return p1;    } else {        return p2;    }
}

fn main() {    let gustavo = Player {        nick_name: String::from("Gustavo"),        total_point: 18,    };    let mikel = Player {        nick_name: String::from("Mikel"),        total_point: 17,    };    let winner = find_winner(&gustavo, &mikel);    println!("Kazanan `{}`", winner.nick_name);    /*        #2 Aşağıda yine enteresan bir yaşam ömrü sorunsalı yer almaktadır.
        schumi ve race_winner iç scope dışında tanımlıdır. Toplam puanlara baktığımızda kazanan schumi'dir ve dolayısıyla,
        #İlginç yazan yerde race_winner, schumi'nin referansını taşıyacağı için bir sorun olmaması beklenmektedir.        Ne var ki find_winner fonksiyonu parametreleri ve geriye dönen Player referansı için aynı yaşam süresini beklemektedir.        Koda göre #İlkÇıkış noktasında hakinen'in ömrü dolmaktadır. Yani schumi, hakinen ve kazanan için aynı yaşam döngüsü kuralı bozulmuştur.
        Bu nedenle derleyici aşağıdaki kod parçası için `hakinen` does not live long enough diyecektir.    */    let schumi = Player {        nick_name: String::from("Schumi"),        total_point: 77,    };    let race_winner;    {        let hakinen = Player {            nick_name: String::from("hakinen"),            total_point: 60,        };        race_winner = find_winner(&schumi, &hakinen);    } // #İlkÇıkış
    println!("Yarışın kazananı {}", race_winner.nick_name); // #İlginç
}

struct Game<'l> {    // color_name: &str, // struct türünde referans türlü alanlarda kullanabiliriz ancak bu şekilde değil. lifetime bildirimi ile kullanabiliriz    color_name: &'l str,    max_player: i32,
}

Birim Test

Aslında en başında her şeye test ile başlamamız gerekirdi. Çalışmakta olduğum Claus Matzinger'in Packt çıkışlı Rust Programming Cookbook kitabı daha ilk bölümden itibaren her şeyi test fonksiyonları ile birlikte ele alıyor. Hatta main fonksiyonu hiç yok diyebilirim. Sadece kütüphaneler ve birim testler var. Rust tarafında yeni bir kütüphane oluşturulduğunda otomatik olarak tests isimli bir modül de oluşturulur. Tüm birim testlerini bu modül içerisinde oluşturabiliriz. Zaten geliştireceğimiz kütüphanelerin beraberinde test modülü ve birim testleri ile birlikte yazılması kod kalitesi, temiz kod ve kod güvenilirliği açısından çok önemlidir.  

/*    Basit Unit Test yazmak    cargo new testing --lib    terminal komutu ile bir kütüphane açtığımızda içerisine otomatik olarak tests isimli bir modül açılır.

    test etmek için terminalden    cargo test    komutunu çalıştırmak yeterlidir.    Test fonksiyonları fail durumuna düştüğünde Rust çalışma zamanı bir panik havası estirir.
*/
#[derive(Debug)]
struct Player {    nick_name: String,    current_point: i32,    attendance: i32,
}

impl Player {    #[allow(dead_code)]    fn calculate_score(&self, _median: f32) -> f32 {        // 0.0 // Birinci durum        ((self.current_point * self.attendance) as f32) * _median    }
}

/*
    İçinde bilinçli olarak exception fırlattığımız(pardon panic ürettiğimiz) fonksiyonlara ait testlerde,    "ben zaten böyle bir exception olmasını istiyorum" diyebiliriz. #[should_panic] niteliği bunun için kullanılmaktadır.
    Person struct'ı için yazdığımız new isimli metoda ait test fonksiyonunda bu durum irdelenmektedir.    age alanının değerinin 13 ile 18 arasında olması istenmektedir. Eğer böyle değilse ortamda panik havası estirilir.
*/
#[derive(Debug)]
struct Person {    name: String,    age: i8,
}

impl Person {    /// Person nesnesi üretme fonksiyonu    ///    /// Bir Person değişkenini, parametre olarak verilen    /// isim ve yaş bilgileri ile oluşturur.
    ///    /// ## Examples    ///    /// ```    /// let p = Person::new(String::from("ben hur"), 19);    /// assert_eq!(p._name, "ben hur");    /// ```    ///    /// ## Panics    ///    /// Fonksiyona gelen _age parametresi 13 ile 18 aralığında değilse panic fırlatılır.
    ///    fn new(_name: String, _age: i8) -> Person {        /*            Aşağıdaki println çıktısı, cargo test ile testleri koşturduğumuzda ekrana çıktı olarak gelmez.            Fonksiyonlardan terminale basılan çıktıları test sırasında da görmek istiyorsak,            cargo test -- --show-output            şeklinde bir terminal komutu kullanmamız gerekir.        */        println!("Yeni bir personel oluşturulacak");
        if _age > 18 || _age < 13 {            panic!(                "Bu oyun eğitim 13-18 yaş arası talebeler içindir. Girilen yaş `{}`",                _age            );        } else {            Person {                name: _name,                age: _age,            }        }    }
}

#[cfg(test)] // test modülü olduğunu belirttiğimiz nitelik (attribute)
mod tests {    use super::*; // bu iç modülden diğerlerine erişebilmek için konuldu. Aksi durumda Player verisine erişemeyiz

    #[test] // test fonksiyonu olduğunu belirttiğimiz nitelik    fn should_calculated_player_score_positive() {        let median_value = 0.08;        let cai = Player {            nick_name: String::from("cobra kai"),            current_point: 44,            attendance: 102,        };        let expected_value = cai.calculate_score(median_value);        assert!(expected_value > 0.0); // assert! makrosu ile kabul kriterimizi yazdık
    }    #[test]    fn should_player_nick_name_length_grater_than_three() {        let gretel = Player {            nick_name: String::from("han"),            attendance: 3,            current_point: 1,        };        let result = gretel.nick_name.len() > 3;        /*            assert! makrosunu aşağıdaki gibi de kullanabiliriz.            Bu durumda test sonuçlarına belirttiğimiz metinsel içerik de yansıyacaktır.
            Teste konu olan alanların ve hata sebebinin sonuçlarda görünmesini istediğimiz hallerde işe yarabilir.        */        assert!(            result,            "Nickname 3 karakterden fazla olmalı. Girilen `{}`",            gretel.nick_name        );    }    #[test]    #[should_panic] // Beklediğimiz gibi panik ürettirirsek bu test OK cevabı alır. Aksine test panik ürettirmiyorsa Fail cevabını basar    fn should_age_available_for_child() {        let ben_hur = Person::new(String::from("ben hur"), 19); // editörde mouse imlecini new fonksiyonu üstünde tutun    }    /*        Test fonksiyonlarının, kriterin ihlali sonucu panic oluşturması yerine Err döndürmesi de sağlanabilir.
    */    #[test]    fn should_total_greater_than_ten() -> Result<(), String> {        if 3 + 6 == 10 {            Ok(())        } else {            Err(String::from("Testi geçemedi. Abicim 3+6 10 olur mu?"))        }    }    #[test]    #[ignore] // ignore niteliği ile bir testi atladığımızı belirtiriz    fn should_div_work() -> Result<(), String> {        let x = 10.0;        let y = 0.0;        assert_eq!(div(x, y)?, 1.0);        Ok(())    }
}

/*
    kobay fonksiyonumuz geriye Ok veya Err döndürmekte.    should_div_work isimli test fonksiyonunda bu fonksiyonun ? ile kullanıldığına dikkat edelim.
*/
fn div(x: f32, y: f32) -> Result<f32, String> {    if y == 0.0 {        Ok(x / y)    } else {        Err("Sıfıra bölme hatası".to_owned())
    }
}

Uygulamanın çalışma zamanı görüntülerini aşağıda bulabilirsiniz.

Testlerden biri başarılı diğeri değil durumuna ait bir görüntü.

Belli bir test maddesini çalıştırdığımız durumdaki görüntü.

/// ile kullanım talimatlarını eklediğimizde VS Code'daki yardım kutucuğunun içeriği.

Kendi Küfelerimizi Geliştirmek(Crate)

Birim test yazdığımız örnekte bir kütüphane kullandık. Aslında küfe veya sandık anlamına gelen ve Crate olarak isimlendirilen bu yapılarda erişilebilirlik, modül yerleşimi de önemlidir. mercury isimli kütüphanede bu konular ele alınmaktadır. 

Örnek kütüphane kendi içinde entity, flight_opt ve reports isimli modüller içermektedir. mercury library içerisinde yer alan src/lib.rs aynı zamanda kök sandık(Crate root) olarak da adlandırılır. Yani crate anahtar kelimesi ile root'a erişip :: operatörü ile iç elementlere inebiliriz. flight_opt modülünde visitor_manager modülünde tanımlı Visitor struct'ını kullanmak için nasıl bir yol izlediğimize dikkat edin. Bunlardan birisi absolute path formatıdır ve Crate ile başlar. 

Absolute path metodunda crate ile bulunduğumuz sandığı işaret etmekteyiz. :: sonrası bu sandık içerisindeki visitor_manager modülüne ve ardından gelen :: ile de Visitor isimli struct veri tipine ulaşıyoruz. Bu arada entity modülü içinde kullanılan pub anahtar kelimelerine de dikkat edelim. Normalde Visitor isimli struct ve alanları private niteliklidir ve flight_opt içerisinden erişilemezler. Bu nedenle pub ile genel kullanıma açık hale getirilmişlerdir. Bu arada flight_opt modülündeki save_visitor metodu içinden entity modülündeki Visitor struct'ına erişmek için super::entity:Visitor şeklindeki yazım notasyonu da kullanılabilir. super, aslında dosya sistemini düşünürsek ..'yı yani bir üst klasörü referans etmektedir.

cargo new --lib mercury

ve kodlarımız.

mod visitor_manager {    pub mod entity {        pub struct Visitor {            pub fullname: String,            pub ticket_no: String,        }        pub struct Spaceship {            pub name: String,            pub flight_no: i32,            pub passenger_capacity: i8,        }        pub struct SpaceLocation(i32, i32, i32);    }    mod flight_opt {        /*            use ile flight_opt içerisinde kullanmak istediğimiz modül elemanlarını bir kere tanımlayıp
            yola devam edebiliriz. Yani SpaceLOcation kullanmak istediğimiz her yerde            Absoulte path veya relative path ya da super kullanarak uzun formatta bildirim yapmak zorunda değiliz.
            Hatta as ile takma ad(alias) da verebiliriz.             Mesela send_spaceship metodundaki target parametresi için SpaceLocation yerine location ifadesi kullanılabilir.
        */        use crate::visitor_manager::entity::SpaceLocation as location;        fn save_visitor(name: String, ticket: String) {            let v = super::entity::Visitor {                fullname: name,                ticket_no: ticket,            };            // let v = crate::visitor_manager::entity::Visitor {            //     //absoulute path tekniği
            //     fullname: name,            //     ticket_no: ticket,            // };            println!(                "{} isimli ziyaretçi için merkür yolculuk kaydı açıldı. Bilet numarası {}",                v.fullname, v.ticket_no            )        }        fn send_spaceship(name: String, no: i32, capacity: i8, target: location) {}    }    mod reports {        fn get_total_visitor(region: String) -> i32 {            // Merkürdeki üs bazında yolcu sayısını döndürüyor. Mesela :)            return 1000;        }    }
}

Bu örnekte tek bir Crate söz konusu ancak içerisinde dikkat edileceği üzere çeşitli seviylerde modüller var. Geliştirdiğimiz kütüphaneleri başka Rust uygulamalarımızda kullanmak isteyebiliriz.Şimdi process-management isimli bir kütüphane geliştirelim ve onu nortrop-client isimli başka bir Rust uygulamasında kullanmaya çalışalım. 

/*    Oluşturmak için: cargo new process-management --lib    Çalıştırmak için cargo test    Bir Rust uygulamasının başka bir rust kütüphanesini nasıl kullanır?
    nortrop-client bu caret olarak isimlendirilen kütüphaneyi kullanmaya çalışacak.
    pub, yani genele erişime açık yapıları diğer uygulamadan kullanmaya çalışacağız.
*/

/*
    Rastgele sayı üretmek için kullanacağımız harici sandık (external caret)    Bunun için bu kütüphanin Cargo.toml dosyasına gerekli dependency tanımını eklemeliyiz.    cargo test ile kodu test etmek üzere çalıştırdığımızda bu bağımlılık paket önbelleğine indirilir ve kullanılır hale gelir.
*/
use rand::Rng;

#[derive(Debug, PartialEq)] //Soru: Neden Debug ve PartialEq trait'leri eklendi?
pub enum ProcessType {    Small,    High,
}

/*
    Kobay find_process _type fonksiyonu level değerine göre    geriye ProcessType enum türünden bir değer dönmekte.
*/
pub fn find_process_type(level: u32) -> ProcessType {    if level < 500 {        ProcessType::Small    } else {        ProcessType::High    }
}

/*
    Bu da süreç tipi ve parametre sayısına göre tahmini işlem süresini hesaplayan bir fonksiyon.    Hayali olarak tabii...    Fonksiyonda match ile p_type'ın değeri kontrol ediliyor ve buna göre bir ağırlık puanı belirleniyor.    İlgili ağırlık puanı 0 ile 10 arasında olan rastgele bir sayı ile işleme tabi tutulup geriye bir değer döndürüyor.
*/
pub fn calc_estimated_time(p_type: ProcessType, parameter_count: u8) -> u16 {    let weight = match p_type {        ProcessType::High => 5_u8,        ProcessType::Small => 1_u8,    };    let mut randomizer = rand::thread_rng();    let result = (randomizer.gen_range(0, 10) * parameter_count) + weight;    u16::from(result) //Soru: Neden burada result değerini u16 türüne dönüştürdük?
}

#[cfg(test)]
mod tests {    use super::*; //tests modülünün üstünde bulunan get_process ve benzer fonksiyonlara erişebilmek için eklenmiştir

    /*        500 altı değerler için ufak süreç olduğu dönmeli testi    */    #[test]    fn should_return_small_less_than_500() {        let result = find_process_type(459);        assert_eq!(result, ProcessType::Small);    }    /*        500 üstü değerler içinse yüksek hesaplamalı bir süreç olduğunu öğrenme testi    */    #[test]    fn should_return_high_greater_than_500() {        assert_eq!(find_process_type(501), ProcessType::High);    }    #[test]    fn should_estimated_time_be_positive() {        // Bu kez kabul kriterimiz eşitlik değil bir boolean işlem sonucu        assert!(calc_estimated_time(ProcessType::Small, 3) >= 1)    }
}

Şimdi de bunu kullanacak olan nortrop-client isimli bir rust client projesi oluşturalım. Toml dosyasındaki dependencies kısmında, kullanacağımız harici kütüphaneyi işaret etmemiz gerektiğine dikkat edelim.

[package]
name = "nortrop-client"
version = "0.1.0"
authors = ["buraksenyurt "]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
process-management = {path='../process-management',version='*'}

Buradaki bildirime göre process-management fiziki konum olarak nortrop-client ile aynı klasör seviyesindedir. nortrop-client içerisinde kullanılan kobay türünde bir Crate'imiz de var. Kobay task_manager modülünün amacı crate prefix'i ile dahili bir modülün nasıl kullanılabildiğini göstermektir. Modülün main.rs içerisindeki manager modülü tarafında nasıl kullanıldığına dikkat edelim.

pub fn start(id: i32) -> i32 {    println!("{} numaralı görev çalıştırıldı", id);    id
}

#[cfg(test)]
mod tests {    use super::start;    #[test]    fn run_task_should_return_ten_for_ten() {        assert_eq!(start(1), 1);    }
}

main içeriğini ise şu şekilde kodlayabiliriz.

/*    Oluşturmak için: cargo new nortrop-client    Çalıştırmak için: cargo run    Testleri için: cargo test    Bulunulan yer(main.rs in olduğu) root module olarak adlandırlır.
        nortrop-client uygulaması yine bizim yazdığımız process-management isimli caret'ı kullanmaktadır.
    Bunun için Cargo.toml dosyasında bir dependency tanımı mevcuttur.    Ana fonksiyondaki invoice_process çağrısı manager isimli dahili modüle yapılmaktadır.
    manager modülü de dikkat edileceği üzere process-management(caret/sandık) içerisindeki tüm public enstrümanları kullanabilmektedir.    manager modülü aynı zamanda main.rs ile aynı klasörde yer alan task_manager.rs içindeki modülü de kullanmaktadır.
*/
mod task_manager; // internal modülü kullanabilmek için gereken bildirim
//Soru: Yukarıdaki modül bildirimini yapmazsak ne olur?
use manager::invoice_process;

fn main() {    invoice_process(450);    invoice_process(650);
}

mod manager {    use process_management::*;    /*        internal task_manager içerisindeki start fonksiyonunun kullanılacağını belirtir        crate üstünden internal modüllere kolaylıkla erişilebilinir
    */    use crate::task_manager::start;    pub fn invoice_process(level: u32) {        let process_type = find_process_type(level);        let estimated_time = calc_estimated_time(process_type, 10);        println!(            "{} puanlı süreç için tahmini tamamlanma süresi {} uzay zamanıdır.",
            level,            estimated_time //Soru: point yerine process_type değişkenini kullanabilir miyiz?        );        start(192);        start(204);    }
}

Basit Bir Komut Satırı Programı

Buraya kadar epey kavram biriktirdik. Şimdi işe yarar bir uygulama kodu geliştirmeye ne dersiniz? Amacımız aşağıdaki içeriğe sahip bir dosyayı parse edecek komut satırı aracını geliştirmek.

1000|A3 Kağıt (1000 Adet)|100|45
1001|Sıtabilo 12li renkli kalem|150|50
1002|Kareli bloknot|15|20

Bunun için reader isimli bir Rust uygulaması oluşturalım ve aşağıdaki gibi kodlayalım.

// Gerekli ortam kütüphaneleri
use std::env; // argümanları okurken
use std::error::Error;
use std::fmt;
use std::fs; 
use std::process; 

fn main() {    let args: Vec<String> = env::args().collect(); // ekrandan girilen argümanları String türünden bir vector dizisine aldık

    /*        unwrap_or_else fonksiyonu Non-Panic stilde çalışır.
        Aslında burada bir closure kullanımı da söz konusu.        Dikkat edileceği üzere unwrap_or_else isimsiz bir fonksiyon çağırıyor ve bunu new'dan Err dönmesi halinde çalıştırıyor.
        Eğer new Ok dönerse kod akışı devam edecektir    */    let prmtr = Parameter::new(&args).unwrap_or_else(|err| {        println!("{}", err);        process::exit(1); // Uygulamadan çıkartır
    });    println!(        "`{}` dosya içeriği için `{}` işlemi yapılacak\n",
        prmtr.filename, prmtr.command    );    // ürün listesini çekiyoruz    let products = read_product_lines(prmtr).unwrap_or_else(|e| {        println!("Kritik hata: {}", e);        process::exit(1);    });    for p in products {        println!("{}", p); // Product struct'ına Display trait'ini implemente ettiğimiz için bu ifade geçerlidir.    }
}

/*
    Terminalden gelen agrümanları Parameter isimli bir struct'ta toplayabiliriz.    Ayrıca doldurulması için de bir constructor kullanabiliriz. (new metodu)
*/
struct Parameter {    command: String,    filename: String,
}

impl Parameter {    // Constructor    fn new(args: &[String]) -> Result<Parameter, &'static str> {        /*            Ekrandan girilen argüman sayısını kontrol edelim.            Aslında iki parametre isterken 3 tane kontrol etmemiz tuhaf değil mi?            Nitekim cargo kelimesinden sonra gelen run komutu da terminal argümanı sayılıyor.
            Yani run komutundan sonra gelen argümanları ele alacağız.
        */        if args.len() != 3 {            return Err("Argüman sayısı 2 olabilir"); // Panic yerine Error mesajı döner        }        let command = args[1].clone();        let filename = args[2].clone();        Ok(Parameter { command, filename }) // Sorun yoksa Parametre örneği döner    }
}

/*
    read_lines fonksiyonu argümanların toplandığı Parameter struct'ını kullanır ve dosya içeriğini satır satır okur.    Bu fonksiyonda non-panic stilde yazılmıştır.
    Geriye Ok veya hata durumuna göre Error trait'ini uygulayan hata referansları dönebilir.    Ne tür bir hata döneceğini bilemediğimiz için dynamic trait kullanılmıştır.
    ?'te panic yerine Ok veya Error durumlarını döndürmektedir.
*/
fn read_product_lines(prmtr: Parameter) -> Result<Vec<Product>, Box<dyn Error>> {    let content = fs::read_to_string(prmtr.filename)?;    let mut products: Vec<Product> = Vec::new();    // doğrudan content içeriğini lines fonksiyonu ile okuyoruz ve satır satır dolaşabiliyoruz
    for row in content.lines() {        // pipe işaretine göre satırı parse edip sütunları bir vector içinde topluyoruz        let columns: Vec<&str> = row.split("|").collect();        // yeni bir Product değişkeni oluşturup alanlarını atıyoruz
        let prd = Product {            id: columns[0].parse::<i32>().unwrap(),            description: String::from(columns[1]),            price: columns[2].parse::<f32>().unwrap(),            quantity: columns[3].parse::<i32>().unwrap(),        };        // ve products isimli vector dizisine ekliyoruz        products.push(prd);    }    Ok(products) // Buraya kadar sorunsuz geldiysek ürün listesini tutan vector'ü geriye dönüyoruz
}

struct Product {    id: i32,    description: String,    price: f32,    quantity: i32,
}
/*    Display trait'ini Product struct'ımız için uyguluyoruz.    Böylece println! makrosunda buradaki formatta ekrana bilgi yazdırılması mümkün.
*/
impl fmt::Display for Product {    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {        write!(            f,            "[{}] - {}. Birim Fiyat {}. Stokta {} adet var.",            self.id, self.description, self.price, self.quantity        )    }
}

İşte çalışma zamanı çıktımız.

Kapamalar(Closures)

Rust fonksiyonları değişkene atama, başka fonksiyonlara parametre olarak geçme, fonksiyondan fonksiyon döndürme ve benzeri birçok fonksiyonel dil özelliğini bünyesinde barındırır. Özellikle closure, iterator, pattern matching Rust'ın öne çıkan fonksiyonel dil yetenekleridir. Sıradaki kodlarda Closure konusunu anlamaya çalışacağız. Closures isimsiz fonksiyon olarak düşünülebilir. Parametre olarak fonksiyonlara aktarılabilirler veya bir değişkende tutulabilirler. Değişkenlere atanabilmeleri bir yerde tanımlanıp tamamen farklı bir context içerisinde kullanılabilmelerine olanak sağlar.

fn main() {    /*        #1 Basit Closure örnekleri ile başlayalım.
    */    // Örneğin isimsiz bir fonksiyonu bir değişkende tutabilir ve kod akışına göre çağırılmasını sağlayabiliriz
    let div = |x: f32, y: f32| -> f32 {        if y == 0.0 {            panic!("SIfıra bölme sorunu")        }        x / y    };    println!("10/2.4={}", div(10.0, 2.4)); // div değişkenine atanmış fonksiyonu çağırdık

    /*        Tabii yukarıdaki kullanımın bir fonksiyon çağırımı ile neredeyse aynı olduğunu ifade edebiliriz.        Ancak closure'ları fonksiyonlara parametre olarak geçebilmek veya döndürebilmek önemli bir avantajdır.
        Şimdi buna bakalım.

        call fonksiyonu generic tanımlanmıştır ve F için Fn trait'i ile ifade edilmiştir. Buna göre f32 tipinden parametre        alan ve yine f32 türünden değer döndüren closure'lar call fonksiyonuna yollanabilir.        closure'ları parametre olarak geçerken FnOnce, FnMut ve Fn trait'lerine ihtiyacımız vardır nitekim bir closure bunlardan en az birini uyarlamak zorundadır(Generic kullanımlarda bu önem kazanıyor)
    */    call(div, 3.2, 9.4);    /*        Closure tanımlarken dönen türü belirtmek zorunda değilizdir.
        Rust derleyici bunu tahmin eder. Ancak burada dikkat edilmesi gereken bir durum vardır.
        Aşağıdaki tanımlamaya dikkat edelim.        do_something türü belli olmayan value isimli bir parametre alıyor ve bunu aynen geriye döndürüyor.    */    let do_something = |value| value;    let summary = do_something(3); // Burada tipi tahmin etti ve artık i32 ile çalışacağı belli oldu    println!("{}", summary);    //let other_summary = do_something(3.1415); // Bu satırda ise kod derlenmeyecektir. "expected integer, found floating-point numberrustc(E0308)"    // Çünkü ilk kullanımla birlikte do_something fonksiyonunun çalışacağı tür i32 olarak belirlenmiştir

    /*        Game struct'ının closure ile birlikte kullanımı.
        new(constructor)'a bir fonksiyon aktardık. Artık içerideki find_medal fonksiyonu bu fonksiyonu baz alarak çalışacak
    */    let mut blizard = Game::new(|point| point + 1);    println!("{:?}", blizard.find_medal(18));    println!("{:?}", blizard.find_medal(32));    // blizard.medal_calculator = |p| (p + 10 / 2);    // println!("{:?}", blizard.find_medal(16));    /*        Closure'ları fonksiyonlardan ayıran bir özellik de,        bulundukları kapsamdaki değişkenlere erişebiliyor olmalarıdır.
        Aynen aşağıdaki örnekte olduğu gibi.        Tabii bu durumda closure'un çevreden çektiği değişkeni sahiplenmesi söz konusudur ki bu da bellekte bu değişkenler için yer ayırdığı anlamına gelir.        Performans açısından dikkat edilmesi gereken bir durum.    */    let some_number = 10;    let process_function = |n: i32| n + some_number; // isimsiz fonksiyon içerisinde yukarıda tanımlı (main scope'una dahil) some_number değişkenine erişilmiştir
    let processed = process_function(5);    println!("{},{}", processed, some_number);    /*        Yukarıda closure ile yaptığımız şeyi aşağıdaki gibi yapamayız.
        Derleyici "can't capture dynamic environment in a fn item" şeklinde hata verecektir    */    // let another_number=11;    // fn add(nbr: i32) -> i32 {    //     nbr + another_number    // };    /*        Bu arada process_function kullanımı ile ilgili olarak şunu da öğrendim gibi.        Closure'un çevre değişkenleri sahiplenmesi 3 Trait ile mümkün oluyor. Fn, FnMut ve FnOnce        process_function, some_number'ı sadece okuduğu için Fn Trait'ini uygular.        Ama kod bloğunda some_number'ı değiştirip kullanmak istersek aşağıdaki gibi bir yol izlememiz gerekir        ki bu durumda FnMut Trait'i devreye girer.(mut kullanımlarına dikkat)    */    let mut some_number2 = 10;    let mut process_function2 = |n: i32| {        some_number2 += 1;        n + some_number2    };    let processed2 = process_function2(5);    println!("{},{}", processed2, some_number2);    /*        Bir closure farklı bir thread'e alınırken sahiplendiği verinin de mutlak suretle taşınmasını istersek        move komutunu kullanabiliriz. Bu durum Concurrency konusunda değer kazanacak.    */    /*        Fonksiyonlardan fonksiyon dönebileceğimizden de bahsetmiştik.
        Aşağıda örnek bir kod yer alıyor.
        get_fn fonksiyonu, parametre olarak gelen Enum türüne göre geriye Fn(i32,i32)->i32 türünden uygun fonksiyonu döndürüyor.        Eğer toplama işlemi yaptırmak istersek toplama, çarpma yaptırmak istersek de çarpma fonksiyonu gibi...    */    let now_what = get_fn(Process::Division);    println!("Division {}", now_what(16, 4));    let now_what = get_fn(Process::Extraction);    println!("Extraction {}", now_what(12, 6));
}

// Geriye fonksiyon döndüreceğimiz için impl Fn gibi tanım yaptık (FnMut, FnOnce da söz konusu olabilir tabii)
fn get_fn(process: Process) -> impl Fn(i32, i32) -> i32 {    // match ile process enum durumlarına bakıyoruz
    // ve uygun bir fonksiyonu geriye döndürüyoruz. Süper değil mi?    match process {        Process::Addition => |x, y| x + y,        Process::Multiplication => |x, y| x * y,        Process::Division => |x, y| {            if y == 0 {                panic!("Sıfıra bölme durumu");            } else {                x / y            }        },        Process::Extraction => |x, y| x - y,    }
}

enum Process {    Addition,    Multiplication,    Extraction,    Division,
}

fn call<F>(closure: F, a: f32, b: f32)
where    F: Fn(f32, f32) -> f32,
{    let result = closure(a, b);    println!("{}", result);
}

/*
    Closure'ları parametre olarak geçebildiğimizden bahsediyoruz.    Örneğin bir Struct'ın bir alanını da closure olarak tanımlayabiliriz.

    Game isimli generic struct i32 tipinden değer alıp yine i32 türünden değer döndüren bir fonksiyonu    medal_calculator alanında taşıyacak şekilde tanımlandı.

    new(constructor) fonksiyon parametre olarak gelen fonksiyonu medal_calculator alanına atıyor.
    find_medal fonksiyonunda ise gelen argüman değeriner göre closure fonksiyonunu çağırıyor.
    Struct'a atanan hesaplama fonksiyonu ne ise (medal_calculator'a atanan fonksiyon) o icra ediliyor.
*/
struct Game<T>
where    T: Fn(i32) -> i32,
{    medal_calculator: T,    current_point: i32,
}

impl<T> Game<T>
where    T: Fn(i32) -> i32,
{    fn new(calc: T) -> Game<T> {        Game {            medal_calculator: calc,            current_point: 0,        }    }    fn find_medal(&mut self, arg: i32) -> i32 {        let value = (self.medal_calculator)(arg);        self.current_point = value;        value    }
}

Kapama ifadelerini kullanarak .Net dünyasındaki LINQ(Language INtegrated Query) benzeri bir çatı bile geliştirilebilir. Aşağıdaki senaryoda Person struct dizisi taşıyan bir vector tipi bulunuyor. Bu vector üzerinde search isimli fonksiyon ile arama yapıyoruz. İşin sihri, arama fonksiyonunu kodu yazarken biz söylüyoruz ve bunu isimsiz fonksiyon kullanarak gerçekleştiriyoruz.

fn main() {    let team = fill_players();    println!("***Seviyesi 300 üstünde olan oyuncular***");    let level_grater_than_300 = search(&team, |p: &Player| {        return p.level >= 300;    });    for p in level_grater_than_300 {        println!("{}[{}] (Avg:{})", p.nickname, p.level, p.average_point);    }    println!("\n***Sayı ortalaması 16 altında olan oyuncular***");    let point_high = search(&team, |p: &Player| {        return p.average_point < 16.0;    });    for p in point_high {        println!("{}[{}] (Avg:{})", p.nickname, p.level, p.average_point);    }    println!("\n***Leykırs takımında olan oyuncular***");    let point_high = search(&team, |p: &Player| {        return p.team == "Leykırs";
    });    for p in point_high {        println!("{}[{}] (Avg:{})", p.nickname, p.level, p.average_point);    }
}

// Oyuncu bilgilerini taşıyan struct
#[derive(Clone)] // search fonksiyonundaki for döngüsünde o anki player örneğinin bir klonunun oluşturulabilmesi için kullandığımız nitelik
struct Player {    nickname: String,    average_point: f32,    level: i32,    team: String,
}

/*
    generic search fonksiyonumuz.    İlk parametrede Person tipinden bir vector alıyor ve ikinci parametre de F tipinden bir closure.    Fn trait'i ile ifade edilen closure'un Person referansı aldığı ve geriye true veya false döndürmesi gerektiğini belirtiyoruz (where kısmı)
    Fonksiyon kendi içinde yeni bir vector oluşturuyor ve bunu geriye döndürüyor. Bu yeni vector içindeki Person nesnelerinin eklenme kriteri ise    f fonksiyonu ile icra edilen koşul. Örneğin level'ı 300'den büyük olan oyuncuların çekilmesi gibi.
*/
fn search<F>(person_list: &Vec<Player>, f: F) -> Vec<Player>
where    F: Fn(&Player) -> bool,
{    let mut result: Vec<Player> = Vec::new();    for p in person_list {        if f(&p) {            let plyr = p.clone();            result.push(plyr);        }    }    result
}

fn fill_players() -> Vec<Player> {    let mut team: Vec<Player> = Vec::new();    let mj = Player {        nickname: String::from("M.J."),        average_point: 32.50,        level: 310,        team: String::from("Şikago"),
    };    let scoti = Player {        nickname: String::from("Scoti pipin"),        average_point: 15.5,        level: 250,        team: String::from("Şikago"),
    };    let bird = Player {        nickname: String::from("Leri börd"),        average_point: 21.5,        level: 320,        team: String::from("Boston"),    };    let longle = Player {        nickname: String::from("Luk longley"),        average_point: 10.5,        level: 100,        team: String::from("Şikago"),
    };    let conson = Player {        nickname: String::from("Mecik Conson"),        average_point: 28.95,        level: 350,        team: String::from("Leykırs"),
    };    let doncic = Player {        nickname: String::from("Luka doncic"),        average_point: 22.34,        level: 310,        team: String::from("Dallas"),    };    let detler = Player {        nickname: String::from("detler şiremğ"),
        average_point: 15.99,        level: 280,        team: String::from("Dallas"),    };    let karim = Player {        nickname: String::from("karim abdul cabbar"),        average_point: 21.99,        level: 350,        team: String::from("Leykırs"),
    };    team.push(mj);    team.push(scoti);    team.push(bird);    team.push(longle);    team.push(conson);    team.push(doncic);    team.push(detler);    team.push(karim);    team
}

ve bu örneğe ait çalışma zamanı çıktısı da aşağıdaki gibi olacaktır.

Iterators

Bir başka fonksiyonel dil kabiliyeti iterator kullanımıdır. Esasında Itereator kalıbı bir nesne dizisinde ileri yönlü hareket ederken her bir dizi öğesi için belli bir fonksiyonelliği çalıştırmak gibi işlemlerde kullanılır. Rust dilinde kendi veri yapılarımızı  tasarlayabildiğimizi düşünecek olursak bu oldukça önemli bir kabiliyettir. iterator fonksiyonları ile sıralı veri yapıları üzerinde dönüştürme, arama, tekil elemena indirgeme vb işlevsellikler de kullanılır. Bu fonksiyonlar birden fazla adımı tek bir iterasyon içerisinde ele almayı kolaylaştırır. Iterator'lar standart kütüphanedeki Iterator isimli Trait'i uygularlar ve iter() arkasından gelen map, filter, for_each, find vb pek çok fonksiyon(ki bunlara iterator adaptor deniliyor) parametre olarak closure'lara başvurur. Uygulamamızı cargo new iteration-practices --lib ile oluşturabiliriz.

#[cfg(test)]
mod tests {    use super::*;    #[test]    fn should_find_qualifiers_works_with_old_fashion() {        /*            Önce bilinen yollarlar bir kod parçası yazalım.
            Amaç oyuncu listesinde ortalama puanına göre kimleri elemelere kaldığını bulup bir vector'de toplamak.            test veri kümesini yükledikten sonra vector listesini döngü ile dolaşıyoruz.
            Her bir oyuncunun average_point değerine bakıp 70 barajının üstünde ise ("Yes",oyuncu id) değilse ("No",oyuncu id) şeklinde
            tuple değişkenleri oluşturup qualifiers vectöründe topluyoruz.            Kabul kriterleri de bu ikili içeriklerine göre oluşturuluyor.
        */        let players = load_players_data();        let mut qualifiers: Vec<(String, u16)> = vec![]; //Soru: Neden mutable tanımlanmıştır?
        for p in players {            if p.average_point > 70 {                qualifiers.push(("Yes".to_string(), p.id));            } else {                qualifiers.push(("No".to_string(), p.id));            }        }        let mut iter = qualifiers.iter();        assert_eq!(iter.next(), Some(&("No".to_string(), 1001))); //Soru: Neden Some kullanılmıştır?
        assert_eq!(iter.next(), Some(&("No".to_string(), 1002))); //Soru: Neden & ile referans noktası alınmıştır?
        assert_eq!(iter.next(), Some(&("No".to_string(), 1003)));        assert_eq!(iter.next(), Some(&("Yes".to_string(), 1004)));        assert_eq!(iter.next(), Some(&("No".to_string(), 1005)));        assert_eq!(iter.next(), Some(&("Yes".to_string(), 1006)));    }    #[test]    fn should_get_qualitifed_players_as_binary_array() {        let players = load_players_data();        // map parametresi olarak dış fonksiyon yerine kod bloduğu da (closure) kullanılabilir.
        let qualified_iterator = players            .iter()            .map(|p| if p.average_point > 70 { 1 } else { 0 }); // p olarak ifade edilen Player değişkenidir. Nitekim map, players vektörüne ait iterator üzerinde uygulanmaktadır
        let qualified: Vec<u16> = qualified_iterator.collect::<Vec<u16>>();        assert_eq!(qualified, vec![0, 0, 0, 1, 0, 1, 0, 0, 1, 1]);    }    #[test]    fn should_get_players_qualification_state_in_yes_or_no() {        let players = load_players_data();        /*            map, bir closure'u parametre olarak alır. Örnekte closure is_qualified isimli fonksiyondur.            Uygulandığı players koleksiyonundaki her Player değişkeni için çalışacak is_qualified fonksiyonunu kullanan yeni bir iterator değişkeni üretir.            zip fonksiyonu iki iterator üzerinde dolaşan tek bir iterator üretilmesini sağlar.
            parametre olarak aldığı qualify_counter iterasyonunu kullanadırarak players koleksiyonu üzerinde is_qualified uygulanmasını sağlar.
            Sonuçlar ilk iterasyondaki Player değişkenleri ile parametre olarak gelen iterasyondan elde edilen sonuçların tuple olarak birleştiği veri dizisini dolaşacak yeni bir iterator nesnesidir.            Elde edilen iterator, collect fonksiyonu çağırılarak bir koleksiyona indirgenir(Örnekte bu bir vector'dür)        */        //Soru: Elemeye kalanları başka bir built-in fonksiyon ile daha kolay elde edebilir miyiz?        let qualify_iterator = players.iter().map(is_qualified);        let qualifiers: Vec<(&Player, String)> = players.iter().zip(qualify_iterator).collect();        // Temsili olarak finale kalan ve kalmayan birer örnek Player değişkeni kabülü        assert_eq!(            qualifiers[0],            (                &Player::new(                    1001,                    "niko rosberg".to_string(),                    100,                    56,                    "West".to_string()                ),                "No".to_string()            )        );        assert_eq!(            qualifiers[3],            (                &Player::new(                    1004,                    "engila beatriys".to_string(),                    500,                    71,                    "West".to_string()                ),                "Yes".to_string()            )        );    }    #[test]    fn should_get_total_score_with_map_and_fold() {        let players = load_players_data();        /*            fold, map-reduce'taki reduce fonksiyonuna benzetilebilir.            iterasyon üzerinden tek bir değerin hesaplanmasında kullanılır.
            Örnekte map ile oyuncuların ortalama skorlarını dolaşacak bir iterasyon yakalanır.
            fold, ilk parametre olarak bir başlangıç değeri alır.
            İkinci parametre bir fonksiyon çağrısıdır. point o anki oyuncunun average_point değerini tutar.            Accumulator bir inceki hesaplamanın sonucudur. Her ikisi closure'a girmektedir.            Yani map,fold ile oyuncularının ortalama skorları toplamı bulunmuştur. Klasik yol dışında fonksiyonel bir yaklaşımla bu hesap edilmiştir.
        */        let total = players            .iter()            .map(|p| p.average_point)            .fold(0, |point, accumulator| point + accumulator);        assert_eq!(total, 127);    }    //Soru: Aşağıdaki testin FAIL etmesini sağlayın
    #[test]    fn should_get_only_west_sides_players() {        let players = load_players_data();        /*            filter fonksiyonu da bir closure kabul eder ve geriye bir iterasyon nesnesi döndürür.            parametre olarak kullanılan closure'un true veya false döndürmesi gerekir        */        let filtered = players.iter().filter(|&p| p.region == "West");        for f in filtered {            assert_eq!(f.region, "West");        }    }    #[test]    fn should_get_first_three_players_with_id_and_region() {        let players = load_players_data();        /*            Önce map ile id(region) formatında bir string koleksiyonunu elde ettik.            Bu listeyi dönecek iterasyon üzerinden take ile ilk 3 elemanı dolaşacak başka bir iterator örnekledik.        */        let mut mapped = players            .iter()            .map(|p| format!("{}({})", p.id, p.region))            .take(3);        assert_eq!(mapped.next(), Some("1001(West)".to_string()));        assert_eq!(mapped.next(), Some("1002(West)".to_string()));        assert_eq!(mapped.next(), Some("1003(East)".to_string()));        assert_eq!(mapped.next(), None);    }    #[test]    fn should_engila_beatriys_in_players_but_noname_is_not() {        let players = load_players_data();        /*            find, true veya false dönen bir closure ile çalışır. Kriter sağlanırsa Some(Player), sağlanmazsa None döner        */        //Soru: map fonksiyonundaki _ operatörü neyi temsil eder ve niçin kullanılmıştır? Kullanılmak zorunda mıdır?
        assert_eq!(            players                .iter()                .find(|p| p.name == "engila beatriys")                .map(|_| "Yes"),            Some("Yes")        );        assert_eq!(            players.iter().find(|p| p.name == "No name").map(|_| "Yes"),            None        );    }    #[test]    fn aggregation_tests() {        let players = load_players_data();        /*            max, min ve sum gibi fonksiyonlar aggregation işlemlerinde kullanılırlar.
            Yani tek bir sonuç üretilmesinde ele alınırlar.
            Player bir struct olduğundan max,min,sum ile hangi alanı ele alacağımızı söylememiz gerekir.            map burada işimizi kolaylaştırmaktadır.
        */        let average_point_iterator = players.iter().map(|p| p.average_point);        let biggest = average_point_iterator.max();        assert_eq!(biggest, Some(90));        let smallest = players.iter().map(|p| p.average_point).min();        assert_eq!(smallest, Some(-110));        let sum_of_avg_points: i16 = players.iter().map(|p| p.average_point).sum();        assert_eq!(sum_of_avg_points, 127);    }    #[test]    fn chain_test() {        /*            chain fonksiyonu ile iki iterasyon zincirleme birbirine bağlanabilir.
            Örnekte blue_team_points ile red_team_points vektorlerine ait iki iterasyon uç uca bağlanmıştır.
            Kendi iteratörleri 70ten büyük puanlar almakta, chain ile birleştirilen zincirle elde edilen iteratör ise 90'dan büyük olanları kullanmaktadır.
            Yeni iteratör üzerinden çağırılan count ile 90 her iki gruptan 90 üstü alan toplam kaç kişi olduğu bulunur.        */        //Soru: İkiden fazla iteratör chain ile birbirlerine bağlanabilir mi?        let blue_team_points = vec![32, 55, 78, 91, 88, 90, 0, 15];        let red_team_points = vec![44, 50, 98, 60, 99, 40, 72, 77, 79];        let iter_a = blue_team_points.iter().filter(|&n| n > &70);        let iter_b = red_team_points.iter().filter(|&n| n > &70);        let total = iter_a.chain(iter_b).filter(|&n| n >= &90).count();                assert_eq!(total, 4);    }    fn is_qualified(p: &Player) -> String {        if p.average_point > 70 {            "Yes".to_string()        } else {            "No".to_string()        }    }
}

///
/// Test amaçlı oyuncu verisi yükleyen fonksiyondur
///
fn load_players_data() -> Vec<Player> {    vec![        Player::new(            1001,            "niko rosberg".to_string(),            100,            56,            "West".to_string(),        ),        Player::new(            1002,            "raiyukunen du".to_string(),            600,            -89,            "West".to_string(),        ),        Player::new(            1003,            "di tomassi no".to_string(),            200,            -25,            "East".to_string(),        ),        Player::new(            1004,            "engila beatriys".to_string(),            500,            71,            "West".to_string(),        ),        Player::new(            1005,            "barbıra".to_string(),
            200,            -78,            "Dangen Zone".to_string(),        ),        Player::new(            1006,            "morata".to_string(),            300,            90,            "Blue Lagon".to_string(),        ),        Player::new(            1007,            "fat-ma".to_string(),            300,            50,            "Blue Lagon".to_string(),        ),        Player::new(            1008,            "bloumquvits".to_string(),            400,            -110,            "Wild Wild West".to_string(),        ),        Player::new(            1009,            "indi yama guşi".to_string(),
            100,            77,            "Mordor".to_string(),        ),        Player::new(            1010,            "raçel ways".to_string(),            500,            85,            "Gondor".to_string(),        ),    ]
}

///
/// Bir oyucunun numarası, seviyesi ve ortalama skorunun tutulduğu veri yapısıdır
///
#[derive(Debug, PartialEq)]
pub struct Player {    id: u16,    name: String,    level: i16,    average_point: i16,    region: String,
}

///
/// Player veri yapısı için yardımcı fonksiyonlar içerir
impl Player {    ///    /// Parametrelerden yararlanarak yeni bir Player değişkeni örnekler    ///    fn new(no: u16, nm: String, lvl: i16, avg: i16, rg: String) -> Self {        Player {            id: no,            name: nm,            level: lvl,            average_point: avg,            region: rg,        }    }
}

Bir iterator örneği daha yazalım. Pratik olsun.

/*    bir iterator kullanım örneği daha.    Bu kez kendi veri yapımızın alanları üzerinde filtreleme işlemi gerçekleştiriyoruz.
*/

/*
    Birkaç klasik oyun bilgisini tutacak bir struct.
*/
#[derive(PartialEq, Debug)]
struct Game {    name: String,    year: u16,    publisher: String,    value: f32,    platform: Platform,
}

/*
    Oyunun hangi console'da oynandığı bilgisini de bir enum ile tutalım.
    Bu arada game_by_platform fonksiyonundaki == operatörünü kullanabilmek için PartialEq niteliğini kullanıyoruz
*/
#[derive(PartialEq, Debug)]
enum Platform {    Commodore64,    Atari2600,    Atari5200,
}

/*
    Belli bir yıldan önceki oyunları döndüren bir fonksiyon.    Game türünden vector parametre olarak gelir, _year değerine göre filtreleme yapılır
    ve bu kritere uyan oyunlar geriye dönülür.    Tüm arama fonksiyonlarında into_iter iterator'u kullanılıyor. Bu vector'ün sahipliğini üstlenen bir iterator oluşturmak için kullanılıyor.
    Sahipliği almadığımız takdirde collect fonksiyonu derleme hatası verecektir.
*/
fn before_year(games: Vec<Game>, _year: u16) -> Vec<Game> {    games.into_iter().filter(|g| g.year <= _year).collect()
}

/*
    Belli bir platform için yazılmış oyunların bulunması
*/
fn games_by_platform(games: Vec<Game>, _platform: Platform) -> Vec<Game> {    games        .into_iter()        .filter(|g| g.platform == _platform)        .collect()
}

/*
    İçinde parametre olarak gelen kelimeyi içeren oyunlar
*/
fn games_include_this(games: Vec<Game>, _word: String) -> Vec<Game> {    games        .into_iter()        .filter(|g| g.name.contains(&_word))        .collect()
}

/*
    Örnek birkaç oyun bilgisi yüklediğimiz fonksiyon
*/
fn load_samples() -> Vec<Game> {    vec![        Game {            name: String::from("Crazy Cars II"),            year: 1988,            publisher: String::from("Titus"),            value: 1.5,            platform: Platform::Commodore64,        },        Game {            name: String::from("1942"),            year: 1986,            publisher: String::from("Elit"),            value: 2.85,            platform: Platform::Commodore64,        },        Game {            name: String::from("Pitstop II"),            year: 1984,            publisher: String::from("Epyx"),            value: 0.55,            platform: Platform::Commodore64,        },        Game {            name: String::from("The Last Ninja"),            year: 1987,            publisher: String::from("System 3"),            value: 1.49,            platform: Platform::Commodore64,        },        Game {            name: String::from("Spy Hunter"),            year: 1983,            publisher: String::from("US Gold"),            value: 2.40,            platform: Platform::Commodore64,        },        Game {            name: String::from("3-D Tic Tac Toe"),            year: 1980,            publisher: String::from("Atari"),            value: 6.75,            platform: Platform::Atari2600,        },        Game {            name: String::from("Asteroids"),            year: 1981,            publisher: String::from("Atari"),            value: 6.70,            platform: Platform::Atari2600,        },        Game {            name: String::from("Gremlins"),            year: 1986,            publisher: String::from("Atari"),            value: 2.75,            platform: Platform::Atari5200,        },        Game {            name: String::from("Mario Bros."),            year: 1988,            publisher: String::from("Nintendo"),            value: 9.85,            platform: Platform::Atari5200,        },    ]
}

/*
    Test modülümüzü de ekleyelim.    Eklenen fonksiyonları test ederek ilerleriz
*/

#[cfg(test)]
mod tests {    use super::*;    /*        Mesela veri setimize göre Atari5200 platformundan iki oyunun olduğu bir vector dizisi dönmeli    */    #[test]    fn should_games_include_two_atari5200_games() {        let retro_games = load_samples();        let finding = games_by_platform(retro_games, Platform::Atari5200);        assert_eq!(            finding,            vec![                Game {                    name: String::from("Gremlins"),                    year: 1986,                    publisher: String::from("Atari"),                    value: 2.75,                    platform: Platform::Atari5200,                },                Game {                    name: String::from("Mario Bros."),                    year: 1988,                    publisher: String::from("Nintendo"),                    value: 9.85,                    platform: Platform::Atari5200,                },            ]        )    }    /*        1986 dahil öncesinde geliştirilen de 6 oyun olmalı
    */    #[test]    fn should_return_six_for_games_before_1986() {        let retro_games = load_samples();        let finding = before_year(retro_games, 1986);        assert_eq!(finding.len(), 6);    }    /*        Adında II geçen oyunların testi.    */    #[test]    fn should_return_games_for_name_contains_two() {        let retro_games = load_samples();        let finding = games_include_this(retro_games, String::from("II"));        assert_eq!(            finding,            vec![                Game {                    name: String::from("Crazy Cars II"),                    year: 1988,                    publisher: String::from("Titus"),                    value: 1.5,                    platform: Platform::Commodore64,                },                Game {                    name: String::from("Pitstop II"),                    year: 1984,                    publisher: String::from("Epyx"),                    value: 0.55,                    platform: Platform::Commodore64,                },            ]        );    }
}
fn main() {}

Test sonuçlarını aşağıda görebilirsiniz.

Tahmin edeceğiniz üzere kendi geliştirdiğimiz türler veya hash map gibi diğer koleksiyonlar için kendi iterator fonksiyonlarımızı da yazabiliriz. Tek yapmamız gereken Iterator trait'ini uygulamaktır. Ancak bunun için uygun senaryolara da ihtiyacımız vardır. Şunu da bir açıklığa kavuşturalım; Iterator demek veri için bir sonraki veriyi döndüren ve nerde durması gerektiğini bilen bir next fonksiyonu demektir.

Ben dili öğrenmeye çalıştığım sırada bu konuyla ilgili olarak iki örnek üzerinde çalışmıştım. Sizde bunları sırasıyla kodlayarak ilerleyebilirsiniz. Önce bir öğrencinin notları üzerinde for döngüsü ile dolaşabilmemizi sağlayacak uyarlamaya bakalım(Birim Testsiz sürüm)

// Öğrencinin ders ortalamalarını tutan bir veri yapısı düşünelim
struct Point {    math: f32,    lang: f32,    phys: f32,    chem: f32,    vart: f32,
}

/*
    Şimdi de bunu kullanan bir öğrenci veri yapısı tasarlayalım.
    Sanırım amaç anlaşıldı. Bir öğrenicinin notlarını for döngüsü ile dönebilmek istiyorum.    Bu iterasyon sırasında verinin haricinde verinin durumunu ve hangi konumda olduğumu da bilmem lazım.
    O nedenle position ve data isimli iki alanımız var.    İlk versiyonda points verisini olduğu gibi tutmuştuk. Lakin verinin referansını tutmamız yeterli.    Tabii Point referansını tutacağız ama Rust, Student veri yapısının taşıyacağı bu referans ile olan ilişkinin ömrünü bilemeyecek.    O nedenle <'a> ile lifetime ilişkisini eşitliyoruz.
*/
struct Student<'a> {    fullname: String,    school: String,    position: i32,    points: &'a Point,
}

/*
    iterator trait'inin uygulanması.
    Eğer <'_> şeklinde isimsiz lifetime bildirimi yapmazsak 'implicit elided lifetime not allowed here' şeklinde hata alırız.
    Bu nedenle <'_> şeklinde bir bildirim yapıp Rust derleyicisinden bu hatayı göz ardı etmesini rica ediyoruz.
*/
impl Iterator for Student<'_> {    type Item = f32; // Point struct'tındaki türden olduğunda dikkat edelim                     /*                         next sıradaki Item'ı yani puanı yani f32 türünden öğeyi döndürür.                         Kiminkini peki? Self ile ifade ettiğimize göre o anki Student nesnesininkini.                     */    fn next(&mut self) -> Option<Self::Item> {        match self.position {            0 => {                self.position += 1;                Some(self.points.math)            }            1 => {                self.position += 1;                Some(self.points.lang)            }            2 => {                self.position += 1;                Some(self.points.phys)            }            3 => {                self.position += 1;                Some(self.points.chem)            }            4 => {                self.position += 1;                Some(self.points.vart)            }            _ => None,        }    }
}

fn main() {    // ant_man'ın ders not ortalamalarını girdik    let some_points = Point {        math: 78.0,        chem: 55.0,        phys: 80.0,        lang: 90.0,        vart: 67.5,    };    let ant_man = Student {        fullname: String::from("Ant-Man"),        school: String::from("Mystery Forrest High School"),        points: &some_points, // referans adresi verdiğimize dikkat edelim        position: 0, // Aslında bu atama ile iterator'un 0ncı konuma inmesini sağlıyoruz.
    };    println!("{} ({})", ant_man.fullname, ant_man.school);    // bu for döngüsü ant_man'ın tüm ders notlarını dolaşabiliyor.
    // Iterator implementasyonu sayesinde    for p in ant_man {        println!("{}", p);    }
}

Bir programlama dili çeşitli türde veri yapıları sağlar. Geliştiriciler bunları kombine ederek bir eco system inşa edebilirler.
İyi programlama dilleri bu ekosistemin inşasını kolaylaştırır. Iterator deseni GoF'un belirttiği tasarım prensiplerinden birisidir ve Rust dilinde de trait'leri üzerinden kurgulanabilir. Böylece kendi veri yapılarımız içerisinde ileri yönlü hareket edebiliriz.
Örnek kod parçasında TeamSquad veri yapısı için bir iterasyon tasarlanmaya çalışılmaktadır. Buna göre for döngüsü veya next fonksiyonları ile takım üyeleri üstünde ileri yönlü hareket edilmesi sağlanır. cargo new custom-iterators --lib ile uygulamamızı oluşturup kodlayalım.

#[cfg(test)]
mod tests {    use super::*;    #[test]    fn should_next_over_team_squad_works() {        let mut blue_team = TeamSquad::new();        blue_team.game_color = Some(Color::Blue);        blue_team.push(Player::new("börd".to_string(), 79));        blue_team.push(Player::new("bıraynt".to_string(), 88));        blue_team.push(Player::new("barkli".to_string(), 76));        blue_team.push(Player::new("cordın".to_string(), 93));        let mut iter = blue_team.into_iter(); // next fonksiyonu ile TeamSquad nesnesinden hareket etmemizi sağlar

        assert_eq!(iter.next(), Some(Player::new("cordın".to_string(), 93)));        assert_eq!(iter.next(), Some(Player::new("barkli".to_string(), 76)));        assert_eq!(iter.next(), Some(Player::new("bıraynt".to_string(), 88)));        assert_eq!(iter.next(), Some(Player::new("börd".to_string(), 79)));        assert_eq!(iter.next(), None);    }    #[test]    fn should_for_loop_over_team_squad_works() {        let mut red_team = TeamSquad::new();        red_team.game_color = Some(Color::Red);        red_team.push(Player::new("poo".to_string(), 65));        red_team.push(Player::new("obi van".to_string(), 85));        red_team.push(Player::new("leya".to_string(), 76));        red_team.push(Player::new("rey".to_string(), 92));        red_team.push(Player::new("kaylo ren".to_string(), 84));        red_team.push(Player::new("han solo".to_string(), 71));        let mut total_team_power: i32 = 0;        for plyr in red_team {            total_team_power += plyr.level;        }        assert_eq!(total_team_power, 473);    }
}

/*
    Amaç TeamSquad nesnesi üzerinde ileri yönlü hareket ederken içerdiği oyuncuları dolaşabilmek
    Iterator'ları ayrı birer struct olarak tanımlamak değerleri sahiplenmek yerine referanslarını kullandırabilmek açısından önemlidir.    Soru: Generic veri yapıları için iterator kalıbı uygulanabilir mi?
*/
pub struct TeamSquadIterator {    squad: TeamSquad,
}

impl TeamSquadIterator {    fn new(team: TeamSquad) -> TeamSquadIterator {        TeamSquadIterator { squad: team }    }
}

/*
    Türlere iterator yeteneğini kazandırmak için Iterator ve IntoIterator trait'lerini uygulamak gerekir.
*/
impl Iterator for TeamSquadIterator {    type Item = Player;    fn next(&mut self) -> Option<Player> {        self.squad.pop()    }
}

impl IntoIterator for TeamSquad {    type Item = Player;    type IntoIter = TeamSquadIterator;    fn into_iter(self) -> Self::IntoIter {        TeamSquadIterator::new(self)    }
}

///
/// Takım veri yapısı.
/// İçinde takım rengi ve oyuncular dizisi yer alır
pub struct TeamSquad {    game_color: Option<Color>,    players: Vec<Player>,
}

///
/// TeamSquad veri yapısın için uygulanan fonksiyonları içerir
///
impl TeamSquad {    ///    /// Yeni bir TeamSquad nesnesi örnekler    ///    pub fn new() -> Self {        TeamSquad {            players: Vec::new(),            game_color: None, // Option<Color> türünden tanımladığımız için bu mümkün. Henüz oluşturulmamış bir takım için ideal değer.
        }    }    ///    /// Takıma eklenen son oyuncuyu geri verir ve listeden çıkartır
    ///    pub fn pop(&mut self) -> Option<Player> {        self.players.pop()    }    ///    /// Takıma yeni bir oyuncu ekler    ///    pub fn push(&mut self, p: Player) {        self.players.push(p)    }
}

///
/// Takım rengi
///
pub enum Color {    Red,    Blue,    Green,
}

///
/// Oyuncu bilgilerini tutan veri yapısıdır
///
#[derive(Debug, PartialEq)] // assert_eq! da karşılaştırma == üstünden yapıldığı için eklendi
pub struct Player {    name: String,    level: i32,
}

///
/// Player veri yapısına ait fonksiyonlar
///
impl Player {    ///    /// name ve level parametrelerini kullanarak Player nesnesi oluşturur
    ///    fn new(n: String, l: i32) -> Self {        Player { name: n, level: l }    }    ///    /// Player hakkında bilgi verir    ///    fn to_string(&self) -> String {        format!("{},{}", self.name, self.level)    }
}

(HOF)Higher Order Function

Rust dilinin bir diğer fonksiyonel özelliği de Higher Order Functions kabiliyetidir. Yani fonksiyonları birbirlerine bağlayıp yeni fonksiyonellikleri çalıştırabiliriz. Fonksiyonlar çıktı olarak fonksiyon döndürebildiklerinden bu oldukça doğaldır. Aslında HOF yeteneği denince benim aklıma hep nokta operatörü sonrası birbirlerine bağlanan LINQ fonksiyon zincirleri gelir.

fn main() {    let mut total = 0;    // imperative yaklaşım
    for n in 500..1000 {        let s = n * n;        if calc(s) {            total += s;        }    }    println!("Imperative stilde toplam {}", total);    // fonksiyonel yaklaşım
    let total2: i32 = (500..1000)        .map(|n| n * n) // Aralıktaki sayıların karelerinden oluşan kümeyi bir alalım
        .filter(|&s| calc(s)) // bunların 3 veya 5 ile bölünebilme hallerine bakalım
        .fold(0, |t, s| t + s); // o kurala uyanları da toplayalım

    println!("Fonksiyonel stilde toplam {}", total2);
}

fn calc(n: i32) -> bool {    n % 3 == 0 && n % 5 == 0
}

Patterns Matching

Fonksiyonel dillerin sık rastlanan özelliklerinden birisi de pattern matching'dir. Karmaşık karar yapılarında kodu basitleştirir ve dallanmaları kolayca yönetmemizi sağlarlar. Enum'larda uygulanabileceği gibi struct türünde de kullanılabilir. Pattern(şablon), basit veya karmaşık bir tipi yapısını eşleştirme yoluyla kontrol etmeye yarayan bir söz dizimi olarak düşünülebilir. Bu deseni Rust dilinde bir çok yerde görebiliriz. Gelin ilk önce temellerini anlayalım ve sonrasında daha özgün bir kullanım örneği yazalım.

fn main() {    /*        #1        Önce pattern(şablon) konusuna bir bakalım.

        İlginç geldi ki aşağıdaki ifadelerde soldaki değişkenler birer pattern'dir.        Sağ taraftan ne gelirse gelsin eşleştirdiğimiz birer aktördür.        let PATTERN = EXPRESSION;    */    let pi = 3.1415; // pi bir pattern    let (x, y, z) = (1, 3, 5); // Burada eşitliğin sağındaki tuple verisini bir pattern ile eşleştirdik(match)
    let (a, b, _, d) = (1, 1, 0, 1); // _ ile pattern içerisindeki bir eşleşmeyi atladığımızı ifade ettik    /*        Aşağıdaki while let döngüsünde colors isimli vector'ün elemanlarını dolaşırken
        pattern matching kullanılmaktadır.
        pop metodu eleman yoksa None döner, eleman varsa da elemanı döner :)        while let ile bu eşleşme Some(color) ile kontrol edilir.        Böylece vector'den eleman çektikçe None veya herhangi biri olma eşlemesine(match) bakılır.
        Bu arada pop fonksiyonu hep son eklenen elemanı vereceğinden döngü renkleri ters sırada ekrana basar.    */    let mut colors = Vec::new();    colors.push("Red");    colors.push("Green");    colors.push("Blue");    while let Some(color) = colors.pop() {        println!("{}", color);    }    /*        Şimdi de aşağıdaki for kullanımına bakalım.
        Burada da (x,v) aslında bir pattern olarak karşımıza çıkar.
        enumerate fonksiyonu geriye iterasyondaki elemanın sıra numarası(index)
        ve değerini(value) döndürür.        for (x,v) bu eşleşmeye bakar.    */    let market_list = vec![        "Bir kilo prinç",        "2 ekmek",        "Yarım kilo un",        "Bir paket dilimlenmiş kaşar peyniri",        "Aç bitir salam",    ]; // Evet bu kısımlarda acıkmışım

    for (x, v) in market_list.iter().enumerate() {        println!("{} -> {}", x, v);    }    /*        if let ifadelerinde de pattern matchin kullanılabilir.
        Aşağıdaki point değişkenin değeri String bir içerikten parse edilerek alınıyor.
        parse geriye Result<Value,Error> döner. Bu Ok() ile eşleştirilebilir.
        parse işlemi başarılıysa Result'ın Value değeri Ok(p) gibi döner.        parse başarılı değilse Ok(p) eşleşmesi ihlal edilir ve else bloğuna girilir.    */    let point: Result<f32, _> = "3.1415".parse(); // Bide, float olarak Parse edilemeyecek bir şey yazıp deneyin    if let Ok(p) = point {        if p > 2.777 {            println!("Harika bir iş");
        } else {            println!("Belki biraz daha çalışmak lazım")
        }    } else {        println!("Problem var");    }    /*        Fonksiyon parametreleri de birer şablon olabilir.        Aşağıdaki örneğe bakalım.
        move_left fonksiyonuna gönderilen location isimli tuple, parametre tarafındaki &(x,y) şablonu ile eşleştirilir.
    */    let location = (10, 20); //location bir pattern    let (a, b) = move_left(&location, 5); // (a,b) de bir pattern    println!("({}:{}) -> ({}:{})", location.0, location.1, a, b);    // Aşağıdaki kod parçasını açınca bir uyarı mesajı alınır. Sizce neden?    // if let value = 10 {    //     println!("{}", value);    // }    /*        Şablonları struct veri türünün değişkenlerini başka değişkenlere kolayca almak için de kullanabiliriz.        Buna Destructuring demişler. Belki de parçalarını çekip çıkardığımız içindir.        Her neyse. Aşağıdaki kullanıma bakalım.
        Bu kod parçasında bird içindeki id ve nick_name bilgilerini let pattern ile sol taraftaki number, player_name isimli        değişkenlere aldık(aynı isimli değişkenler de kullanabiliriz bu durumda : ile isim belirtmemize gerek yok)        ve bir sonraki satırda kullanabildik.    */    let bird = Player {        id: 33,        nick_name: String::from("Leri Böörd"),    };    let Player {        id: number,        nick_name: player_name,    } = bird;    println!(        "{} numaralı formasıyla '{}' geliyorrrr...",        number, player_name    );    /*        Benzer senaryoyu(Destructuring) enum tipi için de uygulayabiliriz.        Bunu match ifadesi ile ele almak oldukça mantıklı.
        eintesin bir enum değişkeni. İçinde bir tuple ve struct türlerine yer verdik.        match ifadesinde Person ve AddValues kısımlarında şablon eşleştirmeleri ile Destructuring işlemi uygulanmaktadır.
    */    let einstein = Genius::Person {        level: 78,        name: String::from("Gauss"),    };    match einstein {        Genius::Person { level: l, name } => {            println!("{} {}", l, name);        }        Genius::AddValues(v1, v2) => println!("{} {}", v1, v2),        Genius::OnGame => println!("Oyunda"), // Burada sabit bir değer söz konusu olduğunda Destructuring olmaz    }    /*        şablonlar ile Destructuring'in bir arada kullanımı aşağıdaki gibi karmaşık kod ifadelerine de sebebiyet verebilir.        Eşitliğin sol tarafındaki şablonda origin_x, origin_y isimli değişkenlere sahip tuple ve Player struct'ını içeren        bir tuple tanımı var. Sağ taraftan da buna göre bir eşleşme yapılıyor.
        Kısaca tuple ve struct içeren bir tuple'ın içeriği Destructuring ile değişkenlere alınıyor.
        Hatta alınırken id değişkenini göz ardı ediyoruz(Eşitliğin solundaki _ kullanımına dikkat)    */    let ((origin_x, origin_y), Player { id: _, nick_name }) = (        (155, 179),        Player {            id: 11,            nick_name: String::from("Kayri Örving"),        },    );    println!(        "'{}' şimdi ({}:{}) konumuna geldi.",        nick_name, origin_x, origin_y    );    /*        #2        Biraz da match kullanımlarına bakıp hatırlayalım.
        match kullanımının belki de en basit hali aşağıdaki gibidir.        currency şablonunun match ifadesindeki durumlardan birisine uygunluğu kontrol edilir.        _ ile hiçbirisine uymayan durum söz konusudur    */    let currency = "TL";    match currency {        "TL" => println!("TL işlemi uygulanacak"),        "USA" => println!("Dolar işlemi uygulanacak"),        _ => println!("Hiç birisi uymadı"),
    }    /*        Şimdi aşağıdaki match kullanımına odaklanalım.
        value_a'nın sahip olduğu değeri kontrol ediyoruz.        Some(50) eşleşmesi çalışmayacak, çünkü value_a Some(100) değerine sahip.        Lakin ikinci Some kontrolü eşleşecek. Buradaki mutable value_b, value_a nın başlangıçtaki değerine sahip olur. Yani Some(100)'e.        Bu nedenle eşleşme kabul edilir ve blok içerisinde value_b değeri 1 artırılıp ekrana basılır ki bu değer 101 dir!!!        match sonrasında ekrana A ve B değerleri 100 ve 10 olarak basılacaktır.
        Burada kafalar karışabilir. Some(mut value_b) eşleşmesi çalışmıştı ve orada value_b değerini 1 artırmıştık. Dolayısıyla value_b'nin 101 kalması gerekir diyebiliriz.        Ancak Some(mut value_b) value_a nın match ifadesinde kullanılmaktadır. Yani kendi bloğu içinde yeni bir değişkendir. Sadece value_a'nın başlangıç değerini almaktadır.
        Bunu etraflıca düşünüp hazmetmeye çalışın :)    */    let value_a = Some(100);    let value_b = 10;    match value_a {        Some(50) => println!("Got 50"),        Some(mut value_b) => {            value_b = value_b + 1;            println!("{}", value_b);        }        _ => println!("Farklı bir koşul"),
    }    println!("A değeri {:?}, B değeri = {:?}", value_a, value_b);    /*        Aşağıdaki match ifadesinde, şablonların veyalanarak ve bir aralık
        belirtilerek kullanılması örneklenmektedir.        Veyalamak için | aralık belirtmek içinse ..=(Matching Range) operatörlerinden yararlanılır.
        Matching Range sayı ve karakter veri tipi için kullanılabilir.
    */    let order_no = 10;    match order_no {        1 | 2 | 3 => println!("İlk üçtesiniz. Sıranız * 3 puan verilir."),        4 | 5 | 6 => println!("Yine de 1 puan verilir"),        7..=10 => println!("7nci ve 10ncu arasındasınız. O zaman 0.5 puan verelim."),        _ => println!("Kontenjan dışı kaldınız :("),    }    let first_letter = 'l';    match first_letter {        'a'..='m' => println!("{} izin verilen listede", first_letter),        _ => println!("{} izin verilen listede değil", first_letter),    }
}

fn move_left(&(x, y): &(i32, i32), v: i32) -> (i32, i32) {    (x + v, y + v)
}

// Destrcuting örnekleri için
struct Player {    id: i32,    nick_name: String,
}

enum Genius {    AddValues(i32, i32),                 // iki eleman tutan Tuple    Person { level: i32, name: String }, // bir struct    OnGame,
}

İkinci örneğimizde ise aslında biraz daha derli toplu bir çalışmamız olacak. Konuyu birim testleri ile birlikte ele alacağız. Bunu da bir kütüphane olarak tasarlayıp birim test fonksiyonları ile güçlendirelim.

///
/// story point değerine göre bir t-shirt boyutu döndürür
///
fn get_tshirt_size(s_point: usize) -> String {    /*        Literal bir tür üstünde yapılan tipik pattern matching örneğidir
    */    let result = match s_point {        1 | 2 => "SMALL",      //1 veya 2 olma hali        3..=8 => "MEDIUM",     // 3 ile 8 arasındaki değerlerden biri gelirse        8..=13 => "BIG",       // 8 ile 13 arasındaki değerlerden biri gelirse        20 | 80 => "EPIC",     // 20 veya 80 olma hali        100 => "DRINK COFFEE", // sabit 100e eşit olursa        _ => "N/A",            // _ ile bunların dışında kalan durumlar ele alınır
    };    //Soru: let ile değişkene almadan da fonksiyondan çıktı üretebilir miyiz?    //Soru: _ ile kalan durumları ele almazsak ne olur?    result.to_owned()
}

///
/// Parametre olarak Gün,Ay,Yıl,Saat,Dakika,Saniye cinsinden gelen bir tuple içinden saat bilgisini döndürür
///
fn get_hour(time: (usize, usize, usize, usize, usize, usize)) -> String {    /*        pattern matching tuple veri türü ile de kullanılabilir.
        _ ile işaretlenen tuple alanları atlanırken h ile tuple içinden dışarıya doğru değişkenin değeri verilir.    */    match time {        (_, _, _, h, _, _) => format!("Saat {}", h),    }
}

/*
    Destructuring.    Pattern Matching ile yapıların alanlarını dallanmalara çıktı olarak verebiliriz.
*/
enum TaskState {    Approved,    Rejected,    Canceled,    Created,    Closed,    Suspended,
}
struct Task {    state: TaskState,    title: String,    story_point: usize,
}

///
/// Parametre olarak gelen Task ile ilgili özet bilgi döner
///
fn task_info(t: Task) -> String {    /*        t ile gelen Task yapısının olası durumları ele alınmaktadır.
        Sembolik olarak Approved, Rejected ve Canceled durumlarına bakılmaktadır.
        Her durum için bir Task örneği oluşturulmakta, Task'a gelen title ve story_pint gibi değişkenler t ve point değişkenleri ile        dışarıya çıkartılabilmekte ve => sonrasındaki format! fonksiyonunda kullanılabilmektedir.
    */    match t {        Task {            state: TaskState::Approved,            title: t,            story_point: point,        } => format!("{},{} -> Onaylandı", t, point),        Task {            state: TaskState::Rejected,            title: t,            story_point: _, // story_point'ı dışarı çıkartıp kullanmayacağımız için _ kullanıldı
        } => format!("{} -> İade Edildi", t),        Task {            state: TaskState::Canceled,            title: t,            story_point: point,        } => format!("{},{} -> İptal Edildi", t, point),        _ => "Kapsam Dışıdır".to_owned(), // Yukarıdaki hallerinde dışında bir TaskState gelirse    }
}

///
/// story_point bazında bir olasılık tahmini yapar
///
fn task_probability(t: Task) -> String {    /*        Burada guard matching kullanımı söz konusudur. (if point > 80 ile eşleşen bir dala koşul konulmuştur)
        .. kullanımlarına dikkat    */    match t {        Task {            story_point: point, ..        } if point > 80 => "Şaka yapıyorsun sanırım".to_owned(), //Task eşleşmesinde sadece story_point'i ele alıp 80den büyük olma haline bakıyoruz,
        Task { .. } => "Story Point elverişli".to_owned(),
    }
}

///
/// Bir Task çiftini story_point bazında karşılaştırıp analiz eder
///
fn compare_task(pair: (Task, Task)) -> String {    /*        guard matchin örneği.
        Fonksiyona gelen tuple içerisinde iki Task örneği var.        (t1,t2) çiftlerini if koşullarına sokup story_point bazında karşılaştırma yapıyoruz.
    */    match pair {        (t1, t2) if t1.story_point > t2.story_point => format!("{} > {}", t1.title, t2.title),        (t1, t2) if t1.story_point < t2.story_point => format!("{} < {}", t1.title, t2.title),        (t1, t2) if t1.story_point == t2.story_point => format!("{} = {}", t1.title, t2.title),        _ => "İlişki kuramadım".to_owned(),
    }
}

#[cfg(test)]
mod tests {    use super::*;    #[test]    fn should_task_state_on_approved() {        let redis_task = Task {            state: TaskState::Approved,            title: "Session bilgileri Redis'e alınacak".to_string(),
            story_point: 13,        };        assert_eq!(            task_info(redis_task),            "Session bilgileri Redis'e alınacak,13 -> Onaylandı"
        );    }    #[test]    fn should_task_state_out_of_scope() {        let core_task = Task {            state: TaskState::Created,            title: "NDAL Kaldırılacak".to_string(),
            story_point: 80,        };        assert_eq!(task_info(core_task), "Kapsam Dışıdır");
    }    #[test]    fn should_task_state_on_just_kidding_grater_then_80_points() {        let convert_task = Task {            state: TaskState::Created,            title: "Backend tarafı mikroservise dönüştürülecek".to_string(),            story_point: 100,        };        assert_eq!(task_probability(convert_task), "Şaka yapıyorsun sanırım");
    }    #[test]    fn should_task_grater_than_other() {        let tasks = (            Task {                title: "Task 1".to_string(),                state: TaskState::Created,                story_point: 5,            },            Task {                title: "Task 2".to_string(),                state: TaskState::Created,                story_point: 13,            },        );        let comment = compare_task(tasks);        assert_eq!(comment, "Task 1 < Task 2");    }    #[test]    fn should_medium_for_5_story_point() {        let size = get_tshirt_size(5);        assert_eq!(size, "MEDIUM");    }    #[test]    fn should_small_for_1_or_2_story_point() {        let size = get_tshirt_size(1);        assert_eq!(size, "SMALL");        let size = get_tshirt_size(2);        assert_eq!(size, "SMALL");    }    #[test]    fn should_return_none_for_other_story_points() {        let size = get_tshirt_size(500);        assert_eq!(size, "N/A");    }    #[test]    fn should_get_hour_from_long_time() {        let now = (29, 10, 2020, 17, 32, 65);        assert_eq!(get_hour(now), "Saat 17");    }
}

Smart Pointers

Pointer denince aklımıza bellekteki bir bölgenin adresini tutan işaretçi gelir. Rust dilinde pointer'ların en bilinen tipi ise referans türüdür. Ancak bunların yanında Smart Pointer adı verilen bir veri yapısı daha var. Smart Pointer yine verinin bellek adresini taşır ama ek metadata bilgisi de içerir. Hatta şu ana kadar birkaç smart pointer kullandığımızı söyleyebilirim(Örneğin String ve Vec) Söz gelimi Vector türü sadece verinin referans adresini taşımaz onunla birlikte başlangıç kapasitesi gibi ek bilgi de taşır. Pointer olarak belirtilen referans türleri veriyi ödünç alırlar(borrowing durumu) aksine Smart Pointer'lar adreslenen veriyi sahiplenirler(ownership durumu)Şimdi örnek kod parçası ile konuyu anlamaya çalışalım.

fn main() {    /*        En bilinen Smart Pointer türlerinden birisi Box<T>        Veriyi Heap üzerinde tutmamızı sağlar. Tamam referans türü ile de bunu yapıyoruz ama metadata olayını unutmayalım.
        Ayrıca,
            Büyük bir verinin sahiplini kopyalamadan taşımak istediğimizde (Büyük veriyi Heap'te kutulayacağız)
            Derleme zamanında boyutunu bilemediğimiz bir veri kullandığımızda
        gibi durumlarda tercih edilir.    */    let a_number = Box::new(3.1415); // normalda stack'te duracak bir f32 verisini Heap'te kutuladık
    println!("{}", a_number);    /*        Rust, derleme zamanında tiplerin ne kadar yer tutacağını bilmek ister.        Fonksiyonel dillerde rastlanan cons list gibi türler ise recursive özellik gösterirler ve tipin ne kadar yer tutacağı kestirilemez.        cons Lisp kökenlidir ve iki argüman alan bir fonksiyondur. const list, cons fonksiyonunu recursive olarak çağıran bir elemanlar dizisidir.        Son elemanda Nil görene kadar bu liste devam edebilir.    */    let infinity_war = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); // Kafa karıştırıcı yahu    /*        Şimdi aşağıdaki kullanımlara bakalım.
        Normalde bir değişkenin referansını almak için & operatörünü kullanırız. Referans üstünden değer okurken de *(Dereference) operatörü kullanılır.
    */    let mut point = 23;    let pointer = &point;    assert_eq!(23, *pointer); // * operatörünü kullanmadan denersek 'no implementation for `{integer} == &{integer}`' şeklinde hata alırız
                              // point = 25; // Bunu yapamayız. Çünkü borrowing söz konusudur. point, pointer tarafında referansla da olsa ödünç alınmıştır

    /*       Yukarıdaki kullanımda pointer, point değerini referans eder.       Aşağıdaki kullanımda ise counter değerinin bir kopyası Heap'e alınırken stack bölgesinden referans edilir.    */    let mut counter = 1;    let smart_pointer = Box::new(counter);    assert_eq!(1, *smart_pointer);    counter += 1; // Bu mümkündür çünkü smart pointer borrowing durumunu oluşturmamıştır
                  //assert_eq!(2, *smart_pointer); // counter kendi başına artar. Smart Pointer onun değerini koypalayarak kullandığı için halen 1'e eşittir

    // Kendi smart pointer türümüzün kullanımı
    let lucky_num = 2.777;    let magic_box = MagicBox::create(lucky_num);    assert_eq!(2.777, *magic_box);
}

/*
    Kutsal Rustacean Kitabına göre Box yapısının referans türlerinden farkını anlamanı en iyi yolu kendi Smart Pointer türümüzü geliştirmekmiş.
    Tabii herhangi bir türle çalışması isteneceğininde generic tanımlanıyor.
    Tanımlayacağımız MagicBox yapısına create isimli bir fonksiyon da ekledik.(Box<T> türünün new fonksiyonu olarak düşünebiliriz)        Ayrıca yukarıkdaki assert_eq!(2.777,*magic_box); satırında Deference operatörünün kullanımı söz konusu.     Bunu da kendi Smart Pointer yapımıza öğretmemiz gerekiyor. Aksi durumda 'type `MagicBox<{float}>` cannot be dereferenced' şeklinde derleme    zamanı hatası alırız. Sonuçta oradaki kıyaslama için de * operatörü ile derefer ederek değeri almamız lazım.
*/
struct MagicBox<T>(T);

impl<T> MagicBox<T> {    fn create(value: T) -> MagicBox<T> {        MagicBox(value) // MagicBox bir Tuple gibi tasarlandığından onu metoda parametre olarak gelen value değeri ile oluşturuyoruz
    }
}

// Dereference için Deref Trait'inin uygulanması
use std::ops::Deref;

impl<T> Deref for MagicBox<T> {    type Target = T;    fn deref(&self) -> &T {        &self.0 //Kara karıştırmasın. Tuple'ın ilk elemanının değerini döndürüyor    }
}

/*
        ConsList enum türüne bakalım. Cons fonksiyonunu ve Nil değerini içeriyor.        Cons fonksiyonu da i32 türünden bir değer ve yine bir ConsList enum türü alıyor. İşte recursive veri yapısı.
        Bunu bu haliyle bırakırsak derleme zamanı 'recursive type `ConsList` has infinite size' şeklinde hata döner.        O yüzden ConsListV2 şeklinde tanımlayıp kullanmamız gerekiyor.
*/
// enum ConsList {
//     Cons(i32, ConsList),
//     Nil,
// }

enum ConsListV2 {    Cons(i32, Box<ConsListV2>), // Box kullandığımız için artık veriyi Heap'ta tutacağımızı belirttik.    Nil,
}

use crate::ConsListV2::{Cons, Nil}; // Bu küfe bildirimini yapmazsak infinity_war kullanımında 'not found in this scope' hatası alırız

Reference Counting

Smart Pointer'lar ile yakından ilişkili olan bir konu da reference counting'dir. Bir değerin birden fazla sahibi olduğu durumlar için geçerli bir konudur. Mesela bir graph ağacında bir boğumu işaret eden n sayıda boğum varsa, işaret edilen boğum için n sayıda sahiplikten söz edilebilir. Rc<T> aynı değeri işaret eden referanslar için muhasebeci görevini üstlenir. Öyleki aynı değeri işaret eden referansların sayısı ancak sıfırlanırsa bellekten atılabilir. Bu yönetim Rc ile kontrol altına alınır. Aslında bu kendi Garbage Collector mekanizmanızı yazmanın yolunu bile açar.

Örneğimizde Recursive veri yapılarından olan cons list kullanımı ele alınıyor. points1, points2 ve points3 birer Cons List. points2 ve points3 oluşturulurken ilk değerler sonrası points1 listesine bağlanıyorlar. Hem points2 hem points3 aynı listeyi(points1) paylaşmakta ki aslında paylaşamıyorlar. points3 kısmında derleme zamanı hatası oluşuyor. Bu nedenle Box<T> smart pointer türü yerine Rc<T> türünü kullanmak gerekiyor. Rreferance count değerlerini görmek için strong_count fonksiyonunu nasıl kullandığımıza bir bakın. points1 ilk oluştuğunda bu değer 1 dir. points2 points1'i kullanarak oluştuğunda bu değer 2'ye çıkar. points3 devreye girdiğinde sayaç 3'e çıkar çünkü toplamda 3 referans söz konusudur. {} bloğundan sonra ise points3 scope dışı kalır ve dolayısıyla referance count 1 azalır.

use crate::PointList::{Cons, Nil};
use std::rc::Rc; // Rc<T> veri yapısını kullanabilmek için eklendi

fn main() {    // let points1 = Cons(7, Box::new(Cons(8, Box::new(Cons(9, Box::new(Nil)))))); //7->8->9->Nil şeklinde bir listemiz var    // let points2 = Cons(1, Box::new(points1));    // let points3 = Cons(3, Box::new(points1)); // Normalde bu şekilde kullanırsak, bir üst satırda points1'in sahipliği points2'ye geçtiği için use of moved value: `points1` derleme zamanı hatası alırız

    let points1 = Rc::new(Cons(7, Rc::new(Cons(8, Rc::new(Cons(9, Rc::new(Nil))))))); // Bir önceki kullanımdan farklı olarak Rc::new ile oluşturmaya başladığımıza dikkat edelim    println!("Reference Count {}", Rc::strong_count(&points1));    let points2 = Cons(1, Rc::clone(&points1)); // clone fonksiyonunu kullanarak points1'in referansını geçiyoruz    {        println!("Reference Count {}", Rc::strong_count(&points1));        let points3 = Cons(3, Rc::clone(&points1));        println!("Reference Count {}", Rc::strong_count(&points1));    }    println!("Reference Count {}", Rc::strong_count(&points1));    // let points4 = Cons(10, points1.clone()); // Performans açısından tercih edilmez    /*        Bu arada Rc::clone(&points1) kullanımı yerine points1.clone() da tercih edilebilir ancak        Rc::clone deep copy yapmadığından ve sadece referansmatiği (Counter diyelim) 1 artırdığından çok daha hızlı işlem görür.    */
}

// // Kobay cons list yapımız
// enum PointList {
//     Cons(i32, Box<PointList>),
//     Nil,
// }

// Kobay cons list yapımız
enum PointList {    Cons(i32, Rc<PointList>),    Nil,
}

Fearless Concurrency

Eş zamanlı ve paralel programlama çok çekirdekli işlemcilerin hayatımıza girmesiyle birlikte önem kazanan başlıca iki konu olarak düşünülebilir. Concurrent Programming ile birbirlerinden bağımsız olarak çalışan program parçalarını, Parallel Programming ile de aynı anda çalışan program parçalarını kastediyoruz. Rust dilinin güçlü taraflarından birisi de Concurrency konusunda kendini göstermekte.

Bu zamana kadarki örneklerde ownership, type safety, borrowing vs gibi konuları görmüştük. Bunlar bellek güvenliği(memory safety) ve verimlilik açısından Rust'ı öne çıkaran başlıklar. Bu yetenekler sayesinde Concurrent programlama daha güvenli ve verimli hale geliyor. Nitekim pek çok dilin aksine Rust ortamında, Concurrent çözümlerde yaşanacak sorunlar çalışma zamanında değil daha derleme aşamasındayken görülebiliyor. Kim üretim ortamında gerçekeleşen bir concurrency hatasını geliştirme veya test ortamında tekrarlamaya çalışıp sorunun tam olarak ne olduğunu anlamaya çalışmak için çaba sarf etmek ister ki ;) Dolayısıyla Rust'ın bu sorunlara neden olabilecek sıkıntıları henüz derleme aşamasında söylemesi oldukça önemli. Rust'ın bu gücü için Fearless Concurrency terimi kullanılmakta. Tabii işin sırrı birçok işletim sistemi ve programlama dilinde olduğu gibi Thread'ler ile çalışmaktan geçiyor.

use std::thread; // Thread kütüphanemiz
use std::time::Duration; // Sembolik gecikmeler için

fn main() {    example_one();    // Burada da main thread'i içerisinde çalışan bir döngü var    // Ekrana 10 kere Bar yazacak    println!("Ana thread başladı...");
    for _i in 1..5 {        println!("Bar");        thread::sleep(Duration::from_secs(1)); // ve bu ana thread'de 1er saniye gecikmeli çalışacak
    }    println!("Ana thread bitti...");    /*        Bu ilk örnekte dikkat edilmesi gereken iki nokta var.        A- example_one içerisinde thread'ler henüz bitmese de, yukarıdaki döngü bittiği için uygulama sonlanacak        ve diğer thread'ler de ölmüş olacaktır. (join_handle örneğine bakın)

        B- Ayrıca main içerisindeki sıra nasıl olursa olsun (ki burada example_one içerisindeki thread'ler önce çalışmak üzere        yazılmıştır) ilk olarak ana thread içerisindeki kod çalıştırılır. Bu sebepten diğer thread'ler başlamadan önce        27nci satır mutlaka işletilir ve döngü derhal başlar. (Sanırım Main thread'in öncelikli olduğunu düşünebilirim)    */
}

fn example_one() {    // Bir thread açtık
    std::thread::spawn(|| {        println!("1 başladı...");
        for _i in 1..10 {            // Ekrana 10 defa Foo yazacak            println!("Foo");            thread::sleep(Duration::from_secs(2)); // ve herbir yazma sonrası bu thread 2 saniye bekletilecek        }        println!("1 bitti...");    });    // Burada da ikinci bir thread açtık
    // Bu kez bir vector'ün elemanları üzerinde işlem yaptığımızı varsayıyoruz
    std::thread::spawn(|| {        println!("2 başladı...");
        for color in vec!["red", "green", "blue"] {            println!("{}", color);            thread::sleep(Duration::from_secs(2)); // ve yine 2 saniyelik bir gecikme        }        println!("2 bitti...");    });
}

Örneğin çalışma zamanı çıktısı aşağıdaki gibi olacaktır.

Join Handle

Bir önceki örnekte ana thread işi bitirdiği için başlatılan ve halen devam eden diğer thread'lerin de sonlandığını gördük. Bu çok da istediğimiz bir durum değil. Bunu önlemek için JoinHandle<T> tipinden yararlanılabilir ve halen daha bitmemiş iş parçalarının bitmesinin beklenmesi sağlanabilir.

use std::thread;
use std::time::{Duration, SystemTime}; // Zaman ölçümlemeleri için ekledik

fn main() {    let now = SystemTime::now();    // spawn geriye JoinHandle<T> nesnesi döndürür    let wait_handle = std::thread::spawn(|| {        println!("#1 başladı...");
        for _i in 1..7 {            // Ekrana 10 defa Black yazacak            println!("BLACK");            thread::sleep(Duration::from_secs(2));        }        println!("#1 bitti...");    });    // başka bir thread daha    let another_wait_handle = std::thread::spawn(|| {        println!("#2 başladı...");
        for _i in 1..7 {            // Ekrana 10 defa Black yazacak            println!("RED");            thread::sleep(Duration::from_secs(4));        }        println!("#2 bitti...");    });    println!("Ana thread başladı...");
    for _i in 1..5 {        println!("White");        thread::sleep(Duration::from_secs(1));    }    println!("Ana thread bitti...");    // Main Thread'e ilk thread'in tamamlanmasını beklemesini söylüyoruz    wait_handle.join().unwrap();    // Diğer thread'i de bekle diyoruz    another_wait_handle.join().unwrap();    match now.elapsed() {        Ok(elapsed) => {            println!("Tüm işlemler için geçen toplam süre {}", elapsed.as_secs());        }        Err(e) => {            println!("Error: {:?}", e);        }    }
}

Şimdi örneğin çıktısı aşağıdaki gibi olacaktır.

Join kullanımına ait başka bir örneği de aşağıda bulabilirsiniz. Bu kez iş parçaları bir döngü içerisinde kuyruğa alınmakta. Bu döngüde iş parçasının bitip bitmediğini pattern matching ile kontrol ettiğimize dikkat edelim. 

use std::thread;
use std::time::{Duration, SystemTime}; // Zaman ölçümlemeleri için ekledik

fn main() {    let now = SystemTime::now();    /*        Döngü başlatılan thread'leri bir vector'de topluyor.        Eğer move closure'ını kullanmazsak i değişkeni sahipliğinin ödünç olarak thread içerisine alınamamasından dolayı
        derleme zamanı hatası alırız.
    */    let mut threads = vec![];    for i in 0..5 {        threads.push(thread::spawn(move || {            println!("{} başladı", i);            for j in 1..5 {                println!("Thread #{} da {} için bir şeyler yapılıyor gibi...", i, j);                thread::sleep(Duration::from_secs(1));            }            return i; // thread'den geriye bir değer döndürüyoruz. Bu değeri aşağıdaki pattern matching kullanımında yakaladık

            // println!("{} sonlandı", i);        }));    }    // Bitmeyen thread'ler için Main bekletiliyor.    for t in threads {        let result = t.join();        match result {            Ok(r) => {                println!("#{} tamamlandı", r); // Tamamlanan thread'den dönen değeri r ile alabiliriz            }            Err(e) => {                println!("{:?}", e);            }        }    }    match now.elapsed() {        Ok(elapsed) => {            println!("Tüm işlemler için geçen toplam süre {}", elapsed.as_secs());        }        Err(e) => {            println!("Hata oluştu: {:?}", e);        }    }
}

Counter Örneği

Thread'ler ile ilgili sıradaki örnek bir metin içerisindeki ç harflerinin sayısını bulmak için kullanılıyor. Ahım şahım bir örnek değil ama thread'ler için iyi bir antrenman olduğunu söyleyebilirim. Senaryoya göre document değişkeni pipe işaretlerine göre parçalarına ayrılıyor. Her bir parça üzerinde işlemler yapılması içinse birer thread açılıyor. Thread'ler içerisinde o thread'in ele aldığı içerikteki ç harfleri sayılıyor. Program sonunda bu değerler bir arada toplanarak ele alınıyor.

use std::thread;

fn main() {    // üzerinde çalışacağımız değişken
    let document = "Bugün epeyce Rust çalışmaya çalıştım|Çarşambanın gelişi bir önceki çarşambadan belli olur mu dersin|Kaç tane ç harfi yazalım|çççççççç demek istiyorum|Çok çalışmamız lazım...Çooookk çalışmamız";
    // thread'leri toplayacağımız vector    let mut workers = vec![];    // içeriği | işaretine göre ayrıştırdık
    let rows = document.split('|');    for (i, row) in rows.enumerate() {        println!("#{}->\"{}\"", i, row);        // Burada yeni thread açıp workers'a ekliyoruz        // Thread geriye kaç tane ç olduğunu dönecek        workers.push(thread::spawn(move || -> u32 {            let mut total = 0;            for c in row.chars() {                if c == 'ç' {                    total += 1;                }            }            println!("#{} içerisinde {} tane ç harfi var", i, total);            total        }));    }    let mut all_totals = vec![]; // işlenen her bir satır için bulunan toplam değerleri biriktireceğimiz vector(ne uzun cümle yazdım yahu)        /*        Tüm worker'ların işlerini bitirmesini bekliyoruz.        Worker'ların işi bittikçe döndürülen sonuçlar(ç'lerin toplmaları)
        bir başka vector'de toplanıyor.
        En sonunda da genel toplamı yazdırıyoruz.
    */    for worker in workers {        let sub_total = worker.join().unwrap();        all_totals.push(sub_total);    }    let sum = all_totals.iter().sum::<u32>();    println!("Tüm dokümanda {} adet ç harfi varmış", sum);
}

Bu örneğe ait çalışma zamanı çıktısı aşağıdaki gibidir.

Message Passing

Thread'lerin ortak veriler üzerinde işlem yapması gerektiği durumlarda eşzamanlılığı güvenli bir şekilde sağlamak için mesajlaşma tekniği uygulanır. Go'dan gelen motto burada da geçerliliğini korur; "Hafızayı paylaşarak iletişim kurmayın; bunun yerine iletişim kurarak hafızayı paylaşın" Rust dilinde de Go'dakine benzer şekile channel kullanımı söz konusudur. Kanaldan faydalanarak thread'ler aralarında haberleşme sağlanabilir. Bir channel nesnesi verici(Transmitter) ve alıcı(Receiver) olmak üzere iki parçadan oluşur. Örneğin n adet thread'in bir hesaplama yapıp bu hesaplamaları işlenmek(aggregate)üzere başka bir thread'e gönderdiğini düşünelim. Bu channel nesne kullanımı için ideal bir senaryodur.

use std::sync::mpsc; // Multiple Producer Single Consumer
use std::thread;
use std::time::Duration;

fn main() {    /*        #1        İlk olarak bir kanal nasıl açılır, bu kanale bir thread üstünden mesaj nasıl basılır ve        tabii basılan mesaj başka bir thread tarafından nasıl alınır bakalım.

        channel tanımlandığında geriye bir tuple döner        tx, transmitter(yayıncı) rx ise receiver(alıcı) nesneleri işaret eder    */    let (tx, rx) = mpsc::channel();    // move kullandık ki tx'i closue ile kullanabilelim    let worker1 = thread::spawn(move || {        println!("#1 Jennifer tepeden nehre bir plastik ördek bırakıyor. Aklında bir sayı var.");        let calculated_value = 3.1415;        tx.send(calculated_value).unwrap(); // transmitter ile kanala mesajımızı/değeri bırakıyoruz
                                            /*                                                worker'lar pek tabi eş zamanlı olarak işe başlarlar.
                                                Aşağıda worker1 sembolik olarak uzun süre bir iş yapsa da,                                                yukarıda kanala bir mesaj bırakmıştır ve diğer thread'ler                                                bu mesajı duraksatmaya aldırmadan alıp kullanabilirler ;)                                            */        thread::sleep(Duration::from_secs(3));    });    let worker2 = thread::spawn(move || {        println!("#2 Alice, Jennifer'ın gönderdiği plastik ördeği bekliyor.");        let received_value = rx.recv().unwrap(); //receiver ile kanala bırakılan mesajı yakalıyoruz
        println!(            "Kanala bırakılan plastik ördeğin aklındaki sayı: {}",            received_value        );    });    worker1.join().unwrap();    worker2.join().unwrap();    println!();    /*        #2        Aşağıdaki kod bloğu safe concurency sebebiyle derlenmez.        Transmitter değişkeninin send metodu, outgoing referansının sahipliğini alır
        ve bu değişken gönderildikten sonra bu sahiplik receiver'a geçer.        Bu nedenle spawn bloğunda send çağrısı sonrası outgoing değişkeni artık kullanılamaz.
        Bunun sebebi bir thread'in kanala bıraktığı değeri sonradan kendisinin değiştirmesini engellemektir.        Lakin gönderilen değişkenin gönderildiği haliyle receiver tarafından kullanılmasını isteriz.    */    let (tx, rx) = mpsc::channel();    thread::spawn(move || {        let outgoing = String::from("Eco Eco Bravo 6 Eco...Burası Kartal Kondu. Tamam");        tx.send(outgoing).unwrap();        // println!("{}", outgoing); // Derleme hatasını görmek için bu satırı etkinleştirin
    });    let incoming = rx.recv().unwrap();    println!("{}", incoming);    println!();    /*        #3        Pek tabii en sık başvurulacak senaryolardan birisi de        n sayıda thread'den mesaj gönderip almak.        Yani Multiple Consumer Single Receiver olayı.
        Burada önemli olan nokta transmitter'ın klonlanması.

        Aşağıdaki örnek kod parçasında tx nesnesi klonlanmış ve        diğer thread'ler tarafında kullanılabilir hale gelmiştir.
    */    let (tx, rx) = mpsc::channel();    let tx_kadikoy = mpsc::Sender::clone(&tx);    let tx_besiktas = mpsc::Sender::clone(&tx);    thread::spawn(move || {        let status = String::from("Üsküdar da hava açık ve 23 derece");        tx.send(status).unwrap();        thread::sleep(Duration::from_secs(3));    });    thread::spawn(move || {        let status = String::from("Kadıköy de hava açık ve 22,4 derece");        tx_kadikoy.send(status).unwrap();        thread::sleep(Duration::from_secs(1));    });    thread::spawn(move || {        let status = String::from("Beşiktaş da hava yer yer rüzgarlı ve 21 derece");        tx_besiktas.send(status).unwrap();        thread::sleep(Duration::from_secs(5));    });    let last_standing_man = thread::spawn(move || {        /*            tx, tx_kadikoy ve tx_besiktas transmitter'ları üstünde gelen mesajlar            rx nesnesi üstünden yakalanabilirler.        */        for current_status in rx {            println!("{}", current_status);        }    });    last_standing_man.join().unwrap();    println!();    /*        #4        Transmitter üstünden kanala n sayıda mesaj da bırakılabilir.
        Aşağıdaki kod parçasında aynı thread içinden aralıklarla birkaç        mesaj yollanıyor. Bu mesajlar yine rx'i kullandığımız bir for döngüsü        ile alınıyorlar.

        sleep'ler ile yaptığımız duraklatmalar mesaj yollandıkça dinleyici tarafından
        alınır durumunu göstermek için.    */    let (tx, rx) = mpsc::channel();    thread::spawn(move || {        let value_1 = String::from("1 kilo un");        tx.send(value_1).unwrap();        thread::sleep(Duration::from_secs(1));        let value_2 = String::from("3 yumurta");        tx.send(value_2).unwrap();        thread::sleep(Duration::from_secs(3));        let value_3 = String::from("Yarım çay bardağı kadar şeker");
        tx.send(value_3).unwrap();        thread::sleep(Duration::from_secs(2));    });    for received in rx {        println!("{}", received);    }
}
ve tabii ki çalışma zamanı çıktısı.
 

Mutexes

Rust dilinde kanallardan yararlanarak mesajlaşma yapan eş zamanlı thread'leri nasıl kullanabileceğimizi gördük. Kanallar (Channels) aslında tekil mülkiyet hakkı sağlarlar. Yani bir thread kanala bir veri bıraktığında bu veri onun için artık kullanılabilir değildir. Buna karşın birden fazla thread'in aynı anda aynı bellek bölgesini kullanmak isteyeceği durumlarda mümkündür. Bu tip durumların kanallar haricinde bir diğer yönetim şekli ise Mutex tipi ile gerçekleşir. Kanallar nasıl tekil mülkiyetliği baz alıyorsa, Mutex tipi de smart pointer'lar gibi çoklu mülkiyet/sahiplik hakkını baz alır(Keza Mutex<T> bir smart pointer'dır) Bir thread Mutex'e alınan bir veriyi kullanmak istediğinde bunu öncelikle ona sorar ve eğer müsaitse verinin kilidini(lock) alır. İşi bitince de serbest bırakır.  

Özetle çoklu thread'lerin aynı veriyi kullanmak istemeleri halinde t anında sadece bir tanesinin onu kullanmasına nasıl izin vereceğimizi bilmemiz gerekiyor. Mutex bu anlamda nasıl bir çözüm sunuyor görelim. 

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {    /*        #1        İki thread Mutex kullanırken.
        worker_sue içinde "use of moved value" derleme zamanı hatası alırız.
        *collector satırında ise "borrow of moved value" derleme zamanı hatasını alırız.

    */    // // Mutex nesnesini oluşturuyoruz. Tutacağı veri 32 bit integer ve değeri 1    // let collector = Mutex::new(1);    // // Bir thread başlatılıyor
    // let worker_joe = thread::spawn(move || {    //     // mutex'in kilidini alıyoruz ve işimiz bitene kadar thread'i blokluyoruz    //     let mut point = collector.lock().unwrap();    //     // Burada kilidi bizde olan Mutex verisinin değerini değiştirdik
    //     *point += 5;    // });    // // Scope dışına çıktığımız anda Mutex'in kilidini de devretmiş olduk. Dolayısıyka başka bir thread artık bu kilidi alabilir.    // /*    //     Aşağıdaki satırda "use of moved value" hatası alınır
    //     Rust, collector'un mülkiyetinin birden fazla thread'e alınamayacağını söyler.    //     Dikkat edin. Üst taraftaki thread için derleyici kızmaz. Aşağıdaki move || satırının altını çizerek kızar.
    // */    // let worker_sue = thread::spawn(move || {    //     let mut point = collector.lock().unwrap();    //     *point += 3;    // });    // worker_joe.join().unwrap();    // worker_sue.join().unwrap();    // println!("{}", *collector.lock().unwrap());    /*        #2        Üstteki kodda bildiğiniz üzere mülkiyet sorunu yaşadık.
        Thread Safe olarak Mutex'i diğer thread'lerin de kullanılmasını sağlamak için,        Atomic Reference Counting Arc<T> tipinden yararlanabiliriz.        Kodun çalışır hali aşağıdaki gibidir.        Mutex verisini değiştirmek ve son halini almak için Arc üstünde klonladığımız referanslar olduğuna dikkat edelim.    */    let main_collector = Arc::new(Mutex::new(1));    let mid_collector = Arc::clone(&main_collector);    let last_collector = Arc::clone(&main_collector); // Buna neden ihtiyaç duydum?    let worker_joe = thread::spawn(move || {        let mut point = main_collector.lock().unwrap();        *point += 5;    });    let worker_sue = thread::spawn(move || {        let mut point = mid_collector.lock().unwrap();        *point += 3;    });    worker_joe.join().unwrap();    worker_sue.join().unwrap();    println!("{}", *last_collector.lock().unwrap());
}

Rust dilinde şimdilik buraya kadar gelebildim. Gerçek saha tecrübem olmadığı için halen daha başlangıç seviyesinde olduğumu söyleyebilirim. Sürekli kodlamak sürekli onunla haşırneşir olmak gerekiyor ki biraz olsun hakimiyet kazanabileyim. Bakalım bir sonraki yıl buna zaman ayırabilecek miyim. Gelelim bu uzun öğretiye ait size verebileceğim ödevelere ve sorulara.

Sorular

  • Rust dilinde değişkenler neden varsayılan olarak immutable işaretlenir?
  • factorial örneğindeki expect fonksiyonları hangi hallerde devreye girer? panic durumları bu kod parçasında nasıl ele alınır?
  • lucky_number örneğindeki match kullanımlarının ne işe yaradığını bir arkadaşınıza anlatınız?
  • Büyük veri yapısına sahip bir tipi mutable olarak kullanmak mı uygundur, immutable olarak mı? Yoksa duruma göre değişir mi?
  • shadowing hangi durumlarda mantıklı olabilir?
  • Ne zaman array ne zaman vector kullanmak uygun olur?
  • C# dilinde String atama ve metotlara parametre olarak geçme davranışları ile Rust tarafındakileri karşılaştırın.
  • ownership uygulamasının aldığı derleme zamanı hatasının sebebi nedir?
  • Hiçbir alan(field) içermeyen bir struct tanımlanabilir mi? Buna izin veriliyorsa amaç ne olabilir?
  • structs örneğinde yer alan println!("{}", mouse.title); kod parçası açılırsa neden derlenmez?(Line: 18)
  • Yine structs örneğinin 19ncu satırındaki kod, mouse değişkeni mut ile mutable yapılsa dahi derleme hatasına neden olacaktır. Neden?
  • Bir enum yapısındaki değişkenler başka enum değişkenlerini de içerebilir mi?
  • Bir vector koleksiyonunda farklı tipten elemanlar tutmak istersek ne yaparız?
  • String'leri + operatörü ile birleştirirken neden & ile referans adresi kullanırız?
  • collections örneğinde a_bit_off_word değişkenine siyah isimli metindeki ilk karakteri almak ve panic durumunun oluşmasını engellemek için ne yapılabilir?
  • Unwinding kabiliyeti nasıl etkinleştirilir?
  • traits isimli örnekte yer alan Action içerisindeki initialize metodunun Hyperlink fonksiyonu için kullanılmasını istemezsek nasıl bir yol izlememiz gerekir
  • lifetimes isimli programdaki #1 örneğinde oluşan derleme zamanı hatasını nasıl düzeltiriz?
  • Bir fonksiyon birden farklı generic lifetime parametresi kullanabilir mi?
  • Bir test fonksiyonu sonuç dönebilir mi?
  • Ne zaman normal fonksiyon ne zaman closure kullanılır?
  • iterators2 örneğinde yer alan Game struct'ı için neden #[derive(PartialEq, Debug)] niteliği uygulanmıştır?
  • cons list kullanmamızı gerektirecek bir durum düşünün ve tanıdığınız bir Rustacean'a bunu anlatın.
  • Rc<T> kullanmamızı gerektirecek en az bir senaryo söyleyebilir misiniz?
  • Arc<T>(Atomic Reference Counting) tipi hangi amaçla kullanılır?
  • Bir struct değişkenini match ifadesi ile kullanabilir miyiz?

Ödevler

  • lucky_number örneğindeki cpm işlem sonucunu match yerine if blokları ile tesis ediniz.
  • lucky_number örneğinde loop döngüsü kullanmayı deneyiniz.
  • Bir kitabı birkaç özelliği ile ifade eden bir struct yazıp, bu kitabın fiyatına belirtilen oranda indirim uygulayan metodu geliştiriniz(Metot, impl bloğu ile tanımlanmalı)
  • mercury isimli kütüphaneyi başka bir rust uygulamasında kullanabilir misiniz? Nasıl?
  • Bir String içeriğini tersten yazdıracak fonksiyonu geliştiriniz?(rev kullanmak yasak)
  • error_handling örneğinde 69ncu satırda başlayan ifadede i32'ye dönüşemeyen vector değerlerini hariç tuttuk. Geçersiz olan değerleri de toplayıp ekrana yazdırabilir misiniz?(ipucu : partition fonksiyonu)
  • İki kompleks sayının eşit olup olmadığını kontrol eden trait'leri geliştiriniz.
  • Iterator trait'ini yeniden programlayarak Fibonnaci sayı dizisini belli bir üst limite kadar ekrana yazdırmayı deneyiniz.
  • Fizz Buzz kod katasını Rust ile Test Driven Development odaklı olarak uygulayınız.
  • reader uygulamasındaki akış kodlarını ayrı bir kütüphaneye alın.
  • .Netçiler!!! Birkaç LINQ sorgusunu closure'ları kullanarak icra etmeye çalışınız.
  • Closures örneğinde yer alan get_fn fonksiyonunu inceleyin. Sizde farklı bir senaryo düşünüp geriye koşula göre fonksiyon döndüren ama Fn yerine FnMut trait'ini ele alan bir kod parçası yazmayı deneyin.
  • iter fonksiyonu üstünden örneğin 1den 100e kadar olan sayılardan sadece kendisi ve 1 ile bölünülebilenleri(asal olanları) elde etmeye çalışın.
  • hof örneğinde 28nci satırdaki filter fonksiyonuna bakın. Burada calc fonksiyonunu çağırmadan aynı hesaplamayı yaptırmaya çalışın.
  • M:N ve 1:1 thread modelleri nedir, araştırınız? Öğrendiklerinizi bir arkadaşınızla paylaşıp konuyu tartışarak pekiştiriniz.
  • counter uygulamasını genişletelim. En az 20 paragraftan oluşan bir word dokümanı hazırlayın. Herbir paragraf için ayrı bir thread çalıştırın. Herbir thread ilgili paragrafta bizim söylediğimiz kelimelerden kaç tane geçtiğini case-sensitive veya case-insensitive olarak hesaplasın.
  • _ ve .. operatörlerinin kullanım alanları nerelerdir, araştırıp deneyiniz.

Böylece geldik bir SkyNet derlememizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Tie Fighter Değil, Project Tye!

$
0
0

Star Wars'ın figür kabul edilen gemilerinden birisi imparatorluk güçlerinin Tie Fighter'ıdır. Lord Vader ile özdeşlemiş olan bu figürün kulak tırmalayan ama rahatsız etmeyen sesinin Almanların İkinci Dünya savaşındaki hafif bombardıman uçaklarından birisi olan Junkers Ju-87 Stuka'dan (Sturzkampfflugzeug) geldiği bile söylenir.

Aslında ses tasarımcısı Ben Burtt bu efekti oluşturmak için bir filin başka bir file seslenirken çıkardığı bağrış ile ıslak kaldırımda giden araba seslerini birleştirmiştir. Lakin Tie kelimesi okunurken genellikle Tay veya Taiy diye okunur. Belki de okunmaz :P Benzer sesdeşlik Tie ile Tye arasında da vardır. Ancak Tye esasında Microsoft'un deneysel bir çalışmasıdır.

Github'un şuradaki reposunda açık kaynak olarak yayınlanan Project Tye, Microsoft'un deneysel projelerinden birisi. En azından konuya çalıştığım tarih itibariyle böyleydi. Projenin iki temel amacı var; .Net tabanlı mikroservis çözümlerinin daha kolay geliştirilmesini sağlamak ve söz konusu çözümleri az zahmetle Kubernetes ortamına almak(Deployment) Buna göre birden fazla servisi tek komutla ayağa kaldırmak, Redis, RabbitMQ, Zipkin, Elastic Stack, Ingress vb normalde Sidecar container olabilecek bağımlılıkları kolayca yönetmek, kullanılacak servislerin ortam bağımsız rahatça keşfedilmesini sağlamak(Service Discovery), uygulamaların container olarak evrilmesi için gerekli hazırlıkları otomatikleştirmek, olabildiğince basit ve tekil bir Kubernetes konfigurasyon dosyası desteği vermek, projenin genel amaçları olarak düşünülebilir.

Elbette bu komut satırı aracının faydalarını görebilmek için sahada denemek gerekir. Bu anlamda yararlandığım başlıca iki önemli kaynak var. Amazon'dan kısa süre önce aldığım Adopting .NET 5: Understand modern architectures, migration best practices, and the new features in .NET 5 isimli kitap ve Microsoft Program Yöneticisi rolünde çalışan Amiee Lo'nun şu adresteki giriş makalesi. Her iki kaynaktaki örnekleri de kopyalama yapmadan bizzat yazarak çalıştım ve sonuçta github reposundan bazı notlar birikti. Şu anda bu notları bir araya topladığım yazıyı okumaktasınız.

Örneklere geçmeden önce uygulamaları geliştirdiğim sistemden bahsetmem gerekiyor. Windows 10 üzerinde, Visual Studio 2019 Community Edition kullanıyorum. Ortamda .Net 5 yüklü durumda. Kubernetes özelliği aktif olan bir Docker Desktop var. Dolayısıyla sonradan ihtiyacımız olacak kubectl komut satırı aracı kullanılabilir halde. Ayrıca Windows Subsystems on Linux(WSL), 2.0 sürümüne güncellenmiş durumda. Geliştireceğimiz her iki örnekte Service Discovery için yerel bir adres kullanacak ancak gerçek hayat senaryolarında bunun yerini DockerHub veya Azure Container Registry gibi bir hizmet alması muhtemeldir. Tabii tüm bunların yanında bize tye komut satırı aracının kendisi de lazım :D İşte başlangıç adımları için gerekli terminal komutlarımız.

# Sisteme tye yüklemek için aşağıdaki terminal komutu kullanılabilir(Son sürüme bakmak lazım. Sonuçta bu şimdilik deneysel bir proje)
dotnet tool install -g Microsoft.Tye --version "0.5.0-alpha.20555.1"

# Kubernetes deployment öncesi Service Discovery için kullanacağımız local registry
docker run -d -p 5000:5000 --restart=always --name registry registry:2

# Docker Desktop tarafında Enable Kubernetes seçeneğinin de işaretli olması lazım
# Kubernetes'in etkin olduğunu anlamak içinse aşağıdaki komut işletilebilir
kubectl config current-context
# Bize docker-desktop cevabını vermeli

Hello World Örneği: StarCups

StarCups kod adlı ilk çalışmada bir frontend, bir backend(servis tabanlı) ve birde Redis mevzu bahis. Senaryoda StarCups isimli hayali bir kahve firması var. HeadOffice isimli web arayüzünden İstanbul'un çeşitli semtlerindeki kahve dükkanlarının malzeme taleplerini anlık olarak görebiliyoruz. Malzeme bilgileri StockCollector isimli REST tabanlı çalışan bir Web API servisi üstünden geliyor. Redis ise StockCollector'un çektiği veriyi belli süre cache'lemek için kullanılıyor(Aslında en genel uygulama geliştirme pratiği olarak düşünebiliriz. Önyüz tarafı iş fonksiyonellikleri için arka taraftaki bir servisle konuşur) Bu Hello World kıvamındaki örnekte amaç, Tye aracı ile uygulamaların kolayca ayağa kaldırılması, denenmesi, zahmetsizce dockerize edilmesi, loglarına bakılması, çevre değişkenlerinin yaml bazlı yönetilmesi ve Kubernetes tarafına en basit şekliyle Deploy edilmesi şeklinde özetlenebilir. İlk çözümü oluşturmak için aşağıdaki terminal komutları ile hareket edebiliriz.

mkdir Starcups
cd Starcups
# Bir tane frontend uygulaması. Razor tipinde.
dotnet new razor -n HeadOffice
# frontend'in konuşacağı bir WebAPI
dotnet new webapi -n StockCollector
dotnet new sln
dotnet sln add HeadOffice StockCollector

tye run

Bu komut sonrası solution içerisindeki uygulamalar otomatik olarak kendileri için tahsis edilmiş process ve adreslerden ayağa kalkacaktır.

Şu haldeyken tye ile çözümü çalıştırıp localhost:8000 adresine gidebiliriz. Her iki uygulama da Dashboard üstünde görünür ve ayrı ayrı incelenebilir ki inceleyin derim :) View kısmına bir bakın, Bindings kısmından sayfalara gitmeye çalışın. Tabii Api servis için bir rest çağrısı şeklinde gitmeniz gerekir.

Şık ve uygulamaların kolayca erişilip, loglarına bakıldığı arayüz dışında ortada henüz bir numara yok. Örneğin frontend ile backend şu anda birbirlerinden bihaberler. Frontend'in backend ile konuşuyor olması da lazımdı. Şimdi WebAPI tarafına OrderData sınıfını ekleyip WeatherForecastController tipini de OrderController olarak değiştirip kodlayarak ilerleyelim.

OrderData sınıfımız;

using System;

namespace StockCollector
{
    public class OrderData
    {
        public string ShopName { get; set; }

        public string ItemName { get; set; }

        public double Quantity { get; set; }
        public DateTime Time { get; set; }
    }
}

OrderController sınıfımız;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace StockCollector.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class OrderController : ControllerBase
    {
        private static readonly string[] ShopNames = new[]
        {
            "Capitol", "Balat", "Taksim Meydan", "Pendik Marina", "Bebek", "Koşuyolu", "Bakırköy", "Moda", "Beşiktaş Arena", "Maslak 1881"
        };
        private static readonly string[] Items = new[]
        {
            "Peçete (100 * Adet)", "Karıştırma Kaşığı (100 * Adet)", "Şeker (Kilo)","Short Bardak (100 * Adet)"
        };

        private readonly ILogger<OrderController> _logger;

        public OrderController(ILogger<OrderController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<OrderData> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 10).Select(index => new OrderData
            {
                ItemName = Items[rng.Next(Items.Length)],
                Quantity = rng.Next(1, 10),
                ShopName = ShopNames[rng.Next(ShopNames.Length)],
                Time = DateTime.Now
            }).ToArray();
        }
    }
}

Kod rastgele OrderData nesneler listesi üretip geri döndüren basit bir operasyona sahip. Frontend tarafının bu servise gelmesini istiyoruz. Normal şartlarda localhost üstündeki ilgili backend adresini alıp kullanan bir HttpClient nesnesi pekala işimizi görebilir. Lakin bu örneği yarın öbür gün Kubernetes'e alacağız. Dockerize edilerek çalışacak Container için adres bilgileri çevre değişkenlerden gelebilir, hatta uzak bir konfigurasyon yöneticisinden bile desteklenebilir. Yani frontend'in hangi servisteki backend uygulaması ile konuşacağını kolayca keşfedebilmesi önemlidir. Bu işi tye üstünden yapmak istediğimiz için frontend tarafında küçük bir hazırlık yapmalıyız. İlk olarak Microsoft.Tye.Extensions.Configuration nuget paketini HeadOffice uygulamasına ekleyelim.

cd HeadOffice
dotnet add package --prerelease Microsoft.Tye.Extensions.Configuration
cd ..

Sonrasında HeadOffice isimli frontEnd uygulamasından REST çağrısı yaparken kullanacağımız OrderClient ve gelen veriyi nesne olarak ele alacağımız OrderData(Backend taraftaki ile aynı yapıdadır) sınıflarını geliştirelim.

OrderClient sınıfı REST çağrısı yapmamızı kolaylaştıran bir tip.

using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

namespace HeadOffice
{
    public class OrderClient
    {
        private readonly JsonSerializerOptions options = new JsonSerializerOptions()
        {
            PropertyNameCaseInsensitive = true,
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        };

        private readonly HttpClient client;

        public OrderClient(HttpClient client)
        {
            this.client = client;
        }

        public async Task<OrderData[]> GetOrdersAsync()
        {
            var responseMessage = await this.client.GetAsync("/order");
            var stream = await responseMessage.Content.ReadAsStreamAsync();
            return await JsonSerializer.DeserializeAsync<OrderData[]>(stream, options);
        }
    }
}

Derken HeadOffice'deki Index.cshtml(cs ile birlikte) sayfasını da aşağıdaki gibi düzenleyelim.

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}<div class="text-center"><h1 class="display-4">Melaba!!! Kahvenin hası burada.</h1><p>Star Cups Mağzaları...</a>.</p></div>

Son Siparişler

<table class="table"><thead><tr><th>Tarih</th><th>Dükkan</th><th>İstenen</th><th>Miktar</th></tr></thead><tbody>
        @foreach (var ord in @Model.Orders)
        {<tr><td>@ord.Time.Ticks</td><td>@ord.ShopName</td><td>@ord.ItemName</td><td>@ord.Quantity</td></tr>
        }</tbody></table>

Index.cshtml.cs sınıfı

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace HeadOffice.Pages
{
    public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;
        public OrderData[] Orders { get; set; }

        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;
        }

        public async Task OnGet([FromServices] OrderClient client)
        {
            Orders = await client.GetOrdersAsync();
        }
    }
}

Tekrar tye tarafına dönelim. Çözüm içerisindeki servislerle ilgili çevre konfigurasyon ayarlamaları için bir yaml dosyasına ihtiyacımız olacak. Bu dosyayı solution klasöründe aşağıdaki terminal komutu ile kolayca oluşturabiliriz.

tye init

Tye.yaml içeriği aşağıdaki gibi oluşur. Buna göre iki servis söz konusudur. Tye, .net odaklı bir enstrüman olduğundan solution içindeki proje dosyalarını otomatik olarak algılayıp gerekli servis bildirimlerini yapar.

name: starcups
services:
- name: headoffice
  project: HeadOffice/HeadOffice.csproj
- name: stockcollector
  project: StockCollector/StockCollector.csproj

Bu aşamada çözüm çalıştırılır ve tarayıcı ile HeadOffice uygulamasına gidilirse ekran görüntüsünde olduğu gibi servis tarafıyla konuşulabildiği görülür. Şu noktada HeadOffice tarafında, backend için bir adres bildirimi yapmadığımız dikkatinizden kaçmamalıdır. Tye çalışmaya başladığında backend'i hangi adresten ayağa kaldırdıysa, frontend tarafında da o adres kullanılır.

tye run


StarCups için Redis Desteğinin Eklenmesi

Dağıtık mimariler söz konusu olduğunda Redis, RabbitMQ gibi hizmetler eğer single node üstünde çalışılıyorsa genellikle Sidecar Container olarak ele alınabilirler. Tye bu konuda bize bazı kolaylıklar sağlar. Ne demek istediğimi anlatamabilmek için backend servisine Redis desteğini ekleyerek devam edelim. Redis desteği'ni de yaml dosyaları ile yöneteceğiz. Öncelikle backend uygulamasında Redis kullanabilmek için gerekli Nuget paketini ilave ediyoruz.

cd StockController
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
cd ..

Sonrasında OrderController sınıfındaki Get metodunu Redis'i kullanacak hale getiriyoruz.

[HttpGet]
public async Task<string> Get([FromServices] IDistributedCache cache)
{
	var keyOrder = await cache.GetStringAsync("keyOrder");
	if (keyOrder == null)
	{
		_logger.LogInformation("Redis Key boştu");
		var rng = new Random();
		var orders = Enumerable.Range(1, 10).Select(index => new OrderData
		{
			ItemName = Items[rng.Next(Items.Length)],
			Quantity = rng.Next(1, 10),
			ShopName = ShopNames[rng.Next(ShopNames.Length)],
			Time = DateTime.Now
		}).ToArray();

		keyOrder = JsonSerializer.Serialize(orders);
		_logger.LogInformation($"Veri serileştirildi {keyOrder}");

		await cache.SetStringAsync("keyOrder", keyOrder, new DistributedCacheEntryOptions
		{
			AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10)
		});
	}
	return keyOrder;
}

ve Redis için Startup.cs içerisindeki ConfigureService metodunda gerekli düzenlemeyi yapıyoruz.

public void ConfigureServices(IServiceCollection services)
{
	services.AddControllers();

	// Redis için aşağıdaki satır eklendi
	// Bağlantı bilgisi yaml üstünden gelecek
	services.AddStackExchangeRedisCache(o =>
	{
		o.Configuration = Configuration.GetConnectionString("redis");
	});

	services.AddSwaggerGen(c =>
	{
		c.SwaggerDoc("v1", new OpenApiInfo { Title = "StockCollector", Version = "v1" });
	});
}

Burada altını çizmemiz gereken bir nokta var ki o da GetConnectionString'e gelen redis ifadesi. Normalde projemizin appSettings.json dosyasında redis için bir bölüm bulunmuyor. Tahmin edeceğiniz üzere buradaki redis adres tanımı tye.yaml üstünden okunuyor. Bu nedenle tye.yaml içeriğini aşağıdaki şekilde güncellemeliyiz.

name: starcups
services:
- name: headoffice
  project: HeadOffice/HeadOffice.csproj
- name: stockcollector
  project: StockCollector/StockCollector.csproj
- name: redis
  image: redis
  bindings:
  - port: 6379
    connectionString: "${host}:${port}"
- name: redis-cli
  image: redis
  args: "redis-cli -h redis MONITOR"

Güncel yaml içeriğinde redis ve redis-cli isimli iki yeni bildirim görüyorsunuz. Standart olarak 6379 portundan hizmet veren redis sunucusu ve kolay bir şekilde onu monitor etmemizi sağlayan redis-cli hizmeti. 

Artık backend uygulaması Redis ile çalışır hale geldi. Bu aşamada yine tye run ile örneği çalıştırıp, redis servislerinin ayağa kalkıp kalkmadığına bakmak ve 10 saniyede bir cache'in düşüp yeni bilgilerin getirildiğini görmek iyi olacaktır. tye run ile sistem ayağa kaldırıldığında aşağıdaki ekran görüntüsünden de görüldüğü gibi redis hizmeti de çalışmaya başlar. Bu arada redis için docker imajı kullanıldığını fark etmiş olmalısınız. Yani redis hizmeti bir Container olarak ayağa kalkar. Aynı işleyip redis-cli hizmeti için de söz konusudur (Buradan terminal komutu loglarını okumanın faydalarını da görebilirsiniz)

 

Tye dashboard üstünde de benzer şekilde redis ve redis-cli hizmetlerinin çalışıyor olduğunu görmemiz lazım.

Hatta redis-cli loglarına gidersek cache'e atılan JSON içeriklerini de takip edebiliriz.

StartCups'ın Kubernetes Ortamına Alınması

Gelelim diğer bir hedefimize. Buraya kadar yapılan işlemler sayesinde solution içindeki uygulamaları bağımlı servisleri ile birlikte basitçe çalıştırıp, monitör edebildik. Ancak bunları Kubernetes gibi bir ortama nasıl alırız? Bu aşamada Sidecar gibi görünen redis için ayrı bir yaml dosyasına ihtiyacımız olacak. Bunu redis servisini Kubernetes ortamına ayrıca almak için kullanacağız. Söz konusu dosyayı aşağıdaki gibi oluşturabiliriz. 

redis.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  labels:
    app.kubernetes.io/name: redis
    app.kubernetes.io/part-of: starcups
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: redis
  replicas: 1
  template:
    metadata:
      labels:
        app.kubernetes.io/name: redis
        app.kubernetes.io/part-of: starcups
    spec:
      containers:
        - name: redis
          image: redis
          resources:
            requests:
              cpu: 100m
              memory: 100Mi
          ports:
            - containerPort: 6379

---
apiVersion: v1
kind: Service
metadata:
  name: redis
  labels:
    app.kubernetes.io/name: redis
    app.kubernetes.io/part-of: starcups
spec:
  ports:
    - port: 6379
      targetPort: 6379
  selector:
    app.kubernetes.io/name: redis

Redis için kubernetesçe bir içerik söz konusu. Kubernetes konusuna çok hakim olmadığım için anladığım kadarıyla ifade etmeye çalışayım. Kubernetes'e redis için kullanacağı docker imajını, replika adedini, port bilgisini, cpu ve memory gibi ayrılması istenen sistem kaynaklarını, kısaca dağıtım ve servis manifestosunu bildiriyoruz. Bu manifestoyu Kubernetes tarafının işletmesi içinse aşağıdaki terminal komutunu kullanmamız gerekiyor(Yazının başlarında kubectl'ye ihtiyacımız olacağını söylemiştim)

kubectl apply -f redis.yaml

Redis'in Kubernetes tarafında ayağa kaldırılması tek başına yeterli değil. Buraya yapılan dağıtım sonrası servislerin keşfi için de bir registry kullanılması gerekiyor. Bunu tye.yaml dosyasında aşağıdaki gibi bildirebiliriz.

name: starcups
registry: localhost:5000
services:
- name: headoffice
# Diğer kısımlar

Tabii bunu söylemek de yeterli değil. localhost:5000 adresinde gerçekten bir Registry servisinin olması lazım. Bunun içinse aşağıdaki terminal komutuna ihtiyacımız var. registry imajını kullanan ve açıkça kapatılana kadar sürekli çalışacak bir container.

# container registry için aşağıdaki komut kullanılabilir.
docker run -d -p 5000:5000 --restart=always --name registry registry:2

Kubernetes deployment işlemi için deploy komutunu aşağıdaki gibi kullanmamız gerekiyor. Harici bir servis olarak Redis kullandığımızdan, ona hangi adresle erişeceğimiz de sorulur. Bu soruyu redis:6379şeklinde cevaplayarak ilerleyebiliriz.

tye deploy --interactive

# Aşağıdaki komutlar ile kubernetes deployment ve pod durumları kontrol edilir.

kubectl get deployment
kubectl get svc
kubectl get secrets
kubectl get pods

İşlemler sırasında terminal hareketlilikleri takip edilirse, tye.yaml üstünde belirtilen projeler için Dockerize işlemlerinin otomatik olarak yapıldığı da görülebilir. Dikkat ederseniz herhangibir Dockerfile oluşturmadık. Deployment işlemi başarılı ise get pods ile aktif olarak çalışan pod'ları görebilmemiz gerekir. Aynen aşağıdaki ekran görüntüsüne olduğu gibi.

Frontend uygulamasının web arayüzüne erişmek için port-forward işlemi uygulamamız gerekebilir(Cluster dışından erişmek istediğimiz için) Bunun için aşağıdaki terminal komutunu çalıştırmak yeterli olacaktır.

kubectl port-forward svc/headoffice 80:80

Sonrasında localhost:80 adresine gidilirse web uygulamasına ulaşıldığı ve anlık olarak kahve dükkanlarımızın beklediği malzemeler görülebilir. Aynen aşağıdaki ekran görüntüsünde olduğu gibi.

Çok doğal olarak şu noktada Kubernetes ortamına yapılan dağıtımı geri almak isteyebilirsiniz. Tye bu işlemi basitleştirir.

tye undeploy

Peki şimdi ne oldu? Normal bir .net çözüm ailesini kullanışlı bir dashboard üstünden izlemeyi, kolayca çalıştırmayı(Solution Run'dan farklı olarak), redis'i hem development hem kubernetes için nerededir diye düşünmeden bağlamayı ama daha da önemlisi bu çözümü kubernetes'e taşımak istersek o ortamda da çalışabileceğini görmüş olduk. Sizde bu örneği güzel bir şekilde tamamladıysanız ikincisine geçebiliriz. Bu kez senaryo okuduğum kitaptan geliyor.

Bonus: SchoolOfMath Senaryosu

Yeni pratiğimizde aşağıdaki şekilde görülen senaryo söz konusu olacak.

Çok daha keyifli bir senaryo olduğunu söyleyebilirim. Benim için yeni deneyimler içeriyordu. Kısaca çözümdeki aktörlerin ne işe yaradığını anlatarak devam edelim.

  • Einstein, gRPC tabanlı bir servis sağlayıcı. İçinde Palindrom sayıları hesap eden(Kitapta asal sayı buluyordu :P) bir fonksiyon desteği sunuyor. Servis cache stratejisi için Redis'i kullanacak. Cache'te ne mi tutacağız? Daha önceden Palindrome olarak işaretlenmiş bir sayı varsa onu kendi adıyla Cache'e alacağız ve bir saat boyunca saklamasını isteyeceğiz. Aynı sayı tekrar istenirse hesaplanmadan doğrudan cache'den gelecek. Sırf Redis hizmeti bu senaryoda olsun diye. Ayrıca bir mesaj kuyruğu sistemi de var ki bu noktada RabbitMQ'dan yararlanacağız.
  • Evelyne, Bruce ve Madeleine aktörleri Worker tipinden istemci servisler(Onları, ayağa kalktıktan sonra sürekli olarak talep gönderen servisler olarak düşünebiliriz) Belli bir sayıdan başlayarak Eintesein'a talep gönderiyorlar ve gönderikleri sayının Palindrom olup olmadığını öğreniyorlar.
  • Robert ise RabbitMQ kuyruğunu dinleyen diğer bir Worker servis.

Amacımız bir önceki örnekte olduğu gibi bu çözümü Tye destekli olarak inşa edip az zahmetle Kubernetes'e alabilmek.

Proje İskeletinin Oluşturulması

Bunun için aşağıdaki adımları icra edelim. Öncelike Palindrom sayı hesaplayan Einstein gRPC servisini geliştirelim.

mkdir SchoolOfMath
cd SchoolOfMath

dotnet new sln
dotnet new grpc -n Einstein
dotnet sln add Einstein

Protos klasöründeki greet.proto ile servis tarafını değiştirmemiz gerekiyor.

palindrome.proto içeriği şöyle oluşturulabilir. long tipinden değer alıp bool olarak cevap veren iki mesaj söz konusu. Fonksiyonumuz ise IsItPalindrome. gRPC için gerekli şemayı bu şekilde tanımlamış olduk.

syntax = "proto3";

option csharp_namespace = "SchoolOfRock";

package palindrome;

service PalindromeFinder {
  rpc IsItPalindrome (PalindromeRequest) returns (PalindromeReply);
}

message PalindromeRequest {
  int64 number= 1;
}

message PalindromeReply {
  bool isPalindrome= 1;
}

PalindromeFinderServis sınıfı;

using Grpc.Core;
using Microsoft.Extensions.Logging;
using SchoolOfRock;
using System.Threading.Tasks;

namespace Einstein
{
    public class PalindromeFinderService
        : PalindromeFinder.PalindromeFinderBase
    {
        private readonly ILogger<PalindromeFinderService> _logger;
        public PalindromeFinderService(ILogger<PalindromeFinderService> logger)
        {
            _logger = logger;
        }

        public override async Task<PalindromeReply> IsItPalindrome(PalindromeRequest request, ServerCallContext context)
        {
            long r, sum = 0, t;
            var num = request.Number;
            for (t = num; num != 0; num /= 10)
            {
                r = num % 10;
                sum = sum * 10 + r;
            }
            if (t == sum)
                return new PalindromeReply { IsPalindrome = true };
            else
                return new PalindromeReply { IsPalindrome = false };
        }
    }
}

Servis tarafını şimdilik bırakalım ve ilk istemci uygulama kodlarını yazarak devam edelim.

dotnet new worker -n Evelyne
dotnet sln add Evelyne
# Evelyne'nin gRPC servisini kullanabilmesi için gerekli Nuget paketleri eklenmelidir.
cd Evelyne
dotnet add package Grpc.Net.Client
dotnet add package Grpc.Net.ClientFactory
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
# Ayrıca Tye konfigurasyonu için gerekli extension paketi de yüklenir
dotnet add package --prerelease Microsoft.Tye.Extensions.Configuration
cd ..

Visual Studio 2019 kullanıyorsak Add new gRPC Service Reference(Connected Services kısmından) ile Einstein'daki proto dosyasının fiziki adresini göstererek gerekli proxy tipinin üretilmesini kolayca sağlayabiliriz. İşte bu noktalarda Visual Studio ile çalışmanın avantajları ortaya çıkıyor. Uygulamanın program.cs ve worker.cs içeriklerini de düzenlememiz lazım.

Program sınıfı;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SchoolOfRock;
using System;

namespace Evelyne
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    // gRPC istemcisini çalışma zamanına ekliyoruz
                    services.AddGrpcClient<PalindromeFinder.PalindromeFinderClient>(options =>
                    {
                        // servis adresini Tye extension fonksiyonu üstünden çekiyoruz
                        // Eğer debug modda çalışıyorsak (tye.yaml olmadan tye run ile mesela) einstein'ın 7001 nolu adresine yönlendiriyoruz.
                        options.Address = hostContext.Configuration.GetServiceUri("einstein") ?? new Uri("https://localhost:7001");
                    });
                    services.AddHostedService<Worker>();
                });
    }
}

Program sınıfında gRPC servis adresinin nasıl alındığına dikkat edelim.

Worker sınıfı 100 milisaniyede bir Einstein servisine talep gönderecek şekilde kodlanmış durumda.

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SchoolOfRock;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Evelyne
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly PalindromeFinder.PalindromeFinderClient _client;

        // gRPC servisini constructor üzerinden içeriye enjekte ediyoruz
        public Worker(ILogger<Worker> logger,PalindromeFinder.PalindromeFinderClient client)
        {
            _logger = logger;
            _client = client;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            // Servisin ayağa kalkması için bir süre bekletiyoruz. Makine soğuk. 
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
            _logger.LogInformation("### Servis başlatılıyor ###");
            long number = 1; // Evelyne, 1den itibaren sayıları hesap etmeye başlayacak
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    var response = await _client.IsItPalindromeAsync(new PalindromeRequest { Number = number });
                    _logger.LogInformation($"{number}, palindrom bir sayıdır önermesinin cevabı = {response.IsPalindrome}\r");
                }
                catch (Exception ex)
                {
                    // Bir exception oluşması halinde Worker'ın işleyişini durduracağız
                    if (stoppingToken.IsCancellationRequested) 
                        return;
                    
                    _logger.LogError(-1, ex, "Bir hata oluştu. Worker çalışması sonlanıyor.");
                    await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
                }

                number++;

                if (stoppingToken.IsCancellationRequested) 
                    break;

                await Task.Delay(TimeSpan.FromMilliseconds(100), stoppingToken); // İstemci 100 milisaniyede bir ateş edecek :P
            }
        }
    }
}

İlk Worker servise benzer şekilde Bruce ve Madeleine isimli Worker servisleri de ekleyerek devam edebiliriz. Buradaki kodlar benzer olduğu için eklemedim ancak github üstünden alabilir ya da aşağıdaki notlarda olduğu gibi Palindrome başlangıç değerleriyle oynayarak yukarıdaki kodu kullanabilirsiniz.

# Bruce için tek fark Palindrome sayı taleplerine 1den değil de 10000den başlamasıdır
dotnet new worker -n Bruce
dotnet sln add Bruce
cd Bruce
dotnet add package Grpc.Net.Client
dotnet add package Grpc.Net.ClientFactory
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
dotnet add package --prerelease Microsoft.Tye.Extensions.Configuration
cd ..

# Madeleine de benzer şekilde eklenir
dotnet new worker -n Madeleine
dotnet sln add Madeleine
cd Madeleine
dotnet add package Grpc.Net.Client
dotnet add package Grpc.Net.ClientFactory
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
dotnet add package --prerelease Microsoft.Tye.Extensions.Configuration
cd ..

Yukradaki işlemler tamamlandıktan sonra en azından aşağıdaki terminal komutu ile servislerin ayağa kalkıp kalkmadığına bakmakta yarar var. Bu arada uygulamalarımız için herhangi bir Dockerize işleminin olmadığı dikkatinizden kaçmamıştır diye düşünüyorum. Nitekim henüz Kubernetes hazırlıklarına başlamadık. Bu nedenle tye söz konusu uygulamaları localhost:random_port_number formasyonunda birer process olarak ayağa kaldırmıştır.

tye run


Redis ve RabbitMQ Desteğinin Eklenmesi

İlk Hello World örneğinde Redis desteğini eklemiştik. Aynı adımları burada da uygulayacağız. Ayrıca rabbitmq hizmetini de dahil edeceğiz. Özellikle dağıtık mimarinin event-based modelinde uygulamalar arası haberleşmede mesaj bazlı kuyruk sistemleri sıklıkla karşımıza çıkıyor. Kafka ve RabbitMQ sanıyorum ki en çok başvurduklarımız. Dolayısıyla RabbitMQ için aranan Sidecar container'lardan birisi olduğunu ifade etsek yeridir. Şimdi gelin bu iki aktörü sisteme dahil ederek Kubernetes hazırlıklarına geçelim. 

# İşe tye.yaml dosyasının oluşturulmasıyla başlıyoruz.
tye init

# tye.yaml dosyasına redis için gerekli ekleri yaptıktan sonra
# einstein (gRPC API servisimiz) cache desteği için gerekli nuget paketlerini ekleyip devam ediyoruz
cd einstein
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
#Sonrasında rabbitmq paketini ekliyoruz.
dotnet add package RabbitMQ.Client
cd ..

Palindrome sayılar buldukça bunları RabbitMQ'ya mesaj olarak yollayacak bir düzenek ekleyeceğimizi de söylemiştik. RabbitMQ'da, Redis gibi çalışma zamanında ayakta olması beklenen bir servis. Bu nedenle tye.yaml dosyasında RabbitMQ için gerekli eklemeler aşağıdaki gibi yapılmalı.

name: schoolofmath
registry: localhost:5000 # container registry adresi
services:
- name: einstein
  tags:
    - backend
  project: Einstein/Einstein.csproj
  replicas: 1
  env: #rabbitmq için kullanıcı adı, şifre ve varsayılan kuyruk adı bildirimi
  - RABBIT_USER=guest
  - RABBIT_PSWD=guest
  - RABBIT_QUEUE=palindromes
- name: evelyne
  tags:
    - client
  project: Evelyne/Evelyne.csproj
- name: bruce
  tags:
    - client
  project: Bruce/Bruce.csproj
- name: madeleine
  tags:
    - client
  project: Madeleine/Madeleine.csproj
- name: robert
  tags:
    - middleware
  project: Robert/Robert.csproj
- name: redis
  tags:
    - backend
  image: redis
  bindings:
  - port: 6379
    connectionString: "${host}:${port}"
- name: redis-cli #redis cache tarafında ne olduğunu izlemek için ekledik. Ancak mecburi değil. Opsiyonel.
  tags:
    - backend
  image: redis
  args: "redis-cli -h redis MONITOR"
- name: rabbitmq # RabbitMQ servisini MUI arabirimi ile birlikte ekliyoruz.
# Mui arabirimine aşağıdaki kriterlere göre localhost:15672'den quest/quest log in bilgisi ile erişebiliriz
  tags:
    - middleware
  image: rabbitmq:3-management
  bindings:
  - name: mq-binding # mq_binding veya mui_binding şeklinde kullanınca K8s deploy işleminde kullanılan secret değerlerinde hata alındı. - veya . olarak yazılmalı.
    port: 5672
    protocol: rabbitmq
  - name: mui-binding
    port: 15672

Elbette PalindromeFinderService sınıfı ve Startup.cs'in de Redis ve RabbitMQ için yeniden revize edilmeleri gerekiyor.

PalindromeFinderService sınıfı

using Einstein.Rabbit;
using Grpc.Core;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using SchoolOfRock;
using System;
using System.Threading.Tasks;

namespace Einstein
{
    public class PalindromeFinderService
        : PalindromeFinder.PalindromeFinderBase
    {
        private readonly ILogger<PalindromeFinderService> _logger;
        private readonly IDistributedCache _cache;
        private readonly PalindromeReply True = new() { IsPalindrome = true };
        private readonly PalindromeReply False = new() { IsPalindrome = false };
        private readonly IMessageQueueSender _mqSender;
        private readonly string _queueName;
        public PalindromeFinderService(ILogger<PalindromeFinderService> logger, IDistributedCache cache, IMessageQueueSender mqSender)
        {
            _logger = logger;
            _cache = cache; //Dağıtık cache servisi olarak Redis konumlanacak. Startup'ta onu ekledik çünkü.
            _mqSender = mqSender; // MQ nesnesini alıyoruz
            _queueName = Constants.GetRabbitMQQueueName(); //MQ adını alıyoruz.
        }

        public override async Task<PalindromeReply> IsItPalindrome(PalindromeRequest request, ServerCallContext context)
        {
            long r, sum = 0, t;
            var number = request.Number;

            var inCache = await _cache.GetStringAsync(request.Number.ToString()); // bu sayı Redis Cache'te var mı?
            if (inCache == "YES")
            {
                _logger.LogInformation($"{request.Number} palindrom bir sayıdır ve şu an Redis'ten getiriyorum. Hesap etmeye gerek yok");
                return True;
            }

            for (t = number; number != 0; number /= 10)
            {
                r = number % 10;
                sum = sum * 10 + r;
            }
            if (t == sum)
            {
                _logger.LogInformation($"{request.Number} palindrom bir sayı ama Redis cache'e atılmamış. Şimdi ekleyeceğim.");
                // Sayı adını Key olarak kullanıp Cache'e atıyoruz ve ona value olarak YES değerini atıyoruz.
                await _cache.SetStringAsync(request.Number.ToString(), "YES", new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60)
                });
                // Palindrome sayı ise onu Redis Cache'e atıyoruz.

                // Ayrıca RabbitMQ kuyruğuna da sayıyı atıyoruz.
                _mqSender.Send(_queueName, request.Number.ToString());
                return True;
            }
            else
                return False;
        }
    }
}

Einstein, Startup.cs'in son hali;

using Einstein.Rabbit;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Einstein
{
    public class Startup
    {
        public IConfiguration Configuration { get; }
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddGrpc();
            // RabbitMQ Desteği eklendi
            services.AddRabbitMQ();

            // Redis bildirimini yaptık. PalindromeFinderService, consturctor'dan alacak.
            services.AddStackExchangeRedisCache(o =>
            {
                o.Configuration = Configuration.GetConnectionString("redis") ?? "localhost:6379";
            });
        }

       // Diğer kısımlar

Kod tarafında RabbitMQ kullanımı için gerekli tipler, GoldenHammer isimli sınıfta yer alıyor. Bunu baştan yazmak biraz zahmetli ama yine de üşenmeyin yazın derim. Yazarken düşünecek ve neden böyle kullanılmış ki diyeceksiniz. Kitabın yönlendirmesi ile ben bu adrese gittim ama kendimde teknik borç riskini göze alarak bir GodObject oluşturdum. Eğer sayfadan ayrılmadan kodu kullanmak isterseniz notların sonundaki Yardımcı Kodlar kısmından yararlanabilirsiniz. Bu noktada yine tye run ile ilerlemek önemli. Redis'in çalıştığından ve http://localhost:15672 adresine gittiğimizde RabbitMQ tarafının işler olduğundan emin olmakta fayda var.

Robert: AMQP İstemcisinin Eklenmesi

Robert isimli Worker tipinden olan son istemci uygulama, RabbitMQ'ya atılan palindrome sayıları içeren mesajları okumakla görevli. Basit bir RabbitMQ Consumer olduğunu söyleyebiliriz. Einstein isimli servis Palindrome sayı buldukça RabbitMQ'ya bunu mesaj olarak yollayacak şekilde ayarlanmıştı. Consumer üstünden bunları yakalamayı bekliyoruz. Aşağıdaki terminal komutları ile Worker servisini oluşturalım.

dotnet new worker -n Robert
dotnet sln add Robert
cd Robert
# RabbitMQ istemcisi olacağı için eklenecek paket
dotnet add package RabbitMQ.Client
# ve pek tabii Tye özelliklerini kullanabilmesi için de gerekli konfigurasyon paketi
dotnet add package --prerelease Microsoft.Tye.Extensions.Configuration

Bu Worker'ın kodlarını da aşağıdaki gibi geliştirebiliriz.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Robert
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }

        // Servis çalışmaya başladığı zaman devreye giren metodu ezip kendi istediklerimizi yaptırıyoruz.
        public override async Task StartAsync(CancellationToken cancellationToken)
        {
            try
            {
                // RabbitMQ tarafı henüz ayağa kalkmamış olabilir diye burayı 1 dakika kadar duraksatalım
                await Task.Delay(TimeSpan.FromSeconds(60), cancellationToken);

                // Rabbit ile konuşmak için kullanılacak kanal nesnesi alınıyor
                var queue = CreateRabbitModel(cancellationToken);

                // queue tanımlanır
                queue.QueueDeclare(
                    queue: "palindromes",
                    durable: false,
                    exclusive: false,
                    autoDelete: false,
                    arguments: null
                    );

                // Tanımlanan kuyruğu dinleyecek nesne örneklenir
                var consumer = new EventingBasicConsumer(queue);

                // dinlenen kuyruğa mesaj geldikçe tetiklenen olay metodu
                consumer.Received += (model, arg) =>
                {
                    var number = Encoding.UTF8.GetString(arg.Body.Span); // mesaj yakalanır
                    _logger.LogInformation($"Yeni bir palindrom sayısı bulunmuş: {number}");
                };

                queue.BasicConsume(
                    queue: "palindromes",
                    autoAck: true,
                    consumer: consumer);
            }
            catch (Exception exc)
            {
                _logger.LogError($"Bir hata oluştu {exc.Message}");
                throw;
            }
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                await Task.Delay(10000, stoppingToken);
            }
        }

        private IModel CreateRabbitModel(CancellationToken cancellationToken)
        {
            try
            {
                // Önce bağlantı oluşturmak için factory nesnesi örneklenir
                var factory = new ConnectionFactory()
                {
                    HostName = Rabbit.Constants.GetRabbitMQHostName(), // Rabbit Host adresi alınır (Environment'ten gelir)
                    Port = Convert.ToInt32(Rabbit.Constants.GetRabbitMQPort()), // Port bilgisi
                    UserName=Rabbit.Constants.GetRabbitMQUser(), // Kullanıcı adı
                    Password=Rabbit.Constants.GetRabbitMQPassword() // ve Şifre
                };

                var connection = factory.CreateConnection(); // Bağlantı nesnesi oluşturulur. Exception yoksa bağlanmış demektir.
                _logger.LogInformation("RabbitMQ ile bağlantı sağlandı");
                return connection.CreateModel(); //Queue işlemleri için kullanılacak model nesnesi döndürülür
            }
            catch (Exception exc) 
            {
                _logger.LogError($"Rabbit tarafına bağlanmaya çalışırken bir hata oluştu. {exc.Message}");
                throw;
            }
        }
    }
}

Robert'ın kodları tamamlandıktan sonra tye run ile sistemi çalıştırıp dashboard üzerinden ulaşabileceğimiz logları kontrol etmekte yarar var. Bakalım Robert'ın loglarında RabbitMQ daki palindromes isimli kuyruğa düşen mesajlar var mı?

Sadece Belli Uygulamaları Çalıştırmak

İlerlemeden önce tye ile sadece belli uygulamaları nasıl çalıştıracağımıza da bir bakalım isterim. tye.yaml dosyasında tag bildirimlerini kullanarak tye run sonrası sadece belli servislerin ayağa kaldırılması sağlanabilir. Bu yaklaşım, Debug işlemleri için idealdir. N tane servisin olduğu bir senaryoda her şeyi ayağa kaldırmak yerine sadece istenenleri kurcalama noktasında çok faydalıdır. Söz gelimi yaml dosyamızda sadece middleware tag'ine sahip servisleri çalıştırmak istediğimizi düşünelim. run komutunu aşağıdaki gibi kullanabiliriz.

tye run --tags middleware #sadece middleware tag'ine sahip servisleri çalıştırır.

Birden fazla namespace'te bir arada ayağa kaldırılabilir. Mesela aşağıdaki kullanım ile backend ve middleware tag'ine sahip servisler ayağa kaldırılacaktır. Şimdi yaml içerisindeki tag elementlerinin ne işe yaradığınız daha iyi anlamış olmalısınız.

tye run --tags backend middleware

Debug Etmek ve Breakpoint Noktalarına Geçmek

Kod debug etmek adettendir :D Lakin tye ile çalışırken ayağa kaldırılan aktörleri debug etmek için biraz meşakkatli bir yol izlemek gerekiyor. İlk olarak gerekli yerlere breakpoint konulur. Örneğin;

Sonrasında aşağıdaki komut ile çözüm çalıştırlır.

tye run --debug

Debug edilmek istenen uygulamanının terminal loglarına düşen process id değeri bulunur.

Visual Studio -> Debug -> Attach to Process adımları kullanılarak ilgili process çalışma zamanına alınır.

Çayımızdan/kahvemizden bir yudum alınır ve Breakpoint noktasına gelinmesi beklenir.

Hepsi bu kadar ;) Ya da doğru düzgün tasarladığımız hata yönetim mekanizmasının ürettiği sistem loglarına gidilir ve sorunun ne olduğu anlaşılmaya çalışılır.

Kubernetes Deploy İşlemleri

Artık notlarımızın sonuna doğru geliyoruz. Bal yapmayan arı olmamak için bu örneği de Kubernetes tarafına almamız lazım. Windows 10 üstündeki Docker Desktop'ın K8s Enabled özelliğinin açık olduğundan emin olalım. Buna göre sistemde tye.yaml tarafındaki servislerin alınabileceği bir Kubernetes Cluster mevcut kabul edilir. İkinci olarak bir container registry'ye ihtiyaç vardır ki ilk Hello World örneğimizde bunu localhost:5000 adresinde konuşlandırmıştık. Güncel örnek iki harici servis kullanmakta; Redis ve RabbitMQ. Bunları şu an için Kubernetes ortamına el yordamıyla kendi manifesto dosyaları üzerinden deploy etmemiz gerekiyor ama bu durum tye'ın ilerleyen sürümlerinde daha da kolaylaşabilir. Hello World örneğinde kullandığımız redis.yaml'ı burada da kullanabiliriz. RabbitMQ tarafı içinse aşağıdaki manifesto içeriği işimizi görecektir.

RabbitMQ.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rabbitmq
  labels:
    app.kubernetes.io/name: rabbitmq
    app.kubernetes.io/part-of: schoolofmath
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: rabbitmq
  replicas: 1
  template:
    metadata:
      labels:
        app.kubernetes.io/name: rabbitmq
        app.kubernetes.io/part-of: schoolofmath
    spec:
      containers:
        - name: rabbitmq
          image: rabbitmq:3-management
          resources:
            requests:
              cpu: 100m
              memory: 100Mi
          ports:
            - containerPort: 5672
            - containerPort: 15672

---
apiVersion: v1
kind: Service
metadata:
  name: rabbitmq
  labels:
    app.kubernetes.io/name: rabbitmq
    app.kubernetes.io/part-of: schoolofmath
spec:
  ports:
    - port: 5672
      protocol: TCP
      targetPort: 5672
  selector:
    app.kubernetes.io/name: rabbitmq
---
apiVersion: v1
kind: Service
metadata:
  name: rabbitmq-mui
  labels:
    app.kubernetes.io/name: rabbitmq
    app.kubernetes.io/part-of: schoolofmath
spec:
  type: NodePort
  ports:
    - port: 15672
      protocol: TCP
      targetPort: 15672
      nodePort: 30072
  selector:
    app.kubernetes.io/name: rabbitmq

Hem RabbitMQ hem de onu daha kolay okumamızı sağlayacak görsel MUI arabirimi için iki ayrı deployment tanımı söz konusudur. Bu dosyalardan yararlanarak ilgili servisleri Kubernetes ortamına aşağıdaki terminal komutları ile alabiliriz.

kubectl apply -f .\rabbitmq.yaml
kubectl apply -f .\redis.yaml

Kubernetes deployment adımını da aşağıdaki komutla başlatabiliriz.

tye deploy --interactive

Büyük ihtimalle redis ve rabbitmq için adres sorulacaktır. Redis için redis:6379, rabbitmq içinse rabbitmq:5672(Mui sebebiyle iki kez sorulabilir ki bana öyle oldu) adresleri kullanılabilir. Sonuç olarak Docker Desktop'a baktığımızda dağıtımların yapıldığını görmeliyiz. 

Yukarıdaki ekran görüntüsünde dikkat edileceği üzere servislerimiz localhost:5000 ön adresi üzerine konumlanmış duruyorlar. Bunun sebebi container registry olarak bu adresi bildirmiş olmamız(yaml dosyasındaki ilgili kısmı hatırlayın) 

Tekrar belirtmekte fayda var ki kendi uygulamalarımız dağıtım işlemi sırasında yine otomatik olarak dockerize edilmişlerdir. Robert isimli Worker servise ait tye çalışma zamanının yaptıklarını aşağıdaki ekran görüntüsünde görebilirsiniz(Normalde bunlar için bir Dockerfile hazırlamamız gerekirdi diye düşünüyorum)

Oluşan diğer imajları Docker Desktop üzerinde görebiliriz.

Şu anda RabbitMQ tarafı da aktif haldedir ve eğer localhost:30072 adresine gidersek o ana kadar ki mesaj trafiğini izleyebiliriz.

Yapılan Deployment işlemini geri almak ve Kubernetes dağıtımlarını kaldırmak içinse tye undeploy terminal komutu kullanılır.

Bu çalışma deneysel bir projeyi hem basılı hem de çevrimiçi bir kaynaktan yazarak anlamam noktasında bana önemli değerler katmış durumda. Ancak işi burada bırakmamak lazım. Tye projesinin bir geleceği olacaksa diğer örnek kullanımları incelemekte de yarar var. Söz gelimi bir loglama senaryosunu işin içerisine katmak, performans izleme aktörünü dahil etmek gibi konular üstünde de denemeler yapmak yararlı olabilir. Dahası açık kaynak kod reposuna gidip tye run dediğimizde arka planda neler nasıl çalışıyoru anlamaya çalışmak çok daha yararlı olabilir. Bir teknoloji tüketicisi olarak en azından nasıl kullanılır ve ne işe yararı bir nebze olsun anladığımı ve siz değerli okurlarıma aktarabildiğimi düşünüyorum. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Kaynaklar

Adopting .NET 5. By Hammad Arif , Habib Qureshi

Introducing Project Tye Amiee Lo, Program Manager, Microsoft ASP.NET

Project Tye Github

Project Tye: Creating Microservices in a .NET Way Shayne Boyer, CODE Focus Magazine: 2020 - Vol. 17 - Issue 1 - .Net 5.0

Project Tye: Building Developer Focused Tooling for Kubernetes and .NET - David Fowler

Yardımcı Kodlar

Notların dışına çıkmadan GoldenHammer ve Constant sınıflarını almak isterseniz aşağıdaki kod parçalarından yararlanabilirsiniz.

Robert projesindeki Constants.cs sınıfı

using System;

namespace Robert.Rabbit
{
    /// Kaynak: https://github.com/PacktPublishing/Adopting-.NET-5--Architecture-Migration-Best-Practices-and-New-Features/tree/master/Chapter04/microservicesapp
    public static class Constants
    {
        public const string RABBIT_HOST = "SERVICE__RABBITMQ__MQ_BINDING__HOST";
        public const string RABBIT_PORT = "SERVICE__RABBITMQ__MQ_BINDING__PORT";
        public const string RABBIT_ALT_HOST = "SERVICE__RABBITMQ__HOST";
        public const string RABBIT_ALT_PORT = "SERVICE__RABBITMQ__PORT";
        public const string RABBIT_ALT2_PORT = "RABBITMQ_SERVICE_PORT";
        public const string RABBIT_USER = "RABBIT_USER";
        public const string RABBIT_PSWD = "RABBIT_PSWD";
        public const string RABBIT_QUEUE = "RABBIT_QUEUE";

        public static string GetRabbitMQHostName()
        {
            var v = Environment.GetEnvironmentVariable(RABBIT_HOST);
            if (string.IsNullOrWhiteSpace(v))
            {
                v = Environment.GetEnvironmentVariable(RABBIT_ALT_HOST);
                if (string.IsNullOrWhiteSpace(v))
                    return "rabbitmq";
                else return v;
            }
            else return v;
        }

        public static string GetRabbitMQPort()
        {
            var v = Environment.GetEnvironmentVariable(RABBIT_PORT);
            if (string.IsNullOrWhiteSpace(v))
            {
                v = Environment.GetEnvironmentVariable(RABBIT_ALT_PORT);
                if (string.IsNullOrWhiteSpace(v) || v == "-1")
                    return Environment.GetEnvironmentVariable(RABBIT_ALT2_PORT);
                else return v;
            }
            else return v;
        }

        public static string GetRabbitMQUser()
        {
            var v = Environment.GetEnvironmentVariable(RABBIT_USER);
            if (string.IsNullOrWhiteSpace(v))
                return "guest";
            else return v;
        }

        public static string GetRabbitMQPassword()
        {
            var v = Environment.GetEnvironmentVariable(RABBIT_PSWD);
            if (string.IsNullOrWhiteSpace(v))
                return "guest";
            else return v;
        }

        public static string GetRabbitMQQueueName()
        {
            var v = Environment.GetEnvironmentVariable(RABBIT_QUEUE);
            if (string.IsNullOrWhiteSpace(v))
                return "primes";
            else return v;
        }
    }
}

Einstein tarafındaki GoldenHammer sınıfı

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using System;
using System.Text;

namespace Einstein.Rabbit
{
    // Kaynak: https://github.com/PacktPublishing/Adopting-.NET-5--Architecture-Migration-Best-Practices-and-New-Features/tree/master/Chapter04/microservicesapp
    public interface IMQClient
    {
        IModel CreateChannel();
    }

    public interface IMessageQueueSender
    {
        public void Send(string queueName, string message);
    }

    public static class Constants
    {
        public const string RABBIT_HOST = "SERVICE__RABBITMQ__MQ_BINDING__HOST";
        public const string RABBIT_PORT = "SERVICE__RABBITMQ__MQ_BINDING__PORT";
        public const string RABBIT_ALT_HOST = "SERVICE__RABBITMQ__HOST";
        public const string RABBIT_ALT_PORT = "SERVICE__RABBITMQ__PORT";
        public const string RABBIT_ALT2_PORT = "RABBITMQ_SERVICE_PORT";
        public const string RABBIT_USER = "RABBIT_USER";
        public const string RABBIT_PSWD = "RABBIT_PSWD";
        public const string RABBIT_QUEUE = "RABBIT_QUEUE";

        public static string GetRabbitMQHostName()
        {
            var v = Environment.GetEnvironmentVariable(RABBIT_HOST);
            if (string.IsNullOrWhiteSpace(v))
            {
                v = Environment.GetEnvironmentVariable(RABBIT_ALT_HOST);
                if (string.IsNullOrWhiteSpace(v))
                    return "rabbitmq";
                else return v;
            }
            else return v;
        }

        public static string GetRabbitMQPort()
        {
            var v = Environment.GetEnvironmentVariable(RABBIT_PORT);
            if (string.IsNullOrWhiteSpace(v))
            {
                v = Environment.GetEnvironmentVariable(RABBIT_ALT_PORT);
                if (string.IsNullOrWhiteSpace(v) || v == "-1")
                    return Environment.GetEnvironmentVariable(RABBIT_ALT2_PORT);
                else return v;
            }
            else return v;
        }

        public static string GetRabbitMQUser()
        {
            var v = Environment.GetEnvironmentVariable(RABBIT_USER);
            if (string.IsNullOrWhiteSpace(v))
                return "guest"; 
            else return v;
        }

        public static string GetRabbitMQPassword()
        {
            var v = Environment.GetEnvironmentVariable(RABBIT_PSWD);
            if (string.IsNullOrWhiteSpace(v))
                return "guest";
            else return v;
        }

        public static string GetRabbitMQQueueName()
        {
            var v = Environment.GetEnvironmentVariable(RABBIT_QUEUE);
            if (string.IsNullOrWhiteSpace(v))
                return "palindromes"; // Consumer'ın dinyeceği varsayılan kuyruk adı. Normalde RABBIT_QUEUE ile çevre değişken üzerinden gelmezse bu kullanılır.
            else return v;
        }
    }

    public class RabbitMQClient : IMQClient
    {
        public string hostname { get; }
        public string port { get; }
        public string userid { get; }
        public string password { get; }

        private readonly ILogger _logger;
        private readonly IConnection _connection;
        private IModel _channel;

        public RabbitMQClient(ILogger<RabbitMQClient> logger, IConfiguration configuration)
        {
            _logger = logger;

            hostname = Constants.GetRabbitMQHostName();
            port = Constants.GetRabbitMQPort();
            userid = Constants.GetRabbitMQUser();
            password = Constants.GetRabbitMQPassword();

            try
            {
                logger.LogInformation($"RabbitMQ Bağlantısı oluşturuluyor. @ {hostname}:{port}:{userid}:{password}");
                var factory = new ConnectionFactory()
                {
                    HostName = hostname,
                    Port = int.Parse(port),
                    UserName = userid,
                    Password = password,
                };

                _connection = factory.CreateConnection();
            }
            catch (Exception ex)
            {
                logger.LogError(-1, ex, "RabbitMQ Bağlantısı oluşturulması sırasında hata oluştu.");
                throw;
            }
        }

        public IModel CreateChannel()
        {
            if (_connection == null)
            {
                _logger.LogError("RabbiMQ Kanal bağlantısı oluşturulması sırasında hata oluştu.");
                throw new Exception("RabbitMQClient bağlantı hatası.");
            }
            _channel = _connection.CreateModel();
            return _channel;
        }
    }

    public class RabbitMQueueSender : IMessageQueueSender
    {
        private readonly ILogger<RabbitMQueueSender> _logger;
        private readonly IMQClient _mqClient;

        private IModel _mqChannel;
        private string _queueName;

        private IModel MQChannel
        {
            get
            {
                if (_mqChannel == null || _mqChannel.IsClosed)
                    _mqChannel = _mqClient.CreateChannel();
                return _mqChannel;
            }
        }

        public RabbitMQueueSender(ILogger<RabbitMQueueSender> logger, IMQClient mqClient)
        {
            _logger = logger;
            _mqClient = mqClient;
        }

        public void Send(string queueName, string message)
        {
            if (string.IsNullOrWhiteSpace(queueName)) return; 

            if (string.IsNullOrWhiteSpace(_queueName)) 
            {
                _logger.LogInformation($"{queueName} isimli kuyruk ilk kez oluşturuluyor.");
                MQChannel.QueueDeclare(queue: queueName,
                                            durable: false,
                                            exclusive: false,
                                            autoDelete: false,
                                            arguments: null);
                _queueName = queueName;
            }

            _logger.LogInformation($"Mesaj kuyruğunu gönderiliyor. Queue Name:{queueName}");

            var body = Encoding.UTF8.GetBytes(message);

            try
            {
                MQChannel.BasicPublish(exchange: "",
                                            routingKey: queueName,
                                            basicProperties: null,
                                            body: body);
            }
            catch (System.Exception ex)
            {
                ex.ToString();
            }
            _logger.LogInformation("Mesaj başarılı bir şekilde kuyruğa aktarıldı.");
        }
    }

    public static class RabbitMQServiceCollectionExtensions
    {
        // Startup.cs'de RabbitMQ'yu servis listesine eklememizi sağlayan genişletme fonksiyonu
        public static IServiceCollection AddRabbitMQ(this IServiceCollection services)
        {
            if (services == null)
            {
                throw new ArgumentNullException(nameof(services));
            }

            services.Add(ServiceDescriptor.Singleton<IMQClient, RabbitMQClient>());
            services.Add(ServiceDescriptor.Singleton<IMessageQueueSender, RabbitMQueueSender>());

            return services;
        }
    }
}

Asp.Net Core'a Nasıl Merhaba Deriz?

$
0
0

Yazılım geliştirme işine ciddi anlamda başladığım yeni milenyumun başlarında .Net Framework sahanın yükselen yıldızıydı. Delphi’den kopup gelen Anders’in yarattığı C# programlama dilinin gücü ve .Net Framework çatısının vadettikleri düşünülünce bu son derece doğaldı. Aradan geçen neredeyse 20 yıllık süre zarfında .Net Framework’te evrimleşti ve sürekli güncellendi. Versiyon 2.0 ile gelen generic tipler, 3.0'la birlikte SQL yazar gibi sorgulanabilir nesneler(LINQ-Language INtegrated Query), sonrasında karşımıza çıkan WCF(Windows Communication Foundation), WF(Workflow Foundation), Entity Framework vs derken Microsoft’un açık kaynak dünyasına girişi, benimsediği platform bağımsız stratejiler(Miguel De Icaza’nın Mono’suna da saygı duyalım), Linux, MacOS gibi bir zamanların ciddi rakipleri ile el sıkışarak hamle yapması sonrasında da son birkaç yıllık zaman diliminde karşımıza çıkan .Net Core. Yeni gelişmeler Microsoft’un sıklıkla yaptığı üzere bazı kavram karmaşalarını da beraberinde getirdi elbette. En nihayetinde tek ve birleşik bir .Net 5 ortamından bahsedilmeye başlandı. (Photo by Element5 Digital on Unsplash)

Gelişmeleri zaten sizler de benim gibi takip ediyorsunuzdur. Bu durum benim de kişisel olarak kendimi yenilemem gereken bir dönemi tetikledi. Bir süredir özellikle Amazon’dan getirttiğim kitaplardan .Net 5 dünyasını tanımaya, .Net 4.7.2 gibi versiyonlarda yazılmış uygulamarı yeni sürüme göç ettirmenin(migration) yollarını öğrenmeye çalışıyorum. Bu kişisel çabayı da çalışmakta olduğum şirketin iç eğitim programından gelen talepleri karşılamak için kullanıyorum.

Sabahsız gecelerimin birisinde zen merkezim olan çalışma odamdaki kanepeya uzanmış boş boş tavana bakıyordum. Hoş aklımda cevap arayan güzel bir soru da vardı. Geleceğe Giriş programı kapsamındaki bir Asp.Net Core eğitimine nasıl başlamalıydım? Nasıl bir Hello World olmalıydı? Doğrudan üretilecek uygulamanın kendisini en başından gösterip; “İşte bu uygulamayı nasıl yazacağımızı adım adım öğreneceğiz” şeklinde mi yol almalıydım. Yoksa Hello World deme şekli .Net Core sonrası daha mı farklıydı?

Bu işe başladığım yıllarda beni eğitenler veya okuduklarım Nesne Yönelimli Dil(Object Oriented Programming) konusunun ne denli önemli olduğundan bahseder, kalıtım(Inheritance), çok biçimlilik(Polymorphism) ve soyutlama(Abstraction) gibi kavramların önemine vurgu yapardı. İş, düşük maliyeti nedeniyle çok sık tercih edilen monolitik mimarinin en yaygın kullanılan örneklerinden olan çok katmanlı(n-tier)çözümlere geldiğinde ise mahşerin beş atlısı SOLID ilkeleri, sayısız yazılım prensibi ve tasarım kalıbı ile karşılaşırdık. Gerçekten yazılım mühendisliğinden bahsettiğimiz noktaya gelindiğinde ise Autofac, Ninject, Unity, Castle Windsor gibi bileşenler arası bağımlılıkları yöneten çatıları kullanmaya başlardık. O günleri düşünürken aklıma .Net Core'u(esasında .Net 5'i) bu bağlamda ele almak geldi. Çok üst düzey yetenekleri olmasa da zaten dahili bir DI(Dependency Injection) mekanizmasına sahipti.

Belki sadece DI deyip geçtiğimiz ve bazen şuursuzca IServiceCollection üzerinden bağımlıkları kayıt etmemize olanak sağlayan bu kavram esas itibariyle Single Responsibility, Dependency Inversion Principle ve Inversion of Control esasları üzerine oturuyor. Bu sebepten basit bir Asp.Net Core eğitimine başlarken bile sadece Model nesnesi oluşturup bir liste döndüren Controller ile View kullanmak kafi olmayabilir. Öncesinde ve mutlak suretle eğitimdeki değerli zihinlere .Net Core'un DI mekanizmasının nasıl çalıştığını, neden önemli olduğunu göstermek gerekir...

Diye notlar alarak geçmişim bu yazının başına. Amacım, eğitim için basit ve hızlı okunabilir bir ön doküman hazırlamaktı. Bu dokümanı eğitim katılımcılarına gönderip, "şuna bir göz atın, anlamaya çalışın, sondaki sorulara cevaplar bulun ve derse öyle gelin" demekti belki de. Sonunda aşağıdaki içeriğe sahip basit bir rehber ortaya çıktı(Level 101 diyebiliriz)

Hello World'ler artık bildiğim Hello World'ler gibi değiller.

Sıfır Noktası

Şu bir gerçek ki, Asp.Net Core tarafında kullanılan MVC, Razor, Blazor, Web API vb uygulama tipleri ile bunların sıklıkla kullandığı Hosting, Routing, Logging, Configuration, ApplicationLifetime gibi servisler doğrudan Microsoft.Extensions.DependencyInjection yapısı üzerine oturuyorlar(Bu arada Microsoft.Extensions.DependencyInjection kütüphanesinin harici olarak da kullanılabilen bir NuGet paketi olduğunu ve bu sepele bir Console uygulamasında dahi DI mekanizmasını kullanabilmemize olanak sağladığını da hatırlatalım) Onlar için ekstra bir çaba sarf etmeden daha çalışma zamanı ayağa kalkarken sisteme dahil ediliyorlar. Aslında yine kavramlar arasında kayboluyor gibiyiz. Belki de DI kullanmadığımız bir örnekteki basit kusuru görmeye çalışırsak daha iyi olur. DI demişken bu kısaltmanın adını duymuş olmalısın; Dependency Injection! Bu terime alışsan iyi olur, nitekim şirketin temel ilkelerinden birisi de onunla iyi geçinmek. Ancak öncesinde sana problemi göstermem lazım. Yazılımcıların pek de sevmediği bir durum. Tightly-Coupled(birbirine sıkı sıkıya bağlı) olma hali. Haydi gel, bir örnekle durumu açıklayalım.

Sisteminde .Net 5 yüklüğü olduğunu varsayıyorum. Hangi platformda olduğunun çok da önemi yok. Bir Terminal penceresi aç ve aşağıdaki komutu işleterek basit bir MVC projesi oluştur.

dotnet new mvc -o FunnyHello

Masum Kodlar Basamağı

Sonrasında Visual Studio Code, Visual Studio 2019 Community Edition veya muadili bir IDE ile projeni aç. Model klasöründe aşağıdaki içeriğe sahip Game isimli bir sınıf oluştur ve ilk kodlarını yazmış ol. Sen ve arkadaşlarının sevdiği oyunların isimlerini ve liste fiyatlarını tutacağımız basit bir nesne bu aslında.

namespace FunnyHello.Models
{
    public class Game
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public decimal ListPrice { get; set; }
    }
}

Başka bir zaman diliminde onu Entity Framework Core üstünden SQL ile ya da Azure Cosmos Db ile veya istediğin başka bir Repository ile ilişkilendirebilirsin. Şimdilik Web uygulaması alanında dolaşımda olacak ve kullanıcının göreceği sayfayı kurgulayan View nesnesi için anlam ifade eden bir model şablonu olduğunu söylesek yeterli. Sıradaki adımımız Data isimli bir klasör oluşturmak ve içerisine aşağıdaki içeriğe sahip GameRepository sınıfını yerleştirmek. 

using FunnyHello.Models;
using System.Collections.Generic;

namespace FunnyHello.Data
{
    public class GameRepository
    {
        public List<Game> GetAllGames()
        {
            return new List<Game>
            {
                new Game{ Id=1, Title="Commandos II",ListPrice=10.5M },
                new Game{ Id=2, Title="Prince of Persia",ListPrice=9.45M },
                new Game{ Id=3, Title="Prince of Persia",ListPrice=9.45M }
            };
        }
    }
}

Bizi sonuca götürecek, görsel ortamda birkaç veriyi kullanmamızı sağlayan aptalca bir sınıftan başka bir şey değil ama senaryo için yeterli. Şu ana kadar seni zorlayan pek bir şey olmadığı düşüncesindeyim. Haydi o zaman devam edelim. Madem Model View Controller türevli bir Web uygulaması geliştiriyoruz, View ile Model arasındaki iletişim Controller sınıfının görevi olmalı. Öyleyse hali hazırda var olan Controllers klasörüne GameController isimli yeni bir sınıf ekle ve kodlamasını aşağıdaki gibi yaparak devam et. MVC ve detayları içinse üzülme. Eğitim sırasından ondan da bolca bahsedeceğiz.

using FunnyHello.Data;
using Microsoft.AspNetCore.Mvc;

namespace FunnyHello.Controllers
{
    public class GameController : Controller
    {
        public IActionResult Index()
        {
            GameRepository gameRepository = new GameRepository();
            var games = gameRepository.GetAllGames();
            return View(games);
        }
    }
}

Gayet prüzsüz bir sınıf. Controller türevli olması bir yana dursun Index isimli fonksiyon(ki uygulama için çağırılabilir bir Action anlamına geliyor) GameRepository sınıfını kullanarak oyun listesini alıp kendisi ile ilişkili olan View'a gönderiyor. Hangi View'a gideceğini nereden mi biliyor? Hımmm...Bunu bir düşünelim. GameController'ın Controller kelimesini çıkarırsak geriye Game kalıyor. View tarafında da Game isimli bir klasör olur ve içinde Index isimli bir sayfa olursa sanırım otomatik bir yönlendirme düzeneği tesis edilmiş olur. Aynı varsayılan şablonla gelen HomeController ve View/Home alıntdaki Index.cshtml düzeneğinde olduğu gibi. O halde sıradaki görevin belli. View klasörüne geçip Game isimli yeni bir klasör oluştur ve altına aşağıdaki içeriğe sahip Index.cshtml dosyasını ekle.

@model IEnumerable<Game><div><h1>Tüm Oyunlar</h1><hr /><table><thead><tr><th>Id</th><th>Title</th><th>List Price</th></tr></thead>
        @foreach (var g in Model)
        {<tr><td>@g.Id</td><td>@g.Title</td><td>@g.ListPrice</td></tr>
        }</table></div>

Belki kafana takılan bazı sorular olabilir. Neden başlangıça @model diye bir direktif var? Bu sayfa ile arka plandaki nesneler arasında gerekli olan bağlantı nasıl gerçekleşiyor? for döngüsünü biliyorum lakin buradaki kullanım tüm oyun listesini dolaşmak için mi acaba? ve benzerleri. Lütfen sabırlı ol. Amacımız şimdilik bu detaylarla ilgili değil. Minik bir parça daha ekleyelim. Shared klasöründeki _Layout.cshtml sayfasını bul ve içerisine aşağıdaki kod parçasını ekle.

<li class="nav-item"><a class="nav-link text-dark" asp-area="" asp-controller="Game" asp-action="Index">Games</a></li>

Nereye koyman gerektiğini söylemiyorum ancak basitçe bulacağından eminim ;) Tahmin edeceğin üzere yeni bir menü öğesi yerleştirdik ve ona basılınca hangi Controller nesnesinin hangi Action üyesinin tetiklenmesi gerektiği ifade ettik. Eğer hazırsan terminalden dotnet run komutunu vererek ya da Visual Studio ortamındaysan F5 tuşuna basarak örneği çalıştırabilirsin. Aşağıdaki ekran görüntüsündekine benzer bir sonuç elde etmeni bekliyorum.

Nasıl? Hiç yoktan iyidir değil mi? Mesela oyun bilgilerinin veritabanından geldiğini düşün. Hatta yeni oyun ekleme, fiyat değiştirme, oyunlara kapak fotoğrafları ekleme, yorum alma ve puan verme gibi kullanıcı etkileşimi yüksek fonksiyonellikler dahil ettiğini düşün. Hatta önyüz tarafında hazır Bootstrap çatısını kullanarak makyaj yaptığını ve albenisi yüksek, moda tabirle UX(User Experience) açısından zengin bir uygulama inşa ettiğini. Etkileyici bir Web uygulaması ortaya koymamız işten bile değil :) Ama ortada bir sorun var gibi.

Problem Ne?

Şu anda bir MVC uygulamasına Hello World demiş olduğumuzu sanabilirsin. Biraz üstünde durup düşününce, Controller sınıfının ne yaptığını, View bileşeninin bir Action ile nasıl ilişkilendiğini ve kendisi ile alakalı model nesnelerini nasıl kullandığını anlamış olabilirsin. Ne var ki uygulama şirketimizde çalışan yazılımcıların rahatsız olacağı bir kod parçası içeriyor. Biraz düşünüp neresi olduğunu bulmak ister misin? Arzu edersen bunu bir kahve molası eşliğinde daha da derinlemesine düşünebilirsin.

Photo by Matt Hoffman on Unsplash

Tekrar hoşgeldin ;) GameController içerisindeki aşağıdaki kullanıma odaklanmalısın. Bu kullanım yazılımcıların hoşuna gitmez. Gelecek ile ilgili endişeler duymalarına sebebiyet verir.

GameRepository gameRepository = new GameRepository();

Bunda ne sorun olabilir ki dediğini duyar gibiyim. Aynen bana da öğretildiği üzere oyun nesnesi üstünde temel CRUD(Create Read Update Delete) operasyonlarını üstelenen ve dolayısıyla sadece bu sorumluluğu üstüne alan bir sınıfın nesne örneğini alıp güzelce kullandın. Bir şekillde ifade etmek istersek kabaca aşağıdaki gibi bir durumun söz konusu olduğunu ifade edebilirim (Ve lütfen çizimimin kötü olmasına aldırma)

İşte o terim; Tightly-Coupled! Yine karşımıza çıktı :D Sorun, GameController nesnesinin GameRepository sınıfını doğrudan kullanması. Bu sıkı bir arkadaşlığın göstergesi gibi. Ancak uygulama kodları arttıkça ve proje ister istemez büyüdükçe GameRepository nesnesinin farklı yerlerde kullanımı da söz konusu olacak. Ya Low-level bileşen olarak ifade edilen GameRepository'nin(yapması gereken işle ilgili kaynaklara doğrudan erişip karmaşık bir şeyler yapan nesne) işleyişi farklılaşır veya adı değişirse? Ya onu kullanan bir test metodunda gerçekten veritabanına gitmeden sırf test senaryosunun kalanını işletmek için hayali bir Game listesi döndürmesi istenen bir fonksiyon gerekirse? Mesela GameRepository, GameController'a küser ve fonksiyonunu kaldırırsa :P İşin şakası bir yana GameController'ın çalışması ve oyuncu listesini View'a vermesi, GameRepository'nin ellerindedir. Bu sıkı bağımlı bileşkeler GameRepository'yi başka bir şeyle değiştirmeyi zorlaştırır.

Nasıl Çözeriz?

Sanırım sorun kısmen de olsa anlaşıldı. Bu ikili arasındaki sıkı dostluğa lafımız yok ama ilişkilerine bir mesafe koymalarında yarar var. Peki ya bunu nasıl sağlarız? Aşağıdaki şekle bakmadan biraz düşün derdim ama şu anda onu gördüğünü biliyorum :)

Yapılması gereken GameController'ı GameRepository sınıfından koparmak ve aradaki ipleri gevşetmek(Loosely-Coupled ilkesini sağlanması) Bir başka deyişle, High-Level Component olan GameController ile asıl işi yapan Low-Level Component GameRepository arasına soyut bir katman(abstraction layer) koymak. Asıl işi yapan sınıfın detaylarını umursamayan ve asıl işi yapan sınıfın yaptığı işe ihtiyaç duyan GameController sınıfının isteklerine elçi olan. Ayrıca oyunun kurallarını bir sözleşme ile belirleme ve gerçekten de Controller'ın ihtiyacı olan fonksiyonları verecek asıl nesneyi kullandırma imkanına sahip olacağız. Nesne yönelimli diller açısından baktığımızda bunun en pratik yolu Interface tipini kullanmak. Şimdi üstünlüğü ele geçirelim. Yine Data klasörü altına geç ve IGameRepository isimli aşağıdaki arayüzü ekleyerek çalışmana devam et.

using FunnyHello.Models;
using System.Collections.Generic;

namespace FunnyHello.Data
{
    public interface IGameRepository
    {
        IEnumerable<Game> GetAllGames();
    }
}

GameRepository sınıfını bu arayüzden türet(Belki bir dönüş tipi düzeltmesi de yapman gerekebilir) Aynen aşağıdaki kod parçasında olduğu gibi.

using FunnyHello.Models;
using System.Collections.Generic;

namespace FunnyHello.Data
{
    public class GameRepository
        :IGameRepository
    {
        public IEnumerable<Game> GetAllGames()
        {
            return new List<Game>
            {
                new Game{ Id=1, Title="Commandos II",ListPrice=10.5M },
                new Game{ Id=2, Title="Prince of Persia",ListPrice=9.45M },
                new Game{ Id=3, Title="Prince of Persia",ListPrice=9.45M }
            };
        }
    }
}

Güzellll! Gayet iyi gidiyorsun. Artık GameController sınıfına geçebilir ve GameRepository yerine eklediğimiz soyutlamayı kullanmasını sağlayabilirsin. Bunun için GamesController sınıfının ilgili interface tipi ile çalışmasını sağlaman lazım. Bildiğin gibi bir interface aslında soyutlama için kullanılan bir sözleşmedir(Contract) ve sadece çağırılacak asıl nesnenin içindeki fonksiyonların neler olduğunu GamesController'a söylemekle yükümlüdür. Şunu da biliyorsun ki Interface gibi arabulucu sözleşmeler sınıflar gibi örneklenip kullanılamazlar(new operatörü ile onları örnekleyemezsin) ama nesne referansı taşıyabilirler ;) Belki de onu Controller sınıfına Constructor metot üstünden alıp kullanabiliriz ;)

using FunnyHello.Data;
using Microsoft.AspNetCore.Mvc;

namespace FunnyHello.Controllers
{
    public class GameController : Controller
    {
        private readonly IGameRepository _gameRepository;
        public GameController(IGameRepository gameRepository)
        {
            _gameRepository = gameRepository;
        }
        public IActionResult Index()
        {
            //GameRepository gameRepository = new GameRepository();
            var games = _gameRepository.GetAllGames();
            return View(games);
        }
    }
}

Harika! Sonuca çok yaklaştın. Haydi uygulamayı tekrar çalıştırda, her şey yolunda mı görelim ;)

Aaa...Houston. We have a problem!

Galiba sende benim gibi hiç beklenmedik bir hata ile karşılaştın.

Bu çalışma zamanı hatası da nereden çıktı şimdi!? Doğruyu söylemek gerekirse pek de sevimli bir ekran görüntüsü değil. Oysaki uygulama derlenebiliyor. Senden ricam StackTrace içeriği ile birlikte hata mesajını dikkatlice okuman.

Sorunu görebildin mi?

GameController sınıfına tekrar dön. Yapıcı metot parametre olarak IGameRepository şeklinde bir interface referansı alıyor. Bir başka deyişle, IGameRepository arayüzünü uygulayan herhangi bir sınıf bu yapıcı metoda referans olarak taşınıyor. Lakin .Net çalışma zamanı bunu henüz bilmiyor. Bir yerlerde bir şekilde IGameRepository görüldüğü anda "Acaba bana ihtiyacım olan bir GameRepository nesnesi verebilir misin?" diyebilmeliyiz. İşte Dependency Inversion Principle'ın süreç yöneticisi Inversion of Control'un elçisi Dependency Injection Container'ların dile geldiği yerdeyiz.

Ne Gerektiğini Söylemek

.Net Core içerisindeki built-in DI mekanizması çalışma zamanında yukarıdaki senaryoda görülen bağımlıkların kolayca tanımlanmasına izin verir. Asp.Net tarafı söz konusuysa burası Startup sınıfı içerisindeki IServiceCollection arayüzünün kullanıldığı ConfigureServices metodudur. Oraya aşağıdaki kod parçasını eklemeni rica ediyorum (AddTransient metoduna şimdilik takılma. Raf ömrüne göre farklı kullanım senaryolarımız da var)

public void ConfigureServices(IServiceCollection services)
{
	services.AddControllersWithViews(); //Burası zaten var
	services.AddTransient<IGameRepository, GameRepository>();
}

Artık çalışma zamanında GameController nesnesi IGameRepository üstünden bir fonksiyon işletmek istediğinde gerçekten o işi yapacak asıl nesne(ki senaryomuza göre GameController) elinde hazır olacak. IGameRepository'nin belirlediği sözleşme kurallarının dışına çıkmadığın sürece GameController, GameRepository'deki değişimlerden zerre kadar etkilenmeyecek ;)

Tebrik ediyorum. Eğitimden önce yapman gereken hazırlığı bitirdin ve gerçek anlamda Asp.Net Core için Hello World dedin. Üstelik bunu Constructor Injection tekniği ile yaptın ki bunun dışında metot ve özellik(property) seviyesinde bile Injection tekniklerini kullanacaksın. Lakin her şey daha yeni başlıyor. Neredeyse tüm .Net 5 projelerinde bu DI mekanizmasını kullanacağız. Hatta yarın katmanlar artacak, servisler çoğalacak, repository'ler yerlerini belki de CQRS(Command and Query Responsibility Segregation) desenine bırakacak, nesne arası bağımlılıklar uzak servislere de sıçrayacak vs. Tüm bu serüven sırasında DI Container'lar hep seninle olacak. 

Senden istediğim birkaç şey daha var. Bu bir sonraki adımın için iyi bir hazırlık olabilir. Şu senaryoyu düşün;

Sisteme yeni oyun ekleme özelliği sunan bir fonksiyonun olsun. Bir oyun eklendiğinde, üyelere mail ile bildirim yapacak bir sistem de kurgulamak istiyorsun. SQL'deki trigger veya Button'a basılınca çalışan Click olayı gibi. Şu an bunu GameRepository sınıfına ekleyeceğin bir Add metodu içinden yaparsın diye tahmin ediyorum. Gönderim işini ise MailSender isimli bir sınıfla gerçekleştirmeyi düşünebilirsin. Ancak GameRepository ile MailSender birbirlerine sıkı sıkıya bağlı olmamalılar. Bu bağımlılığı çöz ;)

Tamam tamam. Seni rahat bırakacağım artık. Lütfen son olarak aşağıdaki maddelere de bir göz at ve cevaplarını dokümante etmeye çalış.

  • Single Responsibility, Dependency Inversion prensiplerini, onları bilmeyen birisine nasıl anlatırsın?
  • Inversion of Control, Dependency Inversion Principle ile aynı şey midir? Farklarını nasıl tanımlarsın?
  • High-Level Component ve Low-Level Component ne demektir? Araştırıp birer cümle ile tarifler misin?
  • Projedeki Data içeriğini harici bir kütüphaneye alıp kullanabilir misin?
  • Sence DI Container kullanımının artıları nelerdir?
  • Constructor dışında bir nesne bağımlılığını bildirmenin farklı yolları olabilir mi? Varsa bunları araştırıp örnekler misin?
  • Örnekte kullanıdığımız Transient fonksiyonu tam olarak ne anlama geliyor? Onun yerini alacak farklı versiyonlar varsa bir bakar mısın?
  • Örnekte Built-In mekanizma yerine örneğin Unity veya Ninject'i kullanmayı dener misin?

Son Dakika Gelişmesi

Eğer Constructor Injection dışındaki method, property ve view(Asp.Net MVC 6 sonrası geldi) türevli tekniklerin basit uygulamasına bakmak istersen github'a eklediğim hands-on-aspnetcore-di reposuna uğramanı tavsiye edebilirim. Bu repoda varsayılan main haricinde initial, constructor-injection, method-injection, property-injection ve view-injection isimli ayrı branch'ler var. İşe yarayan bir örnek değil ama temiz bir biçimde bu farklı teknikleri nasıl uygulayabileceğini gösteriyor ;)

Eğitimde görüşmek üzere ;) Sağlıklı günler.

Asp.Net Core - Dependency Injection Türleri

$
0
0

Ayakta durmuş odanın camından dışarıyı izlerken yazıya nasıl bir giriş yapsam diye düşünüyordum. Baharın etkisi ile yapraklarını açmış meşenin yavaş yavaş gölgelediği caddeden İtalyan bayrağı kasklı bir motosikletli geçti aniden. Sadece birkaç metre gerisinden de onu neredeyse aynı süratle takip eden martıya binmiş bir genç. Kaldırımda bir elinde alışveriş poşeti ötekinde onu yola doğru çekiştiren haylazla birlikte yürümeye çalışan orta yaşlarında bir kadın. Hemen binanın önündeki basket sahasında da yaşları beş ile on beş arasında değişen on çocuk. Futbol oynuyorlar. Bağrışlar, çağrışlar. Çekişmeli de gidiyor ama herkesin yüzünde bir maske. Eve kapanmak zorunda kalmadan önce çocukların son bir bahar ziyafetini izliyorum diye iç geçiriyorum.

On yedi günlük evden çıkma yasaklarının bir gün öncesi çünkü bugün, 29 Nisan 2021 Perşembe. Pek tabii hayat evde de olsa devam ediyor. Bende bu dönemi iyi değerlendirmek adına yaz aylarında vereceğim şirket eğitimleri için Amazon'dan getirttiğim kitapları çalışmaya ağırlık vereyim diyorum. Malum .Net 5 güldür güldür geleli çok oldu ve orada öğrenmem gereken birçok konu birikti. En önemli şey ise öğrendiğim bir konuyu olabildiğince basit şekilde anlatabilmek. Bakalım bu yazıda bunu başarabilecek miyim?

Hatırlayacağınız üzere bir önceki yazımızda Asp.Net 5 tarafında nasıl Hello World diyebileceğimizi incelemeye çalışmıştık(Henüz okumadıysanız bir göz atmanızda yarar var) O çalışmada ana odak noktamız dahili Dependency Injection mekanizmasının nasıl kullanıldığını görmekti. Kobay senaryomuzdaki en önemli noktalardan birisi de GameController sınıfı içerisinde IGameRepository yardımıyla low-level bir bileşenin kullanımıydı. Burada Constructor Injection tekniğinden yararlandığımızı ifade etmiştik. Bu teknik dışında kullanabileceğimiz versiyonlar da var. Bağımlı nesne çözümlemesini metot üzerinden, Property yardımıyla ve Asp.Net MVC 6 ile gelen @inject direktifi yoluyla da gerçekleştirebiliriz. İşte bu yazımızdaki amacımız aynı senaryoyu devam ettirerek söz konusu tekniklerin nasıl uygulanabileceğini öğrenmek. Yazıdaki kod parçaları şuradaki github hesabımda yer alıyor. Initial, constructor-injection, method-injection, property-injection ve view-injection şeklinde farklı branch'ler içeriyor. Bu branch'lerde ilgili tekniklerin proje üstünden ayrı ayrı uygulanış şekillerini takip edebilirsiniz. Yazı boyunca ise odak noktamızı kaybetmemek adına sadece gerekli kod parçalarını kullanacağım. Paralel hareket etmeniz gerekebilir. Hazırsanız başlayalım;

Method Injection

Aslında yapıcı metot(Constructor) bir metottur. Dolayısıyla neden bu şekilde farklı bir uygulama tekniği olduğunu düşünebilirsiniz. Ne var ki, Constructor tekniğinde bağımlı nesnenin çözümlenmesi(DI'ın Resolution aşaması) ona ihtiyaç duyan nesne örneklenirken gerçekleşir. Bazı durumlarda sadece belli bir metot içerisinden kullanılan bağımlı nesneler de olabilir. Sanırım örnek bir senaryo üzerinden gidersek konu daha anlaşılır olacak.

Oyun portaline yenilerini eklemek için bir fonksiyon yazacağımızı düşünelim. Ayrıca her oyun eklendiğinde bir dış sisteme mesajla bildirim yapamak istiyoruz. RabbitMQ gibi bir kuyruk sistemi, veritabanı ya da doğrudan e-posta sunucusu bile olabilir. En nihayetinde GameRepository'nin Create metodu içerisinde bu gönderim işlemini yapmaya karar veriyoruz. Bununla birlikte mesaj yayınlama işini üstlenen asıl sınıfı kullanmak yerine, sadece gönder demenin daha doğru olacağını da biliyoruz. Çünkü bu sıkı bağlı(tightly-coupled) yapıda söz konusu olan xyz sistemi için gerekli gönderim adımlarını, bununla ilgisi olmayan GameRepository sınıfının anlamasına gerek yok. Hatta tightly-coupled durumlarda ısrarla kaçınmaya da çalışıyoruz. Haydi gelin kodlamaya başlayalım. İlk olarak IPublisher isimli bir arayüz geliştirelim.

public interface IPublisher
{
	void Send(string message);
}

IPublisher arayüzü senaryomuz için oldukça ilkel bir sözleşme sunuyor. Geriye değer döndürmeyen ve string tipte parametre alan Send isimli bir metot bildirimi taşıyor. Bu arayüzü kullanan örnek bir sınıfı da aşağıdaki gibi geliştirdiğimizi düşünelim.

public class RabbitPublisher
	: IPublisher
{
	public void Send(string message)
	{
	   //todo something
	}
}

Biliyorum, fonksiyon içerisinde bir şey yapmıyoruz ama unutmayın; amacımız Method Injection'ı uygulamak. Bu durumda GameRepository sınıfı için düşündüğümüz Create metodunu nasıl yazarız, bir düşünün. İdeal olanı aşağıdaki kod parçasında olduğu gibidir.(Bu arada IGameRepository arayüzünde de Create bildiriminin yapılması gerektiğini hatırlatayım. Nitekim IGameRepository üstünden kullanacağımız bir fonksiyon olmalı)

public Game Create(Game game, IPublisher publisher)
{
	publisher.Send("A new game has been added to inventory");
	return game;
}

Metodun belki de en önemli kısmı ikinci parametresidir ve IPublisher arayüz referansı kullanılmaktadır. Dolayısıyla onu uygulayan bir nesneyi metot içerisine alabilir ve Send fonksiyonunu çağırabiliriz. Bir başka deyişle Create metodunun ihtiyacı olan asıl nesne arayüz üzerinden kullanılır. Bu, Method Injection tekniği ile bağımlılığın çözümlenmesidir. Ancak ortada halen daha bir soru var. Create metodunun hangi IPublisher türevi ile çalışacağını nerede söyleyeceğiz? Yani bağımlı nesne için gerekli kayıt işlemini(Dependency Injection Service Registration) nerede yapacağız? Tahmin edileceği üzere bu sorunun cevabı Create metodunun çağırıldığı yerdir ve GameController sınıfındaki Create metodu bunun için uygundur. 

[HttpPost]
public IActionResult Create(Game game)
{
	_gameRepository.Create(game, new RabbitPublisher());
	return RedirectToAction("Index");
}

Dikkat edileceği üzere ikinci parametrede bir RabbitPublisher nesne örneği kullanılıyor. Yani Create metodunun ihtiyaç duyduğu asıl nesneyi metot üzerinden göndermiş oluyoruz. 

Property Injection

Yukarıdaki senaryoyu düşündüğümüzde aklımıza şöyle bir soru da gelebilir; ortada bir IPublisher referansı yoksa Send metodunun çalışmamasını nasıl sağlarız? Yani Create metodunun IPublisher ile çalışmasını opsiyonel olarak sunmak istersek nasıl bir yol izleriz? Bu senaryo için IGameRepository arayüzünü aşağıdaki gibi değiştirelim.

public interface IGameRepository
{
	IEnumerable<Game> GetAllGames();
	IPublisher Publisher { get; set; }
	Game Create(Game game);
}

Dikkat edileceği üzere Create metodunun parametresi olarak kullandığımız IPublisher arayüzünü, özellik olarak tip seviyesine aldık. Çok doğal olarak GameRepository sınıfının içeriği de buna uygun şekilde değiştirilmeli.

public class GameRepository
	: IGameRepository
{
	public IPublisher Publisher { get; set; }
	public IEnumerable<Game> GetAllGames()
	{
		return new List<Game>
		{
			new Game{ Id=1, Title="Commandos II",ListPrice=10.5M },
			new Game{ Id=2, Title="Prince of Persia",ListPrice=9.45M },
			new Game{ Id=3, Title="Prince of Persia",ListPrice=9.45M }
		};
	}

	public Game Create(Game game)
	{
		if (Publisher != default)
			Publisher.Send("A new game has been added to inventory");
		return game;
	}
}

Lütfen Create metodunun içerisindeki if kullanımına dikkat edelim. Eğer IPublisher türünden olan Publisher isimli özellik(property) gerçekten bir nesne referansı taşıyorsa Send metodu çağırılacaktır. Böylece Create metodunun bağımlılığını property seviyesine çekerek tercihe bırakmış olduk. Tabii bağımlı nesnenin kayıt işlemini de yapacağımız bir yer olmalı öyle değil mi? Yine GameController sınıfındaki Create metodunda bunu yapabiliriz.

[HttpPost]
public IActionResult Create(Game game)
{
	_gameRepository.Publisher = new RabbitPublisher();
	_gameRepository.Create(game);
	return RedirectToAction("Index");
}

Görüldüğü üzere _gameRepository nesnesinin(ki o da GameController sınıfına Constructor üzerinden enjekte edilmektedir) Publisher özelliğine yeni bir RabbitPublisher referansı atadık. Dolayısıyla Create metodu çağrıldığında RabbitMQ'ya mesaj gönderen asıl fonksiyon işleyecektir. Lakin Publisher özelliğine bir atama yapılmazsa herhangi bir gönderim işlemi de olmayacaktır. Seçime bağlı bu nesne çözümlemesi için Property Injection tekniğini nasıl kullanacağımızı da görmüş olduk.

View Injection

Aslında Asp.Net tarafına MVC 6 ile birlikte gelen ve genel Dependency Injection teknikleri içerisinde olmadığını düşündüğüm bir yöntem daha var. @inject direktifinin kullanımı. MVC/MVVM desenlerinde bir View'u, Controller'dan veya View-Model'den ayrıştırmak istediğimiz durumlarda kullanabileceğimiz bir yöntem olarak karşımıza çıkıyor. Yöntem sayesinde DI Container üstünden kayıt edilen bir nesne veya servis metodunun View üstünden doğrudan çağrılması sağlanabiliyor. Senaryomuzda portaldeki hareketliliklerle ilgili veri toplayan ve örneğin aktif kullanıcıların sayısını veren aşağıdaki gibi bir sınıf olduğunu düşünelim.

public class DataCollectorService
{
	public async Task<int> GetActiveUserCount()
	{
		return await Task.FromResult(new Random().Next(10,50));
	}
}

DataCollectorService içerisindeki GetActiveUserCount metodunun ne iş yaptığının çok önemi yok ama bu metodu bir View bileşeninde aşağıdaki gibi doğrudan kullanmamız mümkün.

@inject C64Portal.Agent.DataCollectorService dataCollectorService<div><h2>Envanterdeki C64 Oyunları</h2><hr /><h3>Current active user count is @await dataCollectorService.GetActiveUserCount() </h3></div>

Tabii örneği bu haliyle çalıştırıp Inventory sayfasına gitmek istersek kaçınılmaz olarak aşağıdaki hata mesajı ile karşılaşırız.

View nesnesi bir nesne çözümlemek istemektedir ancak bu nesne dahili DI Container'ın servis koleksiyonunda yer almamaktadır. Dolayısıyla Startup sınıfındaki ConfigureServices metodunda DataCollectorService isimli servis için bir kayıt işlemi yapılmalıdır.

public void ConfigureServices(IServiceCollection services)
{
	services.AddControllersWithViews();

	services.AddTransient<IGameRepository, GameRepository>();
	services.AddTransient<DataCollectorService>();
}

Sonrasında uygulamanın sorunsuz çalıştığı gözlemlenebilir.

Bu ve önceki yazıyla birlikte Asp.Net 5'in temel Dependency Injection uygulama tekniklerini görmüş olduk. Tabii Dependency Injection konusu bunlarla bitmiyor. ConfigureServices metodunda servisleri kayıt altına alırken hep AddTransient metodunu kullandığımızı fark etmiş olmalısınız. Oysa ki AddScope ve AddSingleton metotları da var. Yani kayıt altına alınan bir DI servisinin hangi anda örnekleneceğini ve yaşam ömrünün ne olacağını da belirleyebiliyoruz. Bu konu ile ilgili fırsatım olursa bir şeyler karalamaya çalışacağım. Böylece geldik bir yazımızın daha sonuna. Tekrardan görüşünceye dek hepinize sağlıklı günler dilerim.

Asp.Net Core - Dependency Lifetimes

$
0
0

Çalışmakta olduğum şirketin çok büyük bir ERP(Enterprise Resource Planning) uygulaması var. Microsoft .Net Framework 1.0 sürümünde düşünce olarak hayat geçirilip geliştirilmeye başlanmış. Milyonlarca satır koddan ve sayısız sınıftan oluşan, katmanlı monolitik mimari üstünde yürüyen, sahada on binden fazla personelin kullandığı çok etkili bir ürün. Geçtiğimiz yıl bu uygulamanın modernizasyonu kapsamında başlatılan IT4IT çalışmaları bünyesinde nesne bağımlılıklarının yönetimi için Dependency Injection mekanizmasının nimetlerinden de epeyce yararlanıldı. Doğruyu söylemek gerekirse koda yaptıkları dokunuşları hayranlıkla izledim.

Elbette başa dert olan ve sahada fark edilmesi güç bazı konular da gündeme gelmedi değil. Bunlarda birisi de bağımlı nesnelerin yaşam ömürleri ile alakalıydı. Gerçekten böylesine büyük bir sistemde AddTransient ile mi gitmeli yoksa AddScoped olarak mı bırakmalı gibi sorulara cevap vermek kolay değil. Öncelikle şu nesne yaşam ömrü meselesini anlamak gerekiyor. Bende hazır evden çıkmamız yasak kitaplarıma gömülmüşken bu meseleyi iyice bir öğreneyim istiyorum. Kapak fotoğrafı mı? Her zaman ki gibi konumuzla bir alakası yok. Sadece yazıyı yazarken dinlemekte olduğum Bon Jovi'nin 1984 çıkışlı stüdyo albümüne ait :D 

Aslında Asp.Net 5 açısından bakıldığında da Dependency Injection ile ilişkili kafa karıştıran ve saha çözümlerinde dikkat gerektiren konulardan birisi servis yaşam süreleri(Hoş, .Net Remoting ve WCF tarafındaki nesne yaşam döngülerini düşününce nispeten çok daha kolay bir konu) Bu kısa yazıda söz konusu meseleyi öğrendiğim kadarıyla sizlere anlatmaya çalışacağım. Örneğimiz bir önceki yazıda da değindiğimiz .Net çözümü(hands-on-aspnetcore-di)üzerinde koşuyor olacak. Ayrıca kodun detaylarına github adresinden bakabilir ve eksik kısımları tamamlayabilirsiniz. Ben odaklanmamız gereken yerleri ve sonuçları paylaşmaya çalışarak bakmamız gereken alanı daraltmak niyetindeyim. Her şeyden önce senaryomuza bir göz atalım(Taslak çizimin kusurlarını lütfen mazur görün)

Anlamsız bir model ancak nesne yaşamlarını öğrenmek için hem kitaplarda hem de internet kaynaklarında kullanılan yaygın bir yöntemi değerlendireceğiz; Guid tipi yardımıyla hayattaki nesnelerin takibi. Senaryomuzda GameController tipinin bağımlı olduğu dört farklı bileşen var. Bu bağımlılıklar IGameRepository, IPartRepository, IShopRepository ve arayüzleri üstünden gelen sınıflar ile PerformanceCounter tipi. İşin ilginç yanı PerformanceCounter sınıfının da IGameRepository, IPartRepository ve IShopRepository referansları üzerinden gelen bileşenlere bağımlılığı var. Bu kurguda amaç, çalışma zamanında DI Container servislerine kayıt edilen IGameRepository, IPartRepository ve IShopRepository türevlerinin, PerformanceCounter içerisine alınırken farklı yaşam süresi seçimlerine göre nasıl tepki geliştirdiklerini öğrenmek.

Dependency Injection servis koleksiyonuna kayıt edilen bileşenler için normal şartlarda üç tip yaşam ömrü seçeneği bulunuyor. Transient, Scoped ve Singleton. Genellikle konuya yabancı olan ben gibiler kolaya kaçıp Transient seçeneğini tercih ediyor. Fakat duruma göre uygun olan modeli belirlemek lazım. Örneğin Entity Framework tarafına ait DbContext servisi kayıt edilirken neden Scoped olarak dahil ediliyor? Peki ya ILogger'ın varsayılan ömrü neden Singleton? Dolayısıyla aradaki farkları anlamamız önemli.

DI Container'a Scoped türünde kayıt edilen bir servis her web talebi için yeniden örnekleniyor. Singleton modelinde ise servis bir kere örnekleniyor ve uygulama(Web App) ayakta kaldığı sürece yaşamaya devam ediyor. Dolayısıyla onu çözümleyen(Resolve) bileşenler hep aynı nesne örneğini kullanıyorlar. Son olarak Transient seçeneğinde, bağımlı bileşen her nerede çözümlenirse çözümlensin hep yeni bir örneği oluşturularak kullanılıyor.

İyi güzel hoş ama bunu canlı bir örnekle nasıl analiz ederiz? Yukarıdaki şekle göre gerekli kodlarımızı yazmaya başlayım. IGameRepository, IShopRepository ve IPartRepository arayüzleri Guid tipinden birer özellik sunuyorlar. Bu Guid'leri onları uygulayan asıl bileşenlerin(Concrete Instance)çalışma zamanındaki takibini yapmak için kullanacağız. IShopRepository ve ShopRepository tiplerinin içeriğini aşağıda bulabilirsiniz. Diğerleri de benzer bir düzeneğe sahipler.

using C64Portal.Models;
using System;

namespace C64Portal.Data
{
    public interface IShopRepository
    {
        public Guid InstanceID { get; set; }
        void Sell(Game game,decimal offer);
    }
}

ve onu uygulayan asıl sınıf(Concrete Class).

using C64Portal.Models;
using C64Portal.Queue;
using System;
using System.Collections.Generic;

namespace C64Portal.Data
{
    public class ShopRepository
        : IShopRepository
    {
        public Guid InstanceID { get; set; }
        public ShopRepository() :this(Guid.NewGuid())
        {
        }
        public ShopRepository(Guid instanceID)
        {
            InstanceID = instanceID;
        }

        public void Sell(Game game, decimal offer)
        {
            // Do Something
        }
    }
}

İşe yarayan bir fonksiyon yok ancak yapıcı metodun(constructor) nasıl kullanıldığı bizim için önemli. ShopRepository sınıfına ait bir nesne örneklenirken yeni bir Guid oluşturuyoruz. Varsayılan yapıcı metot, DI kayıt işlemi(Register) sırasında gerekli olduğu için çağrıldığında parametre ile donatılan diğer yapıcı metodu tetikliyor. Doğal olarak seçilen lifetime kriterine göre takip edeceğimiz benzersiz bir değere sahip olmuş olacağız. Diğer arayüz ve uyarlamalarını yazdıktan sonra PerformanceCounter sınıfını da aşağıdaki gibi geliştirebiliriz.

using System;

namespace C64Portal.Data
{
    public class PerformanceCounter
    {
        public Guid ShopRepositoryID { get; set; }
        public Guid GameRepositoryID { get; set; }
        public Guid PartRepositoryID { get; set; }
        private readonly IGameRepository _gameRepository;
        private readonly IShopRepository _shopRepository;
        private readonly IPartRepository _partRepository;
        public PerformanceCounter(IGameRepository gameRepository, IShopRepository shopRepository, IPartRepository partRepository)
        {
            _gameRepository = gameRepository;
            _shopRepository = shopRepository;
            _partRepository = partRepository;
            GameRepositoryID = _gameRepository.InstanceID;
            ShopRepositoryID = _shopRepository.InstanceID;
            PartRepositoryID = _partRepository.InstanceID;
        }
        public void CalculateMemoryUsage()
        {
            //Do Something
        }
    }
}

İğrenç bir sınıf değil mi? :D Ancak yapıcı metoda yine dikkat edelim. Sınıfın bağımlı olduğu bileşenler, tasarladığımız arayüzler üzerinden çözümlenerek içeri alınıyor ve gelen nesne örneklerinin Guid tipli özelliklerinin herbiri için ayrılmış alanlara atanıyorlar. Şimdi de GameController içeriğini aşağıdaki gibi değiştirelim.

using C64Portal.Data;
using C64Portal.Models;
using C64Portal.Queue;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace C64Portal.Controllers
{
    public class GameController : Controller
    {
        private readonly IGameRepository _gameRepository;
        private readonly IShopRepository _shopRepository;
        private readonly IPartRepository _partRepository;
        private readonly PerformanceCounter _performanceCounter;
        private readonly ILogger<GameController> _logger;
        public GameController(
            IGameRepository gameRepository
            ,IShopRepository shopRepository
            ,IPartRepository partRepository
            , PerformanceCounter performanceCounter
            , ILogger<GameController> logger)
        {
            _logger = logger;
            _performanceCounter = performanceCounter;
            _gameRepository = gameRepository;
            _shopRepository = shopRepository;
            _partRepository = partRepository;
        }
        public IActionResult Index()
        {
            _logger.LogInformation($"\n[SINGLETON]\tShopRepo ID:{_shopRepository.InstanceID},In Perf Counter:{_performanceCounter.ShopRepositoryID}");
            _logger.LogInformation($"\n[TRANSIENT]\tGameRepo ID:{_gameRepository.InstanceID},In Perf Counter:{_performanceCounter.GameRepositoryID}");
            _logger.LogInformation($"\n[SCOPED   ]\tPartRepo ID:{_partRepository.InstanceID},In Perf Counter:{_performanceCounter.PartRepositoryID}");

            var games = _gameRepository.GetAllGames();
            _performanceCounter.CalculateMemoryUsage();
            return View(games);
        }

        public IActionResult Create()
        {
            return View();
        }

        [HttpPost]
        public IActionResult Create(Game game)
        {
            _gameRepository.Publisher = new RabbitPublisher();
            _gameRepository.Create(game);
            return RedirectToAction("Index");
        }
    }
}

GameRepository'dekine benzer bir durum burada da söz konusu. Sadece fazladan PerformanceCounter ve ILogger bağımlılıkları da var. Lakin fazladan dediğimiz PerformanceCounter kullanımı önemli. Web uygulaması çalıştığında GameController tipi her ne zaman çağırılırsa yapıcı metodu sebebiyle DI Container'dan IGameRepository, IShopRepository, IPartRepository ve PerformanceCounter referansları isteyecek. Bu da asıl sınıfların örneklendiği(Constructor'ların tetiklenmesi) ya da örneklenmeyip örneklenmiş olanların verildiği bir operasyon anlamına geliyor. Diğer yandan PerformanceCounter'ın çağırılması halinde onun da istediği IGameRepository, IPartRepository ve IShopRepository referansları var. PerformanceCounter sınıfı bunları da DI Container'dan isteyecek(Hatta onu bilerek AddTransient olarak kayıt edeceğiz ki her örneklendiğinde DI'dan diğer arayüz referanslarını istesin) İşte bu ikinci isteklerde söz konusu servislerin hangi yaşam döngüsü seçeneğine göre kaydedildiği önem kazanıyor. Diğer yandan ufak bir detay ama Index isimli Action içerisinde bir Log yayınladığımızı da fark etmiş olmalısınız. Loglamayı, Controller'a gelindiğinde ve Index fonksiyonu çağırıldığında oluşan bağımlı bileşenlerin güncel Guid değerlerini kaydetmek için kullanıyoruz. Bu arada tüm bileşenlerin Constructor Injection tekniği ile çözümlendiğine dikkat edin ve başka hangi tekniklerden bahsetmiştik hatırlayın. 

Bu arada loglamayı dilerseniz fiziki olarak bir Text dosyasına da yapabilirsiniz. Ben bunun için Serilog.Extensions.Logging.File isimli Nuget paketini projeye ekledim ve Startup sınıfındaki Configure metodunu da aşağıdaki gibi değiştirdim.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
 ILoggerFactory loggerFactory)
{
	var path = Directory.GetCurrentDirectory();
	loggerFactory.AddFile($"{path}\\Logs\\Log.txt");

Gelelim bileşenlerin DI Servis kataloğuna kayıt edilmesine ki burası yazımızın dönüm noktası. Bunun için Startup sınıfındaki ConfigureServices metodunu aşağıdaki gibi kullanabiliriz.

public void ConfigureServices(IServiceCollection services)
{
	services.AddControllersWithViews();

	services.AddTransient<IGameRepository, GameRepository>();
	services.AddScoped<IPartRepository, PartRepository>();
	services.AddSingleton<IShopRepository, ShopRepository>();
	services.AddTransient<PerformanceCounter>();

	services.AddTransient<DataCollectorService>();
}

IGameRepository üstünden bağlanan GameRepository, AddTransient fonksiyonu ile eklenmiş durumda. Buna göre kendisine her ihtiyaç duyulduğunda tekrardan örneklenecek. Yani onun adına hep yeni bir Guid değeri görmemiz gerekiyor. PartRepository sınıfı ise AddScoped metodu ile dahil edilmiş durumda. Buna göre aynı Scope içerisinde kalındığı sürece hem Controller hem de PerformanceCounter'da tekil bir PartRepository nesnesinin kullanılmasını bekliyoruz. Ta ki farklı bir scope'a geçip tekrar buraya dönene kadar(Bunu diğer bir Controller'a geçip geri gelerek kontrol edebiliriz) Son olarak sıra AddSingleton ile eklenen ShopRepository nesnesinde. Buna göre web uygulaması çalıştığı sürece, sayfa yenilense(Örneğin F5 ile) ya da farklı Controller ve Action metotları çalışsa bile, uygulama yeniden başlatılıncaya kadar tek bir ShopRepository örneğinin kullanılıyor olması lazım.

Bu aşamaya geldiyseniz uygulamayı çalıştırıp logları takip etmeniz yeterli. Ben örneğin çalışma zamanına ait iki ekran görüntüsü bırakmak istiyorum. İlki komut satırından yürütülen çalışma zamanına ait. Console penceresine düşen logları görebilirsiniz(O değilde kopyala yapıştırın acı bir sonucu var burada. İki tane Prince of Persia eklenmiş yahu)

Paylaşmak istediğim diğer görüntü ise Guid bilgilerini topladığım Excel'e ait.

Guid değerlerinin hangi durumda nasıl farklılaştığını görebiliyor musunuz? Bir nesnenin hangi aksiyonda nasıl davranış sergilediğini anlamak oldukça kolay. ShopRepository sisteme Singleton modelde alındığı için hangi aksiyon olursa olsun üretilen Guid hep aynı kalmakta. Sayfa yenilense de scope değişse de fark etmiyor. Yani GameController için de, onun içinden çağırılan PerformanceCounter için de aynı nesne kullanılıyor ve sayfa yenilense bile bu nesne yaşamaya devam ediyor. Lakin PartRepository nesnesine ait Guid bilgisi gerçekleşen aksiyon bazında değişmiş görünüyor. Fakat bir fark var. Aynı Scope'a dahil olan PerformanceCounter'da aynı PartRepository nesne örneğini kullanıyor. Bu nedenle Guid aksiyon bazında aynı kalmış halde. Bu noktada Scoped tekniğinin, Singleton ile sürekli olarak karıştırıldığını ifade edebilirim. Biri uygulama ayakta kaldığı müddetçe aynı kalırken diğeri sadece ortak Scope'a dahil olan farklı aksiyonlar boyunca aynı kalıyor. O nedenle yapılan her aksiyonda yeni bir PartRepository örnekleniyor ve hem GameController hem PerformanceCounter bu aynı nesneyi kullanıyor. GameRepository ise oldukça şımarık :) Aksiyon ne olursa olsun hep yeni bir Guid oluşmuş görünüyor; GameController tarafında da PerformanceCounter tarafında da.

İşte bu kadar :)

Bu örnekle bağımlı bileşenlerin nesne ömürlerinin nasıl şekillendiği kafamda biraz daha netleşmiş oldu. Elbette gerçek hayat senaryolarında bu seçimler oldukça kritik öneme sahip. Tüm uygulama yaşamı boyunca yaşayacak bir nesne örneği her ne kadar cazip görünse de bellek tüketiminin bir anda artmasına sebebiyet verebilir. Ya da web talebi için bir nesne örneklenmesi, ilk oluşturma maliyeti yüksek olan bileşenler düşünüldüğünde performans kaybına neden olabilir. Her nesne gerektiğinde yeni bir örnek oluşturulması basit bir seçim gibi dursa da network trafiğinin aşırı derecede artmasına sebebiyet verebilir. Kaynaklar en kötü karar bile kararsızlıktan iyidir felsefesini benimseyerek AddTransient olarak ilerleyin diyor. Bense vakaya göre seçim yapmamız gerekiğini düşünüyorum(It depends hali). Notlarıma burada son vermeden önce araştırmanız için iki konu bırakıyorum.

  • Sizce bir arayüz üstünden n sayıda bağımlı bileşeni DI Container servisine kayıt edebilir miyiz?
  • Çalışma zamanının herhangi bir noktasında DI Container'a kayıt edilmiş servisleri tek tek veya toplu olarak silebilir miyiz?

Böylece geldik bir yazımızın daha sonuna. Tekrardan görüşünceye dek hepinize sağlıklı, huzur dolu günler dilerim.


Asp.Net Core - Önce Razor Sonra Blazor

$
0
0

Kendime geldiğimde hiçbir şey göremediğimi fark ettim. Üstüme çöken zifiri karanlığa rağmen halen daha hayatta olduğuma dair tek şey yağmur damlalarının birkaç metre üstümde olduğunu sandığım metal tavana vurarak çıkardıkları seslerdi. Ensemden neredeyse ayak parmaklarıma kadar yayılan ağrı hiçbir şeyi umursamaz bir tavırda yattığım yerden doğrulmamı güçleştiriyordu. Son hatırladığım CloudTown'dan birkaç sibernetik coder ile karşılaştığım belli belirsiz yansımalardan ibaretti. Kısa süre sonra yakınlarımda koşuşturan bazı ayak sesleri işittim. Yer yer duraksıyor yer yer su birikintilerine girip çıkıyorlardı. Fısıltılar daha duyulur sesler haline gelmeye başladı. Tavandaki kapağı açmak üzere içlerinden birinin elindeki anahtarları hazırladığını işittim. Sonrası gözlerim için çok korkunç bir deneyimdi. Bu zifiri karanlıkta ne kadar kaldığımı bilmiyorum ama gözlerim dışardan gelen o parlak ışığa karşı adeta haykırıyordu. Üstüme boca edilen bir kova soğuk suyun ardından gelen kaba ses ise çok tanıdıkdı. Ve şöyle seslendim; "Reyzor! Sen haaa" :P

Efendim bendeniz yine yazıya giriş yapacak güzel bir şeyler bulamayınca böyle garip bir hikayeyi ortaya atıverdim. Gel gelelim konumuz baş karakterimiz Reyzor ile de alakalı.(Photo by Jack Finnigan on Unsplash)

Asp.Net Core 5 cephesine baktığımızda üç temel uygulama modelini desteklediğini görürüz. Web uygulamaları, servisler ve gerçek zamanlı iletişim araçları. Gerçek zamanlı(Real-Time) iletişim tarafında SignalR karşımıza çıkar. Servisleri göz önüne aldığımızda ise Web API ve gRPC başrolde yer alır. Web tarafını düşündüğümüzde oldukça zengin bir ürün çeşitliliği söz konusudur. MVC(Model View Controller), Razor Pages, Blazor, Angular ve React tabanlı SPA'lar(Single Page Applications). Kuvvetle muhtemel Asp.Net Core 5 tarafına yeni başladıysanız ve haberleri yeterince takip ediyorsanız en çok ilgi çeken geliştirme çatısının Blazor olduğunda hem fikir sayılırız. Fakat Blazor' a doğrudan geçmeden önce bazı temellerin de öğrenilmesi gerekir.

Söz gelimi dahili Dependency Injection mekanizmasının nasıl çalıştığını anlamak bunlardan sadece birisidir. Bana göre bir diğer önemli konu da Razor View Engine ya da sık telafuz edilen adıyla Razor'dur. Nitekim MVC, Razor Pages ve Blazor geliştirme çatıları alt yapı olarak Razor View Engine üzerine oturmaktadır. Dolayısıyla aynı bileşen yazım şablonlarını farklı web geliştirme çatıları için kullanabiliriz. Bu yetenek çatılar arası geçiş yapmamızı da kolaylaştırır. Öğrenilmesi oldukça kolay olan Razor, sadece C# ve HTML bilgisi gerektirir. Son yıllarda desteklediği yardımcı takılar(Tag helper) sayesinde arayüz tasarımcıları için dostane bir sözdizimi(syntax) sunar. Ayrıca test edilebilirliği basitleştirir. Razor en temel tanımıyla dinamik içerikle HTML çıktısı üretmeyi amaçlayan C# temelli bir işaretleme şablonudur(Markup Template)İşte bu yazıdaki amacımız örnek kodlar yardımıyla ona merhaba demek.

Dilerseniz vakit kaybetmeden örnek kodlara geçelim. Microsoft .Net 5 yüklü herhangi bir platformda aşağıdaki terminal komutları ile işe başlayabiliriz. Sonrasında Visual Studio Code veya Visual Studio Communit Edition veya kendimizi rahat hissettiğimiz bir IDE ile ilerleyebiliriz.

dotnet new sln -o HelloRazor
cd .\HelloRazor\
dotnet new mvc -o FirstContact
dotnet sln add .\FirstContact\

HelloRazor isimli bir Solution var. Ona FirstContact isimli bir de MVC(Model View Controller) projesi ekledik. Bu, en hafif MVC şablonu olarak düşünülebilir. Varsayılan haliyle baktığımızda dahi Razor tabanlı sayfalar içerdiğini görebiliriz(Index.cshtml, Privacy.cshmtl gibi) Cshtml uzantılı bu dosyalar kendi içlerinde hem HTML hem de sunucu tarafında çalışan C# kodlarını barındırır. Bu noktadan sonra karşımıza sıklıkla @ sembolünün çıkacağını ifade edebilirim. @ Sembolü ile C# ifadelerini veya kod bloklarını HTML içerisinde kullanabiliriz. Hemen index.cshtml dosyasına geçelim ve içeriğini aşağıdaki gibi güncelleyelim.

@{
    ViewData["Title"] = "Space Traveler's Base";
}<div><h1 class="display-4">@ViewData["Title"]</h1><p>
        Today is @DateTime.Now.DayOfWeek ! Well, where do you want to go?</p><ul>
        @{
            var locations = new List<string> { "Sirius", "Altair", "Betelgeuse", "Algol", "Messier 31", "Eta Carinae" };
            var count = locations.Count;

            foreach (string location in locations)
            {
                <li>@location</li>
            }
        }</ul></div>

Index.cshtml içerisinde hem HTML hem de C# kodları olduğunu görebiliriz. @{ } ifadeleri razor kod blokları olarak adlandırılır. İçlerinde özgürce C# kodlaması yapabiliriz. Kodun iki yerinde bu kullanım söz konusu. İkinci kullanım biraz daha ilgi çekici. string türden bir List nesnesini kullanarak tarayıcıya çeşitli yıldızların isimlerini basıyoruz. @location kullanımı mutlaka dikkatinizi çekmiştir. Zaten bir Razor kod bloğundaysak neden döngüdeki location değişkeni başında @ işareti var? Çünkü kod bloğu içerisinde bir HTML elementi kullandık. li elementi kullanılan kısımda bir anda Razor kod bloğu dışına çıkmış ve HTML dünyasına girmiş oluruz. Doğrudan HTML içerisinde C# ifadelerini çalıştırmamız gereken bu gibi hallerde, ifadenin başına @ işareti koyarak hareket edebiliriz(Razor Implicit Expression olarak adlandırılan teknik). location için geçerli olan bu kullanımın bir benzeri de hangi günde olduğumuzu yazdıran kısımda yer almaktadır.  

Uygulamayı çalıştırdığımızda aşağıdakine benzer bir sonuç almamız gerekir.

Pek tabii locations listesinin bir model nesnesi baz alınarak farklı bir bileşenden View tarafına çekilmesi asıl amaç olmalıdır. MVC ve Blazor uygulamalarında sıklıkla bavşuracağımız bir yoldur. Bu durumu daha iyi anlamak için Model klasörü altına StarModel isimli bir sınıf oluşturarak devam edelim. Bu literatürde ViewModel olarak adlandırılan tiptir.

using System.ComponentModel.DataAnnotations;

namespace FirstContact.Models
{
    public class StarModel
    {
        [Required]
        [Display(Name="Star No")]
        public int ID { get; set; }
        [Required]
        [MinLength(3,ErrorMessage = "The name of the star must be at least 3 characters.")]
        public string Name { get; set; }
        [Required]
        [Range(1,750)]
        [Display(Name="Distance from Earth(LY)")]
        public double Distance { get; set; }
        [Required]
        public double SurfaceTemperature { get; set; }
    }
}

Özelliklerin üzerinde kullanılan nitelikler(Attribute)şu an için gerekli değil ama yazının ilerleyen kısmındaki Tag Helper kullanımında işimize yarayacaklar. Bunu bir kenara bırakırsak basit bir ViewModel tanımladığımızı söyleyebiliriz. Bu modele ait yükleme, silme, güncelleme gibi işlemleri ise farklı bir sınıfın sorumluluğuna vermemiz doğru olacaktır. Örneğin Data isimli yeni bir klasör altına koyacağımız Star isimli bir sınıf bu iş için ideal görünüyor (Konumuzla doğrudan alakalı olmasa da alışkanlık edinmemiz için bir interface tipi ile birlikte bu bileşenleri tanımlamakta ve Dependency Injection ile birlikte kullanmakta yarar var)

IStar arayüzü;

using FirstContact.Models;
using System.Collections.Generic;

namespace FirstContact.Data
{
    public interface IStar
    {
        List<StarModel> GetStars();
    }
}

ve Star sınıfı;

using FirstContact.Models;
using System.Collections.Generic;

namespace FirstContact.Data
{
    public class Star
        :IStar
    {
        public List<StarModel> GetStars()
        {
            return new List<StarModel>
            {
                new StarModel{ID=1,Name="Sirius",Distance=8.60,SurfaceTemperature=9940},
                new StarModel{ID=2,Name="Altair",Distance=16.73,SurfaceTemperature=7700},
                new StarModel{ID=3,Name="Betelgeuse",Distance=642.5,SurfaceTemperature=126000},
                new StarModel{ID=4,Name="Algol",Distance=92.95,SurfaceTemperature=13000},
                new StarModel{ID=5,Name="Eta Carinae",Distance=7.5,SurfaceTemperature=35200},
            };
        }
    }
}

Sadece Index sayfasında kullandığımız yıldız listesinin bir benzerini döndüren kobay bir sınıf. Peki Razor View Engine tarafı bu sınıfı nasıl kullanacak? Şu anki MVC senaryomuza göre HomeController tarafındaki hazır Action metodu bunun için uygun görünüyor.

using FirstContact.Data;
using Microsoft.AspNetCore.Mvc;

namespace FirstContact.Controllers
{
    public class HomeController : Controller
    {
        private readonly IStar _star;

        public HomeController(IStar star)
        {
            _star = star;
        }

        public IActionResult Index()
        {
            var starList = _star.GetStars();
            return View(starList);
        }
    }
}

Index metodunda bir Star nesne örneği kullanarak yıldız listesini almaktayız. Sonrasında bu listeyi View metoduna parametre olarak geçerek Home isimli View'a göndermekteyiz. Sınıfın yapıcı metodunda görüleceği üzere Star bileşenini Dependency Injection Container'dan istiyoruz. Dolayısıyla Star bileşeninin DI servislerine eklenmesi gerekiyor. Daha önceki bir yazımızda bu konuya değinmiştik ;) Nasıl yaparım diyorsanız buradaki yazıya bakmanızı önerebilirim ama "işimi uzatma" derseniz de yapmanız gereken tek şey Startup sınıfındaki ConfigureServices metoduna aşağıdaki satırı eklemekten ibaret.

services.AddTransient<IStar, Star>();

Bu ara hazırlıklardan sonra yeniden Razor tarafına dönelim ve Index sayfasının içeriğini aşağıdaki gibi değiştirelim.

@{
    ViewData["Title"] = "Space Traveler's Base";
}
@model List<StarModel> <div><h1 class="display-4">@ViewData["Title"]</h1><p>
        Today is @DateTime.Now.DayOfWeek ! Well, where do you want to go?</p><ul>
        @{
            foreach (var star in Model)
            {<li>@star.Name (@star.Distance) Light Year from Earth</li>
            }
        }</ul></div>

Bir önceki Razor örneğinden farklı olarak burada dikkat etmemiz gereken en önemli nokta @model ve Model enstrümanları. @model direktifi ile sayfanın kullanacağı ViewModel nesnesini belirtiyoruz. Star sınıfındaki GetStars metodunun dönüş tipini düşününce bunun List<StarModel> olması son derece doğal. Başta yapılan bu işaretleme, sayfanın devamındaki Model değişkenlerinin List<StarModel> tipinden bir referans olacağını belirtmekte. Bu nedenle for döngüsü içerisinde @star ifadesinden sonra StarModel nesnesinin özelliklerine erişebiliyoruz. Model nesnesini kimin doldurduğu sorusunun cevabını ise HomeController sınıfının Index metodundaki View çağrısı vermekte.

Yaptığımız bu son değişikliklere göre çalışma zamanı çıktısı aşağıdaki gibi olacaktır.

Fark ettiyseniz Razor söz dizimi oldukça kolay. Görselliği basit dokunuşlarla artırmak da mümkün. Yardımcı takılara(Tag Helper) değinmeden önce dilerseniz sayfamızı aşağıdaki şekilde yeniden düzenleyelim.

@{
    ViewData["Title"] = "Space Traveler's Base";
}
@model List<StarModel><div><h1 class="display-4">@ViewData["Title"]</h1><p>
        Today is @DateTime.Now.DayOfWeek ! Well, where do you want to go?</p><table class="table table-dark"><thead class="bg-primary"><tr><th>
                    No</th><th>
                    Name</th><th>
                    Distance (LY)</th><th>
                    Surface Temperature (Kelvin)</th></tr></thead><tbody>
            @foreach (var star in Model)
            {<tr><td><label>@star.ID</label></td><td><label class="font-weight-bold">@star.Name</label></td><td><label class="font-italic">@star.Distance</label></td><td><label>@star.SurfaceTemperature</label></td></tr>
            }</tbody></table></div>

Bu sefer HTML Table elementini işin içerisine kattık. Sonuç biraz daha umut verici. En azından benim için ;)

Şu ana kadar yaptıklarımızla Razor söz dizimini en temel haliyle tanıdık ve bir Razor sayfasını çalışacağı Model ile nasıl bağlayabileceğimizi öğrendik. Bilmemiz gereken giriş seviye konulardan bir diğeri de Tag Helper kullanımı. Tag Helper ifadeleri giriş kontrollerinde(input), veri doğrulamasında(validation), yönlendirmelerde(routing) ve form ektileşimlerinde(actions) kullanılabilen basitleştirici ifadeler olarak düşünülebilir. Normalde HTML Helper'lar da söz konusudur ancak son yıllarda yardımcı takı ifadeleri hem okunabilir olmaları hem de bir HTML elementine doğrudan eklenebilmeleri sebebiyle öne çıkmaktadır. HTML Helper'lar okunması zor fonksiyon ifadelerinden oluştuğu için zamanla cazibesini kaybetmiştir. Şimdi Home klasörü altına Add isimli bir View ekleyelim. Sisteme yıldız eklemek için kullanılan basit bir form olduğunu düşünebiliriz. Bu form içerisinde de bazı yardımcı takılardan yararlanacağız. Birkaç tanecik :|

@{
    ViewData["Title"] = "Space Traveler's Base";
}
@section scripts{<partial name="_ValidationScriptsPartial" />
}
@model StarModel<div><form asp-controller="Home" asp-action="OnSave" method="post"><div class="form-group"><label asp-for="@Model.ID"></label><br /><input asp-for="@Model.ID" /><br /><span asp-validation-for="@Model.ID" class="text-danger"></span></div><div class="form-group"><label asp-for="@Model.Name"></label><br /><input asp-for="@Model.Name" /><br /><span asp-validation-for="@Model.Name" class="text-danger"></span></div><div class="form-group"><label asp-for="@Model.Distance"></label><br /><input asp-for="@Model.Distance" /><br /><span asp-validation-for="@Model.Distance" class="text-danger"></span></div><div class="form-group"><label asp-for="@Model.SurfaceTemperature"></label><br /><input asp-for="@Model.SurfaceTemperature" /><br /><span asp-validation-for="@Model.SurfaceTemperature" class="text-danger"></span></div><div class="form-group"><button class="btn-primary" type="submit">Save</button></div></form></div>

asp- şeklinde başlayan ifadeler tag helper bildirimleridir. Örneğin form elementinde asp-controller ve asp-action isimli iki yardımcı kullanılmıştır. Bildiğiniz üzere bir web formunu sunucu tarafına gönderirken form elementinden yararlanılır. Submit tipinden bir butona basıldığında hangi Controller'ın hangi Action fonksiyonunun devreye gireceğini bu yardımcı takılar ile belirleyebiliriz. Buna göre HomeController'ın OnSave isimli metodu çağıralacaktır. Kullanılan bir diğer yardımcı takı ise asp-for'dur. Kullanıldığı HTML elemanına göre farklı davranışlar sergileyebilir. Bir label ile ilişkilendirildiğinde ViewModel nesnesinin varsa Display değerini, yoksa özellik adını kullanır. input elementi ile kullanıldığında ise ekrandan girilen içeriğin modelin hangi özelliğine bağlanacağını belirtir. Sayfada bazı doğrulama kontrolleri de söz konusudur. Girdi ihlallerine ait bilgiler span elementleri içerisinde yazılırken asp-validation-for isimli yardımcı takı kullanılmıştır. Buna göre takriben aşağıdaki ekran görüntüsündekine benzer bir sonuç elde ederiz.

Konumuz giriş verilerinin kontrolü değil bu nedenle çok fazla detaya girmiyoruz. Lakin sayfanın HTML çıktısına bakarsak asp-validation-for bilgilerinin istemci bazlı doğrulama işlemlerinde kullanılan jQuery için data-val-* formatına evrildiğini de görebiliriz. Örneğin Name input kontrol için üç karakterden az olmaması gerektiğini ifade etmiştik ya da dünyaya olan uzaklığın sıfır ışık yılı olmayacağını. Aşağıdaki ekran görüntüsünden de fark edileceği üzere tek bir asp-for-validation bildirimi HTML çıktısında data-val, data-val-minlength, data-val-minlength-min, data-val-required şeklinde çözümlenmiştir. Bunların bir kısmının da ViewModel nesnesinde kullandığımız DataAnnotations niteliklerinden(Attribute kullanımlarına bakın) kaynaklandığını ifade edebiliriz. Dolayısıyla Razor tarafındaki bir tag helper'ın işi nasıl kolaylaştırdığını net bir şekilde görmüş oluyoruz.

Razor tarafında kullanılan pek çok yardımcı takı var. Bir tanesini daha örneğe eklemeye ne dersiniz? Söz gelimi yıldızların dahil olduğu birkaç galaksiyi bir Enum sabiti olarak tuttuğunuzu ve yeni yıldız eklerken de bunları bir select elementinde göstermek istediğinizi düşünün. Bunun için asp-items isimli yardımcı takıyı kullanabilirsiniz. Haydi bir deneyin ;) Buna ek olarak geliştirdiğimiz yıldız ekleme sayfasını HTML Helper fonksiyonları ile tekrardan yazmaya çalışın. Tag Helper'ların sağladığı kolaylığı daha net görebileceğinizi temin ederim :)

Bu kısa çalışmada MVC, Blazor ve Razor Pages gibi çatıların temel yapıtaşı olan Razor View Engine tarafını basitçe anlamaya çalıştık. Artık bunun üstüne daha zengin MVC ve Blazor örnekleri geliştirebileceğimizi düşünüyorum. Başta da belirttiğimiz gibi önce Razor sonra Blazor. Hatta önce Razor sonra MVC, sonra Blazor :) Böylece geldik bir yazımızın daha sonuna. Tekrardan görüşünceye dek hepinize sağlıklı günler dilerim.

Birisi Sana "Blazor Nedir?" Diye Sorarsa

$
0
0

Yeni bir on yılın arifesini çoktan geçtik ve bu on yıla girmeden önce Microsoft, milenyumun başında da yaptığı üzere önemli ürünlerin altına imzasını attı. Açık kaynak dünyasına hızlı bir girişten sonra yıllardır süregelen Mono projesi daha da anlam kazandı. Artık Silverlight, Windows Phone, Web Forms, .Net Remoting gibi kavramlardan neredeyse hiç söz etmiyoruz. Üstelik bazıları yıllar önce rafa kalktı. Rafa kalkanların, eskiyenlerin bıraktığı tecrübe yeni nesil ürünlerin başarısını artırdı. Unity ile platform bağımsız oyunlar, Xamarin ile macOS ve linux ayırt etmeksizin çalışan kodlar vs derken .Net Core hayatımıza girerek büyük sükse yaptı.

Dahası da var. 2017'de başlatılan ve standart haline gelen WASM(Web Assembly) Microsoft cephesinin gözünden kaçmadı. 2018 yılında deneysel bir çalışma olarak başlayan Blazor kısa sürede evrimleşti ve şu anda yatırım yapılması gereken bir konu haline geldi(Örneğin Asp.Net Web Forms tabanlı ürünlerinizi modernize etmek istiyorsanız) Ancak ortada önemli bir sorun var. Onu bir arkadaşına nasıl anlatırsın? (Photo by Museums Victoria on Unsplash)

Ben 1995 yılından beri kodlama yapıyorum. Microsoft'un tüm .Net sürümlerinde geliştirme yapma fırsatı buldum. C# dilinin duyrulduğu zamanlardaki .Net Framework çatısının günümüzdeki halini alıncaya kadar geçirdiği evrimi gözlemleme fırsatı bulan şanslı programcılardanım. Şöyle bir durup geriye baktığımda beni en çok zorlayan konunun dağıtık sistemlerde kodlama yaparken kullandığım .Net Remoting olduğunu düşünüyorum(90ların dll hell sendromunu düşünmezsek) Sanıyorum günümüz .Net programcılarının bir çoğu onu duymamıştır.

Remoting tarafındaki zorlukları sırtını SOAP(Simple Object Access Protocol) standardına dayayan XML Web Service'ler büyük ölçüde kapatıyordu. Lakin yüksek performans, TCP bazlı çalışma imkanları, küçük paket boyutları gibi bazı önemli detaylar ortaya çıkınca .Net Remoting'e başvurmadan da olmuyordu. Sonrasında Windows Communication Foundation duyuruldu. Servis tabanlı geliştirme tek bir çalışma modeline indirgendi ve bu bana göre devrimsel bir dönüşümdü(Microsoft bu tip yapısal birleşimleri sıklıkla yapar. Bakınız Unified .Net) İşi gücü bırakıp onu öğrenmeye, ürünlerde kullanmaya başladık. Neredeyse her tür servis iletişimini destekliyor ve bunu oldukça kolaylaştırıyordu. .Net Remoting'e göre öğrenilmesi de çok daha kolaydı, her türlü WS-I profilini destekliyordu. SOA'cıların gözdesi haline gelmekteydi. Üstelik Workflow Foundation ile birlikte ele alındığında hafif siklet iş akış yönetim mekanizmalarını da kurgulayabiliyordunuz. Bizzat çalıştığım bir kurumun iş akış şemalarını Workflow Foundaction, WCF ve Visual Studio genişletmeleri yardımıyla ürünleştirdiğine şahidim.

Fakat web ve mobil dünyasındaki gelişmeler daha zengin kullanıcı deneyimine sahip istemci uygulamalar istiyordu. Öyle bütün bir sayfayı sürekli kullanıcı ve ana makine arasında dolaştırmak yerine istemciye inip orada çalışan ve sunucu ile kısmi haberleşen zengin arabirimli uygulamalar bekleniyordu. Uygulamaların mobil sistemlerde çevrimdışı çalışabilir olması elbette büyük bir avantajdı, lakin senkronizasyon yeni bir problem olarak karşımızdaydı. Ado.Net'in bağlantısız(Disconnected)çalışan versiyonunun boy ölçüşebileceği bir şey değildi. Web ve mobil dünyasının dağıtım stratejisindeki cazibesi masaüstü uygulamaları arka plana atıyordu. Microsoft, ClickOnce ile uygulama dağıtımını kolaylaştıran merkezileşmiş bir strateji sunmuş olsa da bu Windows odaklıydı. Bu nedenle platform bağımsızlık adına tarayıcı tarafında plug-in destekli çalışma modelini göz önüne almış ve Flash'e rakip olacağını düşündüğü bir politika izlemeyi ihmal etmemişti. Doksanların sonları ve milenyumun başındaki ActiveX objelerinin kullanımı artık demode olduğundan farklı bir ürüne ihtiyacı vardı. Karşımıza Silverlight çıktı. Heyecan veriyordu. Oturduk bu kez Silverlight'ı da haldır haldır öğrenmeye çalıştık. İstemci tarafında Silverlight için gerekli çalışma zamanı olduğu sürece işler yolunda gidiyordu. Bu arada masaüstü tarafı Windows Presentation Foundation çatısı ile oldukça zenginleşmiş ve Windows işletim sisteminin sonraki sürümlerinde kullanıcı deneyimi yüksek ürünler geliştirilebilir olmuştu. Yine de tarayıcılar uygulamaların kalesi gibiydi.

Nitekim Javascript kodlarını her tür platform tarayıcısında çalıştıran Virtual Machine'in gücü ve avantajları bir türlü yakalanamıyordu. Sunucu tarafı ile haberleşmenin zengin bir yolu iyi bir çözüm olabilir miydi? Portföyümüze Rich Internet Application Services(RIA Services) eklendi. Onu HTTP metodları ile sorgulanabilir verileri baz alan Data Service'ler izledi. Silverlight gibi istemciler bu servisler yardımıyla .Net Framework'ün zengin imkanlarına erişebilirdi. Yine de ters giden şeyler vardı ve dikiş bir türlü tutmuyordu. Web dünyası servis iletişiminde yeni yeni şeyler denerken Silverlight unutulmaya başlandı. İstemci ve sunucu arasındaki iletişimin parça parça yapılması için kullanılan Ajax gibi çatılar bile daha az kullanılır oldu.

Derken REST(Representational State Transfer) stil servisler girdi hayatımıza. WCF ilk etapta bunu genişletme kütüphaneleri ile birlikte desteklemeye çalıştı, sonrasında dahili olarak bünyesine kattı. Artık kimse Web Servislerden ya da .Net Remoting ile tasarlanmış dağıtık bileşenlerden, Silverlight ile geliştirilmiş plug-in destekli istemcilerden, Windows Presentation Foundation ya da Workflow Foundation'dan pek bahsetmiyordu. REST hızla evrimleşirken Web API çözümleri yaygınlaşmaya başladı. İstemci ve sunucu iletişiminde anlık gelişmeleri yakalamak için Ajax'tan ziyade soket haberleşme öne çıktı ve SignalR devreye girdi.

Dünyada o kadar çok servis yazılmaya başlanmıştı ki performans, yüksek cevap verme süreleri, kolay ölçeklenebilirlik, hatalar sonrası çabucak toparlanabilmek doğal olarak öncelikli aranan kriterler haline gelmişti. Nitekim basit bir sayfanın içinden bile parça parça onlarca servis çağrısı eş zamanlı olarak yürütülüyordu. Video izleme ve müzik dinleme platformları bununla nasıl başa çıkacaktı? Dağıtık mimarinin çakıl taşlı patikalarında çıplak ayakla yürümeyi hangi mimar isterdi. Paket boyutlarını küçültmek lazımdı belki de.

Çözümler pek çok sefer olduğu gibi açık kaynak dünyasından yayılıyordu. Sektörün öncüleri yeni yeni ürünler ile önümüze birçok şey bırakmaya devam etti. gRPC onlardan biriydi. Çok daha yüksek performanslı bir servis haberleşmesinin yolunu açmıştı. Onu ilk denediğim zamanlarda proto nesnesinin bir örneğini istemci tarafına da koyunca, .Net Remoting zamanlarında yaptığımıza benzer Marshall by Reference/Value yaklaşımları gelmişti aklıma. 

Servis dünyasındaki gelişmeler hızla devam ederken, mobil dünyasının inanılmaz yükselişi ile karşımıza farklı farklı geliştirme modelleri çıktı. Progressive Web App ve Single Page Application yaklaşımları gümbür gümbür yayılıyordu. Elimizden düşürmediğimiz telefonlardaki sosyal medya hareketlerimiz öylesine arttı ki, yazılım firması olmayan ürün sahipleri yeni diller ve platformlar çıkarmaya başladı. React, Vue, Go, Docker vs bu şekilde dünyamıza girdi. Microsoft bu alanda da söz sahibiydi elbette ve Angular ile SPA dünyasını desteklediğini açıkça göstermişti. Fakat Microsoft'un yapması gereken çok daha önemli bir şey vardı. Her şeyden önce bu değerli .Net platformunun ve o güzel C# dilinin Linux, macOS ayırt etmeksizin prüzsüzce çalışabilir olması gerekiyordu. Ücretsiz, herkesin seveceği türden IDE'ler ile, kolayca entegre edilebilen paketlerle. Bunun için şüphesiz ki açık kaynak dünyasının gücünü arkasına almalıydı. Xamarin daha da ciddiye alınmalıydı. İşte bu şekilde yeni düzene geçildi.

Velhasıl herkesin ortak problemleri hep aynı konular üzerinde yoğunlaşıyordu. Kullanıcı sayıları fazla, istekleri sınırsızdı. Mobil uygulamalar geliştirme platformlarını zorluyordu. Performans için bulut çözümler ucuz görünse de pahalıya patlıyordu. Ağ trafiği her zaman sıkıntılı bir konuydu ve parçala yönet şeklindeki dağıtık yaklaşım mimarilerini uygulama ve öğrenme maliyetleri de az değildi. Metodolojiler değişmekte çevik olunmaktaydı. Javascript, Node ile sunucu tarafını etkili kullanarak öne geçiyor ama yine de eş zamanlı hareketlerin çoğalması, Go ve Rust gibi öğrenilmesi zorlu dillerle bir takım çözümler geliştirmeyi mecbur kılıyordu. Sunucular yetersiz gelmekte dünyanın birçok köşesine yeni veri merkezleri açılmaktaydı.

Bu uzun ve kronolojik sıralamaya yer yer uymayan kurguda sizlere zihnimi açmaya çalıştım. Hikayede değinmediğim, unuttuğum birçok kavram da oldu(Mesela Javascript'te zorlanıyoruz diye bize verilen Typescript) Ve sanıyorum ki hiç Windows Forms demedim. Halbuki çok önemli bir detaydı benim gibi yıllanmış programcıların hayatında. Her şeyden önce dünyanın belki de en çok kullanılan işletim sistemi üzerinde pürüzsüz çalışırdı. Kullanıcı dostu formlar geliştirmek için Delphi doğasından kopup gelen Anders'in ekibinin zengin bileşenlerini sürükleyip bırakmak yeterliydi. Çevrimdışı çalışabilmek doğasında vardı. Performansı, yüklendiği makinenin gücüne bağlıydı ve dağıtımı zordu belki ama pekala sunucular ile de konuşabiliyordu. Bazen kapalı bir ağ sisteminde bazen internete açık bir odadaki PC'de gül gibi yaşıyordu.

Ne var ki HTML ve Web standartları o kadar başarılıydı ki daha doksanlı yılların sonlarından itibaren Windows Forms'un yüzüne pek de bakmayışımızı haklı çıkartıyorlardı. Düşünün bir; çoğumuz ofis uygulamalarını web tabanlı tarayıcılardan kullanıyoruz. Bulut servis sağlayıcılarının yine tarayıcılarında çalışan IDE'lerinde kodlama yapıyoruz. Bir PDF dosyasını bile tarayıcıda açıp yazıcı ile bastırabiliyoruz. Kocaman ekranlarda açtığımız web sayfalarını, minik mobil ekranlarda yine aynı tasarım deneyimini kaybetmeden gezebiliyoruz. Web'in nimetlerini düşündüğümüzde bu muazzam bir şey elbette.

Peki ya Blazor! Tarayıcıda çevrimdışı çalışabildiği de düşünülecek olursa o günlere dönüş için geliştirilmiş olabilir mi? Yoksa Silverlight'ın yeni bir versiyonu mu duruyor karşımızda? Kısa sürecek bir hayal mi yoksa? Aslında hiçbiri değil. Fikir olarak benzerlikler olsa da bu kez Microsoft hedefi tam on ikiden vurdu diye düşünüyorum(Umarım on yıl sonra bu yazıya uğrayıp, "Vuramadı" diye not almam)

Benim de mutlak suretle öğrenmem gereken bir uygulama çatısı Blazor. Sahiplenmek değil ama en azından farkına varmam gereken bir çözüm. Onu öğrenmek için birkaç kitaba, bir Pluralsight eğitimine ve bazı dergi yazılarına bakıyorum. Kendimce notlar da alıyorum. Aşağıdaki mektubu yazarken kullandığım kaynakları yazının sonunda bulabilirsiniz. Bilgi vermesi dileğiyle.

Kimsin Sen Blazor?

Sevgili yazılım sevdalısı merhaba, Ben Blazor.

2018 yılında Microsoft'un uzak diyarlardaki bir ofisinde dünyaya geldim. Amacım Angular, React ve Vue gibi Single Page Application tabanlı programlar geliştirmeni kolaylaştırmak. Bunu yaparken Asp.Net Core Web çatısının nimetlerini de sana sunuyorum. Üstelik bunu yıllardır kullandığın C# ve Razor bileşenleri ile gerçekleştirebilirsin. Yanına azcık HTML ve CSS de koyabilirsin. İster Windowsçu ol ister Xamarinci, ister Web Formscu ol ister MVCci fark etmez ;) Beni kolayca öğrenebilirsin. Daha da güzel bir şey söyleyeyim mi? Benimle birlikte geliştirdiğin uygulamaları tarayıcı üstünde çevrimdışı çalışacak şekilde modelleyebilirsin(Blazor WebAssembly)

Ama sana sunucu üstünden çalışan klasik bir model de sunuyorum(Blazor Server) Hangisini tercih edersen artık. Ayrıca arkadaşım Xamarin ile hibrid çözümler için de yardımcı olurum. Mobil dünyasını unutmuş değilim. Tarayıcı üstünde çalışan parçalarım için Javascript kullanmana gerek yok. Gerçekten yok! Sadece C# dilini kullanarak istediğin çözümü geliştirebilirsin. Yine de olur ya Javascript paketleri ile konuşman gerekir, o zaman Javascript Interoperability(JS Interop) isimli bir araç da sunuyorum. .Net içinden Unmanaged bir kod parçasını(mesela bir Win32 sistem fonksiyonunu)çağırmak gibi bir şey aslında. 

Benimle pek çok türde program geliştirebilirsin. Tetris, Astreoid, Diablo, Flappy Bird gibi oyunlar, içerik yönetim sistemleri(CMS-Content Management System), IoT(Internet of Things) sürücüleri, Electron ile hibrid çözümler, mobil uygulamalar, elektronik ticaret siteleri, ofis programları, kod yazma araçları ve daha neler neler. Daha fazla detay ve örnek kod için şuradaki repoya uğrayabilirsin. Hatta istersen o repodaki uygulamalara kolayca ulaşabileceğin ve yine Blazor ile yazılmış bir tarayıcı da kullanbilirsin ki ona da şu adresten ulaşabilirsin.

Benim en önemli yapı taşlarımdan birisi de Razor bileşenleri(component). HTML, C# ve CSS üçlemesini kullanabileceğin Razor bileşenleri sayfandaki bir parça, sayfanın kendisi, bir dialog penceresi veya bir form olabilir. MVC ve Razor Pages için tasarladığın bileşenler varsa onları benimle de kullanabilirsin. Şimdilik hazır bileşen setim çok güçlü olmayabilir ama çevreden birçok firmanın bu alanda sunduğu paketler mevcut. Telerik ve DevExpress onlardan sadece ikisi. Bu firmaların Asp.Net ve Windows Form kontrollerini onlarca yıl kullandın. Tecrübelerini biliyorsun. Şimdi sana sunduğum geliştirme modellerimi anlatmak istiyorum.

Blazor WebAssembly

Benim en çok kıskandıkları özelliğim bu. Ona Client-Side Blazor dedikleri de oluyor. Amacı Javascript yazmana gerek kalmadan C# kodlarının tarayıcıda doğrudan çalıştırılmasını sağlamak. Bu Typescript kodlarının istemci için Javascript'e dönüştürülmesinden farklı bir şey anlıyorsun değil mi? Aslında yazdığın uygulamanın çalışması için gerekli ne kadar DLL ve WASM-based kütüphane varsa, çalışma zamanı ile birlikte istemciye indiriyorum. Çalışma zamanı derken demek istediğim WASM tabanlı .Net Runtime'ı yollamak. Sonrasında uygulaman istemci üzerinde rahatça koşuyor. Sunduğum WASM-based .NET çalışma zamanı WASM standartları üzerine tasarlandı. Bu yüzden WASM'ı destekleyen Edge, Safari, Firefox, Chrome gibi bilinen tarayıcıların tamamında çalışıyor. Ortak standart sonuçta. Büyük babam Silverlight'tan farklı olarak küresel bir standart üzerinde koştuğumdan istemciye bana özel bir plug-in kurmana da gerek yok. Küresel standartlara bağlı kalmak her zaman iyidir.

Kötü Huylarım

Her ne kadar seni mutlu eden şeyler söylesem de bazı kötü huylarım olduğunu da belirtmek isterim.

  • Mesela benim bu modelimi Debug etmek o kadar kolay olmuyor. En azından abim Blazor Server'a göre.
  • Plug-In gerekmiyor belki ama WASM destekleyen bir tarayıcı olması da şart. Belki bildiğin bütün tarayıcılar WASM'ı destekliyor ama Javascript çalışma zamanına göre daha büyük bir motor bloğuna ihtiyacım var. Bu da hafifsiklet tarayıcılarda benim çalışmamı çok zorlaştırıyor. IoT cihazlar belki sana küsebilirler.
  • Yazdığın kodların bulunduğu DLL'ler istemci tarafına iniyor ya. Heh işte ona dikkat et. Şeytanın işi olmaz kod içerisinde unuttuğun bazı hassas verileri açığa çıkabilir. O yüzden paketlerde ne kodladığına dikkat et. Gerekirse onları sunucu tarafında tut.
  • Çok doğal olarak istemcide çalışabilmenin de bir maliyeti var. Hamama giren terler :) İhtiyacın olan kütüphaneler uygulamanın ilk talebinde yükleme süresini uzatabilir. Boyutları itibariyle bu yükleme zaman da alabilir. O yüzden önbelleğe alma stratejilerini göz önünde bulundurmanı öneririm.

Bunların haricinde sana iyi haberlerim de var ;)

İyi Huylarım

En güzel haberi baştan söyleyeyim.

  • Uygulamanın istemcide çalışması için Javascript yazmana gerek yok. Çok iyi biliyorsun ki Node.js çıkınca Javascript ile sadece istemci taraflı değil sunucu taraflı kodlamayı da aynı dille yapabilme şansı elde etmiştin. .Net tarafında ise sunucu bazlı kodlama öne çıkıyor ve istemci için yine Javascript veya Typescript'e başvurman gerekiyor. Artık bu modelimi kullanarak yazdığın C# kodlarının derlenmiş hallerini istemcide doğrudan çalıştırabilirsin.
  • Performans olarak da iyi sonuçlar veren bir model bu. Sunucu tarafında yaşanabilecek ölçeklendirme veya geç cevaplama gibi durumlar pek de söz konusu değil. Sunucu tarafı bağımlılığı ise aslında yok gibi bir şey. Yine de çok istersen elin kolun bağlı değil. Pekala Web API'lerle veya farklı dış dünya servisleri ile iletişim kurabilirsin.
  • Performans demişken bu modelde yazdıkların tarayıcının tam da istediği ara dile önceden derleniyor. Tarayıcının doğrudan derlenmiş kodlarla çalışma şansı oluyor. Basit uygulamalarda değil ama örneğin oyun geliştirirken, etkileşimi yüksek eğitim programları hazırlarken bu çok işine yarayabilir.
  • Modern uygulama dünyasının bilinen yıldızı Progressive Web App için de tam destek sunuyorum. Yani Native uygulama deneyimini bu modelle sunabilirsin.
  • Üstelik bu modelde geliştirdiğin bir uygulamayı istemci tarafına indirdikten sonra internete ihtiyaç yok. Tamamen çevrimdışı çalışabiliyorum. Mariana çukurunda bile çalışırım.
  • Bu modelde yazdığın ve çeşitli iş kurallarını içeren bir kütüphaneyi alıp abim Blazor Server modelinde de kullanabilirsin. Her ikisi de aynı bitlere derlenmiş versiyonlar ile çalışacaklardır. Dolayısıyla Blazor WebAssembly olarak geliştirdiğin bir uygulamayı basit hamlelerle Blazor Server modeline dönüştürmen mümkün.

Bu modelime ait çalışma şeklini aşağıdaki renkli içerikte bulabilirsin(Fark ettim de Document Object Model'in L harfini unutmuşum. Pardon) 

Şimdi sana sunduğum diğer geliştirme modelini anlatayım.

Blazor Server

Bu modelime Server-Side Blazor dedikleri de oluyor. Aslında Client-Based'den önce .Net Core döneminde ortaya çıktı. Tahmin edeceğin üzere uygulamanın ana kodları sunucu tarafında işletiliyor. Dolayısıyla sana Asp.Net Core ortamının nimetlerinden yararlanma fırsatı sunuyorum. İstersen .Net Standart kütüphanelerinden de faydalanabilirsin. İstemci tarafına yolladığım şey ise DOM(Document Object Model)'in kendisi. Bunun içinse Razor bileşenlerinin sunucu tarafında render edilmiş hallerini kullanıyorum. Tahmin ediyorum ki aklına takılan bir soru da var. İstemciye giden bileşenler ile sunucu kodlarını nasıl haberleştireceksin, değil mi? Bunun için sana DOM ve uygulama tarafındaki farkları takip etme stratejisi üzerine kurgulanmış SignalR enstrümanını sunuyorum. 

İyi Huylarım

Şimdi bu modelin sana sunduğu güzelliklerden bahsedeyim.

  • Tekrarı gibi olacak ama, bu modelde de tek satır Javascript yazmana gerek yok. Üstelik küçük kardeşim Blazor WebAssembly ile aynı sözdizimini kullanan bileşenlerim var(Razor Components) Teşekkürler Razor!
  • Uygulama kodun sunucu üstünde güvenli bir alanda duruyor olacak. Zengin hosting seçeneklerin var. Web API servisleri ile konuşma, bulut servis nitemlerinden yararlanma, veritabanı gibi diğer bileşen bağımlılıklarını kullanma gibi imkanlardan bahsediyorum. Büyük ihtimalle de bulut bilişim servislerini tercih edebilirsin ki Azure senin için ideal bir konumlandırma olacaktır.
  • İstemciye sadece Render edilmiş bir şeyler gönderiyorum ve tüm kod sunucuda çalıştığı için uzun indirme süreleri, büyük dosyalar veya geç çalışma zamanı açılışları yok.
  • Bir yazılımcının vazgeçemediği enstrümanlardan olan debug etme kabiliyetlerinde oldukça cömertim.
  • Son olarak ne iş olsa yaparım ve her tarayıcıda çalışırım demek istiyorum.

Kötü Huylarım

Sana bu modelin birçok iyi özelliğini saydım ama kötü yanları da yok değil.

  • SignalR ile yakın ilişki içerisinde olduğumu ifade edeyim. Zaman zaman çok fazla konuşuruz. Mesela her sayfa örneklendiğinde ayrı bir SignalR bağlantısı tesis ederim. Takibi, bakımı ve ölçeklenebilirliği zordur. Azure tarafındaki SignalR yapısını kullanarak bu sorunu aşabilirsin ama bulut tabanlı dünyaya geçiş yapman gerekir ki orası da esasında ayrı bir uzmanlıktır.
  • Arabirimle etkileşim o kadar fazladırki ağ trafiği bazen İstanbul iş çıkış saati trafiğini aratmaz. Belki uygulamanın dağıtıldığı yere göre uygun sunucu konumlandırmaları yapabilirsin(Yani Avrupa kıtasından gelenler Almanya'daki bir sunucuya yönlensin, Amerika kıtasındakiler Kanada'daki bir sunucuya yönlensin tadında) ama bu da ne demek biliyorsun, bolca kiralama bedeli.
  • Bir diğer sorunumsa .Net çalışma zamanına olan bağımlılığım. Kardeşim Blazor WebAssembly gibi çalışacağım tarayıcıya kendi runtime motorumu indirip işe başlayamıyorum. Sunucuda benim için bir .Net çalışma zamanı kurulu olmak durumunda. 
  • Ne yazık ki çevrimdışı çalışma desteğim yok denecek kadar az. Hatta sürekli çevrim içi olmayı beklerim ki SignalR alt yapım sorunsuz çalışabilsin.
  • Sunucu çökerse ne mi olur? Düşünmek bile istemiyorum.

Çevremdeki dostlarım bu modeli genellikle aşağıdaki çizimle hatırlarlar. Pek çok kaynakta tıpkısını göreceksin(Document Object Model'i doğru yazmışım. Enteresan!)

Blazor WebAssembly ve Server modelleri genellikle performans açısından sıklıkla kıyaslanırlar. Bu konuda Telerik'ten David Grace'in güncel araştırma yazısını okumanı tavsiye ederim.

Gördüğün gibi desteklediğim iki modelin birbirlerine göre avantaj ve dezavantajları var. Duruma göre uygun olan modeli tercih etmek gerekiyor. Ancak bu işe ilk kez başlıyorsan ve Blazor'a merhaba demek istiyorsan kuracağın kulübe katılacak arkadaşlarının herhangi bir zaman diliminde gördükleri bir kitabın fotoğrafını koyup yorumlayabildikleri, puan verebildiği bir sistemi aşağıdaki topolojiye göre oluşturmayı deneyebilirsin.

Büyük resme baktığında şunları anlaman önemli. Çözümde her iki Blazor modeli de kullanılıyor. İstemci tarafında çevrimdışı olarak çalışabilen bir uygulaman var. Okumakta olduğun kitabın fotoğrafını çekebilir, hakkında bir şeyler yazabilir ve çevrimiçi olduğunda da Web API üstünden HTTP Post komutu ile bu bilgileri Backend uygulamasına gönderebilirsin. Backend tarafı bunu alıp istediğin Repository ortamında kalıcı olarak saklayacaktır. Entity Framework kullanırsan Repository bağımsız hareket etme şansın da var ama mecburi değilsin. İlişkisel bir veritabanı modeli seçebileceğin gibi NoSQL nimetlerinden yararlanabilirsin. Yorgun argın eve geldikten sonra da istersen bilgisayarındaki tarayıcıdan Web uygulamasını açar, arkadaşlarının eklediği kitapları Web API'den HTTP Get ile çekersin. Yapacağın veri odaklı güncellemeleri de HTTP Put ile yollayabilirsin. Hatta sen sayfada gezindiğin sırada bir arkadaşın Web Assembly uygulaması üstünden yeni bir kitap bilgisi eklerse SignalR mekanizması bu değişikliği Web üstünden bağlı tüm istemcilere de göndereceği için sen de değişiklikten anında haberdar olabileceksin. Ya da tam tersi sen sayfada gezindiğin sırada bir kitabın bilgisini değişitirirsen bu değişiklikten de diğer bağlı istemciler anında haberdar olacak. Gördüğün üzere SignalR sadece chat, anlık borsa veya stok hareketlerini takip etmek için kullanılan bir yapı değil.

Benimle Geliştirme Yaparken

Bu arada aklıma gelen birkaç noktayı daha ifade etmek istiyorum. Ben MVC(Model View Controller), MVP(Model View Presenter) ve MVVM(Model View ViewModel) kalıplarından farklı olarak genelde Vertical Slices Architecture yaklaşımını kullanmanı öneriyorum. Yani kodunu fonksiyonlara göre gruplamaktan ziyade, özellik(Feature) bazlı gruplamanı öneriyorum. Yanlış anlama, onları kullanamazsın demiyorum ancak bildiğin üzere ben bileşenleri(Components) etkin kullanan SPA modelini öncelikli olarak benimsiyorum. Bileşen odaklı bu sade yaklaşımım nedeniyle Vertical Slice Architecture'ın aşağıdaki kurgusu kullanmak için çok ideal. Her kutunun bir bileşen olduğuna dikkat et lütfen.

Sonuç Olarak

Esasında sonda anlattığım örnek senaryo farklı uygulama geliştirme çatıları veya programlama dilleri ile de yapılabiliyor. Buradaki avantaj çoğu zaman Javascript tarafından kaçan ve yıllardır .Net üzerinde geliştirme yapan birisinin C#'ın gücünü kullanmaya devam ettiği Blazor WebAssembly tarafı. Diğer yandan SignalR odaklı Server modelinin performansı da istemci-sunucu etkileşimi açısından bakıldığında diğer modellere göre daha iyi olabilir. Yine de kesin bir şey söylemek zor. Ancak şu bir gerçek ki büyük ihtimalle şirketinde kullanılan Asp.Net Web Forms kökenli bir uygulaman varsa onu modernize etmenin iyi yollarından birisi Blazor tarafına geçirmek olabilir. Bu arada Blazor WebAssembly alternatifi olan çalışmalar da var. Mesela Lara bunlardan birisi. Onu da kurcalaman da yarar var. Bir Nuget paketi uzağında.

Sözlerime burada son verirken önümüzdeki yılların sana ve tüm sevdiklerine sağlık getirmesini diliyorum.

Kaynaklar

A New Era of Productivity with Blazor, Ed Charbeneau,Code Magazine

Asp.Net Core 5 for Beginners, Andreas Helland, Vincent Maverick Durano, Jeffrey Chilberto, Ed Price, Packt Publishing

Software Architecture with C# 9.0 and .NET 5, Gabriel Baptista, Fancesco Abbruzzese, Packt Publishing

An Atypical ASP.NET Core 5 Design Patterns Guide, Carl-Hugo Marcotte, Packt Publishing

Effective Engine — Bir Uzay Macerası

$
0
0

Altunizade’nin bahar aylarında insanı epey dinlendiren yeşil yapraklı ağaçları ile çevrelenmiş caddesinin hemen sonunda, köprüye bağlanmadan iki yüz metre kadar öncesinde dört katlı bir bina vardır. Araba ile geçerken eğer kırmızı ışığa denk gelmediyseniz pek fark edilmez ama yürürken önündeki geniş kaldırımları ile dikkat çeker. Ana cadde tarafındaki yeşillikler binanın ilk katlarını gizemli bir şekilde saklar. Binanın bulunduğu adanın etrafı cadde ve ara sokaklarla çevrilidir. Bir sokağın karşısında yeni yapılmış hastane ile metro çıkışı, ana cadde tarafında ev yapımı tatlılarının kokusu ile insanı baştan çıkaran pastane, eczane, kuaför, camii ve minik bir park bulunur. Dört yola bakan diğer cadde tarafında ise eskiden karakol olan ama çok uzun zamandır kreş olarak işletilen bir komşu yer alır.

Bu binanın bende özel bir yeri vardır. Bir zamanlar Netron adıyla da bilinen yazılım eğitim kurumlarının genel merkeziydi. Şirket eğitimlerindeki başarısı ve iyi eğitmenleri ile adından söz ettirmiş bir kurumdu. 2005 yılının sonlarına doğru adım attığım eğitmenlik kariyeriminin başlangıç noktasıydı. Geniş, yemyeşil bir bahçesi ve Proxy isimli bir köpeği vardı. Her sabah o güzel bahçeye bakan camekanlı kafesinde taze simit ve poğaçalar olur, ücretsiz dağıtılırdı. İsteyen istediği kadar alabilirdi. Camekanlı kafeye giriş kapsının önündeki basamaklardan indiğinizde solunuzda kalan birde bilgisayar vardı. Şöyle CRT tüplü monitörü olan bir bilgisayar. Otururak kullanabildiğiniz değil de bir bar sandalyesi üstünde hafif rahatsız biçimde kullanabildiğiniz yavaş bir bilgisayar. Uzunca bir süre sınıflardaki birkaç eğitmen bilgisayarı dışında o binada internete bağlanabilinen tek bilgisayar O olmuştu. Şifresiz ve herkesin kullanımına açıktı. O zamanlar öğrencilerin ders sırasında veya arasında sınıftaki bilgisayarları kullanarak internete çıkmalarını pek istemezdik. Nitekim derste öğretilenler ile bir şeyleri çözebileceklerini umut eder, onları buna yönlendirmeye çalışırdık — Lakin bir defasında Ogame filosuna saldırdıkları için oldukça endişeli görünen bir öğrencim sebebiyle ders arasını birkaç dakika erken vermiştim.

O yıllarda eğitimler genellikle Microsoft’un sertifika sınavlarına göre şekillendirdiği müfredata uygun olarak verilirdi. Microsoft’un eğitimciler için hazırladığı eğitim dokümanları epey kallavi olurdu ve ön hazırlıkları bile zaman alırdı. Bazen bir ders saati için tüm haftasonumu heba ettiğim olurdu. özellikle kurumsal bir eğitim söz konusu ise şirketlerin deneyimli personelinden gelen acımasız soruları cevaplayabilmek adına ekstra efor sarf etmek gerekiyordu. Bu durum zamanla deneyim kazanan eğitmenler için sorun olmasa da yeni başlayan bir eğitmen ve onun ilk öğrencileri için bazı hazin sonuçlara sebebiyet verebilirdi. Derken sektörün ihtiyaçlarına bakıp kendi içeriklerimizi planlamaya başladık. Hatta çoğu zaman kendi eğitim materyallerimizi hazırladık. Keyifli ama bir o kadar da külfetli bir işti. Nitekim sahip olunan insan gücü düşünüldüğünde sürekli değişen yeniliklere adapte olmak ve materyalleri güncellemek başlı başına zor oluyordu. Sanki sadece yenilikleri takip edip bu materyalleri hazırlayacak ayrı bir ekip gerekliydi. Gerçi bu endişeler çok geride kaldı.

Profesyonel anlamda eğitmenliği bırakalı oldukça uzun bir zaman oldu. En azından on yıldan fazladır bir eğitim kurumunda eğitmen olarak görev almıyorum. Sadece son dönemlerde çalıştığım firmalar iç eğitmenlik programları kapsamında benden destek istediler. Elimden geldiğince yardımcı olmaya çalıştım/çalışıyorum. İşte 2021'in şeker bayramında eve kapandığımız vakitlerde böyle sıfırdan uzun soluklu bir eğitim vermem gerekse nasıl hazırlanırdım diye düşünmeye başladım.

öyle ya, artık eğitimlerin veriliş şekilleri ve eğitmenlerden beklentiler çok değişti. Artık sınıf eğitimlerinden ziyade çevrimiçi ulaştığımız ve önceden hazırlanıp kaydedilmiş eğitimler daha popüler görünüyor. Ekran görüntüsü kaydetmenin, akış olarak internet ortamına yüklemenin çok daha kolay olduğu bu zaman diliminde hafif hazırlıklar ile bir eğitimi sunmak daha kolay görünüyor — sizi sıkıştıran anlık soruların olmadığı bir ortam olması sebebiyle bireysel anlamda yeni nesil eğitmenleri ne kadar zorluyor bu da tarışılır tabii. Bununla birlikte uzun metrajlı içerikler algı ve odaklanma sürelerimize göre yerini mikro anlatımlara bırakıyor. Fiziki sınıf eğitimlerinde karşımızdakilerle beden dili kullanarak kurduğumuz sıcak ilişkiyi çevrimiçi ortamda sağlamak zor olduğundan, Icebreaker denen oyunlaştırılmış araçlar kullanılıyor — ki bana göre hiçbir şey gerçek anlamda görsel ve işitsel temas ile kurulan bağın ötesine geçemez.

Ancak her ne olursa olsun yazılım alanındaki bir eğitimin bazı temel prensipleri ve araçları değişmemelidir. Bu anlamda inandığım bazı ilkeler var.

  • Halen daha eğitmenin konuya olan hakimiyeti çevrimdışı bir eğitim bile olsa çok önemli. Bu hakimiyet öğrenciye sorulacak sorular için de zemin hazırlıyor. Yeri geldiğinde karşıda video kaydedici bile olsa düşündürücü bir soru yöneltip es vermek gerekiyor.
  • Kendi eğitmenlik zamanlarımda da önem arz eden bir diğer konu ise saha tecrübesi. Teorik bilgi birikimi ne kadar yüksek olursa olsun sahada karşılaşılan problemlerin verdiği tecrübe aktarımı bir başka oluyor. Nitekim yazılım eğitimlerinin en zor kısımlarından birisi soyutlaşan kavramların gerçek hayatla örtüştüğü noktaları karşı tarafa aktarabilmek — ki benim üstadlarım bana “gerekirse konuyu çöp adam kullanarak tahtaya çizip anlatmaya çalış” demişti. Tahtanın yerini şimdi Whiteboard ve dokunmatik ekranlar aldı belki ama prensip aynı; Basitleştirerek anlatmak.
  • Bir eğitim her şeyi eğitmenin yaptığı değil aksine öğrencinin de bir şeyler yaptığı şekilde olmalı. çünkü araştırma ve sorgulayarak cevap bulma kasları bilişim sektörü personeli için çok önemli. Bu nedenle eğitmenin bilhassa açık bıraktığı bazı noktaları keşfetmesi için öğrencilerine ödevler vermesi gerekiyor.
  • Görev addetmek mühim bir mesele olsa da onu takip etmek ve karşılıklı müzakere yollarını keşfederek önerilerde bulunmak çok daha önemli. öneride bulunup öğrencinin yerine yapmaya çalışmak ise iyi bir pratik değil.
  • Senaryolaştırmak, bazen konunun ne olduğuna bağlı olarak öncesinde bitmiş eseri gösterip sonra adım adım ilerletmek de kıymetli bir yaklaşım. Bazen yalın bir Hello World hiçbir şey ifade etmez ama senaryosu olan bir Hello World çok şey ifade edebilir.
  • Eğitime konu olan örneklerin bütünlüğü de kritik bir mesele. Pek çok uygulamalı kitabın ilk noktasından son noktasına gelindiğinde, anlatılan her şeyin kullanıldığı bir ürün ortaya çıkmış oluyor. Bu pratiği eğitimin kendisine yaymak kolay değil ve daha da önemlisi oldukça titiz bir hazırlık süreci gerektiriyor.

Düşünceme göre bir eğitime hazırlanmak gerçekten de kolay değil. Büyük sorumluluk, büyük mesuliyet, iyi hazırlık, iyi meziyet gerektirmekte. Bu vesile ile bende bir deneme yapmak istedim. öncesinde geçmiş yıllarımdan bir tecrübe birkaç anı kırıntısı bulmaya çalıştım. Havaların erken karardığı ve eğitimin akşam 19:00da başladığı bir Netron gününde anlattığım Xml Web Service konusu geldi aklıma. Ne ilginçtir ki o zamandan beri Microsoft’un birçok materyalindeki konsept değişmedi. O bir saatlik eğitime hazırlandığım resmi Microsoft eğitim dökümanının ilgili bölümünde bir hava durumu servisinin geliştirilmesi öğretilmekteydi.

önce bitmiş Web servis çalıştırılıp elde edilmesi beklenen sonuç gösteriliyor, sonrasında Request nesnesine ait XML’in hazırlanması öğretiliyor ve nihayetinde bu işin geliştirme ortamında nasıl yapılacağına değiniliyordu. Felsefesi gayet doğruydu. Bir Web Servis temel olarak ne işe yarar baştan görebiliyordunuz. Eğitim seviyesi sebebiyle iç dinamiklerinde XML ne anlama geliyor, bir SOAP talebi hangi parçalardan oluşuyor öğreniyordunuz. Ne elde edeceğim, nasıl çalışıyor terapisinden sonra pratiğe geçiliyor ve uygulama geliştiriliyordu. Bugünkü .Net 5 dünyasına — veya .Net Core tarafına baktığımızda da bir Web API servisi söz konusu ise benzer bir senaryo koşulduğunu biliyor olmalısınız. Artık bilinç altımıza işlemiş olan, her şablonda karşımıza çıkan WeatherForecast senaryosu.

Pek çok saygın kitapta veya çevrimiçi eğitimde olduğu gibi uzun vadedeki çözümler veri odaklı bir dünya üzerine inşa ediliyor. Ulaşılmak istenen nokta ister Blazor ister MVC olsun, ister Progressive Web App ister Mobil çözüm veya bir başkası olsun o büyük kitabın veya sekiz saatlik eğitimin başlarında bir yerlerde bir REST veya gRPC servisi söz konusu oluyor. üstelik bu servis pratik olması açısından genellikle In-Memory tabanlı, Docker ile kurgulanmış veya local formasyonda çalışan bir veritabanı kullanıyor. İşte eğitimcinin yaratıcılığı bu noktada başlıyor. Renkli bir senaryo kurgulamak, hikayenin bazı noktalarında öğrencinin isteklerini kabul ederek yeni şeyleri sürece katmak(Farklı entity nesneleri veya fonksiyonellikler gibi), In-Memory veritabanı ile başlatıp Docker ile diğer türlere geçişlerin yapılacağı ödevler vermek vs

Peki ya bunu nasıl yapacak? İşte bir eğitmenin bence sahip olması gereken en önemli özelliklerden birisi. Yazarak adım adım planlamak. Ben bu tip bir işe kalkışsam sanırım en büyük yardımcılarım Markdown formatındaki bir Readme dosyası ile kodları planlayarak tutabileceğim github benzeri bir kaynak deposu olurdu. Hatta o depoyu da belki branch’ler ile kurgulayarak önce şöyle, şimdi böyle, sonra da öyle gibi ifade etmek gerekirdi. örneğin…

Sonuç olarak aşağıdaki gibi amatör bir kurgu oluşturdum. Umarım şirket içi eğitmenlere yol gösterici olur.

Senaryo

Haftasonu sıkılan .Net geliştiricisi için eğlencelik bir Web API kodlaması düşündüm. Şirket içi eğitimlerde bir Web API’ye ihtiyaç duyduğumuz durumlar için güzel olabilir. Hani kobay bir Web API servisi olur ya hep, görsellik katılınca sükseli duran. İşte onun için güzel bir senaryo olabileceğini düşünüyorum. Senaryoyu aşağıdaki gibi çizmeye çalıştım.

Gelecekte geçen bir zaman diliminde galaksinin uzak diyarlarını keşfetmek üzere Uzay Yolu’nu izlemiş mürettabattan oluşan gemiler vardır. Güneşin ve ayın konumuzla bir alakası yok ama kompozisyonu tamamlarlar diye düşünüp resme dahil ettim. Bir uzay gemisi(Spaceship) içinde en az 2 en fazla 7 mürettebat(Voyager) olabilir. Mürettebat görev kontrolün(MissionControl) uygun gördüğü gemiyle bir göreve(Mission) çıkar. Her görev tek bir gemiyle ilişkilendirilir ama itirazınız varsa bunu çoklayabiliriz de. Görevin başlatılması için bir adının olması, kendilerine has takma isimleri olan mürettebatın bulunması, görev süresi verilmesi(En az 12 en fazla 24 ay), bir gemiyle görevin ilişkilendirilmesi yeterlidir. Senaryoyu birlikte genişletebiliriz ama varsayılan hali aşağıdaki gibidir.— Burası öğrencilere senaryonun anlatıldığı kısım. Eğlenceli olmalı, ilgi çekmeli, hatta eğitmen bunu canlı olarak çizerek anlatmalıdır.

0 — Başlangıç

Solution ve projenin ilk aşamasıdır. Giriş kısmı olduğu için sarf edilen sözler önemlidir. Neden bir Class Library açarak başladık ve ona neden EntityFrameworkCore diye bir paketi ekledik anlatmamız gerekir.

# Bir Solution oluşturdum
dotnet new sln -o GalaxyExplorer

# Sonra Voyager, Spaceship ve Mission olarak adlandırdığım nesneler için Entity ile DbContext'in duracağı bir class library oluşturup solution'a ekledim.
cd GalaxyExplorer
dotnet new classlib -o GalaxyExplorer.Entity
dotnet sln add .\GalaxyExplorer.Entity\GalaxyExplorer.Entity.csproj

# EntityFrameworkCore kullanacağım için birde gerekli paketi ekledim
cd GalaxyExplorer.Entity
dotnet add package Microsoft.EntityFrameworkCore -v 5.0.6

1 — Entity Sınıflarının İnşası

Uzay gemilerini Spaceship sınıfı ile işaret edeceğiz. Adı ve ışık yılı olarak gidebileceği mesafeyi taşıması yeterli.

namespace GalaxyExplorer.Entity
{
    public class Spaceship
    {
        public int SpaceshipId { get; set; }
        public string Name { get; set; }
        public double Range { get; set; }
        public bool OnMission { get; set; }
        public int MaxCrewCount { get; set; }
    }
}

Mürettebatı ise Voyager olarak tanımlayabiliriz. Şimdilik aşağıdaki gibi kullanacağız. Kaşifin adı, rütbesi, ilk görev tarihi, aktif olup olmadığı bilgileri olsun yeterli.

using System;

namespace GalaxyExplorer.Entity
{
    public class Voyager
    {
        public int VoyagerId { get; set; }
        public string Name { get; set; }
        public string Grade { get; set; }
        public DateTime FirstMissionDate { get; set; }
        public int MissionId { get; set; }
        public bool OnMission { get; set; }
    }
}

Bir görev söz konusu. Bunu Mission sınıfı ile temsil edebiliriz. Bir görev bir gemiyle ilişkili olmalıdır diye ifade etmiştik. Ayrıca bir göreve birden fazla mürettebat da dahil olabilmelidir. Bu düşünceleri resmeden bir sınıfı aşağıdaki gibi yazabiliriz.

using System;
using System.Collections.Generic;

namespace GalaxyExplorer.Entity
{
    public class Mission
    {
        public int MissionId { get; set; }
        public int SpaceshipId { get; set; }
        public string Name { get; set; }
        public int PlannedDuration { get; set; }
        public DateTime StartDate { get; set; }
        public IEnumerable<Voyager> Voyagers { get; set; }
    }
}

“Neden bu entity sınıflarını inşa ediyoruz?” diye sormalı karşılıklı görüş almalıyız.

2 — DbContext Sınıfının Yazılması

Senaryomuzda hangi veritabanını kullanacağımıza henüz karar vermedik lakin Entity Framework Core’dan yararlanmaktayız. Code First modeli ile ilerliyoruz ama Model First ve Database First şeklinde farklı versiyonlar olduğunu da hatırlayalım. Şu anda Domain’e ait tipleri tasarlayıp sonrasında veritabanına geçeceğiz. İlerleyen derslerde isteyen istediği veritabanı ile çalışabilir olacak(Uygun olan veritabanı tabii) Bu amaçla GalaxyExplorerDbContext sınıfını aşağıdaki gibi yazarak devam edelim. İçinde kullanıma hazır uzay gemileri de var — Burada öğrencilerden de uzay gemisi adları alabiliriz. Hayal güçlerini kullanmaları her zaman etkileşimi yükseltir

using Microsoft.EntityFrameworkCore;

namespace GalaxyExplorer.Entity
{
    public class GalaxyExplorerDbContext
        : DbContext
    {
        public GalaxyExplorerDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Spaceship> Spaceships { get; set; }
        public DbSet<Voyager> Voyagers { get; set; }
        public DbSet<Mission> Missions { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Mission>().HasMany(m => m.Voyagers).WithOne();

            modelBuilder.Entity<Spaceship>().HasData(
                new Spaceship
                {
                    SpaceshipId=1,
                    Name = "Saturn IV Rocket",
                    OnMission = false,
                    Range = 1.2,
                    MaxCrewCount=2
                },
                new Spaceship
                {
                    SpaceshipId = 2,
                    Name = "Pathfinder",
                    OnMission = true,
                    Range = 2.6,
                    MaxCrewCount = 5
                },
                new Spaceship
                {
                    SpaceshipId = 3,
                    Name = "Event Horizon",
                    OnMission = false,
                    Range = 9.9,
                    MaxCrewCount = 3
                },
                new Spaceship
                {
                    SpaceshipId = 4,
                    Name = "Captain Marvel",
                    OnMission = false,
                    Range = 3.14,
                    MaxCrewCount = 7
                },
                new Spaceship
                {
                    SpaceshipId = 5,
                    Name = "Lucky Tortiinn",
                    OnMission = false,
                    Range = 7.7,
                    MaxCrewCount = 7
                },
                new Spaceship
                {
                    SpaceshipId = 6,
                    Name = "Battle Master",
                    OnMission = false,
                    Range = 10,
                    MaxCrewCount = 5
                },
                new Spaceship
                {
                    SpaceshipId = 7,
                    Name = "Zerash Guidah",
                    OnMission = true,
                    Range = 3.35,
                    MaxCrewCount = 3
                },
                new Spaceship
                {
                    SpaceshipId = 8,
                    Name = "Ayran Hayd",
                    OnMission = false,
                    Range = 5.1,
                    MaxCrewCount = 4
                },
                new Spaceship
                {
                    SpaceshipId = 9,
                    Name = "Nebukadnezar",
                    OnMission = false,
                    Range = 9,
                    MaxCrewCount = 7
                },
                new Spaceship
                {
                    SpaceshipId = 10,
                    Name = "Sifiyus Alpha Siera",
                    OnMission = false,
                    Range = 7.7,
                    MaxCrewCount = 7
                }
            );
        }
    }
}

3 — DTO Tipleri için Bir Kütüphane Oluşturulması

Görev kontrol tarafına ilk etapta sadece bir başlatma emri gelsin istiyoruz. Görevin adı, katılacak mürettebatın isimleri gibi az sayıda bilgi yeterli olabilir. Entity türlerini doğrudan API üzerinden açmak yerine bir ViewModel vasıtasıyla sadece aksiyona özgü değişkenlerle sunmak niyetindeyiz. O yüzden Data Transfer Object olarak düşünülebilecek sınıfları kullanacağız— “DTO’lar yazılım dünyasının hangi noktasında karşımıza çıkarlar? Bu senaryoda ki kullanım amaçları dışında bir rolleri olabilir mi?” şeklinde sorular sorup müzakere etmek gerekiyor.

# DTO Projesini açtım
dotnet new classlib -o GalaxyExplorer.DTO

# ve Solution'a ekledim
dotnet sln add .\GalaxyExplorer.DTO\GalaxyExplorer.DTO.csproj

Sonrasında yeni bir görev başlatmak için kullanacağımız aşağıdaki DTO sınıflarını ekleyerek devam edelim.

Göreve katılacak mürettebat için VoyageRequest sınıfı.

using System.ComponentModel.DataAnnotations;

namespace GalaxyExplorer.DTO
{
    public class VoyagerRequest
    {
        [Required]
        [MinLength(3)]
        [MaxLength(25)]
        public string Name { get; set; }
        [Required]
        public string Grade { get; set; }
    }
}

Görevin kendisi içinse MissionStartRequest sınıfı. En az iki en fazla yedi mürettebat katılabilen görevlerden bahsetmiştik. Gemi ataması ise havuzdaki müsait olanlardan yapılmalı. Bu yüzden görev gemisi ile ilgili bir bilgi eklemedik. Bu noktada da fark edeceğiniz üzere bir görevi başlatmak için ihtiyaç duyulan veri modeli ile Entity tam olarak örtüşmüyor. İşte Data Transfer Object için bir başka bahane. — Bu noktada gerçekten doğru bir şeyler söylüyor muyum diye sorgulatmak lazım. öğrencilerle tartışılması gereken bir konu daha.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace GalaxyExplorer.DTO
{
    public class MissionStartRequest
    {
        [Required]
        [MinLength(10)]
        [MaxLength(50)]
        public string Name { get; set; }
        [Required]
        [Range(12,24)] // En az 12 en fazla 24 aylık görev olabilir
        public int PlannedDuration { get; set; }
        [Required]
        [MinLength(2)]
        [MaxLength(7)] //Minimum 2 maksimum 7 mürettebat olsun diye
        public List<VoyagerRequest> Voyagers { get; set; }
    }
}

Görevi başlatma sırasında oluşacak hatalar ile ilgili ayrı bir dönüş tipi kullanmak yararlı olabilir. Bunu sağlamak için MissionStartResponse sınıfını ekleyebiliriz. — Bu noktada “Servis portlarının girdi ve çıktı mesajlarında bir standart kullnamak gerekir mi?” sorusunu sorup tartışabiliriz.

namespace GalaxyExplorer.DTO
{
    public class MissionStartResponse
    {
        public bool Success { get; set; }
        public string Message { get; set; }
    }
}

Başka ne tür validasyon nitelikleri kullanılabilir, araştırmalarını söyleyebiliriz. Tabi söylemek yetmez takibini de yapmamız gerekir. Bir sonraki ders kısa bir tekrar sonrası sorulan sorular üstünde tartışmak verimli bir öğrenim süreci sağlar.

4 — Servis Bileşenleri için Kütüphane Eklenmesi

Web API haricinde buradaki kurguyu farklı bir ortamda da kullanmak isteyebiliriz. Controller tipinin kullanacağı Entity Framework işlerini başka bir kütüphanede toplayacak şekilde proje bazında soyutlasak güzel olabilir. Hatta servisleştirirsek çok daha iyi olur. Böylece Dependency Injection çatısını kullanarak asıl ürüne eklememiz de kolay olur. önce bir kütüphane oluşturalım ve gerekli projeleri referans edelim— Dependency Injection. Hassas, çok hassas bir konu. Burada gerekirse uzun süreli es verip karşılıklı konuşmak, önceki derslerde anlatılan kısımlara refernas ederek yönlendirmek gerekebilir.

# Projeyi oluştur
dotnet new classlib -o GalaxyExplorer.Service
# Solution'a ekle
dotnet sln add .\GalaxyExplorer.Service\GalaxyExplorer.Service.csproj
# Proje içine gir
cd .\GalaxyExplorer.Service
# DTO projesini referans et
dotnet add reference ..\GalaxyExplorer.DTO\GalaxyExplorer.DTO.csproj

# DbContext'e ihtiyacım olacak.
dotnet add reference ..\GalaxyExplorer.Entity\GalaxyExplorer.Entity.csproj

önce soyutlamayı sağlayacak arayüz tipini ekleyelim.

using GalaxyExplorer.DTO;
using System.Threading.Tasks;

namespace GalaxyExplorer.Service
{
    public interface IMissionService
    {
        Task<MissionStartResponse> StartMissionAsync(MissionStartRequest request);
    }
}

Sonra asıl işi yapan sınıfı(Concrete Class) yazalım.

using GalaxyExplorer.DTO;
using GalaxyExplorer.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace GalaxyExplorer.Service
{
    public class MissionService
        : IMissionService
    {
        private readonly GalaxyExplorerDbContext _dbContext;
        // Servisi kullanan uygulamanın DI Container Service Registery'si üzerinden gelecektir.
        // O anki opsiyonları ile birlikte gelir. SQL olur, Postgresql olur, Mongo olur bilemiyorum.
        // Entity modelin uygun düşen bir DbContext gelecektir.
        public MissionService(GalaxyExplorerDbContext dbContext)
        {
            _dbContext = dbContext;
        }
        public async Task<MissionStartResponse> StartMissionAsync(MissionStartRequest request)
        {
            using var transaction = await _dbContext.Database.BeginTransactionAsync(); // Transaction başlatalım
            try
            {
                // Mürettebat sayısı uygun olup aktif görevde olmayan bir gemi bulmalıyız. Aday havuzunu çekelim.
                var crewCount = request.Voyagers.Count;
                var candidates = _dbContext.Spaceships.Where(s => s.MaxCrewCount >= crewCount && s.OnMission == false).ToList();
                if (candidates.Count > 0)
                {
                    Random rnd = new();
                    var candidateId = rnd.Next(0, candidates.Count);
                    var ship = candidates[candidateId]; // Index değerine göre rastgele bir tanesini alalım

                    ship.OnMission = true;
                    await _dbContext.SaveChangesAsync(); // Gemiyi görevde durumuna alalım

                    // Görev nesnesini oluşturalım
                    Mission mission = new Mission
                    {
                        Name = request.Name,
                        PlannedDuration = request.PlannedDuration,
                        SpaceshipId = ship.SpaceshipId, // Gemi ile ilişkilendirdik
                        StartDate = DateTime.Now
                    };
                    await _dbContext.Missions.AddAsync(mission);
                    await _dbContext.SaveChangesAsync(); // Görev nesnesini db'ye yollayalım

                    // Gelen gezginlerin listesini dolaşıp
                    var voyagers = new List<Voyager>();
                    foreach (var v in request.Voyagers)
                    {
                        Voyager voyager = new Voyager // Her biri için bir Voyager nesnesi örnekleyelim
                        {
                            Name = v.Name,
                            Grade = v.Grade,
                            OnMission = true,
                            MissionId = mission.MissionId // Görevle ilişkilendirdik
                        };
                        voyagers.Add(voyager);
                    }
                    await _dbContext.Voyagers.AddRangeAsync(voyagers); // Bunları topluca Voyagers listesine ekleyelim
                    await _dbContext.SaveChangesAsync(); // Değişiklikleri kaydedelim.
                    await transaction.CommitAsync(); // Transaction'ı commit edelim

                    return new MissionStartResponse
                    {
                        Success = true,
                        Message = "Görev başlatıldı."
                    };
                }
                else // Müsait veya uygun gemi yoksa burda durmamızın anlamı yok
                {
                    await transaction.RollbackAsync();

                    return new MissionStartResponse
                    {
                        Success = false,
                        Message = "Şu anda görev için müsait gemi yok"
                    };
                }                
            }
            catch (Exception exp)
            {
                await transaction.RollbackAsync();
                return new MissionStartResponse
                {
                    Success = false,
                    Message = $"Sistem Hatası:{exp.Message}"
                };
            }
        }
    }
}

Yazılan servis kodundan çeşitli sorular sorulabilir. örneğin hangi tür injection tekniği kullanılmaktadır, başka ne türleri vardır, veritabanı belli midir, belli ise bağlantı bilgisi nerededir, transaction açılmasının sebebi nedir, temel transaction ilkeleri nelerdir vb. Buradan yola çıkarak “BASE’i duymuş muydunuz?” diye bir soru sorulabilir ve NoSQL ilkelerine geçilip dağıtık sistemler için önem arz eden CAP teoremine atıfta bulunulabilinir. Detaylar ders harici zamanlarda merak edenlerle konuşulur veya araştırma ödevi olarak atanır.

5 — Sırada Controller var. Yani Web API’nin İnşası

önce projeyi oluşturup gerekli paketleri ve proje referanslarını aşağıdaki gibi ekleyelim.

# Web API projesini oluştur
dotnet new webapi -o GalaxyExplorer.API
# Solution'a ekle
dotnet sln add .\GalaxyExplorer.API\GalaxyExplorer.API.csproj
# Proje klasörüne geç
cd .\GalaxyExplorer.API
# EntityFrameworkCore paketini ekle
dotnet add package Microsoft.EntityFrameworkCore -v 5.0.6
# Local SQL kullanmak istedim. Onun paketini ekle
dotnet add package Microsoft.EntityFrameworkCore.SqlServer -v 5.0.6
# Migration için gerekli olacak paket
dotnet add package Microsoft.EntityFrameworkCore.Design -v 5.0.6

# WeatherForecast* tiplerini sildim

# Service ve DTO projelerini referasn ettim
dotnet add reference ..\GalaxyExplorer.Service\GalaxyExplorer.Service.csproj
dotnet add reference ..\GalaxyExplorer.DTO\GalaxyExplorer.DTO.csproj
dotnet add reference ..\GalaxyExplorer.Entity\GalaxyExplorer.Entity.csproj

Startup.cs içerisindeki ConfigureServices metodunu da takip eden kod parçasında olduğu gibi düzenleyelim.

public void ConfigureServices(IServiceCollection services)
{
    // DI serivslerine DbContext türevini ekliyoruz. 
    services.AddDbContext<GalaxyExplorerDbContext>(options =>
    {
        // SQL Server baz alınacak ve appsettings.json'dan GalaxyDbConnStr ile belirtilen bağlantı bilgisi kullanılacak.
        options.UseSqlServer(Configuration.GetConnectionString("GalaxyDbConnStr"), b => b.MigrationsAssembly("GalaxyExplorer.API"));
    });
    services.AddControllers();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "GalaxyExplorer.API", Version = "v1" });
    });
}

Bu senaryo özelinde makinelerimizde de hazır olması sebebiyle Local SQL Server’ı kullanmayı tercih edebiliriz. Gerekli ConnectionString bilgisini AppSettings.json dosyasına aşağıdaki gibi eklemek gerekir — Sınıfı katılımcı sayısına göre gruplara bölüp farklı veritabanı ile çalışmalarını da sağlatabiliriz. Postgresql’in Docker Container kullanan bir versiyonu ideal çözüm olabilir.

"ConnectionStrings": {
      "GalaxyDbConnStr": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=GalaxyExplorer;Integrated Security=True"
    }

Ardından projeye MissionController isimli bir Controller sınıfını ekleyelim.

using GalaxyExplorer.DTO;
using GalaxyExplorer.Service;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace GalaxyExplorer.API.Controller
{
    [Route("api/[controller]")]
    [ApiController]
    public class MissionController : ControllerBase
    {
        // DI Container'a kayıtlı IMissionService uyarlaması kimse o gelecek
        private readonly IMissionService _missionService;
        public MissionController(IMissionService missionService)
        {
            _missionService = missionService;
        }
        [HttpPost]
        public async Task<IActionResult> StartAsync([FromBody] MissionStartRequest request) // JSON Body'den request nesnesini alsın
        {
            if (!ModelState.IsValid)
                return BadRequest(); // Model validasyon kurallarında ihlal olursa

            // Servis metodunu çağıralım
            var startResult = await _missionService.StartMissionAsync(request);
            if (startResult.Success) // Sonuç başarılı ise HTTP OK
                return Ok(startResult.Message);
            else
                return BadRequest(startResult.Message); // Değilse HTTP Bad Request
        }
    }
}

Controller sınıfının IMissionService implementasyonunu kullanabilmesi için Startup dosyasında yer alan DI servislerine gerekli bildirimi yapmayı da ihmal etmemek lazım. — Kullanılan Constructor Injection tekniğine göre bu Controller’a talep geldiğinde hazır edilecek bileşenin nerede bildirilmesi gerektiğini önce sınıfa soralım, sonrasında biz gösterelim.

services.AddTransient<IMissionService, MissionService>();

Artık bir şeyleri elle tutulur şekilde gösterebilmek de gerekiyor. Bunun için veri tabanının oluşması lazım. Dolayısıyla mevzu Migration. Migration işlemleri için dotnet ef aracını kullanabiliriz ancak öğrencilerin sisteminde bu kurulu olmayabilir. Bu gibi durumlarda sorun yaşamamak ve öğrenciyi kaybetmemek adında “Eğitime Gelmeden önce Makinenizde Yapmanız Gerekenler” tadında basit bir kılavuz hazırlayıp paylaşmak iyi olabilir. Biz aşağıdaki gibi ilerleyerek devam edelim.

# Tool kurulumu için
dotnet tool install --global dotnet-ef
# tool'u güncellemek için
dotnet tool update --global dotnet-ef
# tool'u projede kullanmak için
dotnet add package Microsoft.EntityFrameworkCore.Design
# kurulduğunu görmek için
dotnet ef

# Aşağıdaki komutları Web API projesi içinde çalıştırdım.
dotnet ef migrations add Initial -o Db/Migrations
dotnet ef database update

Tam bu noktada SQL tarafına geçip bir veri tabanı oluştuğundan ve hatta Spaceship tablosuna örnek verilerin dolduğundan emin olmak lazım. Diğer yandan Local SQL yerine Docker’dan yararlanarak popüler bir başka veritabanını basitçe kullanabileceğimizi de belirtmemiz önemli. Şuradaki gibi diyerek referans da gösterebiliriz.

6 — öncü Testler

Artık testlere başlanabilir. Şükür ki Swagger gibi yapılar artık proje şablonlarına entegre edilmiş şekilde geliyorlar. Dolayısıyla örneğimizde Web API’yi doğrudan çalıştırınca aşağıdaki şık arayüzle karşılaşmamız gerekir. Dolayısıyla ilk testleri yapmak oldukça kolay olur. Eskiden buralar dutluktu.

örnek bir JSON içeriğini aşağıdaki gibi uygulayabiliriz.

{
  "name": "Ufuk ötesi Macerası",
  "plannedDuration": 18,
  "voyagers": [
    {
      "name": "Kaptan Tupolev",
      "grade": "Yüzbaşı"
    },
    {
      "name": "Melani Garbo",
      "grade": "Bilim Subayı"
    },
    {
      "name": "Dursun Durmaz",
      "grade": "Seyrüseferci"
    }
  ]
}

Gerekirse diye bir Curl komutu da verebiliriz — Her platformu düşünmemiz lazım.

curl -X POST "https://localhost:44306/api/Mission" -H  "accept: */*" -H  "Content-Type: application/json" -d "{\"name\":\"Ufuk ötesi Macerası\",\"plannedDuration\":18,\"voyagers\":[{\"name\":\"Kaptan Tupolev\",\"grade\":\"Yüzbaşı\"},{\"name\":\"Melani Garbo\",\"grade\":\"Bilim Subayı\"},{\"name\":\"Dursun Durmaz\",\"grade\":\"Seyrüseferci\"}]}"

Bu örnek JSON talebi sonrası elde edilen sonuçlar da istediğimiz gibi olmalıdır— öğrencilerin elde ettiği sonuçları da gözlemlemek gerekir.

Doğrulama ifadelerinin işe yarayıp yaramadığını görmek içinse aşağıdaki gibi bir JSON talebi kullandırabiliriz. Mümkün mertebe her tür testi göstermemiz yararlı olabilir.

{
  "name": " ",
  "plannedDuration": 10,
  "voyagers": [
    {
      "name": "The Choosen One",
      "grade": "Hacker"
    }
  ]
}

Buna göre şöyle bir çıktı elde etmemiz gerekir. İşler yolunda gitmekte.

Bu andan itibaren başka ne gibi fonksiyonelliklere ihtiyacımız olabilir diye tartışmaya açmak lazım. Düşünülen yeni fonksiyonellikleri öğrencilerin uygulaması istenebilir. öncü olması açısından da “Ek Geliştirmeler” başlığı altındaki adımlar paylaştırılabilir.

7 — Ek Geliştirmeler

Temel senaryo aslında tamam ancak…

Gezginler zaman içerisinde sayıca artacaktır. Genelde bu tip senaryolarda HTTP Get ile çağırılan fonksiyonlar tüm listeyi döndürür. En azından basite kaçtığımız senaryolarda böyledir. Ancak satır sayısı fazla ise servisten her şeyi döndürmek iyi bir pratik olmayabilir. Bunun yerine kriter bazlı veri döndürmek daha iyi olur. örneğin aktif görevde olan veya olmayanların listesini çekmek. Bu bile fazla sayıda satır dönmesine sebebiyet verebilir. Ağ trafiği ve servislerin cevap verebilirlik süreleri her zaman kritiktir. Şimdi olmasa bile kullanıcı sayısı arttığında önem arz edecektir. Dolayısıyla sayfalama kriteri eklemek iyi bir çözüm olabilir. Bu sebeple Response ve Request için bazı DTO tiplerini aşağıdaki gibi tasarlayabiliriz. — Gerçek hayat senaryolarından dem vurarak bazı öğütlerde bulunmamız oldukça elzem.

Controller tipinin ilgili metoduna gelecek talep için aşağıdaki sınıfı tasarlayarak devam edelim. Kaçıncı sayfadan itibaren kaç satır alınacağını belirttiğimiz basit bir kurgu var. Ek olarak görevde olup olmama durumunu taşıdığımız boolean bir özellik bulunuyor.

using System.ComponentModel.DataAnnotations;

namespace GalaxyExplorer.DTO
{
    public class GetVoyagersRequest
    {
        [Required]
        public int PageNumber { get; set; }
        [Required]
        [Range(5,20)] // Sayfa başına minimum 5 maksimum 20 satır kabul edelim
        public int PageSize { get; set; }
        public bool OnMission { get; set; }
    }
}

API metodunun dönüşünü ise aşağıdaki gibi geliştirelim. Toplam gezgin sayısı, aktif görevdeki gezgin sayısı, istenen sayfa listesi ve sonraki sayfaya geçiş için yardımcı bağlantı bilgisini döndürmeyi düşünebiliriz. Sayfalama yapılan servislerde önceki ve sonraki bölümlere geçişi kolaylaştıran referans linkleri paylaşmak standart bir pratiktir. — önceki sayfa linkinin eklenmesi, gidilecek sayfa kalmaması halinde alınması gereken önlem veya yapılması gereken işin ne olduğu öğrencilere görev olarak verilebilir.

using System.Collections.Generic;

namespace GalaxyExplorer.DTO
{
    public class GetVoyagersResponse
    {
        public int TotalVoyagers { get; set; }
        public int TotalActiveVoyagers { get; set; }
        public List<VoyagerResponse> Voyagers { get; set; }
        public string NextPage { get; set; }
    }
}

Bu response tipinde kullanılan liste elemanını ise aşağıdaki gibi ekleyelim. Gezginin adı ve rütbesi dışında hakkında detaylı bilgi almak için Detail isimli bir özellik de bulunmakta.— Detay kısmında büyük ihtimalle ID kullanılması gerekecektir. Bunu söylemeden Detail kısmını nasıl oluşturmamız gerektiği öğrenciler ile karşılıklı olarak tartışılabilinir.

namespace GalaxyExplorer.DTO
{
    public class VoyagerResponse
    {
        public string Name { get; set; }
        public string Grade { get; set; }
        public string Detail { get; set; }
    }
}

Sonrasında Servis arayüzüne yeni fonksiyon bildirimini eklememiz gerekir.

Task<GetVoyagersResponse> GetVoyagers(GetVoyagersRequest request);

Pek tabii eklenen yeni operasyonun MissionService üzerinde uygulanması gerekir. Bunu aşağıdaki şekilde yapabiliriz.

public async Task<GetVoyagersResponse> GetVoyagers(GetVoyagersRequest request)
{
    var currentStartRow = (request.PageNumber - 1) * request.PageSize;
    var response = new GetVoyagersResponse
    {
        // Kolaylık olsun diye sonraki sayfa için de bir link bıraktım
        // Lakin başka kayıt yoksa birinci sayfaya da döndürebiliriz
        NextPage = $"api/voyager?PageNumber={request.PageNumber + 1}&PageSize={request.PageSize}&OnMission={request.OnMission}", 
        TotalVoyagers = await _dbContext.Voyagers.CountAsync(),
        TotalActiveVoyagers = await _dbContext.Voyagers.CountAsync(v => v.OnMission == true)
    };

    var voyagers = await _dbContext.Voyagers
        .Where(v => v.OnMission == request.OnMission)
        .Skip(currentStartRow)
        .Take(request.PageSize)
        .Select(v => new VoyagerResponse
        {
            Name = v.Name,
            Grade = v.Grade,
            Detail = $"api/voyager/{v.VoyagerId}" // Bu Voyager'ın detaylarını görmek için bir sayfaya gitmek isterse diye
        })
        .ToListAsync();
    response.Voyagers = voyagers;

    return response;
}

Bu yeni fonksiyonu kullanabilmek için Controller tarafına da müdahale etmek gerekir. Voyager ile ilgili bir işlem söz konusu olduğundan VoyagerController isimli yeni bir Controller tipi eklemek çok daha doğrudur.

using GalaxyExplorer.DTO;
using GalaxyExplorer.Service;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace GalaxyExplorer.API.Controller
{
    [Route("api/[controller]")]
    [ApiController]
    public class VoyagerController : ControllerBase
    {
        // DI Container'a kayıtlı IMissionService uyarlaması kimse o gelecek
        private readonly IMissionService _missionService;
        public VoyagerController(IMissionService missionService)
        {
            _missionService = missionService;
        }
        [HttpGet]
        public async Task<IActionResult> GetVoyagers([FromQuery] GetVoyagersRequest request) // Parametreleri QueryString üzerinden almayı tercih ettim
        {
            var voyagers = await _missionService.GetVoyagers(request);
            return Ok(voyagers);
        }
    }
}

Burada biraz durup tartışma başlatmak da gerekiyor. İdeal bir Controller dağılımı söz konusu gibi. Voyager ile ilgili operasyonları VoyagerController, Mission ile ilgili operasyonları MissionController üstleniyor. Açıkta bıraktığımız nokta her ikisinin IMissionService türevli bileşenleri kullanması. İdeal bir tasarımda IVoyagerService de söz konusu olmalıdır. Lakin bunun bir soru olarak gelmesini beklemeliyiz. Gelmezse “Sizce ideal bir tasarım oldu mu?” şeklinde sorup öğrencileri bu noktaya çekmeliyiz.

Uygulamayı tekrar çalıştırıp başka görevler de başlattıktan sonra Get metodunu yine Swagger arabirimi üzerinden test etmemiz gerekir.

# curl ile test etmek isterseniz
curl -X GET "https://localhost:44306/api/Voyager?PageNumber=1&PageSize=5&OnMission=true" -H  "accept: */*

Aşağıdakine benzer bir çıktı alabilmeliyiz.

{
  "totalVoyagers": 14,
  "totalActiveVoyagers": 11,
  "voyagers": [
    {
      "name": "Kaptan Tupolev",
      "grade": "Yüzbaşı",
      "detail": "api/voyager/1"
    },
    {
      "name": "Melani Garbo",
      "grade": "Bilim Subayı",
      "detail": "api/voyager/2"
    },
    {
      "name": "Di Ays Men",
      "grade": "İkinci Pilot",
      "detail": "api/voyager/4"
    },
    {
      "name": "Healseying",
      "grade": "Sağlık Subayı",
      "detail": "api/voyager/6"
    },
    {
      "name": "Kaptan Fasma",
      "grade": "Tugay Komutanı",
      "detail": "api/voyager/7"
    }
  ],
  "nextPage": "api/voyager?PageNumber=2&PageSize=5&OnMission=True"
}

Tabi sonraki sayfayı da nextPage ile gelen url bilgisini kullanarak denememiz lazım ki işe yarayıp yaramadığını görelim.

Buraya kadar öğrenciler başarılı bir şekilde gelebiliyse harika! Eğlenceli sayılabilecek ama açık noktaları da olan bir senaryo üstünden iki temel fonksiyon kullanmış olduk. Biraz Dependency Injection, biraz Entity Framework, biraz LINQ, biraz asenkron operasyon kullanımı, biraz migration işleri, biraz Swagger farkındalığı vs… Bu kazanımları “Aklınızda neler kaldı?” diyerek öğrencilere anlattırmak gerekiyor. Sorular da alındıktan sonra onlara bazı ödevler vermek şart.

öğrenciye Neler Yaptırılabilir?

  • Voyager listesinden herbir gezginin şu ana kadar katıldığı toplam görev sayısını döndürebiliriz.
  • Voyager listesinden dönen Detail özelliğinin karşılığı olan Controller metodunu tamamlayabiliriz.
  • Aktif görevler ve bu görevlerdeki gezginlerin listesini döndürecek bir fonksiyon ekletebiliriz.
  • VoyagerController için MissionService yerine başka bir soyutlama yaptırabiliriz (IVoyagerService ve VoyagerService gibi)
  • Tamamlanan görevle ilgili güncellemeri yapacak bir PUT fonksiyonu dahil ettirilebiliriz. Bu, ilgili görevin durumunu tamamlandıya çekip, göreve katılan mürettebatı yeni görev almaya uygun olarak işaretleyen bir fonksiyon olabilir. Eksik Entity alanları varsa onların fark edilmesi ve yeni bir Migration planı hazırlanıp çalıştırılmasını isteyebiliriz.
  • ve öğrencilerin aklına gelen diğer ekler.

Görüldüğü üzere çok sık yazılan, anlatılan, öğretilen bir konu için hazırlık yapmak önemli bir efor ve çaba gerektiriyor. üstelik varılan sonuçların tutarlı olması ve ortak stadartlar üzerinde durması da önemli.

Faydalı olması ve ilham vermesi dileğiyle…

 

Monolitik Uygulamalarda Teknik Borçlanma ile Mücadele (Teori)

$
0
0

İş hayatına adım attığımda tarihler 1999 yılını gösteriyordu. Bilgi İşlem Sorumlusu unvanı ile yarı zamanlı başladığım şirkette bir çağrı merkezi uygulamasının geliştirilmesinden, bilgisayarların kurulumlarından ve kullanıcı destek işlemlerinden sorumluydum. O zamanlar sahip olduğum bilgiler çok kıt ve tamamen programlama üzerindeydi. Ne katmanlı mimarilerden ne de tasarım kalıplarından bihaberdim. Hal böyle olunca yazdığım uygulama buton arkası kodlamanın ötesine geçemiyordu. Üstelik web tabanlı değil Windows tabanlı bir programdı ve dağıtımı çağrı merkezi bilgisayarlarına kopyala yapıştır usulüne göre yapılıyordu (Neyse ki şirkette sadece on iki çağrı personeli vardı) ancak bir sonraki işimde dengeler tamamen değişti. Bu sefer yazılım dünyasının milenyum başındaki yükselen yıldızlarından olan .Net platformu üstünde yeni yetme bir yazılımcı olarak işe başlamıştım. Bana Junior Software Developer unvanı vermişlerdi ve bu kez web tabanlı bir uygulama ile bol miktarda katman söz konusuydu. Tipik olarak katmanlı mimariye göre geliştirilmiş ve müşterisi olan bir ürün üstünde çalışan birkaç yazılımcıdan birisiydim.

Aradan yıllar geçti ve ben yazıyı yazdığım tarih itibariyle yedinci şirkette olduğumu fark ettim. Halen daha görev aldığım Doğuş Teknoloji’de dördüncü yılımı doldurmak üzereyim. İlginç olan şu ki hem burası hem de öncesinde altı yıla yakın çalıştığım ING Bank, ciddi anlamda dönüşüm geçiren iki kurum. Her ikisi de var olan legacy sistemlerini yenilemek veya baştan yazmak için hem kültürel hem de teknolojik altyapı değişimi geçirdiler, geçiriyorlar. Bende geliştirici olarak bu dönüşüm çalışmalarında ipin bir ucundan da olsa tutma fırsatı buluyorum. Şüphesiz ki değişim yazılım dünyasının olmazsa olmaz bir parçası. Diğer yandan son on yılda içinde yer aldığım işlerden gözlemlediğim kadarıyla monolitik yaklaşıma uygun olarak geliştirilmiş katmanlı sistemlerde var olanı yeniden yazmak da modernize etmek de hiç kolay değil. Çok planlı olunması ve iyi bir strateji ile hareket edilmesi gerektiği ortada. Her şeyi en ince ayrıntısına kadar düşünmek gerekiyor. Seçilecek mimari, bu konuda çalışacak insan gücünün sahip olması gereken yetkinlikler, revize edilecek süreçler, yük olan lisanslamalar ve alternatifleri, kullanılacak açık kaynak ürünler, nelerin SaaS (Software as a Services) haline geleceği vb.

Tüm bu gelişmelere ek olarak bir süredir şirketin iç girişim programına dahil edilen bir ürün fikri için kıymetli bir meslektaşıma destek olmaya çalışıyorum. Özellikle fizibilite safhasında yirmiden fazla firmanın önemli pozisyonlardaki çalışanları ile karşılıklı görüşme fırsatımız oldu. Konumuz ürünle alakalı olsa da benim dikkatimi çeken nokta özellikle beş yaş üstü şirketlerin neredeyse tamamında birçok uygulamanın yenilenmesinin söz konusu oluşu. İster kırk yıllık mainframe sistemler etrafında koşan uygulamalar olsun ister iki yaşında bir ürün mutlak suretle bir değişimden (yenilemeden, modernizasyondan…Artık nasıl düşünürseniz) bahsediliyor. Son on yıldır görev aldığım iki kurumun uygulama modernizasyonlarına tanıklık etmiş birisi olarak sektörü dinlediğimde ortaya çıkan sonuçların örtüşmesi dikkatimi monolitik sistemin katmanlı uygulamalarına çekti. Onlardan her yerde bolca bulmak mümkün.

Yazılımcı olmanın kaçınılmaz bir gerçeği üretim ortamından gelen problemler ile uğraşmaktır belki de. Çalışmakta olduğumuz sistemlerin giderek büyümesi, iş kurallarının zamanla karmaşıklaşması, nasıl yapılır nereye bakılır bilgisinin ayrılan iş gücü nedeniyle eksilmesi, entegrasyon noktalarının çoğalması ve daha birçok sebepten ötürü bu kaçınılmazdır. Her birimiz yazılım yaşam süresi boyunca farklı tipte mimariler üzerinde çalışırız. Örneğin 2021 yılının ilk çeyreğinde hazırladığım ve yedi yüzden fazla kişinin katıldığı “Teknik Borç Farkındalık Anketi” isimli çalışmanın sonuçlarına göre beşimizden dördünün katmanlı mimari olarak adlandırdığımız monolitik sistemlerde görev aldığını söyleyebiliriz. Hele ki sektörde yirmi yılı deviren bir yazılım geliştirici iseniz (ki yine anket sonuçlarına göre neredeyse %40ımız 10 yaşından büyük ürünlerle çalışmış) böyle bir sistemle yolunuzun kesişmemiş olması pek mümkün değildir.

Sonuçların bilimselliği bir kenara dursun katmanlı sistemlerin en büyük dertlerinden birisi kolayca modernize edilemeyişleridir. Bunun en büyük sebeplerinden birisi de kontrolsüz büyümenin ve birçok Anti Pattern ihlalinin teknik borç yükünü artırmış olmasıdır. Ward Cunningham’ın bu terimi programcı olmayan insanlara neden kodu refactor etmeleri gerektiğini anlatmak için metafor olarak kullanmasının üstünden çok süre geçmiş olsa da pek çok sistemin baş etmek zorunda kaldığı bir gerçektir teknik borç. Bu baş belası için bazı kaynaklarda ürkütücü The Silent Company Killer terimi kullanılır ve bence bu son derece isabetli bir ifade.

Neden legacy kabul edilen bir sistemi modernize etmeye çalışıyoruz da onu sıfırdan yazmıyoruz sorusu aklınıza gelebilir. Ancak ölçekçe büyük, müşteri nezdinde fonksiyonel olarak yüksek memnuniyet sağlayan sistemlerde veya mainframe gibi kolayca kopartılamayacak bağımlılığı bulunan yapılarda Big Bang olarak da ifade edilen sistemi komple değiştirmek kurumun hareket etme kabiliyetini olumsuz yönde etkileyebilir ve hatta bir noktada işleyen operasyonun yürütülmesini engelleyebilir. 

Bir üst seviyeye geçmeden önce(örneğin yeni bir mimari modele) domain olarak var olan süreçleri hem veri hem işleyiş bazında ayrıştırmak daha iyi bir yaklaşım olacaktır. Nitekim büyük resmi görmek ve sonrasında detaylara inebilmek böylesine büyük yapılarda çok zordur. Bu da bir stratejidir ve bunu gerçekleştirme noktasında var olan yüklerden kurtulmak yani teknik borcu hafifletmek önemlidir. Hatta var olanın yenisini paralelde yazmayı seçtiğimiz durumda da sistemi anlamak ve iyileştirmek için önemlidir.

Teknik borç yazılım tarafındakiler için anlamlı bir terim (bazen) olmasına karşın bazen IT personeli ve daha da önemlisi iş birimi ile paydaşlar için pek önemli bir kavrammış gibi görünmez. Sonuçta ölçülebilir değerler sunmadığımız ve bunların maliyet tablosundaki etkilerini göstermediğimiz sürece paydaşlar için anlamlı olmaz. Oysaki Gartner, McKinsey, Price Waterhouse Coopers, CAST ve CISQ (Consortium for Information & Software Quality) gibi kurumların zaman içerisinde yaptığı çeşitli çalışma sonuçları ve raporlar durumun ne kadar ciddi olduğunu gözler önüne sermektedir. İşte konu ile ilgili dikkat çekici bazı istatistikler;

  • CISQ’ in 2020 yılı bazlı The Cost of Poor Software Quality in the US raporuna göre Birleşik Devletler’ de ciddi sorunlara yol açan teknik borç düzeltme maliyeti 1.13 Trilyon dolar civarındadır.
  • Gartner’ a göre ciddi sorunlara yol açabilecek teknik borç düzeltme maliyeti sadece 2020 için 2.84 Trilyon dolar civarındadır ki aynı danışmanlık firmasının 2011’de yayınladığı raporda 2015 için bu borcun 1 Trilyon dolar civarında olacağı ön görülmüştür.
  • NYSE borsasında tahtası olan ve devlet tarafından belirlenen bir regülasyonu hatalı uygulayan bir firma, uygulamanın eksik dağıtımı sebebiyle 45 dakika içinde 462 milyon dolar zarar etmiştir.
  • McKinsey’ nin değeri 1 milyar doların üstünde olan şirketlerin CIO’ ları ile yapığı çalışma sonuçlarına göre CIO’ ların %60’ı son üç yıllık dönemde teknik borçların gözle görülür şekilde arttığını belirtmektedir.
  • Yine aynı rapora göre yeni ürünlere ayrılan bütçenin %10 ile %20 kadarı teknik borçların giderilmesi için harcanmaktadır. Üstelik teknik borçların tüm teknoloji mülkiyeti içerisindeki payının %20 ile %40 arasında değiştiği belirtilmiştir.
  • CAST’in 160 farklı organizasyondan 550 milyon satır koda sahip 1400 uygulamayı inceleyen raporuna göre kod satırı başına ortalama teknik borç maliyeti tahmini 3.61 dolar seviyesindedir.
  • Tricentis Software Fail Watch 2017 yılında 606 ölümcül yazılım hatası raporlamış ve bunların 304 firmaya 1.7 trilyon dolar civarında zarar yol açtığı sonucuna varmıştır.

Bazı rakamlar ütopik değerler gibi görünse de çalıştığım şirketlerdeki gözlemlerim bu bulguların oldukça gerçekçi olduğu hissiyatını uyandırmakta. Diğer yandan dünya çapında bilinen kod tabanının evrenimiz gibi genişlediği de söylenebilir. Yine CISQ raporlarına göre 2005’te yıllık ortalama 35 milyar satır kod üretildiği öne sürülmüştür. Bu değer 2020'de ortalama 100 milyar satır kod olarak revize edilmiştir. Rapordaki tahmin projeksiyonuna göre 2020 yılında dünya üstünde yaklaşık 1.655 trilyon satır kod olduğu öngörülmüştür. Yakın zamanda karşılaştığım Tanrı Parçacığından bozma kod dosyasındaki bir sınıfın 600 kitap sayfasına eş değer satırı olduğunu hesaplayınca bu rakamlarda gerçeklik payı olduğunu ve kayıt dışı olanlarla birlikte çok daha büyük bir havuzda yaşadığımızı söyleyebilirim.

Teknik borç yönetiminde ustalaşmış firmalar teknoloji borcunu finansal sermaye yapılarını yönetirken kullandıklarına benzer stratejik bir süreçle idare ederler.

Her ne kadar belirtilen rakamlar oldukça karamsar bir tablo çizse de teknik borcu iyi yöneten firmaların çeşitli kazanımlar elde ettiği de ortadadır. Sadece teknik borç ödeme yönetiminin bilinçli olarak kabulü ve buna göre hareket edilmesi bile fark yaratır. İsmini vermek istemeyen bulut tabanlı bir bilişim firmasının CIO’ suna göre teknik borçla mücadele yöntemlerinde yapılan değişiklik sonrası yazılımcıların bu işler için normal mesailerinden ayırdıkları zaman %75’ten %25’e kadar düşmüştür — McKinsey. Hatta aynı rapora göre teknik borcu aktif olarak yönetebilen firmalarda mühendisler zamanlarının %50 kadar fazlasını olması gerektiği gibi iş hedeflerine harcayabilmektedir.

Teknik borçlanma yazılımın kalitesine de doğrudan etki eder. Hangi metrik olursa olsun belli standartların üzerinde koşan kaliteli ürünlerde bile kusurlara rastlanabilir. Bu kusurlara çevresel faktörler de eklenince ortaya korkutucu sonuçlar çıkar. 2019–2020 aralığı büyük yazılım hatalarının tarihe geçtiği yıllar olarak hafızlara kazınmıştır. Fidye programları, siber saldırılar, beklenmedik IT kesintileri ve veri sızıntıları gibi nedenler havacılıktan bankacılığa, devlet kurumlarından savunma sanayine kadar birçok sektörde zarara sebep olmuştur. Örneğin NASA’nın Boeing Startliner mekiği iki büyük yazılım sorunu yüzünden insansız görevi sırasında Uluslararası Uzay İstasyonuna kenetlenememiş ve kargosunu bırakamadığı gibi dünyaya geri dönmek zorunda kalmıştır. Bu hatalar sebebiyle yeni bir uçuş planlanmış ve bunun maliyeti dört yüz milyon doları geçmiştir. İngilizce ders kitaplarına da konu olan Britanya’nın ünlü Heathrow havalimanı, Check-In sistemindeki kusurlar yüzünden aynı gün içinde yüzden fazla uçuş planının bozulmasına şahit olmuş, yaşanan sorun ancak sonraki gün düzeltilebilmiştir. Yoğun geçen bir yaz dönemi sonrasında belki de yorgun düşen yazılım sisteminin çökmesi sonrasında British Airways’in yüzden fazla uçuşu iptal edilmiş, üç yüzden fazlası da gecikmeli olarak sefer yapmıştır. 

Şirketler bu ve benzeri olaylar neticesinde sadece para değil itibar da kaybederler. Ne yazık ki hepsinden kötüsü yazılım sorunlarının ölümcül sonuçlara sebep verdiği gerçeğidir. Dünyanın ünlü şirketlerinden birisinin göz bebeği olup Titanik misali övülen hava taşıtı yazılımındaki hata sebebiyle başka bir uçakla çarpışmış ve bu hazin olay sonucu 346 insan yaşamını yitirmiştir. Sonuç olarak program sonlandırılmış, şirket sonradan yine kazansa da hatırı sayılır derece hisse değeri kaybetmiştir. Peki ölenler geri gelebilmiş midir?

Görüldüğü gibi üzerinde çalıştığımız ve bizzat geliştirmesine doğrudan etki ettiğimiz yazılımlar aynı zamanda bizlere farklı sorumluluklar yüklerler. Sonraki yazılımcılara temiz bir ortam bırakmak gibi sade bir mesuliyetten insan hayatını korumaya kadar uzanan önemli bir etik çizgi söz konusudur. 

Tanım

Raporlar ve rakamlar bu işin ciddiyetini ortaya koymaktadır ancak her şeyden önce bir tanım yapılması gerekir. Üstelik bu tanım bilişim personeli, iş sahipleri ve tüm paydaşlarca anlaşılır olmalıdır. Nitekim farkında olunması gereken bir mevzu söz konusudur. Big Commerce firmasından Shawn McCormick’e göre bir proje olgunlaştıkça çevikliği azaltan her kod teknik borçtur ve gerçek teknik borç tesadüfi değil kasıtlı olarak ortaya çıkmaktadır. Forbes’tan Brad Sousa’ya göre bir işletmenin doğru çözüm için gereken zaman ve parayı harcamak yerine mevcut kodu yeni kodlarla yükseltmeyi seçtiğinde maruz kaldığı gerçek maliyet teknik borcun ta kendisidir. Git Connected’ tan Trey Huffine’ e göre teknik borç, hızlı kazançlar elde etmek için yazılıma eklenen ve ileride düzeltilmesi için daha fazla çaba gerektiren herhangi bir koddur. Hackernoon’ dan fpgaminer kod adlı kişi ise onu şöyle yorumlamıştır; hedefe daha hızlı ulaşmak için koda kestirme yollar eklediğinizde bugün normalden daha fazlasını yapabilirsiniz ama sonra daha yüksek bir maliyet ödersiniz. Konu ile ilgili daha akademik bir tanım da mevcuttur. ScienceDirect teknik borcu şöyle ifade eder; Teknik borç kasti veya değil müşteri isteklerine veya uygulama kısıtlarına (deadline gibi)öncelik veren yazılım geliştirme eylemlerinin bir sonucudur.

Ben ise teknik borcu şöyle tanımlamak isterim; Belirli sebeplerle yeni bir özelliğin geliştirilmesi ya da bir sorunun çözümü sırasında kullanılan ancak tam olarak doğru olmayan yaklaşımların çoğalmasıyla oluşan ve ivmesi sürekli yükselen faizli kredi borcudur.

Teknik Borç Türleri

Teknik borcun bu bahsi geçen türlü tanımları onun farklı kategorilerde ele alınmasını da gerektirmiş ve buna göre bazı farklar ortaya konmuştur.

Örneğin Steve McConnel 2007 yılında teknik borçları kasıtlı olarak yapılan ve yapılmayanlar şeklinde iki ana kategoriye ayrıştırmıştır. Gerçekten de bazı durumlarda oluşacak teknik borçlanma stratejik bir karar sebebiyle sineye çekilebilir. Buna karşın ilerde borcun ödenmesi gerekir. Software Engineering Institute tarafından 2014 yılında yapılan bir çalışmaya göre teknik borç birçok kategoride değerlendirilir. Mimari yaklaşımlardaki anomaliler de teknik borca sebebiyet verebilir, personelin sahip olduğu yeteneklerdeki eksiklik de teknik borca sebebiyet verebilir gibi. Bana göre bu sınıflandırmalar teknik borcun domain bazlı ayrılabilmesine ve buna göre ekiplerin kontrolüne verilmesine de olanak sağlamaktadır. 

Çok doğal olarak hepimizin seveceği ve dikkat kesileceği çeşitlendirme Martin Fowler tarafından yapılmıştır. Martin’in Quadrant’ına göre durum öncelikle kasıtlı veya kazara mı meydana geliyor ona bakılmalıdır. Buna göre umursamaz ve pervasız bir şekilde hareket etmekle tedbirli ve ihtiyatlı bir şekilde hareket etmek arasında farklılıklar oluşacaktır. “Tasarım için zamanımız yoktu” sözü genellikle kasten ve umursamaz olduğumuz bir durumu ifade etmek için idealdir. Bazen bilerek teknik borca girmemiz kaçınılmaz olur. Hemen çıkmamız gereken bir özellik için bu sonucun oluşacağının bilincindeysek durumu tedbirli bir borçlanma gibi yorumlayabilir ve riski göze alabiliriz. Tabii alınan riskin etkin bir şekilde yönetilmesi de gerekir.

Sebepler

Neden teknik borç oluşur sorusunun farklı cevapları vardır. Örneğin McKinsey bunları dört ana başlık altında toplamış ve aşağıdaki gibi kategorize etmiştir.

Stratejik Sorunlar

  • IT girişimlerinin şirket stratejisi üzerindeki etkilerini ölçememek.
  • Portföy yönetimindeki sorunlar sebebiyle senkronize olmayan kaynak tahsisi ve toplam maliyet tahmini yapılamaması yüzünden finansman ile yaşanan uyum sorunları.
  • Birleşme ve satın almalardan sonra teknoloji entegrasyonunun yetersiz kalması — Birkaç şirketi satın alan bir firmanın kendi ürünleri ile gelen eski ürünler arasındaki iletişimde yaşayacağı her türlü teknik, kültürel zorluk olarak düşünebiliriz.
  • Ürünlerdeki aşırı karmaşıklık.

Süreçsel Sorunlar

  • Proje Backlog maddelerine doğru öncelik verilememesi veya bu maddeler listesinin etkin bir şekilde kullanılmaması.
  • Geliştirme ve bakım sürecinin zayıf yönetimi.
  • Nadiren yapılan kod kalite ölçümleri.
  • Zayıf olağanüstü durum (Disaster Recovery diyelim) yöntemlerinin kullanılması sebebiyle IT operasyonlarındaki düzensizlik ve tutarsızlık.

Yetenek Bazlı Sorunlar

  • Ürünlerin kullanıcılara teslimini geciktiren, kaynak riski oluşturan beceri eksiklikleri.
  • Ekip kapasitesinin nadiren teknik borcu azaltmak için tahsis edilmesi.
  • Karar vermede teknoloji borcunun göz ardı edilmesi.
  • Ekiplerin sadece kısa vadede özellik (feature) sağlamaya odaklanması.

Mimari Bazlı Sorunlar

  • Uygulama sunucuları, veri tabanları, alt yapı platformlarındaki güncelleme hataları.
  • Legacy sistemlerden kalan düzeltilmemiş sorunlar.
  • Yeniden kullanılabilirliği sınırlayan zayıf arabirime sahip monolitik bloklar.
  • Özelleştirilmiş paketlere sahip esnek olmayan yazılımlar.
  • PoC (Proof of Concept) formatında başlanıp ürün haline getirilmiş yazılımlar ki en sevdiğimiz günahlardan birisidir.
  • Yazılım içerisine sıkıştırılmış, değiştirilmesi zor yerleşik iş kuralları. Hatta bazen yazılım kodundan çıkıp veri tabanı içinde hayat bulan iş kuralları.
  • Tutarlı bir veri modeli üzerinde anlaşamama ve düşük veri kalitesi.
  • Standard sistem entegrasyonu yaklaşımlarının doğru kullanılmaması sebebiyle çoğalan uygulamadan uygulamaya entegrasyon noktaları (Point-to-Point entegrasyon noktalarının çoğalması olarak düşünülebilir ki bazıları zamanla atıl hale gelip unutulur ve hayalet bağımlılıklar oluşur)

Teknik Borç Nasıl Yönetilir?

Büyük resme bakıldığında teknik borç sadece kodun kötü parçalarından ayıklanması ya da iyileştirilmesiyle alakalı değildir. Şirketin genelini ilgilendiren bir sorundur ve herkes tarafından kabul edilmelidir. Ayrıca teknik borcun rakamsal olarak ifadesi ve denk düştüğü eksi maliyetler şeffaf bir şekilde sunulmalıdır. Teknik borç, yönetim kademesinden yazılımcısına kadar herkesin bilinçli şekilde baş etmesi gereken bir sorundur. Buna göre bazı çözüm yöntemleri önerilir. Yine sıkılıkla referans olarak kullandığım McKinsey raporlarına göre bu mücadele aşağıdaki sıralama ile yapılabilir.

1. Ortak Tanım Yap:İş birimleri ve bilişim personeli liderleri teknik borcun ne anlama geldiğinin ortak tanımını yapmalıdır.

2. İşle İlgili Bir Sorun Olduğunu Kabul Et: Teknik borcun sadece teknolojik değil aynı zamanda işle ilgili olduğunu kabul edin.

3. Şeffaf Ol: Teknik borçları rakamsal olarak açıkça ifade edin.

4. Karar Verme Sürecini Resmileştir: Belli kurallar üzerinde anlaşılmış bir portföy yönetimini takip edin.

5. Kaynak Ayır: Sadece teknik borca adanmış bir görev gücü oluşturabilirsiniz.

6. Büyük Patlamaya Dikkat Et: Mega projelerde teknik borcu tutarlı, tahmin edilebilir ve stratejik yol haritasına bağlı olarak ayırın ki rekabet etme yeteneğinizi kaybetmeyin. Yani topyekün değiştirmek yerine parçalayarak mücadele edin. Nitekim bir teknik borcu düzelteceğiz derken çalışan ve sahanın göz bebeği olan ürünü işlemez ve geliştirilemez hale de getirebilirsiniz.

7. İflas Noktalarını Belirle: Teknolojik varlıklar değerinin %50’ sini aşan borçlarda yeni bir Stack oluşturmayı düşünün. Maraton projeler için “IT Platform in a Box” şeklindeki yaklaşımları da tercih edebilirsiniz — ya da ürünler için bir raf ömrü belirleyebilirsiniz. Beş yılı geçen ürünleri yeni nesil teknolojilere dönüştürüyoruz gibi bir politika pekala belirlenebilir.

Teknik borcu yönetmek için çeşitli yöntemler olduğu aşikardır. Diğer yandan bu işin önemli bir parçası olan yazılım mühendislerinin işi pek de kolay değil. Yazılımların giderek büyümesi ve daha da karmaşıklaşması, artan ve ciddi boyutlarda zarar veren siber saldırılar, kirli bilgi dezenformasyonu sebebiyle yanlış öğrenilenler, sonu gelmeyen kullanıcı ihtiyaçları, teknoloji geliştirme hatlarının(tech stack diyelim) sürekli evrimleşmesi, gerekli teknik becerilerin çabucak eskimesi, hangi çevik metodoloji olursa olsun BT üzerinden kalkmayan zaman baskısı, işletme modellerinin değişen müşteri ihtiyaçları ve teknolojilere ayak uydurmak için devamlı değişmesi, veri güvenliği ve siber güvenlik ile ilgili regülasyonlar, yazılan her kod satırının potansiyel bir hata noktası olma ihtimali ve daha bir çok nedenden ötürü teknik borçla mücadele noktasında yazılım geliştirenleri bekleyen pek çok zorluk bulunmaktadır.

Bu arada teknik borcu yönetme noktasın teşhis koymak, acı noktalarını belirlemek, devam kararı alıp almamak için skor kartlarına başvurulabilir. Bu konu ile ilişkili olarak The Art of Service tarafından yayınlanan Technical Debt — A Complete Guide (Practical Tools for Self-Assessment) kitabını tavsiye edebilirim. Kitabın genel amacı aşağıdaki skor kartlarının çeşitli kategorideki sorulara verilen cevaplara göre puanlanarak doldurulmasıdır. 

Mimari

Ben ve benim gibi yazılımcılar açısından bakıldığında mimari seçim ve uygulanış biçiminin teknik borçlanma üzerinde etkisi olduğu söylemek kaçınılmazdır. Yazılım mimarileri oldukça geniş ve kapsamlı bir konudur ancak çok yüksek irtifadan meseleye bakıldığında işimize yarayacak bir özet üzerinde durabiliriz.

Yazılım mimarileri temel olarak ikiye ayrılır. Monolitik sistemler ve dağıtık olanlar. Pek çoğumuzun yakinen tanıdığı katmanlı tarz monolitik tarafta yer alırken pek çoğumuzun da çalışmak istediği hatta bazen kurtarıcı olarak gördüğü mikro servis yaklaşımı dağıtık mimari kategorisinde bulunur.

Sıfırdan bir uygulama tasarlarken genellikle katmanlı modelde başlanması ve ihtiyaç olması halinde mikro servis mimarisine geçilmesi tavsiye edilir ama belki de bu bir şehir efsanesidir. Bu mit bir kenara Clean Architecture diye de güzide bir yaklaşım vardır ve mutlak suretle incelenmelidir.

Tipik olarak katmanlı çözümler aşağıdakine benzer bir kurguya sahiptir ve pek çok kitapta bu şekilde resmedilir.

Hatta bu yapının aktörleri zaman zaman dağıtılabilir farklı parçalara da bölünebilir. Bu daha çok ölçeklenebilirliği mümkün mertebe etkin kullanmak amacıyla yapılır. Aynen aşağıdaki şekilde görüldüğü gibi.

Veri tabanı katmanı ve diğer kısımlar ayrı bir şekilde dağıtılabilirken uygulamanın tamamı tek parça halinde de üretim ortamına uğurlanabilir arkasından bir bardak su dökülerekten. Bizim üzerinde durduğumuz katmanlı yapı ile diğerlerini kıyasladığımızda ele alınması gereken birçok kriter de vardır. Pek çok kriter zaman içerisinde ortaya çıkan ihtiyaçlar doğrultusunda önem kazanmıştır. Netflix, Amazon, Google, Spotify ve benzeri öncülerin sürüklediği sistemler mimarilerin değişmesine neden olmakla kalmaz hata toleransından sistemin ayakta kalabilir olmasına kolay dağıtımdan esnekçe genişleyebilmeye kadar birçok faktörün de dikkate alınmasına sebep olur. Aşağıdaki tabloda söz konusu mimari yaklaşımlar arasındaki avantaj ve dezavantajları görebilirsiniz. Bir yıldız çok zayıf, beş yıldız çok güçlü anlamındadır.

Katmanlı mimari küçük çaplı, basit uygulamalar ile web siteleri gibi çözümler için son derece idealdir. Ayrıca başlangıç bütçesi düşüktür. Bu nedenle kurumsal çapta düşünüldüğünde fikirleri hayata geçirme noktasında sıklıkla tercih edilir. Nitekim çabuk sonuç verir. Dağıtık sistemlere geçildikçe düşünülmesi, yönetilmesi gereken ayrık bileşenler çoğalır ve daha iyi bir teknik yönetim gerekir. Hata yönetimi bile dağıtık sistemler düşünüldüğünde katmanlı yapılara göre çok daha zordur. Anlamlı loglar atmanız, bunları yorumlamanız, yorumlarınıza uygun alarm sistemleri kurmanız, çakılan servisler kendine geldiğinde ne yapmalıyı planlamanız, versiyonlamaları nasıl yöneteceğinizi düşünmeniz, ağ trafiğini gözlemleyip iyileştirmeniz, kara cumalara aylar öncesinden hazır olmanız vs gerekir.

Hoş bugün kullanmakta olduğumuz izleme, performans ölçücü ve alarm mekanizmaları zaman içerisinde teknik borcun keşfi için bir belirteç de olabilir. Bir yıl öncesinde çok fazla bellek sorunu yaşamayan, sıklıkla çökmeyen, taleplere cevap verme süreleri düşük olmayan uygulamaları keşfettiğinizde “şuna bir bakalım” diyerek herhangi bir araca(sonarqube, cast vb) bağımlı kalmadan teknik borçla mücadeleye başlayabilirsiniz.

Semptomlar

Monolitik yapılar iyidir hoştur ama zaman her şeyin ilacı olduğu kadar bazen de çaresi olmayan bir virüstür. Kontrolü kaybettiğimiz noktadan itibaren teknik borç sistemin tüm damarlarına sirayet etmeye başlar. Esasında bazı semptomlar teknik borcun artmış olduğunun iyi birer göstergesidir. Bunları aşağıdaki gibi özetleyebiliriz.

  • Loglar sorun tespitinde yetersiz kalır ve üretim ortamında sıklıkla debug yapılır.
  • Anti Pattern pratikleri çoğalır.
  • Yerleşik iş kurallarını dışarıya almak sadece zor değil neredeyse imkansız hale gelmiştir.
  • Test yazmak zordur ve hatta test yazılmaz.
  • Yazılım tek paket olarak üretime çıkar ve bu sırada kesintiler olup diğer paydaşlar bundan etkilenir.
  • Bir katmandaki hata diğerlerini de etkiler.
  • Basit bir sınıf değişikliği yeniden dağıtım gerektirir.
  • Mean Time to Recovery süreleri dakikalar mertebesinde artar.
  • Benzer hatalar sürekli olarak karşımıza gelir.

Monolitik yapılar büyüyüp karmaşıklaştıkça dağıtık mimarilere nazaran sahip oldukları kolay anlaşılırlık, düşük inşa ve bakım maliyeti gibi avantajlarını kaybederler. Yukarıdaki semptomlar çok doğal olarak bir şeyler yapılmasını gerektirir. Bu noktada bazen ilk akla gelen monolitik mimariyi terk edip dağıtık sistemlerin göz bebeği olan mikro servis yaklaşımına geçmektir. 

Monolitik yapılardan mikro servislere geçmeden önce teknik borcu azaltmak, maliyetleri düşürmek adına iyi bir yaklaşımdır.

Büyük resme Legacy sistem terimi üstünden de bakmak gerekiyor. Günümüzde monolitik yapıları görünce genellikle Legacy sistemler olarak anıyoruz. Onlar eski çağdan kalmış, yeni neslin çalışmak istemediği, korkunç teknolojiler içeren ürünler! Değil mi? Adı her ne olursa olsun bu tip yazılımların kalitesini artırmanın ve daha da önemlisi ömrünü uzatmanın bilinen belli başlı yolları da mevcut (Çalışan sisteme dokunma derler ya. Az biraz dokununca güzel sonuçlar da çıkabiliyor aslında.) 

  • Tüm sistem detaylarını izole edecek şekilde API’lere ve Container’lara geçmek— Encapsulate
  • Sadece bug’lar ile uğraşıp bakım yapmak — Repair
  • Yeni donanımlara geçerek sistemi hızlandırmak — Replatform
  • İnce ayar çekmek — Rebuild, yeni bir teknolojik platforma adapte etmeye çalışmak — Rearchitect,
  • Mainframe gibi parçaları bulut servis sağlayıcılarına taşımak — Rehost,
  • Çözümü Software as a Service ile değiştirmek — Replace
  • Teknik borçları azaltıp olası hataların önüne geçmek — Refactor 
  • ve en nihayetinde üstteki maddelerin birçoğunu bir arada yapmaya çalışmak. 

Fakat bu iyi bir organizasyonu ve çözüm metodolojisini gerektirir.

TDML (Technical Debt Management Lifecycle)

Yaklaşık olarak dört yıldır çalıştığım Doğuş Teknoloji ve öncesinde görev aldığım bankada teknik borçlanma ile mücadele cephesinde savaştım, savaşıyorum. En azından üstüme düşen görevleri yapmaya çalıştığımı ifade edebilirim. Statik ve dinamik kod analiz araçları bu mücadelenin önemli birer parçasıdır ancak teknik borcun sadece kodla ilgili olmadığını düşündüğümüzde yeterli değildir. Dolayısıyla diğer konular için yönetsel seviyede destek almak bu mücadele açısından hayatidir. 

Teknik borç yükü ile mücadelede başarılı olan ve bunun bir etkisi olarak kaliteli ürünler geliştiren firmaların belli karakteristik özellikleri vardır; Proje yönetimi konusunda iyilerdir, yetenek yönünden yeterli seviyede istihdamları vardır, iyi tanımlanmış geliştirme süreçlerine sahiplerdir, süre tahmini konusunda mükemmel metotlar kullanırlar, müşteri memnuniyetine önem verirler, toplam kalite yönetimi konusunda bir kültür benimserler ve kusurları önleme konusunda beceriklidirler.

Edindiğim tecrübelere göre monolitik bir sistemde teknik borçlanma ile mücadele için aşağıdaki gibi bir yaşam döngüsünün kullanılması gerekir. Buna TDML(Technical Debt Management Life Cycle) adını verebiliriz ve farklı türden sistemlere de uyarlanabilir.

Döngüye girebilmek için ön gereksinimlerin tamamlanması gerekir. Her şeyden önce teknik borcu kabul etmek, ortak bir tanımını yapmak, yarattığı yükü hesaplayıp genel maliyetini ölçmek ve bunu şeffaf bir şekilde paylaşmak gerekir. Ön gereksinimler için aşağıdaki gibi genel bir kontrol formu kullanılabilir ve Dashboard benzeri bir arabirim ile sistem içerisinde her an görünür olması sağlanabilir. Görünürlük de bu mücadelenin olmazsa olmazlarındandır.

Teknik borç yönetimi yaşam döngüsü belli periyotlarda tekrar eden bir düzenektir. Gereksinimlerin karşılanmasını takiben sırasıyla aşağıdaki adımlara göre işletilir.

  • Keşfet: Keşif aşamasında var olan kodun çarpık yanları ortaya konur. Bir başka deyişle teknik borç keşfi yapılır. Bu amaçla çeşitli araçlardan yararlanılabilir. Statik Kod analiz araçlarından birisi olan SonarQube bunlardan birisidir — ancak tek değildir. Aşağıdaki tabloda diğer araçları ve genel özelliklerini görebilirsiniz. Buna ilaveten IT4IT anlamında yapılması düşünülen yenilikler de keşif aşamasında değerlendirilebilir. Bu, bizim de şirket bünyesinde sıklıkla uyguladığımız bir pratiktir. Örneğin Business Layer içerisindeki fonksiyonların servis olarak dışarı çıkartılması, Session kullanımından vazgeçilip Redis’e dönülmesi, konfigürasyon değerlerinin Secret Vault üstüne alınması, karmaşıklık değeri yüksek fonksiyonların(cognitive complexity) hafifletilmesi, tekrarlı kod bloklarının tekilleştirilmesi, Transaction kullanımından vaz geçilmesi, bağımlılıkların Dependency Injection mekanizmaları ile dışarıya alınması, veri tabanı tarafına yayılmış iş kurallarının otomatik araçlarla kod tarafına çekilmesi vb. Bu noktada yazılımcıları dinlemek oldukça önemlidir. Onlardan toplanan fikirlerin TDML sürecine sokulması noktasında yine bir komite desteğini almakta yarar vardır.
  • Öncelik Ver: Araçlardan elde edilen bulgular ya da hissedilen düzensizlikler sonrası teknik borcun azaltılması için toplanan ve yapılması düşünülen fikirler çoğalacaktır. Bu nedenle bir komite eşliğinde hangilerinin öncelikli olarak yapılacağını belirlemek önemlidir. Nispeten SonarQube gibi bir aracın bulgularını takım bazında parçalamak kolay olsa da genel mimariyi etkileyen önemli değişikliklerin planlanması için bir komite desteği ve görüşü almak gerekir. Burada önceliklerin belirlenmesi, kayıt altına alınması, planlanması ve takibi gibi konularda çevik metodolojiler uygulanmalı ve bir komite eşliğinde ilerlenmelidir. Bazı firmalarda (örneğin bizde) bu amaçla açılmış özerk mangalar (chapter) görebilirsiniz.
  • Dağıt:Öncelik durumlarına göre sıralanan bulgular bu işle uğraşacak bireylere veya takımlara dağıtılır. Bu dağıtım sırasında TDLM Kimlik Kartında belirtilen kişi başına ne kadarlık bir eforun bu işe ayrılacağı mutlak suretle göz önüne alınmalıdır.
  • Refactor Yap: Dağıtımı yapılan işlerin oluşturduğu teknik borçlanma gözden geçirilir ve gerekli müdahaleler yapılarak ortadan kaldırılması için çalışılır.
  • Raporla: Yapılan değişikliklere ait sonuçlar raporlanır ve güncel durum analiz edilir. Bu aşama görev panosunun da güncellenmesi gereken dilimdir. Yapılan son çalışmalara istinaden borçlanmanın genel durumu şeffaf bir şekilde yenilenmeli ve tüm paydaşlara sunulmalıdır. Üstte belirttiğimiz skor kartının bu aşamada yeniden hesaplanması yerinde bir ölçüm olacaktır.
  • Kontrol Et: Burası geri dönülmez kontrol noktası olarak düşünülebilir. Devam etmekte olan geliştirmeler ve önceden var olan kodların yarattığı teknik borçlar törpülense de terk ediş noktası olarak belirlenen hedefin uzağında kalmış olabiliriz. Dolayısıyla döngünün bu safhasında var olan uygulamanın artık yenisi ile değiştirilmesi gerekliliği kararı verilebilir. Diğer yandan göstergeler pozitif anlamda belirlenen bir noktanın üzerindeyse köklü mimari dönüşümler için hazırlıklara başlanması düşünülebilir. Örneğin mikro servis mimari için domain bazlı parçalamalar için gerekli alt yapı hazırlıkları IT4IT işleri kapsamında bitmiş olabilir…

Burada bahsi geçen sistem genel konsept olarak teknik borç yükü altındaki birçok uygulamaya uyarlanabilir. Hatta siz bile kendi TDLM sürecinizi tasarlayıp çeşitli araçlarla donatabilirsiniz. Önemli olan teknik borçla mücadelenin iş birimi, bilişim personeli ve paydaşlar tarafından anlaşılmış, kabul edilmiş olması ve bu çerçevede karar verilen bir strateji ile belli bir metodoloji altında icra edilmesidir. TDLM gibi bir yaşam döngüsü sürekli tekrar eden bir kültür sağlar. Bu kültürün devamlılığının sağlanması da başlı başına bir konudur.

Sunuma bu adresten erişebilirsiniz.

Kaynaklar

Programcıdan Programcıya Rust

$
0
0

İki yıl kadar önce bir merakla başladığım ama sonrasında takıntı haline gelen bir uğraş edindim; Rust programlama dili. Profesyonel iş yaşantımın neredeyse tamamında .Net platformu üstünde geliştirme yaptım ve halen daha maaşımı ondan kazanıyorum. Bazı zamanlar Python, Go, Ruby gibi dillere de baktım ama hep hobi olarak kaldılar. Rust içinse aynı şeyi söylemem zor. Onunla ilgili resmi dokümantasyonu takip edip birkaç satır kod yazmaya başladım ve derken sayısız derleme zamanı hatası ile karşılaştım. Bunların neredeyse büyük çoğunluğu borrowing, ownership, lifetimes gibi konularla ilintiliydi ve her biri Rust’ın temelde bilinmesi gereken demirbaşları.

Bu zorlanma bende daha fazla merak uyandırdı. Derken her zaman olduğu gibi en doğru kaynağın kitaplar olduğuna karar verip güzelim paracıklarımı Amazon’daki kitaplara yatırmaya başladım. No Starch Press’ten The Rust Programming Language, Packt’tan Rust Programming Cookbook, Hands-On Data Structures and Algorithms with Rust, Creative Projects for Rust Programmers ve son olarak da Rust Web Programming. Hepsine zaman zaman bakıp bir şeyler çalıştım ama tabii işten güçten çok da fazla değil. Gerçi acelem yok. Hayatımın bundan sonraki dönemi için hedefe aldığım bir programlama dili olduğundan ona yıllarca vakit ayırabilirim.

İzleyen yazıda Rust Web Programming kitabının birinci bölümü ile ilgili notlarımı bulabilirsiniz. Birebir çeviri değil ama akıştaki örnekleri yer yer değiştirip anlamaya çalışarak yorumladığım bir içerik. Nitekim yazmadan öğrenemiyorum. Faydalı olması dileğiyle.

Doğada element halinde birçok programlama dili var. Bir problemin çözümü veya bir işin bilgisayarlar tarafından yapılması maksadıyla kullanılan sayısız dil. Programcılar için hangi dili seçeceğine karar vermek ezelden beri çok kolay değil. Özellikle sistemlerin geliştirilmesinde ödün verilen şu ikililer düşünülürse; Hız ve kaynaklar(speed/resource) ile geliştirme hızı ve güvenli bellek sahaları(development speed/safety)

C ve C++ gibi işletim sistemine diğerlerinden daha çok yaklaşabilen düşük seviyeli dillerde yüksek hızlı çalışma zamanlarına ve minimum seviyede kaynak tüketime erişmek pekala mümkün. Bunlar her ne kadar önemli avantajlar gibi görünse de birkaç sebepten ötürü handikaba da dönüşebiliyor.

Her şeyden önce günümüzün dinamiklerine göre iş ihtiyaçları açısından çok hızlı yenilenmesi gereken ürünlerde(web uygulamaları gibi) uzun geliştirme süreleri kimsenin işine gelmiyor. Diğer yandan dil dışında işletim sistemlerinin dinamiklerine de çok iyi hâkim olmak gerekiyor. Nitekim belleğin programcı tarafından yönetimi çeşitli güvenlik açıklarına ve bug’lara sebebiyet verebiliyor. Bu yüzden web geliştirme dünyasında C++ ile yazılmış hazır Framework’lere pek rastlanmıyor. Doğal olarak Javascript, C#, Java, Python, PHP gibi yüksek seviyeli diller bu alanda daha çok tercih ediliyorlar. Çünkü zengin framework detayları ile otomatik bellek yönetim mekanizmaları yazılımcının geliştirme hızını artırıp güvenli bir ortam tesis edilmesine imkân sağlıyor. Ancak belleğin daha güvenli bir hale gelmesi için kullanılan Garbage Collector gibi mekanizmaların da bazı sıkıntıları var; Fazladan kaynak tüketimi ve zaman maliyeti. Bu mekanizmalar uygulama ortamında değişkenleri sürekli izleyip çeşitli kontrollere göre kaynakların belleğe geri iadesi üzerine çalışmakta. 

Rust’ın öne çıktığı noktalardan birisi de güvenli bellek sahası için kullandığı yöntemler. Rust, Garbage Collector gibi bir mekanizma yerine birçok şeyi henüz derleme aşamasındayken çözmeyi yeliyor. Derleyici, değişkenlerin belli kurallara göre kullanılması için programcıyı zorluyor. Burada borrow checker, ownership, lifetime gibi konulardan bahsediyoruz. Açıkçası benim gerçekten de ilk kez duyduğum bu kavramlar Rust’ın hız ve efektif kaynak tüketimi yanında güvenli bellek sahasının maliyetsiz olarak tesis edilmesi için önemli kolaylıklar sağlıyorlar. Yani derleme aşamasındayken bu kurallar devreye giriyor ve aşağıda listelenen olası hataların oluşmasının önüne geçiliyor.

  • Program tarafından kullanılan bir bellek bölgesi serbest kaldığında korsanların kod yürütebileceği alanlar haline gelebilir veya bu alanlar durmaya devam ettiği için program çakılabilir => Use After Frees
  • Bir işaretçinin(pointer) referans ettiği bellek adresi ve içeriği artık kullanılmıyordur ancak işaretçi, program içinde aktif kalmaya devam etmektedir. Bu durumda işaretçi rastgele bir veri içeriğini tutabilir => Dangling Pointers
  • Ayrılan bir bellek bölgesi serbest bırakıldıktan sonra ikinci kez tekrar serbest bırakılmaya çalışılır. Bu, verilerin açığa çıkmasına veya korsanların ilgili alanı kullanarak kod işletmesine sebebiyet verebilir => Double frees
  • Program, izninin olmadığı bir bellek bölgesine erişir => Segmentation Faults
  • Bir dizinin sonu okunmaya çalışılır ki bu da programın çökmesine neden olur => Buffer Overrun

Zaten bu sorunların önüne geçmek için pek çok programlama dili managed bir ortamda(hatta sanal çalışma zamanında) yürür. İlerleyen kısımlarda bu hataların önüne geçmek için kullanılan değişken sahiplenme kurallarına(Ownership Rules) kısaca değineceğiz.

Bahsettiğimiz avantajlara ek olarak Rust’ın Web Framwork’ler açısından zengin bir kütüphane(crate, sandık olarak geçer) desteğine sahip olduğu düşünüldüğünde sadece sistem programlama değil, web programlama alanında da önemli bir araç haline geldiğini söyleyebiliriz(Oyun programlama ki bu konuda şu yazıya bakabilirsiniz, gömülü sistem programlama vb) 

Bazı kaynaklarda Rust için kullanıcı ile etkileşime odaklı bir dil olmadığı daha ziyade performans gerektiren arka plan işleri için tasarlandığı ve bellek yönetimini merkeze koyduğu belirtilir.

Ancak ortada halen daha bir problem var. Özellikle benim gibi 45’ini devirmiş bir yazılımcı iseniz ve iş yaşantınızın neredeyse tamamında .Net gibi belleği sizin için yöneten, dil enstrümanları ve kütüphaneler açısından zengin, bir şeyi yapmak için on farklı fonksiyon sunan çatılarla çalışmışsanız, Rust’ın öğrenme eğrisinin oldukça zorlu olduğunu ifade etmek mecburiyetindeyim.

Rust dilinin belli başlı kurallarını baştan öğrenmek dile hâkim olmak açısından son derece önemli. Aksi halde derleme zamanı hataları ile saatler geçirebilirsiniz(Her ne kadar derleme hataları pek çok ipucu veriyor olsa da) Sisteminizde Rust için gerekli geliştirme ortamının yüklü olduğunu varsayarak aşağıdaki terminal komutu ile devam edelim. Tüm kod parçalarını bu projenin main.rs dosyası üzerinde icra edeceğiz. Hemen bir ipucu vereyim, sisteminize Rust ortamının kurulumu için şu adresten yararlanabilirsiniz.

cargo new hello-world

cargo, Rust’ın paket yöneticisi, derleyicisi, testçisi, sürüm hazırlayıcısıdır. Belki de her şeyidir desek yeridir. Onu kullanarak çalıştırılabilir programlar ve yeniden kullanılabilir kütüphaneler(ki Crate olarak isimlendirilirler-küfe veya sandık olarak çevirebiliriz) yazabilir, test koşturabilir, release için platforma göre binary çıktılar üretebiliriz. Şu aşamada giriş noktası main fonksiyonu olan hello-world isimli bir program oluştuğunu söylesek yeterli. İlk kodlarımızı aşağıdaki gibi yazalım.

fn main() {
    let introduction =
        String::from("Merhaba Rustician. Bugün hava 23 derece ve güneşli. Nasılsın?");
    print_sysmsg(introduction);
}

fn print_sysmsg(message: String) {
    println!("Sistem mesajı,\n{}", message);
}

Program main fonksiyonundan çalışmaya başlar. let anahtar kelimesi ile introduction isimli bir değişken tanımlanır ve ona String türünden bir nesne atanır. Nesnenin üretiminde String modülünün from fonksiyonundan yararlanılır. Sonrasında yine String türünde parametre alan print_sysmsg isimli fonksiyon çağırılır. Bu fonksiyon içerisinde println! İsimli bir başka enstrümanın kullanıldığını görüyoruz. Sonunda ! işareti olan fonksiyonlar macro olarak isimlendirilir. Macro’lar n sayıda parametre alabilirler ama daha da önemlisi meta programming için kullanılırlar. Böylece rust ile rust kodları yazabilir, derleme aşamasında işletilebilir kod bloklarını programa ekleyebiliriz(Ön işlemci direktifleri gibi) 

Şimdi println! makrosunu main metodu ile doğrudan kullansaydık ne olurdu diyebilirsiniz. Biraz sabredin, ne demek istediğimizi anlayacaksınız. Öncelikle bu örneği çalıştırıp ekran çıktısına bir bakalım. Bunun için programın olduğu klasöre geçip aşağıdaki komutu vermek yeterli.

cargo run

Şimdi aynı örnek kodu aşağıdaki gibi değiştirelim.

fn main() {
    let introduction = "Merhaba Rustician. Bugün hava 23 derece ve güneşli. Nasılsın?";
    print_sysmsg(introduction);
}

fn print_sysmsg(message: str) {
    println!("Sistem mesajı,\n{}", message);
}

Dikkat edileceği üzere fonksiyonun parametre tipini değiştirdik. Metinsel veriyi işaret edebileceğimiz bir literal kullandık. Aynen Javascript, Python gibi dinamik programlama yaklaşımı içeren dillerde olduğu gibi. Yılların programcısı olarak bu kullanımda hiç bir sorun olmadığını rahatlıkla söyleyebilirsiniz. Şimdi çalışma zamanı çıktısına bakalım.

Birkaç satırlık kod parçası için geniş bir hata mesajları silsilesi :| Odaklanmamız gereken hata mesajı “error[E0277]: the size for values of type `str` cannot be known at compilation time”. Rust derleyicisi parametre olarak gelen str literal’inin çalışma zamanında ne kadarlık bir yer kaplayacağını bilmediği için derlemeyi kabul etmemiştir. Bunun belleğin güvenli bir saha olarak kalması için konulmuş bir kural olduğunu ifade edebiliriz. Ancak sebebin geçerliliğini anlamak için belleğin stack ve heap bölgelerinin çalışmasını da bilmemiz gerekir.

Program başlangıcında boyutları bilinen veriler stack bellek bölgesinde tutulurlar ve bu alan hızlı erişilebildiği için performans açısından oldukça verimlidir. Lakin heap bölgesine göre daha küçük bir alandır. Özellikle büyük boyutlu olan, çalışma zamanında içeriği dinamik olarak değişebilen türler göz önüne alındığında veriyi heap üstünden tutmak, stack’ten ise bu verinin olduğu başlangıç konumlarına işaret eden referansları tutmak tercih edilir.

Stack’de duracak değişkenlerin taşıyacakları veriler başlangıçta bellidir ama heap dinamik olarak çalışma zamanında anlaşılabilir. Rust, str türünün literal olarak heap bellek bölgesinde tutulacağını biliyor fakat içerisine ne kadar büyük bir veri konacağına dair fikir sahibi değil ki bu en sevmediği şey. Dolayısıyla programı derlemiyor. Bunu çözmek için verinin heap üzerinde durduğu konumu referans eden bir kullanıma gitmemizi bekliyor. Ya da boyutu sabitlenecek şekilde kullanmamızı. Aşağıdaki örnek kod parçasına ile devam edelim.

fn main() {
    let introduction = "Merhaba Rustician. Bugün hava 23 derece ve güneşli. Nasılsın?";
    print_sysmsg(introduction.to_string());

    let motto = "Rust çok efektif bir dil";
    print_sysmsg2(&motto);
}

fn print_sysmsg(message: String) {
    println!("Sistem mesajı,\n{}", message);
}

fn print_sysmsg2(message: &str) {
    println!("Sistem mesajı,\n{}", message);
}

Öncelikle uygulama çıktısına bir göz atalım.

Sorun yok. Peki ya neler oldu? İki farklı kullanım görmekteyiz. İlk versiyonda introduction değişkeninin taşıdığı değer metoda gönderilmeden önce to_string() ile String nesnesine dönüşür. String nesnesi verinin tutulduğu heap adresinin referansını, kapasitesini ve uzunluğunu tutar(Hatta henüz değinmedik ama String nesnesinin işaret ettiği veri aslında byte türünden bir vector serisidir) Diğer kullanımda ise motto isimli metin bazlı verinin başına & işareti konularak fonksiyona aktarıldığını görmekteyiz. Benzer şekilde print_sysmsg2 isimli fonksiyonun parametre tanımında da & ile başlayan bir literal değişken bildirimi söz konusudur. Aslında yapılan şey motto değişkeninin referansının print_sysmsg2 fonksiyonuna taşınması ya da o fonksiyon tarafından ödünç alınmasıdır(borrowing)

Rust dilinde String dışında tamsayılar(integers), kayan noktalı sayılar(floats), diziler(arrays), vektörler(vectors), true false(bool) gibi başka veri türleri de vardır. Genelde değişken tanımlamalarında veri tipini belirtmek mecburi değildir ancak tür belirtildiğinde özellikle tamsayılar için dikkat edilmesi gereken bir durum söz konusu olabilir. Tamsayı türleri işaretli(signed) veya işaretsiz(unsigned) tanımlanabilirler. İşaretli versiyonlarda sınırlara dikkat etmek gerekir. Aşağıdaki kod parçasını göz önüne alalım.

fn main() {
    let mut n1: i8 = 127;
    println!("n1 sayısının değeri {}", n1);
    let n2: u8 = 256;
    println!("n2 sayısının değeri {}", n2);
    n1 += 1;
}

Şimdilik çok önemli olmasa da n1 değişkeni için mut anahtar kelimesini kullanmamız dikkatinizi çekmiştir. Varsayılan olarak tüm değişkenler immutable olarak doğarlar. Yani mut operatörü ile aksi belirtilmedikçe sahip oldukları veriler değiştirilemez. Diğer yandan bu kod parçası derlenecek ama çalışma zamanında hata oluşmasına sebep olacaktır.

Rust tarafında çalışma zamanı hataları aslında birer panik halidir(panic)Şimdi kodu dikkatlice inceleyelim. İlk değişkenimiz n1 işaretsiz 8 bit tamsayıdır. Dolayısıyla 2 üzeri 8 yani 256 adet sayı değerinden birini taşıyabilir. Önemli olan hangi aralıktakileri? Benzer şekilde n2 isimli değişken u8 olarak ifade edilmiştir ve o da pozitif olmak kaydıyla 0 ile 255 dahil değerler alabilir. Dikkat edileceği üzere 0 ile 255 dahil aradaki değerler olarak ifade ettik. Bu son derece doğal çünkü 0’dan itibaren gelen pozitif sayıları vurguluyoruz. Lakin n1 değerini 1 arttırdığımızda 128 rakamına geliriz ve esas itibariyle alınabilecek değerler hata mesajının da belirttiği üzere -127 ile 128 arasındadır. Yani işaretli tamsayılar da 0’ın sağ ve soluna doğru ilerlendiğini dolayısıyla 2 üzeri ifadesinde bulunan değerin yarısı kadarlık bir sayı alanının işaret edildiğini söylesek yeridir. Diğer yandan aşağıdaki gibi değişken tanımlamaları yapmak da mümkündür.

let corner1 = 1.234; // varsayılan olarak f32
let corner: f32 = 1.23456; // 32 bitlik floating number
let corner = 1_u8; // son ek vererek de değişkenin hangi türden olacağını söyleyebiliriz. u16,

Biraz araştırma yaparak diğer veri türleri ile ilgili kısa bilgiler bulabilirsiniz. Devam etmeden önce mutable olma durumu ile ilgili birkaç şey söyleyelim. Değerinin değişmeyeceği bilinen değişkenler söz konusu olduğunda uygulamanın bellek sahası güvenliğinin daha kolay sağlanacağı aşikardır. Bu, Rust tarafında değişkenlerin varsayılan olarak immutable olmasını da açıklar. Çünkü güvenli bellek sahaları ön plandadır. Ayrıca immutable kullanımının performans üzerinde de olumlu etkileri vardır.

Mutable demişken çalışma zamanında içeriği değiştirilebilir sıralı veri kümelerine de ihtiyacımız mutlaka olacaktır. Sıralı veri kümeleri için Rust tarafında en temelde array, vector gibi tiplerinden yararlanılır. Tahmin edileceği üzere her ikisi de varsayılan olarak immutable niteliklidir. Dizi(array) zaten programlama dillerinin olmazsa olmaz temel yapıtaşlarından birisidir. Rust tarafında diziler tek tip veri taşıyabilirler ve boyutları sabittir. Vector türü de benzer şekilde tek tiple çalışır. Elbette struct enstrümanını kullanarak kendi veri yapılarımızı tasarlayabilir ve hem array hem de vector türü için kullanabiliriz. Bu iki türün kullanımına ilişkin çok basit bir kod parçasını aşağıdaki gibi ele alalım.

fn main() {
    let points: [i8; 5] = [3, 4, 1, 8, 9];
    for p in points.iter() {
        print!("{},", p);
    }

    print!("\n");

    let colors: Vec<&str> = vec!["mavi", "kırmızı", "beyaz", "gri", "sarı"];

    for c in colors.iter() {
        print!("{}\t", c);
    }
}

Points isimli i8 türünden 5 elemanlı bir dizi ve string literal türünden herhangi bir sayıda eleman içerebilecek colors isimli bir vector tanımlandığını görüyoruz. Diziyi tanımlarken eleman sayısını belirttiğimize dikkat edelim. Gerek dizi gerek vector elemanlarında ileri yönlü döngüler oluşturmak için iter fonksiyonundan yararlanıldığını görüyoruz. Bu şimdilik daha ileride ele alacağımız bir konu. Peki dizinin elemanlarından birisinin değerini değiştirmek ve hatta colors isimli vector’e pink isimli yeni bir değer eklemek istesek... Muhtemelen aşağıdaki gibi bir kod parçası üzerinden ilerleriz.

fn main() {
    let points: [i8; 5] = [3, 4, 1, 8, 9];
    for p in points.iter() {
        print!("{},", p);
    }
    points[0] += 1;

    print!("\n");

    let colors: Vec<&str> = vec!["mavi", "kırmızı", "beyaz", "gri", "sarı"];
    colors.push("pink");
    for c in colors.iter() {
        print!("{}\t", c);
    }
}

Aslında buraya kadar yazılanları dikkatli bir şekilde okuduysanız programı daha çalıştırmadan sorunu söyleyebilirsiniz. Kurala göre tüm değişkenler aksi belirtilmedikçe değiştirilemez(immutable) olarak tanımlanırlar.

fn main() {
    let mut points: [i8; 5] = [3, 4, 1, 8, 9];
    for p in points.iter() {
        print!("{},", p);
    }
    points[0] += 1;

    print!("\n");

    let mut colors: Vec<&str> = vec!["mavi", "kırmızı", "beyaz", "gri", "sarı"];
    colors.push("pink");
    for c in colors.iter() {
        print!("{}\t", c);
    }
}

Şimdi farklı bir şey deneyelim. Dizi elemanlarını bir döngü ile birer sayı artırmak istediğimizi düşünelim. Bu pek çok dilde hiçbir sorun ile karşılaşmadan rahatlıkla yapabileceğimiz bir şey öyle değil mi? Öyleyse bir bakalım.

fn main() {
    let mut points: [i8; 5] = [3, 4, 1, 8, 9];
    for p in points.iter() {
        p += 1;
        print!("{},", p);
    }
}

Upss!!! Hata mesajına göre &i8 türüne += operatörünü uygulayamayacağımız söyleniyor. Hemen alt tarafta da bir öneri yer alıyor(İşte Rust derleyicisinin güzel yanlarından birisi daha) Konuyu daha net anlamak için iter fonksiyonunun kaynak kodlarına da bakabiliriz ancak şimdilik buna gerek yok. Referans edilen değişkeni * operatörü ile dereference ederek devam edelim.

fn main() {
    let mut points: [i8; 5] = [3, 4, 1, 8, 9];
    for p in points.iter() {
        *p += 1;
        print!("{},", p);
    }
}

Hımmm… İstediğimiz tam olarak bu değildi aslında. Görüldüğü üzere bir dizinin elemanlarını, onu dolaştığımız döngü içerisinde değiştirmek istediğimizde iter_mut isimli farklı bir fonksiyon kullanmamız tavsiye ediliyor. Aslında bu metotlar iterator deseninin uygulandığı fonksiyonlardır. Bu desen, dizi ve vector gibi veri türleri için built-in olarak zaten yazılmıştır. iter ve iter_mut fonksiyonları aşağıdaki veri yapılarını(struct) döndürecek şekilde tasarlanmışlardır.

pub struct IterMut<'a, T: 'a> {
    ptr: NonNull<T>,
    end: *mut T,
    _marker: PhantomData<&'a mut T>,
}

pub struct Iter<'a, T: 'a> {
    ptr: NonNull<T>,
    end: *const T,
    _marker: PhantomData<&'a T>,
}

struct veri türüne daha sonra geleceğiz ve hatta bu kodlarda 'a, T gibi bir takım bilmediğimiz ifadeler de var. Şimdilik bu iki veri yapısında kullanılan _marker alanları arasındaki farkı bilsek yeterli. Dikkat edileceği üzere IterMut yapısında T türünün mutable bir referans olarak ele alınması söz konusu. Diğerinde ise varsayılan kullanım olan immutable söz konusu. Şimdi kodlarımızı aşağıdaki gibi değiştirirsek sorun çözülecektir.

Rust görüldüğü üzere değişkenlerin kullanımlarında, değiştirilebilir olup olmamalarında oldukça titiz davranmakta.

Dizi ve vector gibi kullanabileceğimiz başka ardışık veri türleri de var. Standart kütüphanede yer alan HashMap bunlardan birisi. Aşağıdaki örnek kod parçasını ele alalım.

use std::collections::HashMap;

fn main() {
    let mut color_codes: HashMap<&str, u8> = HashMap::new();
    color_codes.insert("Red", 10);
    color_codes.insert("Blue", 20);
    color_codes.insert("Green", 30);

    let blue_code: u8 = color_codes.get("Blue");
    println!("Mavi renk kodu {}", blue_code);
}

Program kodunda HashMap kullanılacağını baştaki crate bildirimi ile yapmaktayız. color_codes bir HashMap ve ilk new fonksiyonu ile örnekleniyor. Ardından bu kümeye string literal ve işaretsiz 8 bit tamsayı çiftlerinden oluşan bazı örnekler ekleniyor. color_codes üstünden get fonksiyonunu kullanaraktan da mavi renk kodunu almak istiyoruz. Aslında farklı bir programlama dilinden gelen birisi gözüyle baktığımızda ortada bir sorun görünmüyor ama…

Haydi bakalım… get fonksiyonu için key karşılığı olan value değerinin yerine Option türünün döndüğü belirtilmekte. Aslında get fonksiyonunun Option enum sabiti ile çalışmasının güzel bir nedeni var. Girilen parametre ilgili koleksiyonda olmayan bir key ise normal şartlarda hata oluşması lazım. Bu nedenle get fonksiyonu Rust içinde aşağıdaki gibi tanımlanmış  bir enum sabiti döner.

pub enum Option<T> {
    None,
    Some(T),
}

Bu generic bir tür. T parametresi türünde Some isimli bir alan ve None içeriyor. None tahmin edileceği üzere yok, olmayan, bulunamadı anlamında. Bir başka deyişle HashMap’in get fonksiyonu eğer parametre olarak gelen değer veri kümesinde varsa Some(T), aksi durumda None şeklinde dönüş yapacak. Çok doğal olarak bunun gibi Option dönen fonksiyonlarda Some, None olma hallerini kontrol etmemiz gerekecek. Bu gibi durumlarda Rust’ın fonksiyonel dillerin güzide özelliklerinden sayabileceğimiz pattern matching imkanlarından yararlanabiliriz.

use std::collections::HashMap;

fn main() {
    let mut color_codes: HashMap<&str, u8> = HashMap::new();
    color_codes.insert("Red", 10);
    color_codes.insert("Blue", 20);
    color_codes.insert("Green", 30);

    match color_codes.get("Blue") {
        Some(code) => {
            println!("Mavi için renk kodu {}", code);
            println!("Burası kod bloğu. Başka şeyler de yapılabilir.")
        }
        None => println!("Renk kodunu kontrol edelim. Veri kümesinde bulunamadı"),
    }
}

match ifadesinde get çağrısından dönen Option nesnesinin olası tüm durumları ele alınmaktadır. Some, yani bir değer bulunmuşsa kısmında süslü parantezler açılmıştır. Sadece bir match dalının kod bloğu içerdiğini göstermek için. None dalında olduğu gibi tek satırlık bir ifade de kullanılabilir( => operatörünü matematikteki “ise” olarak düşünebilirsiniz) Option enum sabiti ve pattern matching içerisindeki kullanımını düşününce şunu da söyleyebiliriz; kendi enum sabitlerimizde farklı türden alanlar söz konusu olduğunda pattern matching ile olası tüm sonuçları kontrol etme şansımız vardır.

İlginç olan durumlardan birisi aslında unwrap ile veya doğrudan [] operatörü ile ilgili değerlere ulaşabilmemizdir. Yani aşağıdaki gibi.

use std::collections::HashMap;

fn main() {
    let mut color_codes: HashMap<&str, u8> = HashMap::new();
    color_codes.insert("Red", 10);
    color_codes.insert("Blue", 20);
    color_codes.insert("Green", 30);

    let blue_code: u8 = color_codes["Blue"];
    println!("Mavi renk kodu {}", blue_code);
    let red_code = color_codes.get("Red").unwrap();
    println!("Kırmızı renk kodu {}", red_code);
}

Ne var ki bu kullanımlar tehlikelidir nitekim yine hatalı renk kodu istenmesi halinde program panikleyerek sonlanır. Kodda Red yerine Redd kullandığımızda thread 'main' panicked at 'called `Option::unwrap()` on a `None` value'şeklinde bir hata mesajı ile karşılaşırız. Gerçekten de Redd’in veri setinde bir karşılığı yoktur ve bu nedenle None değeri üstünden Unwrap ile bir okuma yapılmaya çalışılmaktadır. Indeks operatörünün olduğu yerde benzer bir senaryo söz konusu olabilir. Blue yerine Bluue değerini kullandığımızda uygulama thread 'main' panicked at 'no entry found for key' şeklinde bir hata mesajı ile panikleyecek ve sonlanacaktır.

Option türünün hata yönetimi konusunda da önemli bir yeri vardır. Rust, hata yönetimini Option ve Result türleri ile sağlar. Kendi tasarladığımız fonksiyonlar dahil hataya sebebiyet verebilecek durumlarda dönüş tiplerini Result<T,Err> şeklinde belirtebiliriz. Buna göre, öngörebildiğimiz durumlarda None alanına sahip Option ile tahmin edemediğimiz veya bilhassa çalışmayı durdurmak, hata yaymak istediğimiz hallerde ise Result<T,Err> ile ilerlemek gerektiğini ifade edebiliriz. Bu arada çalışma zamanında bir hata oluşması uygulamanın panikleyerek kırılması anlamına da gelir. Dolayısıyla her tür olasılığı hesaba katmamızı sağlayan Option ve Result türleri ile çalışmak önemlidir. Result türünün Rust kütüphanesindeki tanımı aşağıdaki gibidir.

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

Görüldüğü üzere generic bir türdür. Problem olmayan durumları içeren Ok, hata halleri içinse Err alanları kullanılır. Buna göre bir fonksiyonda her şey istediğimiz gibiyse Ok alanı üstünden döndürmek istediğimiz değeri verebiliriz. Bir problem söz konusu ise built-in gelen ya da kendi yazdığımız hata tiplerinden yararlanabiliriz. Aşağıdaki örnek kod parçasını ele alalım.

fn main() {
    let rng = Range { x: 35, y: 18 };
    let check = check_range(rng);
    launch_missile(check);

    let rng = Range { x: 101, y: 90 };
    launch_missile(check_range(rng));
}

struct Range {
    x: u8,
    y: u8,
}

fn check_range(r: Range) -> Result<Range, &'static str> {
    if r.x > 100 || r.y > 100 {
        return Err("Sistem dışı koordinatlar. Yeniden güdümleyin.");
    } else {
        return Ok(r);
    }
}

fn launch_missile(result: Result<Range, &'static str>) {
    match result {
        Ok(r) => println!("Füze {}:{} konumuna yönlendirildi", r.x, r.y),
        Err(e) => println!("Hata:{}", e),
    }
}

İlk kez kendi veri yapımızı tanımladık. Range bir struct ve Rust içinde kendi veri modellerimizi tanımlamak istediğimizde kullanabileceğimiz önemli bir enstrüman. check_range isimli fonksiyon parametre olarak gelen Range tipinin değerlerine bakıp bir karar veriyor. Dikkat edilmesi gereken nokta Range dışında olunması halinde bir Err nesnesi kullanılması. Diğer durumlarda Ok ile sorun olmadığını belirtiyoruz. launch_missile fonksiyonu ise parametre olarak gelen Result türünün olası sonuçlarını pattern matching tekniği ile değerlendiriyor. Aptalca bir kod parçası ancak hata yönetimi noktasında Result tipi ile çalışmanın basit bir örneği olarak düşünebiliriz. Rust içerisinde built-in gelen fonksiyonlarda Option veya Result dönüşlerine sıklıkla rastlanıyor. Şunu da unutmayalım ki Rust, yönetimli kod(managed code) denilen bir ortama sahip değil ve bu nedenle exception handling gibi bir mekanizması ve doğal olarak try…catch blokları yok. Bu nedenle hata yönetiminin nasıl yapıldığını iyi anlamak önemli. Yani programcının olası her durumu ele alması kritik.

Bu arada fonksiyonlarda dikkatinizi çeken yabancı bir kullanım şekli olmalı. check_range ve launch_missile fonksiyon tanımlarında yer alan &’static str kullanımı. Burada lifetime adı verilen önemli bir kavram söz konusu. Hatta statik olanı. Örnekte kullanmak zorundayız nitekim hata mesajının çalışma zamanı ömrü boyunca yaşaması gerekiyor. Anlayacağınız değişkenlerin yaşam sürelerini kontrol etmemiz gereken durumlar da var. Nitekim bir değişken ömrünü tamamladığında sahiplendiği veri de otomatikman kullanılmaz hale geliyor.

Devam etmeden önce Linkerd isimli service mesh’in yaratıcısı Oliver Gould’un Why the Future of the Cloud will be Built on Rust isimli videosundan öğrendiğim güzel bir örneği de burada paylaşmak istiyorum. Rust dilinde null/nil bulunmuyor. O yüzden Option ve Result türlerinin önemi daha da artıyor. Rust bellekte belirsiz olan değerleri sevmez. Şimdi aşağıdaki Go kodlarına bir bakalım.

package main

import "fmt"

type Product struct {
    category *Category
}

type Category struct {
    name string
}

func main() {
    tavla := new(Product)
    fmt.Printf("Hello %s", tavla.category.name)
}

Category türünden bir alanı bulunan Product isimli struct’ın kullanımı örnekleniyor. Tavla nesnesini oluşturduktan sonra category alanına inip name değerini ekrana bastırıyoruz. Deneyimli bir geliştirici buradaki sıkıntıyı kolayca görebilir. İşte çalışma zamanı çıktısı.

Dikkat edileceği üzere Category nesnesini örneklemediğimizden nil bir pointer referansını kullanmaya çalışıyoruz. Bu da doğal olarak çalışma zamanı hatası anlamına geliyor. Aynı örneği Rust ile yazalım.

fn main() {
    let tavla = Product::default();
    println!("Kategorisi: {}", tavla.category.name);
}

#[derive(Default)]
struct Product {
    category: Category,
}

#[derive(Default)]
struct Category {
    name: String,
}

Bu kod çalışır ama ekrana bir kategori bilgisi yazmaz. Nitekim Default trait’leri ile bezenmiş struct’lar String alanlar için boş değer koyar. Esasında Product içindeki Category türü için Option kullanarak kodu iyileştirebiliriz.

fn main() {
    let tavla = Product::default();
    println!("{}", tavla.category.name);
}

#[derive(Default)]
struct Product {
    category: Option<Category>,
}

struct Category {
    name: String,
}

Neden? Çünkü derleyici, ataması yapılmamış bir category alanına erişmeye çalıştığımızı anlayacak ve bizi aşağıdaki derleme zamanı hatası ile pişman edecektir.

Go ise çalışma zamanında bir hata mesajı verir ki null/nil kullanılan dillerde test edilmeden çıkılan kodlar için bu önemli bir sorundur. Rust derleyicisi bunu önceden bildirir. Tabii yine de bir önceki kod parçasındaki gibi kaçamak yapılabilir. Dolayısıyla her durumda hangi dil olursa olsun test yazmak müthiş önemlidir. Gelelim yukarıdaki kodun nasıl kullanılması gerektiğine. İşte beyle…

fn main() {
    let tavla = Product::default();

    let result = match tavla.category {
        Some(c) => c,
        None => Category {
            name: String::from("Belirsiz"),
        },
    };
    println!("{}", result.name);
}

#[derive(Default)]
struct Product {
    category: Option<Category>,
}

struct Category {
    name: String,
}

Dikkat edileceği üzere Product verisindeki category alanı Option<Category> olarak tanımlanmıştır. Pattern Matching kullanarak gerçekten bir değer taşıyıp taşımadığını anlayabiliriz. Some hali bir kategori değeri olduğunda çalışırken, None dalı kategori nesnesi oluşturulmadığında işler. Dolayısıyla olası iki durumu da ele aldığımız bir kod ortaya çıkar.

Evet kafalar biraz karışıyor değil mi? Bir .Net geliştiricisi olarak pek çok şeyin kontrolünün platform tarafından sağlandığı hallere alışmış olmanın bu kafa karışıklığının sebebi olduğunu düşünüyorum. Şimdi gelin bu karışıklığın önüne geçmek için değişken sahiplik kontrolü (Variable Ownership Control) konusuna bir bakalım.

Yazının giriş bölümünde belleğin güvenli saha olarak kalmasının önüne geçecek bazı durumlarından bahsetmiştik. Rust tarafında uygulanan sahiplenme kuralları ile bu hataların önüne geçilir. En önemli kuralı şudur ki; let ile bir değişken oluşturulduğunda, bu değişkenin taşıdığı kaynak değerin sahibi sadece o değişkendir. Eğer kaynak taşınır veya başka bir değişkene yeniden atanırsa, ilk değişken kaynağın sahipliğini kaybeder. Bu ana fikri aklımızda tutarak sahiplenme kurallarına bir bakalım.

  • Bir değerin(value) sahibi kendisine atanan değişkendir(variable) ve bir değişken kapsam dışına çıktığı zaman işgal ettiği bellek alanından atılır.
  • İlk kurala göre bir değişkenin sahiplendiği değerin atamalar sonrası diğer bir değişken tarafından nasıl kullanılabileceği sorusu ortaya çıkar ki program yazarken bu tip ihtiyaçlar son derece doğaldır. Rust’ın sunduğu teknikler şunlardır; Copy, Move, Immutable Borrow, Mutable Borrow.
    • Copy tekniğine göre değer kullanılacağı diğer değişken için kopyalanır. Kopyalamadan sonra her iki değişken de kendi değerleri ile çalışmaya devam eder.
    • Move yönteminde değer yeni değişkene taşınır ancak klonlamadan farklı olarak orijinal değişken artık bu değere sahip değildir!
    • Immutable Borrow durumuna göre bir değişken başka bir değişkenin değerini referans edebilir. Lakin değeri ödünç alan değişken kapsam dışına çıkarsa, referans ettiği değerde bir mülkiyet hakkı olmadığı için değer bellekten düşürülmez.
    • Son olarak mutable borrow durumuna göre başka bir değişkenin değeri referans alındıktan sonra içeriği değiştirilebilir. Diğer değişkenin değerini ödünç alan değişken kapsam dışına çıkarsa, aynen Immutable Borrow senaryosunda olduğu gibi değeri ödünç alan değişkenin referans üstünde mülkiyeti olmadığından değer bellekten düşürülmez.

Son iki durumda ortaya çıkabilecek bir sorun da vardır. Bunu lifetime ile ilgili kısımda göreceğiz. Kurallara alışmak çok kolay olmamakla birlikte derleyicinin akıllı ipuçları epeyce yol göstericidir. Kuralları daha iyi görmek için scope(kodda süslü parantezler arasını tarif ettiğimiz alanlar) konusuna da kısaca değinmemiz gerekir. Nitekim değişkenler sıklıkla farklı scope’lara girip çıkar. Örneğin metotlara parametre olarak gidip işlenirken. Şimdi aşağıdaki kod parçasını göz önüne alalım.

fn main() {
    let intro: String = String::from("Wellcome to the jungle!");
    {
        println!("{}", intro);
        let status: String = String::from("All is well ;)");
    }
    println!("{}", intro);
    println!("{}", status);
}

Aslında sonuçta şaşılacak pek bir şey yok. intro isimli değişken iç blok dışında tanımlanmış olduğundan hem blok içinde hem de dışında kullanılırken, status için aynı şeyi söylememiz mümkün değildir. Nitekim sadece iç blok içerisinde tanımlı olan bir değişkendir. String veri türünün bilhassa seçildiği bir örnek aslında bu. Olayı daha ilginç hale getirmek için bu kez değişkenin bir fonksiyona aktarıldığı aşağıdaki senaryoyu ele alalım.

fn main() {
    let intro: String = String::from("Wellcome to the jungle!");
    get_count(intro);
}

fn get_count(text: String) -> usize {
    let count = text.len();
    println!("'{}' cümlesindeki karakter sayısı {}", text, count);
    count
}

Pek anlamı olmayan bir kod parçası ama çalışıyor. Şimdi bir satır daha ekleyip yeniden çalıştıralım.

fn main() {
    let intro:String=String::from("Wellcome to the jungle!");
    get_count(intro);
    println!("{}",intro);
}

Upss! Hata mesajına göre String türünün Copy isimli bir trait’i kendisine uyarlamadığı söyleniyor. Bunun sebebi String nesnesinin get_count metoduna taşıma suretiyle aktarılması ve ilk değişkenin geçersiz hale gelmesi. Hatırlanacağı üzere bir değerin sahibi tek bir değişken olabilirdi. Trait’ler ile aslında türlere kazandırabileceğimiz davranışları tanımlarız. Eğer String türü Copy trait’ini uyarlamış olsaydı fonksiyona kopyalanarak aktarılabilir ve dolayısıyla bu ihlal gerçekleşmezdi.

Peki built-in olarak gelen bu türe Copy trait’i neden adapte edilmemiş? Yazının başlarında bir yerlerde String türünün aslında heap alanındaki asıl veriyi tutan byte cinsinden bir vector serisini işaret ettiğini söylemiştik. Rust kurallarına göre bir değere birden fazla referans olması kural ihlali sayılır. Örnekte intro isimli değişken fonksiyona taşındığında(move) yeni bir scope içerisine dahil olur ve dolayısıyla yok edilir ki yine kurallara göre scope dışına çıkan değişkenler hemen bellekten düşürülürler. Eğer değişkeni metoda taşıdıktan sonra ilk referansını korunmaya devam edersek de, sonraki anlarda bu çoklu referansların boşaltılmış olması ve başka şeyler içeren bellek bölgelerini işaret etmemesi garanti edilemez(unsafe olma hali)

Diğer yandan kodda hatanın vuku bulduğu yere de dikkat etmek gerekir. println! fonksiyonunun ilgili değişkeni ödünç almak(borrowing) istediği belirtilir. Halbuki ödünç almak istediği değişken move ile başka bir scope içerisine taşınmış ve doğal olarak kullanılamaz haldedir. Şimdi ödünç alma kavramı daha anlamlı hale gelecek. Bazı durumlarda değişken değerlerini ödünç verebiliriz. Bunun için $ operatörü kullanılabilir. Aynı örneği borrow özelliği ile donatalım.

fn main() {
    let intro: String = String::from("Wellcome to the jungle!");
    get_count(&intro);
    println!("{}", intro);
}

fn get_count(text: &String) -> usize {
    let count = text.len();
    println!("'{}' cümlesindeki karakter sayısı {}", text, count);
    count
}

Herhangi bir sorun olmadığı görülebilir. Nitekim intro değişkeni get_count fonksiyonuna referans suretiyle ödünç verilir. get_count fonksiyonu değer üstünde bir değişiklik yapmaz. Yapmak istese de yapamaz. Gelin aşağıdaki kod parçası ile bu duruma da bakalım.

fn main() {
    let intro: String = String::from("Wellcome to the jungle!");
    get_count(&intro);
    println!("{}", intro);
}

fn get_count(text: &String) -> usize {
    let count = text.len();
    text.push("?".chars().next().unwrap());
    println!("'{}' cümlesindeki karakter sayısı {}", text, count);
    count
}

Uygulamayı çalıştırınca çıktı aşağıdaki gibi olur.

Ödünç alınan değer üstünde değişiklik yapmaya çalışıyoruz. Ancak borrow işlemi varsayılan olarak immutable’dır. Değeri alabilir, okuyabilir ama değiştiremezsiniz. Sahiplenme kurallarında da belirttiğimiz üzere Mutable Borrowing burada çözüm olarak kullanılabilir.

fn main() {
    let mut intro: String = String::from("Wellcome to the jungle!");
    get_count(&mut intro);
    println!("{}", intro);
}

fn get_count(text: &mut String) -> usize {
    let count = text.len();
    text.push("?".chars().next().unwrap());
    println!("'{}' cümlesindeki karakter sayısı {}", text, count);
    count
}

Dikkat edileceği üzere main içerisindeki intro değişkeni mutable olarak değiştirilmiştir. Nitekim değişken get_count fonkisyonu içerisinde değiştirilmek istenmektedir. Ayrıca değer bu fonksiyonun kullanması için ödünç verilirken değiştirilebilir olarak işaretlenmiştir(&mut bildirimi ile) Bir de integer, float gibi stack bellek bölgesinde duran verileri ele alacağımız aşağıdaki aptal kod parçasına bakalım.

fn main() {
    let score = 5_i8;
    print_number(score);
    println!("{}", score);
}

fn print_number(point: i8) {
    println!("{}", point);
}

Dikkat edileceği üzere score değişkeni print_number fonksiyonuna aktarılmış ve dönüşte yine main içerisinde kullanılabilmiştir. String türünü kullandığımız örnekte meydana gelen ihlal burada yaşanmamıştır. Bu son derece doğaldır nitekim integer gibi türler Copy trait’ini uyarlarlar. Dolayısıyla fonksiyonlara kopyalanarak alınırlar. Bu türler stack bellek bölgesinde olup ne kadar yer kapladıkları bilindiğinden kopyalanarak alınmalarında sıkıntı yoktur. Referans türü olduğunda ise sahipliğin ödünç verilmesi ve bu sayede pahalı heap maliyetinin düşürülmesi ve güvenli sahanın korunması esastır. Pek tabii integer bile olsa değerleri referans usulüyle fonksiyonlara taşıyabiliriz. Eğer değerleri bu fonksiyonlar içerisinde değiştirmek istiyorsak, mutable borrowing kuralına uymamız gerekir. Aşağıdaki örnek kod parçasında bu durum görülebilir.

fn main() {
    let mut score = 5_i8;
    print_number(score);
    increase_one(&mut score);
    print_number(score);
    increase_one(&mut score);
    println!("{}", score);
}

fn increase_one(point: &mut i8) {
    *point += 1;
}

fn print_number(point: i8) {
    println!("{}", point);
}

String kullanılan örnekteki gibi &mut operatörü ile referansın mutable olarak alınması ve aktarılması söz konusudur. Doğal olarak increase_one fonksiyonun içinde score değeri değiştirildikçe, main içerisindeki asıl değer de değişir. increase_one fonksiyonunda birde * operatörü kullanıldığı gözünüzde kaçmamış olsa gerek. Bu operatör dereference anlamına gelir. 

Scope Rust için No Memory Leak garantisi anlamına da gelir. Aşağıdaki kod parçasını göz önüne alın.

static mut number: u8 = 7; // global variable
fn main() {
   println!("Number is {}",number); // use of mutable static is unsafe and requires unsafe function or block
}

Tanımlanan global değişkenin bir scope içerisinde kullanılması Rust derleyicisini rahatsız eder. Gerçekten bunu istiyorsanız unsafe bir blok açmanız istenir.

unsafe {
   println!("The number is {}", number);
   number += 1;
   println!("The new number is {}", number);
}

Değişken sahipliğinin ödünç verilmesi ile ilgili dikkat edilmesi gereken bir husus da ölü bir değişkene referans verilme riski taşımasıdır. Burada yine kafaları karıştırabilecek bir senaryo var. Aşağıdaki örnek kod parçasını ele alalım.

fn main() {
    let outer;
    {
        let inner = 1.2345;
        outer = &inner;
    }
    println!("{}", outer);
}

İlk değeri olmayan outer isimli bir değişkenimiz var ve takip eden scope içerisinde inner isimli başka bir değişken tanımlanıyor. Sonrasında inner’dan outer’a doğru bir atama görülüyor. Burada sahiplik referans usulü ile ödünç veriliyor. main fonksiyonundan çıkmadan önce ise outer değeri ekrana basılıyor. Basacağını ümit ediyoruz. main içinde bir scope açtığımız için kodun çalışmasına dair zihninizde birtakım şüpheler oluşmuştur eminim ki. Çalışma zamanı çıktısı da bu şüphelerinizi doğrulayacaktır.

Sorun şu ki inner isimli değişken iç kapsam sona erdiğinde ölür ve hemen bellekten düşürülür. Lakin main fonksiyonu kapsamında yaşayan outer onun değerini ödünç almıştır ve pek tabii println! makrosuna gelindiğinde bu referans artık yoktur. Derleyici haklı olarak inner değişkeninin yeteri kadar uzun yaşamadığını söyleyerek yakınır(Hey programcı ne yaptığına dikkat et!) Görüldüğü üzere son derece basit bir kod parçası ama Rust’ın bir kural ihlaline takılmış durumda. Yani değişkenlerin yaşam ömürleri scope’lara bağlı olarak program çalıştığı müddetçe geçerli olmayabilir. Şimdi aşağıdaki kod parçasını göz önüne alalım.

fn main() {
    let mine = 6;
    let yours = 7;
    let result = find_greatest(&mine, &yours);
    println!("{}", result);
}

fn find_greatest(x: &i8, y: &i8) -> &i8 {
    if x > y {
        return x;
    } else {
        return y;
    }
}

Normal şartlarda find_greatest fonksiyonuna değerleri referans olarak değil de & kullanmadan normal olarak taşıyabilirdik. Teorik olarak bir sorun olmamasını bekleyebilirsiniz. Hatta bir önceki paragraftan hiç bahsetmeseydik kodun çalışacağından yüzde yüz emin olabilirdiniz. Ne var ki derleme zamanında aşağıdaki hatayı alırız.

Bir lifetime parametresi bekleniyor. Üstelik nasıl uygulanması gerektiğine dair bir öneri de help kısmında yer alıyor. Rust’ın derleyici hatalarına istinaden bulunduğu öneriler yazılımcıya epeyce yardımcı oluyor. Gelelim koddaki soruna. mine ve yours değişkenlerini fonksiyona referans olarak verdiğimizde doğal olarak o scope içerisine ödünç veriliyor ve fonksiyon tamamlandığında ölüyorlar. Sorun şu ki fonksiyon geriye bir referans dönüyor ve aslında if koşulu sebebiyle parametrelerden hangisinin döneceği ve dolayısıyla scope dışına çıkıldığında hangisinin yaşamaya devam etmesi gerektiği belirsiz. Bu kullanıma göre bizim açık bir şekilde değişkenlerin yaşam sürelerini belirtmemiz gerekiyor. Hatta bunu yaparken hepsini aynı lifetime değerine bağlayabiliriz.

fn main() {
    let mine = 6;
    let yours = 7;
    let result = find_greatest(&mine, &yours);
    println!("{}", result);
}

fn find_greatest<'a>(x: &'a i8, y: &'a i8) -> &'a i8 {
    if x > y {
        return x;
    } else {
        return y;
    }
}

'a ile lifetime parametresi belirliyoruz. Buna göre x,y ve find_greatest fonksiyonunun dönüş referansı aynı yaşam sürelerine sahip olacak. Lifetime parametrelerinde genellikle a,b,c gibi harfler kullanılmakta ve aslında birden fazla farklı yaşam ömrüne sahip kullanımlar da mümkün. Aynı örnek üzerinden aşağıdaki gibi bir kullanımı ele alalım.

fn main() {
    let mine = 8;
    let yours = 7;
    let result = find_greatest(&mine, &yours);
    println!("{}", result);
}

fn find_greatest<'a, 'b>(x: &'a i8, y: &'b i8) -> &'a i8 {
    if x > y {
        return x;
    } else {
        return y;
    }
}

Bir cinlik yapıp, bilerek ve isteyerek mine değerini yours değerinden büyük verdik. Eh ne de olsa find_greatest metodu x ile gelen yaşam ömrü kadar ömrü olan bir sonuç dönecek. Yemezler :P

Yaşam süreleri ile ilgili olarak Rust derleyicisi ömrü en kısa olana göre hareket etmek ister. Çünkü derleyici, çalışma zamanının mümkün olduğunca az değişkenle uğraşmasını ve referansların gereksiz yere yaşamamasını tercih eder. Bu, kaynakların etkin kullanımı açısından da önemlidir. Lifetime parametrelerini yazmak değil ama hangi durumda nerede kullanılması gerektiğine karar vermek ilk başlarda hiç kolay değil. Ancak Rust’ın standart kütüphane kodlarına bakaraktan da bu konuda çok şey öğrenilebilir. Mesela vector türünün iteratsyon desenini uyguladığı noktada lifetime parametreleri vardır. Şuradan kaynak kodlarını inceleyin derim.

Nesne yönelimli dillerde yaşayan birisi olarak insanın gözleri çoğu zaman sınıfları arıyor. Aslında veri yapısı(data structure) olarak düşünmeyi çoktan unuttuk gibi. Bir veri yapısı çoğunlukla modellemeler için gerekli tüm ihtiyacı karşılar. Adı üstünde verinin yapısını tanımlar. İsterseniz onu fonksiyonlarla donatıp aksiyonlar yükleyebilirsiniz. Normal de C# ile bir sınıf yazıp içerisine o sınıf için gerekli metotları koyarak ilerleriz. Nadiren struct tasarlarız. Rust tarafında sınıf yoktur ve tek kullanacağınız şey struct’tır. Bir struct tanımlarken gerçekten veri yapısı olarak inşa edersiniz. Sonrasında fonksiyonlar ekleyebilirsiniz. Vector kullandığımız kısımda aslında çok basit bir struct kullanmıştık. Yine de line of business insanlarını kırmayalım ve struct konusuna da kısaca bakalım. Aşağıdaki örnek kod parçası ile başlayabiliriz.

struct Product {
    title: String,
    price: f32,
    unit_count: i32,
}

fn main() {
    let keyboard = Product {
        title: String::from("ElCi 103 tuş klavye"),
        price: 99.99,
        unit_count: 6,
    };
    println!("{} ({})", keyboard.title, keyboard.price);
    set_price(keyboard, 95.55);
    println!("{} ({})", keyboard.title, keyboard.price);
}

fn set_price(mut p: Product, price: f32) -> Product {
    p.price = price;
    p
}

Product isimli veri yapısı sembolik olarak bir ürünü temsil etmekte. İçinde çok az alan var. String türünden title, 32 bit float türünden price ve 32 bit integer türünden unit_count. set_price isimli metot bir Product değişkenini alıp fiyatını gelen parametreye göre değiştiriyor ve güya kendisini geriye döndürüyor. main fonksiyonu içerisinde keyboard isimli Product türünden bir değişken tanımlıyoruz. Ardından set_price fonksiyonunu kullanarak ürün fiyatını değiştiriyoruz. Değişimden önce ve sonra ise ürünün birtakım bilgilerini ekrana yazdırıyoruz. Aslında sade bir senaryo ve doğal koşullar altında bir problem olmadan çalışmasını bekliyoruz. Şimdi derleme zamanı sonuçlarına bir bakalım.

İlk örneklerimizde literal str kullandığımızda da benzer bir hata mesajı almıştık. Tasarladığımız struct Copy trait’ini uygulayarak bu işin üstesinden gelebilir ancak biz temel sorunun ne olduğuna bakalım. Keyboard değişkeni, set_price fonksiyonuna alındıktan sonra scope değiştirmiş olur ve fonksiyon sonlandığında da doğrudan silinir. Bu nedenle set_price çağrısı sonrası artık ortada kullanılabilir bir keyboard değeri kalmayacaktır. Kullanabileceğimiz yolları düşünürsek değeri mutable referans olarak taşıyarak ilerleyebileceğimi anlarız. Yani aşağıdaki kod parçasın olduğu gibi.

struct Product {
    title: String,
    price: f32,
    unit_count: i32,
}

fn main() {
    let mut keyboard = Product {
        title: String::from("ElCi 103 tuş klavye"),
        price: 99.99,
        unit_count: 6,
    };
    println!("{} ({})", keyboard.title, keyboard.price);
    set_price(&mut keyboard, 95.55);
    println!("{} ({})", keyboard.title, keyboard.price);
}

fn set_price(p: &mut Product, price: f32) -> &Product {
    p.price = price;
    p
}

Dört yerde değişiklik yaptık. set_price fonksiyonunun parametre ve dönüş türünde, keyboard değişkeninin tanımlanmasında ve set_price fonksiyonunun çağırılmasında. Çalışma zamanı çıktısına bir bakalım.

Bu arada kullanmadığımız unit_count alanı için bir de uyarı verdiğini görebilirsiniz. Ayrıca bu tip kullanımlarınız var ve uyarılar çıkmasın istiyorsanız(ki dili öğrenirken bazen çıktıyı sadeleştirmek için gerekiyor) dead_code kullanımına izin vererek ilerleyebilirsiniz. Nasıl yapılacağına dair bir ipucunu derleyici note alanında belirtiyor. Aslında struct türünü kullanırken onu gerçekten bir veri yapısı olarak tasarlarız. Onunla ilişkilendirmek istediğimiz fonksiyonları ise yukarıdaki gibi değil aşağıdaki örnek kod parçasında olduğu gibi yazarız.

enum Status {
    High(i32),
    Normal(i32),
    Low(i32),
    Note(String),
}

struct Product {
    title: String,
    price: f32,
    unit_count: i32,
    status: Status,
}

impl Product {
    fn new(t: String, prc: f32, c: i32, s: Status) -> Product {
        Product {
            title: t,
            price: prc,
            unit_count: c,
            status: s,
        }
    }
    fn discount(&mut self, rate: f32) {
        self.price -= self.price * rate;
    }
    fn to_string(&self) -> String {
        format!(
            "{}. Fiyat: {}. Stok miktarı: {}",
            self.title, self.price, self.unit_count
        )
    }
}

fn main() {
    let mut keyboard = Product::new(
        String::from("ElCi 103 tuş klavye"),
        59.99,
        9,
        Status::Low(9),
    );
    println!("{}", keyboard.to_string());
    keyboard.discount(0.10);
    println!("{}", keyboard.to_string());
}

Biraz uzun bir kod parçası gibi görünebilir ama kısaca neler yaptığımızı açıklamaya çalışalım. Product veri yapısını bir enum tipi ile genişlettik. Enum tanımı içerisinde farklı türden sabitler barındırabiliriz. Söz gelimi Status enum sabitindeki High, Low ve Normal alanları i32 tipinden değerlerde almaktadır. Stok seviyesini miktarı ile tutabileceğimiz bir sabit değer olarak düşünebiliriz. Diğer yandan hepsinden farklı bir durum için Note isimli String türünden bir alan kullanılabilir. main fonksiyonunda keyboard değişkenini oluştururken Status alanını nasıl atadığımıza dikkat edin.

Genellikle nesne yönelimli dünya insanı için gerçek hayattaki bir şeyin kod tarafındaki soyut tasarımı sınıflar ile yapılır. Sınıflar, yapıcı metotlar ve farklı türde işlevler barındırır. Rust dilinde struct türünden bir nesneyi constructor ile oluşturmak aslında bir fonksiyon çağrısından başka bir şey değildir. Genel olarak new isimli bir fonksiyon kullanılır ve ilgili struct’ı geriye döner(İsmi new olmak zorunda değil, “init” diye de verebilirsiniz “yeni” de ancak genel jargona uymakta yarar var)

Bir Struct ile ilişkilendirilecek metotlar impl ile başlayan bloklarda tanımlanır. Örnekteki fonksiyonlarda dikkat çekici unsurlardan birisi de &mut self ve &self kullanımlarıdır. self ile tahmin edeceğiniz gibi struct’ın çalışma zamanındaki örneğine ait referansı işaret ediyoruz. discount fonksiyonunda fiyat bilgisini değiştirmeye çalıştığımız için mutable bir kullanım söz konusu(Varsayılan olarak her şey immutable unutmayalım)

Peki ya discount ve to_string fonksiyonlarında neden & operatörünü kullandık? Onları kaldırıp kodu denemeden sebebini düşünmeye çalışın. Tahmin edeceğiniz üzere konu dönüp dolaşıp sahipliklere gelecek. to_string ve discount fonksiyonlarındaki & operatörlerini kaldırınca aşağıdaki derleme zamanı hataları ile karşılaşırız.

Dolayısıyla discount ve to_string metotlarında değişkenleri alırken sahipliklerini geçici olarak vermeliyiz ki kodun akışında kullanmaya devam edelim. Aksi durumda fonksiyon kapsamına giren değerler çıkışta öleceğinden main fonksiyonunun devamlılığında kullanılamaz hale geleceklerdir.

Yazı boyunca birkaç kez Copy trait’ini uygulamadın diye derleyicinin hışmına uğradığımız yerler oldu. Trait’ler ile ilgili olarak türlere yeni davranışların kazandırılması noktasında kullanılabilirler demiştik. Ayrıca kod tekrarının önüne geçilmesi, yürütücü parçaların beklediği davranışların entegre edilmesinde de kullanılırlar. Söz gelimi Product nesnesine Copy trait’ini uyarladığımızda Rust derleyicisi otomatik olarak fonksiyon atamalarında kopyalama yöntemi ile aktarımı kullanacaktı.

Diğer yandan sıklıkla farklı veri yapılarının aynı fonksiyonellikleri kullandığı senaryolarla karşılaşırız. Bu gibi durumlarda kod tekrarının önüne geçmek için Trait’ler kullanılabilir. Varsayılan bir davranış sergilerler ve bu davranışlar veri modelini donatabilir ya da veri modeli için bu davranışı yeniden güdümleyebiliriz(Olaya C# tarafından bakınca bunu virtual metot kullanmaya ve override etmeye benzetiyorum. Tüm nesnelerin ToString metodu vardır ama istersen onu ezip kendi türün için farklılaştırabilirsin) Trait’lerle ilgili olarak aşağıdaki örnek kod parçasını ele alalım.

trait AllowDelete {
    fn delete(&self) {
        println!("Silme ile ilgili işlemler.");
    }
}

trait AllowEdit {
    fn edit(&self) {
        println!("Düzenleme ile ilgili işlemler.");
    }
}

struct Action {
    id: u8,
    name: String,
}

impl AllowDelete for Action {}
impl AllowEdit for Action {}

fn main() {
    let parallelizer = Action {
        name: String::from("Paralel Evren İşçisi"),
        id: 1,
    };
    println!("{}-{}", parallelizer.id, parallelizer.name);
    worker(¶llelizer);
    parallelizer.edit();
}

fn worker<T: AllowDelete>(object: &T) {
    object.delete();
}

Öncelikle çalışma zamanı çıktısına bir bakalım sonra da kodu yorumlayalım.

AllowDelete ve AllowEdit isimli iki trait tanımı var. Bunların içerisinde de varsayılan metotlar söz konusu. Action isimli struct için bu trait’lerin kullanılacağı belirtiliyor. Şimdi main fonksiyonu içerisine bir bakalım. parallelizer isimli Action değişkeni worker fonksiyonuna gönderiliyor. worker fonksiyonunun tanımına dikkat edersek C# tarafındaki generic T tipi gibi bir kullanım söz konusu. Üstelik koşulu da var. Koşula göre T türü AllowDelete davranışını uyarlamış olmalı. Dolayısıyla worker fonksiyonu AllowDelete davranışını taşıyan herhangi bir tür için kullanılabilir. Türe eklenen bir trait’i doğrudan çağırmak da mümkün. Bu yüzden parallelizer değişkeni üstünden edit fonksiyonunu doğrudan kullanabiliriz. İstersek bu varsayılan davranışları değiştirmek de mümkün. Örneğin,

impl AllowDelete for Action {
    fn delete(&self){
        println!("Ben biraz daha farklı çalışmak istiyorum.")
    }
}
impl AllowEdit for Action {}

Pek tabii buraya kadar öğrendiklerimiz oldukça az. Girizgâh olarak yeterli gibi ama her birinin çok daha fazla detayı var. Özellikle Rust’ın built-in tasarım kodlarına bakınca öğrenilmesi gereken çok şey olduğunu daha net görebiliyorsunuz. Benim acelem yok o yüzden mevzuya geniş zamana ayırıp öğrenmeye devam edeceğim. Pek tabii bol bol kod pratiği yapmakta yarar var. Aldığım notları burada sonlandırmadan önce işinize yarayacak birkaç kaynağı da paylaşmak isterim.

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

Rust Pratikleri - Loglama

$
0
0

Bugün bir redis, rabbitmq, kafka sunucusu başlattığımızda ya da docker container içerisine komut satırı açtığımızda terminal ekranına akan sayısız log içeriği olduğunu görüyoruz. Bu loglar hataları görmek, kodun akışını izlemek ve uyarıları çabucak fark etmek açısından sistem programcıları için son derece kıymetli bilgilerden oluşuyor. Çok doğal olarak Rust ile yazılan uygulamalar içinden de log yayınlamak isteyebiliriz ki Rust’ın asıl odağının sistem programlama olduğunu düşünecek olursak bu gereklidir. Rust Pratiklerinin bu ilk bölümünde log ve env_logger küfelerini kullanarak basit anlamda loglamanın nasıl yapıldığını öğreneceğiz.

Normalde bir kütüphane geliştiriyorsak sadece log paketini kullanmak yeterlidir. Ancak çalıştırılabilir bir uygulamadan log basmak istersek onu implemente eden crate'leri kullanmamız gerekir. Façade görevi üstlenen bu tetikleyiciler farklı ortamlara log basma kabiliyetlerine de sahiptir. İzleyen örnekte minimal olanlardan env_logger kullanılıyor. Ancak daha karmaşık sistemler için log4rs(log4net gibi düşünün), web assembly'lar için console_log, android, windows, unix gibi ortamlar için de android_log, win_dbg_logger, syslog vb kütüphaneler de var.

Örneğe ait kodlara rust-farm github reposundan ulaşabilirsiniz.

Senaryoda kobay bir Voyager veri yapısı ve üzerinde uygulanan birkaç fonksiyon yer alıyor. Fonksiyonların bazı noktalarına log mekanizması serpiştirilmiş durumda. Amaç terminal ekranına çeşitli türde log basılmasını sağlamak. Örneğimizi aşağıdaki terminal komutları ile oluşturarak devam edelim.

cargo new logging
touch src/lib.rs

İlk olarak toml dosyasına gerekli crate bildirimlerini ekleyelim.

[package]
name = "logging"
version = "0.1.0"
edition = "2021"

[dependencies]
# library'ler için log paketi yeterlidir,
log="0.4.14"
# ancak executable programlarda log'un dışarıya çıkartılması gerekir.
# log paketinin farklı implementasyonları bu amaçla kullanılır.
# örneğin env_logger.
env_logger = "0.9.0"

lib.rs dosyasının içerisinde Voyager isimli kobay bir struct ve onunla ilişkilendirilmiş çeşitli fonksiyonlar yer alıyor.

// log paketinden kullanacağımız macro'lar için gerekli bildirimler
use log::{debug, error, info, trace, warn};

#[derive(Debug)]
pub struct Voyager {
    pub life: u8,
    pub nickname: String,
    pub universe: String,
    pub is_active: bool,
}

impl Voyager {
    pub fn new(nickname: String) -> Self {
        // debug türünden bir log bırakıyoruz
        debug!(target:"app_events","Oyuna {} isimli bir gezgin katılıyor.",nickname);
        Voyager {
            nickname,
            ..Default::default()
        }
    }

    pub fn connect(&mut self, universe: String) {
        if !self.is_active && self.life > 0 {
            // info türünden bir log bırakıyoruz
            info!(target:"app_events","{}, {} evrenine bağlanıyor",self.nickname,universe);
            self.is_active = true;
            self.universe = universe;
        }
    }

    pub fn hited(&mut self) {
        self.life -= 1;
        // warn türünden bir log bırakıyoruz
        warn!(target:"app_events","{} vuruldu ve {} canı kaldı.",self.nickname,self.life);

        if self.life == 0 {
            // error türünden bir log bırakıyoruz
            error!(target:"app_events","{} ne yazık ki tüm canlarını kaybetti. Bağlantısı kesiliyor",self.nickname);
            self.is_active = false;
        }
    }
}

impl Default for Voyager {
    fn default() -> Self {
        let voyager = Voyager {
            life: 3,
            is_active: false,
            universe: String::from("nowhere"),
            nickname: String::from("unknown"),
        };
        // trace türünden bir log bırakıyoruz
        trace!(target:"app_events","Gezgin için varsayılan değerler yükleniyor.{:?}",voyager);
        voyager
    }
}

Voyager'ı çeşitli evrenlere bağlanan bir gezgin olarak düşünelim. Kolayca örneklemek için new fonksiyonuna sahip. Bir evrene bağlanmak için connect ve vurulduğunda can sayısını azaltmak için hited isimli iki fonksiyonu daha var. Ayrıca to_string kullanımı için önerilen Default trait'ini de uyarlamakta. Voyager ve bağlı fonksiyonlarında yer yer loglama yapıldığını görebiliriz. Logun seviyesine göre uygun bir macro fonksiyonu kullanılmaktadır. Uyarı mesajları için warn!, bilgilendirici notlar için info!, debug çıktıları için debug!, hata durumları için error! ve izleme operasyonları için trace! makrolarından yararlanıyoruz. Gelelim main dosyamıza. Onu da aşağıdaki şekilde kodlayarak devam edebiliriz.

use logging::Voyager;

fn main() {
    // önce loglayıcıyı oluşturalım
    env_logger::init();

    let mut gemini = Voyager::new(String::from("Gemini"));
    println!("{}\n", gemini.nickname);
    gemini.connect(String::from("Andromeda"));

    for _ in 0..3 {
        println!("{:?}", gemini);
        gemini.hited();
    }

    gemini.life = 1;
    gemini.connect(String::from("Orion"));
}

Önemli olan nokta env_logger için init fonksiyonunun çağırılmasıdır. Bu sayede terminal ortamına log atmak için gerekli ortam hazırlanmış olur. Kodun ilerleyen kısımlarında Voyager türünden bir değişken oluşturup üzerinde bazı işlemler uyguluyoruz. Önce Andromeda galaksisine seyahat eden gemini yolda birkaç kez vuruluyor. Derken program insafa gelip ona bir hak daha veriyor ve o da bunu Orion galaksisine giderek değerlendiriyor. Bu aşamadan sonra ilk olarak clippy ile ne kadar ideomatic kod yazdığımıza bakmakta yarar var. Eğer uyarılar varsa buna göre kodu düzeltmemiz iyi olacaktır. Ardından run komutu ile örneği çalıştırabiliriz. Ancak ekrana istediğimiz log bilgileri akmayacaktır. Log okumak için RUST_LOG komutundan yararlanılır. Terminalden yapacaklarımızı aşağıdaki şekilde özetleyebiliriz.

# önerileri alıp kodu toparlamak için
cargo clippy
# library'nin başarılı şekilde build olup olmadığını görmek için
cargo build --lib

# varsayılan çalıştırmada sadece ERROR Logları görünür
cargo run

# log çıktılarını okumak için farklı yollar kullanabiliriz.
# warn ve error mesajlarını gösterir
RUST_LOG=warn cargo run

# trace ile birlikte debug,warn,info,error mesajlarını alırız
RUST_LOG=trace cargo run

Sonuç itibariyle aşağıdaki renkli ve iç ısıtan çıktıyı alabilmemiz gerekiyor.

Görüldüğü üzere log ve env_logger küfelerini kullanarak terminale log bırakmak oldukça pratik. Bir başka rust pratiğinde görüşmek dileğiyle, sağlıklı günler.

Rust Pratikleri - Multithreading

$
0
0

Uygulamalar işletim sistemlerince Process olarak ayağa kaldırılırlar. Bir process içerisindeki işleri birbirlerinden bağımsız olarak yapan thread'ler de söz konusu olabilir. Çoğu zaman çalıştırılabilir programın main fonksiyonu ile akan akış tek bir thread ile işleyişini sürdürür ama ihtiyaç dahillinde yeni thread'ler açmak gerekir. Rust için process içerisinde bir thread açmak oldukça kolaydır ve bellek tüketimi açısından maliyeti düşüktür. Ownership ve borrowing kuralları sayesinde bellek sahası güvende kalır ve özellikle data-race sorunları oluşmaz.

Nitekim bir veri parçasının sadece tek bir sahibi olabilir ve bu kural thread'ler için de geçerlidir. Üstelik aynı Process içerisindeki thread'ler birbirleriyle kolayca haberleşebilirler (channels konusunda bakarız) Şu da bir gerçek ki çok uzun zamandır birden fazla çekirdeğe sahip işlemcilerin olduğu sistemlerde çalışıyoruz. Bu işlemcilerdeki her bir çekirdek(core) belli bir anda tek bir thread işletebilir. Dolayısıyla programlarımızdaki thread'leri bu işlemci çekirdeklerine verip bir takım işlerin eş zamanlı çalıştırılmasını da sağlayabiliriz ki bu Parallel Processing olarak da bilinir. Ancak oraya gelmeden önce Rust dilinde thread'leri nasıl kullanırız pratik anlamda bilmemiz gerekiyor. İzleyen örnek Rust dilinde bir thread nasıl oluşturulur ve kullanılır sorusuna en basit haliyle cevap vermeye çalışır.

Senaryoda aynı öğrenci evinde kalan üç kafadar vardır. O güzel güneşli cumartesi gününün akşamında basketbol milli takımımızın maçını izlemek için misafirleri gelecektir. Zaman azdır. Karadenizli Dursun'un pazartesi günü gireceği Lineer Cebir sınavı vardır ama Danimarkalı Yensen ile Yeni Zellandalı Gibsın şimdilik boştadır ve oturma odasında tavla oynamaktadırlar. Dursun ders çalışırken Gibsın kendisine verilen listedekileri almak üzere alışverişe çıkabilir ve Yensen'de evi köşe bucak toparlayıp temizleyebilir. Esasında Yensen, Gibsın ve Dursun belli bir müddet birbirlerinden bağımsız şekilde hareket edip aksiyon alabilirler. Dursun dersini çalışmaya devam ederken, Gibsın alışverişi yapabilir ve Yensen'de evi süpürebilir. İşte size 3 tane thread. Şimdi sıra bu işleyişi programlamakta. İşe aşağıdaki terminal komutları ile başlayabiliriz.

Örneğe ait kodlara rust-farm github reposundan ulaşabilirsiniz.

cargo new fellowship
cd fellowship
touch src/jhensen.rs
touch src/gibson.rs
touch src/dursun.rs
touch src/common.rs

fellowship isimli çalıştırılabilir uygulamada Dursun, Yensen ve Gibsın için ayrı birer modül dosyası yer almakta. Çıktıları izlemek için bir önceki pratikte olduğu üzere loglama modülünü kullanabiliriz. Thread'lerin uzun süren işleri simüle etmesi için yardımcı bir fonksiyonumuz da var, common.rs. Çok yaratıcı bir isim değil ama şimdilik idare eder.

common.rs

use std::thread;
use std::time::Duration;

/// Örnekte thread'leri belli süre durdurup uzun çalışmaları simüle etmek içindir.
pub fn sleep_while(seconds: f32) {
    thread::sleep(Duration::from_secs_f32(seconds));
}

Tek yaptığı parametre olarak gelen süre kadar içerisinde çalıştırıldığı thread'i durdurmak. Şimdi Dursun ile devam edelim. Aşağıdaki basit içeriği oluşturmamız yeterli.

dursun.rs

use log::info;
use crate::common::sleep_while;

pub fn do_homework(work: &str) {
    info!("{} ödevine çalışmaya başladım", work);
    sleep_while(4.0);
    info!("Ödevler bitti");
}

Dursun'un ev ödevini yapma süresini de hesaba katarak çalışan basit bir fonksiyon söz konusu. Benzer şekilde Yensen ve Gibsın dosyalarını da oluşturalım.

gibson.rs

use crate::common::sleep_while;
use log::info;

pub fn clear_home(equipment: &str) -> bool {
    info!("Salonu temizlemeye başladım. Malzeme {}", equipment);
    sleep_while(2.0);
    info!("Şu anda balkonu temizliyorum.");
    sleep_while(3.0);
    info!("Banyo da temizlendi");
    sleep_while(2.0);
    info!("Mutfakta bitmiştir");
    true
}

jhensen.rs

use crate::common::sleep_while;
use log::info;

pub fn do_shopping(list: Vec<&str>) -> bool {
    info!("Alışveriş listesini aldım. Göreve başlıyorum.\n{:#?}", list);
    // sembolik olarak bu thread'i 5 saniye duraksatıyoruz
    sleep_while(5.0);
    info!("Alışveriş tamamlandı ve eve geldim :)");
    true
}

Her üç fonksiyonda da çok özel bir şey yok. Sadece belli operasyonları belli sürelerde icra eden işlevler olduğunu varsaymaktayız. Pratiğin can alıcı kısmı tahmin edeceğiniz üzere main fonksiyonunda yapılanlar.

use crate::dursun::do_homework;
use crate::gibson::clear_home;
use crate::jhensen::do_shopping;
use log::{error, warn};
use std::thread;

mod common;
mod dursun;
mod gibson;
mod jhensen;

fn main() {
    env_logger::init();
    println!("Akşama misafir varrrr!!!");

    let market = vec![
        "Kuruyemiş",
        "Portakal Suyu",
        "8 Adet Muz",
        "2 Kilo Kızartmalık Patates",
    ];
    let mut handles = Vec::new();

    // İki tane thread başlatılıyoruz ve bunları handles'e ekliyoruz.
    // Nitekim ana thread'in bu iki thread'teki işler bitene kadar durmasını da sağlamalıyız.
    let jhensen_handle = thread::spawn(|| do_shopping(market));
    handles.push(jhensen_handle);
    let gibson_handle = thread::spawn(|| clear_home("Roventa Max"));
    handles.push(gibson_handle);

    // dursun'un işi ise main thread içinde çalışan normal bir fonksiyon
    do_homework("Lineer Cebir");

    // Yukarıda eş zamanlı başlatılan threar'lerin bitmesini beklettiğimiz yer
    for handle in handles {
        if handle.join().unwrap_or(false) {
            warn!("Bir iş bitti!");
        } else {
            error!("Upss. Bu işte bir yanlış var sanki");
        }
    }
    println!("Her şey yolunda. Misafirlerimizi bekliyoruz :)");
}

Main zaten process içerisine açılan ana thread içinde yaşar. Ek olarak Yensen ve Gibsın'ın işleri için ayrı thread'ler açıyor ve bu thread'lerin işleyişleri bitmeden de main'in sonlanmasını engelliyoruz. Dursun'un ilgilendiği fonksiyon başka bir modül olarak dursa da main thread'e dahildir. Rust dilinde bir thread başlatmak için spawn fonksiyonundan yararlanılmakta. Bu metot ile başlatılan thread'ler sadece do_shoping ve clear_home fonksiyonlarını çağırıp bir takım parametreler aktarıyorlar. Başlatılan thread'leri ele alan nesneleri bir vector serisinde topluyoruz. Nitekim n sayıda thread olduğunda uygulama akışının belli bir noktasında onların sonuçlarını almadan ilerlemek istemeyebiliriz. Fonksiyon sonundaki for döngüsü bu vector nesnelerini dolaşıyor ve biten olduğu takdirde sonuçları paylaşıp sonraki iterasyondan yola devam ediyor. Dolayısıyla thread'lerdeki işler bitmeden main işlevi, yani program sonlanmıyor.

Senaryomuz en basit haliyle Rust ile thread oluşturma, join ile başka thread'lere dahil etme ve bekleme işlerini icra etmekte. Tabii biz çıktıları log üstünden rengarenk biçimde takip etmek istedik. Bu nedenle örneği aşağıdaki gibi RUST_LOG komutu ile çalıştırmalıyız.

# Alışkanlık olsun, idiomatic öneriler için clippy'yi kullanalım.
cargo clippy

# log paketini kullandığımız için örneği aşağıdaki gibi çalıştıralım.
RUST_LOG=info cargo run

Gelelim çalışma zamanı çıktılarına.

Örnekte move, channels gibi kullanmadığımız önemli kavramlar da var elbette ancak Rust öğrenmeye çalışanlar için eğlenceli bir pratik olduğunu düşünüyorum. Örneği genişletmek elbette sizin elinizde. Bir başka rust pratiğinde görüşünceye dek hepinize mutlu günler dilerim.


Rust Pratikleri - Dokümantasyon

$
0
0

Bir programlama dilini iyi yapan ve onu öne çıkaran bazı önemli unsurlar vardır. İdeal bir söz dizimi oluşturulması için önerilerde bulunmak, kullanılan fonksiyon veya türlerle ilgili yardım dokümantasyonları sunmak, merkezi ve başarılı bir paket yönetim sistemine sahip olmak bunlar arasında sayılabilir. Rust dilindeki pek çok kural sayesinde bellek sahasının güvende kaldığı(memory safe), dangle pointer, data race, memory leak gibi sorunların oluşmadığı, performansı yüksek ve üstelik bütün bunlar için garbage collector benzeri mekanizmalara ihtiyaç duymayacak şekilde geliştirme yapmamız mümkün. Yine de idiomatic olarak ifade edilen ve dilin en ideal şekilde kullanılmasını tarifleyen ihtiyaç için yardım almamız gerekiyor. Bu anlamda cargo clippy en büyük destekçimiz. Ancak kaliteli kodlamanın olmazsa olmaz önemli özelliklerinden birisi de elbette verimli içerik sunan dokümantasyon. Özellikle yazdığımız kütüphaneleri herkesin kullanımına açmak istediğimiz senaryolarda bu konuya azami özeni göstermek lazım.

Rust'ın kendi built-in içeriğinin sağladığı dokümantasyon son derece etkilidir. Neredeyse kitaplara taş çıkartacak kadar iyi bilgi verir ve aynı zamanda ilgili enstrümanın kullanımına dair örnekler sunar. crates.io, Rust kodlamacıların kullandığı en önemli kütüphane deposudur. Buraya çıkan ve silinemeyen kütüphanelerimiz için iyi bir dokümantasyon sunmak programcı olarak vazifemizdir.

Peki Rust tarafında dokümantasyon nasıl sağlanır? Aslında bir .Net geliştiricinin oldukça aşina olduğu şekilde XML Comment benzeri yorum satırları ile kodun dokümantasyonu çıkarılabilmekte. İşin güzel yanı Rust'ın bu dokümanlarda Markdown formatını kullanıyor olması. Yani dokümanda link verebilir, resim gösterebilir, bullet list, heading vs kullanabiliriz. Bu sayede farklı ortamlara kolayca entegre olabilen ve hatta HTML olarak da servis edilebilen bir içerikle karşılaşıyoruz. Gelin bu laf salatalığını bırakalım ve örnek bir kod parçasını dokümante edelim.

cargo new doc_sample
cd doc_sample
touch src/lib.rs

Birkaç dil enstrümanını kullandığımız hafifsiklet bir modülümüz var. Modül içeriğini aşağıdaki gibi geliştirebiliriz. Yorum satırlarına dikkat!

//! Maket uçak yapımı sevenler için koleksiyonlarını yönetecekleri basit kütüphane.
//!
//! # Bazı Yardımcı Bilgiler
//!
//! Model uçak yapımı çok sevilen bir hobidir. Meşakkatli bir iştir ama sonuçları oldukça harikadır.
//! Yeni başlayanlar genelde 1:72 ölçekle çalışır. Az parçadan oluşan maketlerin bazıları için boya,
//! fırça, yapıştırıcı gibi unsurlar paketle birlikte gönderilir. İlk önce parçaların uygun şekilde
//! boyanması gerekir. Sonrasında plana uygun olarak yapıştırma işlemleri icra edilir. En son olarak
//! da logoların yapıştırılması işlemi uygulanır.
//!
//! # İçerik
//!
//! Kütüphanede yer alan temel enstrümanlar.
///
/// Bir maket modelinin temel bilgilerini taşır.
pub struct Model {
    /// Modelin başlığı. Örneğin Messerschmitt 109
    pub title: String,
    /// Model yapımının zorluk derecesi [Level]
    pub level: Level,
    /// Modelin parça sayısı
    pub part_count: u8,
    /// Güncel liste fiyatı
    pub list_price: f32,
}

#[derive(Debug)]
pub enum Level {
    /// Nispeten yapımı kolay olan seviye
    Easy,
    /// Artık güzel bir şeyler görmek isteyenlerin seviyesi
    Hard,
    /// Sınırları zorlayanların seviyesi
    Pro,
}

use std::fmt::{Display, Formatter};

impl Display for Model {
    /// Modelin bilgilerini String formatta geri döndürür.
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}. Zorlu {:?}.{} parça. Liste fiyatı {}",
            self.title, self.level, self.part_count, self.list_price
        )
    }
}

/// Uygulanabilecek en yüsek indirim oranı
pub const MAX_DISCOUNT_LEVEL: f32 = 10.99;

impl Model {
    /// Yeni bir model nesnesi oluşturmak için kullanılır.
    pub fn new(title: String, level: Level, part_count: u8, list_price: f32) -> Self {
        Model {
            title,
            level,
            part_count,
            list_price,
        }
    }

    /// Modelin fiyatına belirtilen miktarda indirim uygular
    pub fn apply_discount(&mut self, amount: f32) {
        if amount <= MAX_DISCOUNT_LEVEL {
            self.list_price -= amount
        } else {
            self.list_price -= MAX_DISCOUNT_LEVEL
        }
    }

    // cargo clippy sonrası aşağıdaki kullanım yerine Display trait'inin uyarlanması önerildi
    /*pub fn to_string(&self) -> String {
        format!(
            "{}. Zorluk {:?}.{} parça. Liste fiyatı {}",
            self.title, self.level, self.part_count, self.list_price
        )
    }*/
}

Kodda benim gibi maket uçak yapmayı sevenler için bir veri yapısı ve bağlı birkaç fonksiyon yer alıyor. Maketin modeli, zorluk derecesi, parça sayısı ve fiyatı gibi az sayıda özellik barındıran Model isimli struct var. Yeni bir nesneyi kolayca oluşturmak için new fonksiyonunu uyarlıyor ve hatta fiyat indirimi için de bir metot sunuyoruz. İlk etapta makete ait bilgileri String türde döndüren to_string isimli bir metot kullandık. Ancak yazının başında kısaca bahsettiğimiz cargo clippy komutunu kullandığımızda idiomatic bir öneride bulunduğunu göreceğiz.

cargo clippy

Dikkat edileceği üzere to_string yerine Display trait'ini implemente etmemiz öneriliyor. Bizde kod içeriğini buna göre düzenledik. Dokümantasyonumuz oldukça sadece. Aslında eklenebilecek bazı kısımlar var. Örneğin bir Model nesnesi nasıl örneklenir ve kod içinde kullanılır yine dokümante edebiliriz. Bu pratikte ihtiyaç duymadım ancak markdown'lar da olduğu gibi bir yol izleyebilirsiniz.

/// # Examples
/// ```
/// // Burada kod kullanım örneği yer alıyor
/// 
/// ```
///

Dikkat etmemiz gereken tek şey yardım dokümantasyonuna ekleyeceğimiz kod parçalarının da çalışır olması. Rust test aracı buradaki kodları denetler ve çalıştırılabilir olmasını bekler ;) Mesela apply_discount fonksiyonumuz için aşağıdaki gibi bir içerik hazırladığımızı düşünelim.

/// Modelin fiyatına belirtilen miktarda indirim uygular
///
/// # Examples
///
/// ```
/// let m109 = Model::new(String::from("Meserrschmitt 109"), Level::Easy, 42, 270);
/// m109.apply_discount(32.0);
/// assert_eq!(m109.list_price,228.0);
/// ```
///
pub fn apply_discount(&mut self, amount: f32) {
   if amount <= MAX_DISCOUNT_LEVEL {
      self.list_price -= amount
   } else {
      self.list_price -= MAX_DISCOUNT_LEVEL
   }
}

Dikkat edileceği üzere examples kısmında bir Model nesnesi üretiyor ve apply_discount fonksiyonunu çağırıyoruz. İlk bakışta bir problem yok gibi görülebilir. Birde aşağıdaki komutla deneyelim.

cargo test --doc

Upsss!!! Görüldüğü üzere Model ve Level türleri bulunamıyor. Bunları ekleyerek ilerlesek bile dokümanı tekrardan test ettiğimizde bu kez immutable değişken kullanımı ve hatta 270 değerini float kullanmamak sebebiyle farklı hatalara da rastlarız. Aşağıdaki ekran görüntüsünde olduğu gibi.

Sözün özü dokümantasyonda gerçekten çalıştırılabilir bir kod parçasının kondması bekleniyor. Dolayısıyla içeriği aşağıdaki şekilde değiştirmeliyiz.

/// Modelin fiyatına belirtilen miktarda indirim uygular
///
/// # Examples
///
/// ```
/// use doc_sample::{Model,Level};
/// let mut m109 = Model::new(String::from("Meserrschmitt 109"), Level::Easy, 42, 270.0);
/// m109.apply_discount(32.0);
/// assert_eq!(m109.list_price,259.01);
/// ```
///

Artık doküman içerisindeki kod parçası da çalıştırılabilir durumda, üstelik testi de başarılı.

Şimdi eklediğimiz yorum satırlarına istinaden nasıl bir doküman çıktısı alabiliriz bir bakalım.

# Normalde doküman üretimi için aşağıdaki komut kullanılır
cargo doc

# Ancak bağımlı kütüphanelerin dokümantasyonunu işin içerisine dahil etmek istemezsek şöyle kullanabiliriz.
cargo doc --no-deps 

# Hatta geliştirme sırasında şu kullanımı daha şık olur. 
cargo doc --no-deps --open

# doküman içine eklenmiş gerçek kod parçaları varsa test edebiliriz
cargo test --doc

Bu arada lib içerisindeki kodların ilk kısmında //! ile başlayan yorum satırları olduğunu görebilirsiniz. Bunlar inner doc olarak ifade edilirler ve HTML dokümantasyonunda aşağıdaki şekilde gösterilirler.

/// olarak kullandıklarımızda ise aşağıdaki sonuçları elde ederiz.

İşin güzel yanı kütüphane içeriğini kullandığımız yerde de IDE'lerin bize yardımcı olmasıdır. main.rs içeriğini aşağıdaki gibi tasarladığımızı düşünelim.

use doc_sample::{Level, Model};

fn main() {
    let mut m109 = Model::new(String::from("Meserrschmitt 109"), Level::Easy, 42, 270.50);
    println!("{}", m109.to_string());
    m109.apply_discount(32.0);
    println!("{}", m109.to_string());
}

Kullandığım IntelliJ IDEA'da apply_discount üstüne gelince aşağıdaki gibi çıktı elde ettim.

Görüldüğü üzere Rust kod dokümantasyonu konusunda pek çok dil veya çatıda olduğu gibi bir standart sunmaktadır. Avantajlı noktalardan birisi bu dokümantasyonun markdown formatını kullanmasıdır. Diğer yandan yorumlara serpiştirilen örnek kod parçaları varsa bunların çalışır hatta testten geçmiş olmasını da garanti edebilir. Elbette dokümantasyonun içeriği, en önemli kısımdır. Enstrümanları kafaları fazla karaştırmadan basit ve kaliteli bir şekilde anlatmak mühimdir. Pek tabii dokümantasyon oluşturmada nasıl bir yol izleneceğine dair en güzel kaynak Rust'ın var olan yardım dokümanlarıdır. Böylece geldik Rust Pratikleri serisinden bir bölümün daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Örneğe ait kodlara rust-farm github reposundan erişebilirsiniz.

Rust Pratikleri - Channels

$
0
0

Thread'ler aralarında haberleşmek için kanallardan(channels) yararlanır. Rust dilinde bu amaçla built-in modüllerinden olan mpsc(multi-producer single-consumer) paketi kullanılır. Bu paket aslında FIFO(First-In First-Out) ilkesine göre çalışan tipik bir kuyruk yapısıdır. Kanallar yardımıyla örneğin iki thread arasında bir yol açıp tek yönlü olarak mesaj göndermek mümkündür. Böylece bir thread'den diğerine çeşitli verileri aktarabiliriz. Hatta asenkron ve olay güdümlü(event-driven) haberleşmeler dahi tesis edebiliriz. Bir veri türünün kanalda akması için Send trait'ini uyarlamış olması gerekir. Primitive tiplerin hepsi bu davranışa sahiptir.

Kanallarla ilgili olarak dikkate değer bir diğer konu da türleridir. Bounded ve Unbounded olmak üzere iki seçenek vardır. Bounded kanallarda kapasite bellidir. Bir başka deyişle bir thread'den diğerine veri taşımak için kullanılan kanalın kapasitesi sınırlandırılır. Eğer kanal kapasitesi dolarsa, yayın yapan(mesaj gönderen) thread doğal olarak bloklanır. Unbounded kanallarda ise bir kapasite sınırı yoktur. Bellek yetersiz olana ya da sistem bir şekilde çökene kadar kanal kullanılabilir.

Kanalların bu iki türünün farklı sayıda alıcı ve yayıncıları olabilir. Örneğin tek bir yayıncının birden çok alıcısı olabilir ve kanala atılan veri bunlardan herhangi birisi tarafından işlenir. Bu senaryoda kanaldaki veriyi hangi thread'in alacağı ise belirsizdir. Çok doğal olarak birden çok yayıncı thread'de olabilir. Bu sefer hangi mesajın kanala ilk sırada gireceği konusu ortaya çıkar ki kural basittir; ilk bırakılan ilk gider. Esasında kanalların her iki yönünde de birden çok taraf olabilir. Buna göre aynı t anında çalışan birden çok yayıncı ve alıcı thread mümkündür.

Tabii bahsettiğimiz bu senaryolarda kanallar hep tek yönlüdür. Yani yayıncı taraftan alıcı tarafa doğru kurulan bir iletişim hattı söz konusudur. Aksi mümkün müdür? Elbette... Pekala ayrı kanallar açarak thread'ler arasındaki iletişimi çift yönlü kanallar üzerinden de sağlayabiliriz. Ancak bu kullanımda deadlock oluşturma ihtimalimiz vardır. Örneğin bounded kanallar kurduğumuz ve verinin döngüsel bir akış içerisinde yer aldığı bir senaryoda tarafların birbirini beklemesi söz konusu olabilir ki bu da deadlock durumunun oluşmasına davetiye çıkarır.

Bu teorik bilgiler bazen can sıkıcı olabiliyor. İyisi mi Rust Pratikleri'nin bu bölümüne kolay bir örnekle devam edelim. Amacımız Rust'ın kanal kullanımlarında öne çıkan kütüphanelerinden crossbeam sandığını kullanarak basit bir senaryoyu işletmek. Örnekte bir simülasyon oyunundaki işçilere çeşitli görevler atayacağız ve thread'ler arasındaki iletişim için bir kanal kurgusunu nasıl tesis edebileceğimize bakacağız. Fakat başlamadan önce Multi-Producer Single-Consumer paketi ile kanal kullanımını kısaca hatırlayalım. Bu amaçla channels isimli bir program oluşturalım.

cargo new channels

channels isimli projemizin kodlarını aşağıdaki gibi yazarak devam edebiliriz.

use std::sync::mpsc;
use std::thread;
use std::thread::sleep;
use std::time::{Duration, SystemTime};

fn main() {
    /*
       Olabildiğince basit bir örnek.
       Main thread içinden iki thread başlatalım.
       Bu thread'ler bir transmitter kullanarak kanala çeşitli bilgiler bıraksınlar.
       Ana thread'de bir dinleyici olarak bu kanala gelen mesajları alsın.
    */

    let chrono = SystemTime::now();

    // channel fonksiyonu bir transmitter ve birde consumer kanalı oluşturur
    let (cmd_transmitter, cmd_receiver) = mpsc::channel();
    // İKinci bir transmitter nesnesini birincisinden klonlarız.
    // Böylece ikinci thread aynı kanala mesaj bırakabilir.
    let cmd_transmitter2 = cmd_transmitter.clone();

    // İki thread açacağız. Bu thread'ler sonlanmadan main bitsin istemeyiz.
    let mut handlers = vec![];

    // bir thread açıyoruz ve cmd_transmitter ile işlem sonunda kanala mesaj bırakıyoruz.
    handlers.push(thread::spawn(move || {
        println!("#{:?} Yolcu#23 sefere başlıyor.", thread::current().id());
        sleep(Duration::from_secs(3));
        cmd_transmitter.send("Yolcu#23 hedefte.").unwrap();
    }));

    // Burada ikinci bir thread söz konusu ve bu thread işini bitirdiğinde ilk transmitter
    // clone'u üstünden yine kanala bir mesaj bırakıyor.
    handlers.push(thread::spawn(move || {
        println!(
            "#{:?} Kaşif#24 warp hızlanma motoru aktif.",
            thread::current().id()
        );
        sleep(Duration::from_secs(5));
        cmd_transmitter2.send("Kaşif#24 öte evrene ulaştı").unwrap();
    }));

    // Başlatılan thread'ler bittikçe kanala bıraktıkları mesajları okuyoruz.
    for h in handlers {
        let _ = h.join();
        let msg = cmd_receiver.recv().unwrap();
        println!("İşlem bilgisi : {}", msg);
    }

    println!(
        "İşlemler {} saniyede tamamlandı",
        chrono.elapsed().unwrap().as_secs_f32()
    );
}

Main bilindiği üzere ana thread olarak çalışır. Örnekte iki farklı thread açılır. Açılan thread'ler içerisinde sembolik olarak uzun süren işler düşünülmüştür. İşler tamamlandığında her bir thread kendi transmitter nesnesini kullanarak aynı kanala birer mesaj bırakır. Ana uygulama thread'i receiver nesnesini kullanarak bu kanala akan mesajları yakalar. Esas itibariyle bir thread'den değer döndürebildiğimiz için aynı işi kanallara başvurmadan da yapabiliriz. Ancak transmitter'ları thread içerisinde çeşitli noktalarda kullanıp duruma göre farklı anlarda kanala mesaj bırakmak gibi bir durum da söz konusu olabilir. Bu tip mesaj akışlarının yer aldığı senaryolarda kanal kullanımı oldukça idealdir. Yukarıdaki örneğin çalışma zamanı çıktısı aşağıdaki gibi olacaktır.

Şimdi gelelim crossbeam paketini kullandığımız örneğe. Bu sefer senaryoyu biraz daha eğlenceli hale getirmeye çalışacağız. İşe projemizi oluşturarak başlayalım.

cargo new vorcraft
cd vorcraft
touch src/lib.rs

Crossbeam harici bir paket olduğundan projenin toml dosyasında gerekli düzenlemeleri yapmalıyız. Örneğimizde kod takibi açısından log mekanizmasından da yararlanıyoruz. Bu nedenle toml dosyasını aşağıdaki gibi düzenleyebiliriz.

[package]
name = "vorcraft"
version = "0.1.0"
edition = "2021"

[dependencies]
crossbeam = "0.8.1"
log="0.4.14"
env_logger = "0.9.0"

Sırada lib dosyası var. Haydi rustgele :P

lib.rs

use crossbeam::channel::{Receiver, Sender};
use log::{error, info, warn};
use std::thread;
use std::time::Duration;

// Yaptıracağımız işleri tutan bir enum türü. Receiver tarafından kullanılır.
#[derive(Debug)]
pub enum Job {
    WheatFarm,
    FishFarm,
    Shack(u8),       // Kaç kişilik bir kulübe olacağını u8 ile atabiliriz
    ArcherTower(u8), // Belki u8 ile okçu kulesinin seviyesini ifade ederiz
    Ditch(f32),      // hendeğin uzunluğunu u32 ile alabiliriz
}

// İşler tamamlandıktan sonra kanala bırakacağımız mesajlar için aşağıdaki enum kullanılabilir.
// Sender tarafından kullanılır.
#[derive(Debug)]
pub enum Harvest {
    WheatFarm,
    FishFarm,
    Shack,
    ArcherTower,
    Ditch,
}

// Fonksiyon Receiver ve Sender türünden iki parametre almakta.
// Buna göre kanaldan mesaj alma ve kanala mesaj bırakma işlevlerini üstlendiğini ifade edebiliriz.
pub fn pesant_worker(job_no: i32, jobs: Receiver<Job>, results: Sender<Harvest>) {
    warn!("{} numaralı iş", job_no);
    // Bir döngü ile gelen Job listesini dolaşıyoruz.
    for job in jobs {
        // her bir Job'u match ifadesi ile kontrol ediyor ve sembolik bir gecikme ile işletip
        // Sender için bir sonuç alıyoruz.
        let response = match job {
            Job::ArcherTower(l) => {
                info!("{} seviyesinde okçu kulesi inşaası", l);
                thread::sleep(Duration::from_secs_f32(2.0));
                Harvest::ArcherTower
            }
            Job::Ditch(l) => {
                info!("{} uzunluğunda hendek.", l);
                thread::sleep(Duration::from_secs_f32(1.5));
                Harvest::Ditch
            }
            Job::FishFarm => {
                info!("Kıyıya balık çifliği inşaası.");
                thread::sleep(Duration::from_secs_f32(3.5));
                Harvest::FishFarm
            }
            Job::WheatFarm => {
                info!("Buğday tarlası inşaası.");
                thread::sleep(Duration::from_secs_f32(0.5));
                Harvest::WheatFarm
            }
            Job::Shack(p) => {
                info!("{} kişilik kulübe inşaası.", p);
                thread::sleep(Duration::from_secs_f32(p as f32 * 0.30));
                Harvest::Shack
            }
        };
        info!("Yapılan iş {:?}", response);
        // İstenen işlem tamamlandıktan sonra sonucu Sender ile kanala bırakmaktayız.
        // send işlemi sırasında bir hata olma ihtimaline karşı da durumu kontrol ediyoruz.
        if results.send(response).is_err() {
            error!("Ups bir hata oluştu.");
            break;
        }
    }
}

Sanki Warcraft benzeri bir oyundayız da köylülerimize tarla, balık çifliği, kulübe gibi unsurları inşa etmek gibi görevler veriyoruz. İşleri ve yapılan çalışma sonuçlarını birer enum sabiti ile tutmaktayız. Hatta bunları birer olay(event) gibi de düşünebiliriz. pesant_worker isimli fonksiyon transmitter ve receiver nesnelerini parametre olarak alıp kanaldan bilgi okuma ve yazma opsiyonlarına sahip. Dolayısıyla mesaj dinleyip(yani gelen görevi alıp) buna bağlı işi icra ettikten sonra kanala bir bilgi yollayabilir(Hangi işin bittiğini söyleyen bir durum bilgisi). Kurgunun şekilleneceği yer ise elbette main fonksiyonu.

main.rs

use crossbeam::channel;
use std::thread;
use vorcraft::{pesant_worker, Job};

fn main() {
    env_logger::init();

    println!("Oyun başladı. Görevler dağıtılacak.");

    // İlk olarak unbounded kanallarımızı oluşturalım.
    // unbounded bir Tuple döner.
    // jt -> Jobs Transmitter, jr -> Jobs Receiver anlamında.
    // rt -> Results Transmitter, rr -> Results Receiver anlamında.
    let (jt, jr) = channel::unbounded();
    let (rt, rr) = channel::unbounded();

    let jr2 = jr.clone();
    let rt2 = rt.clone();
    let jr3 = jr.clone();
    let rt3 = rt.clone();

    // Şimdi üç thread oluşturacağız. Bunları JoinHandle serisinde toplayabiliriz.
    // Tohumlanan thread'ler pesant_worker fonksiyonunu çağırmakta ve buraya birer reciver ile
    // transmitter nesnesi göndermekte. Ancak her thread kendi transmitter ve receiver'ı ile çalışmalı.
    // Bu nedenle bir üst satırda clone'landıklarını görebiliriz.
    let handles = vec![
        thread::spawn(|| pesant_worker(1001, jr, rt)),
        thread::spawn(|| pesant_worker(1002, jr2, rt2)),
        thread::spawn(|| pesant_worker(1003, jr3, rt3)),
    ];

    // Birkaç kobay iş isteiğinden oluşan bir vector hazırlayalım
    let jobs = vec![
        Job::WheatFarm,
        Job::FishFarm,
        Job::Shack(8),
        Job::Ditch(23.0),
        Job::ArcherTower(100),
        Job::Shack(4),
        Job::FishFarm,
        Job::ArcherTower(50),
        Job::Shack(10),
    ];

    // Herbir iş isteğini ilgili kanala bırakacak bir döngü.
    for j in jobs {
        println!("İstenen iş {:?}", j);
        let _ = jt.send(j); // Kanala istenen işi bıraktık
    }
    // Artık kanala göndereceğimiz bir iş isteği kalmadığından transmitter'ı hemen kapatıyoruz.
    drop(jt);

    // Burada da thread'lerin yaptığı iş sonuçlarının aktığı kanalı dinleyerek sonuçları almaktayız.
    for r in rr {
        println!("Tamamlanan iş {:?}", r);
    }

    // İşlemler bitmeden main'in sonlanmasını engelliyoruz
    for h in handles {
        let _ = h.join();
    }
}

Dikkat edileceği üzere Job ve Result türleri için iki ayrı unbounded kanal oluşturulmakta. Dolayısıyla yapılacak işler ve sonuçlar için iki ayrı kanal açtığımızı düşünebiliriz. Senaryoda 3 işçimiz var ve her biri için birer thread oluşturulmakta. Thread'ler pesant_worker fonksiyonunu çağırırken kendileri için gerekli transmitter ve receiver nesnelerinin birer klonunu almaktalar(Clone kullanmadığımız durumda nasıl bir sorun oluşacağını araştırınız)

Artık örneğimizi çalıştırıp sonuçlara bakabiliriz. Pek tabii clippy ile kodun halini hatırını sorup gerekli düzenlemeleri yaparak ilerlediğimi ve olabildiğince idiomatic kod oluşturmaya çalıştığımı baştan söylemek isterim.

# Kendi örneğinizi çalıştırmadan önce sık sık clippy ile uyarıları gözden geçirin
cargo clippy
# Doğrudan çalıştırmak için aşağıdaki gibi,
cargo run

# logları görmek içinse aşağıdaki gibi.
RUST_LOG=info cargo run

İşte kendi sistemimde elde ettiğim çalışma zamanı sonuçları.

Birden çok iş parçacığının yer aldığı ve bu işler arasında haberleşmenin önce çıktığı senaryolarda kanal kullanımı son derece yaygın. Built-in olarak gelen mpsc kütüphanesini kullanabileceğimiz gibi Rust konusunda ileri seviye olanların önerdiği crossbeam paketini de tercih edebiliriz. Ben aradaki farkları yorumlayacak mertebede yetkinliğe sahip olmasam da önerilere kulak verip crossbeam'i tercih ediyorum. Örnek üzerinde bol bol uğraşmanızı öneririm. Söz gelimi thread'lerin açılması için bir for döngüsü kullanabilir miyiz ?

Böylece geldik bir rust pratiğimizin daha sonuna. Tekrardan görüşünce dek hepinize mutlu günler dilerim.

Örnek kodlara Rust Pratikleri github reposundan erişebilirsiniz.

Rust Pratikleri - GDB ile Debug İşlemleri

$
0
0

Rust dilinin en güçlü olduğu yer etkili bellek yönetimi ve olası kaosların önüne herhangi bir garbage collector veya başka bir unsura ihtiyaç duymadan geçebilecek kural setleri barındırmasıdır. Özellikle Memory Leak, Double Free, Data Race gibi C, C++ dillerinde sıklıkla rastlanan durumların oluşmaması için basit kurallar barındırır. Bu kurallar ilk başlarda rust öğrenenleri epey zorlar fakat bir kez alışılınca her şey çok daha net ve berrak hale gelir. Bellek yönetimi denilince içeride neler oluyor bitiyor görmek de önemlidir. Fonksiyonlar birer kapsam olarak Stack'e yığılır, çeşitli veri türleri(String gibi) heap'e açılıp pointer alır, kapsamlar sonlandığında bir şeyler olur vs

Rust ile ilgili öğretilerde bellek yönetimi konusunu incelerken fonksiyon ve değişkenlere ait kapsamların stack ve heap bölgelerine nasıl açıldığını görmek için GNU Debugger'dan(kısaca GDB) yararlanılabilir. Kodu debug etmek deyince insanın aklına Visual Studio gibi gelişmiş IDE'lerin kolaylıkları geliyor ve bu nedenle ilk kez karşılaşanlar için GDB ilkel bir araç gibi görünebilir elbette. Ancak rust ile yazılmış programları terminalden adım adım işletmek ve bellek üzerindeki konumlandırmaları görmek(stack açılımlarını izleyip pointer'ları analiz etmek gibi) adına son derece faydalı bir araçtır. Rust dilinde ilerlemek isteyenlerin bilmesi ve kullanması gereken bir yardımcı olduğunu düşünüyorum. Tabii her şeyden önce onu üzerinde çalıştığım Ubuntu platformuna yüklemem gerekiyor. Bu arada GDB ile ilgili detaylar için şu adrese bakılabilir.

sudo apt-get update
sudo apt-get install gdb

gdb --version

Eğer her şey yolunda giderse aşağıdaki gibi versiyon numarasını görebilmeliyiz.

Gelelim örnek kodlara. Debugger kullanımını basit seviyede deneyimlemek için bir rust projesi oluşturup ilerleyelim.

cargo new debugging

main.rs içeriğini aşağıdaki kod parçasında olduğu gibi geliştirebiliriz.

fn main() {
    let mut calderon = Player {
        id: 1,
        name: String::from("Hoze Kalderon"),
        level: 78,
    };
    increase_level(&mut calderon);
    dbg!(calderon.level);
    decrease_level(&mut calderon);
    dbg!(calderon.level);
}

fn increase_level(p: &mut Player) {
    p.level += 10;
}
fn decrease_level(p: &mut Player) {
    let rate = 10;
    p.level -= rate;
}

#[allow(dead_code)]
struct Player {
    id: u16,
    name: String,
    level: u16,
}

Player isimli bir veri yapısı ve onun level alan değerini artırıp azaltan iki fonksiyon kullanmaktayız. Fonksiyonlara Player nesne örneğini referans olarak geçiyoruz(bir başka deyişle fonksiyon kapsamlarına onu ödünç veriyoruz - borrowing) ve dbg! makrosunu kullanarak debug ekranına bilgi yazdırıyoruz. Kodun uygunluğunu clippy ile iyileştirdikten sonra çalıştığından emin olmalı ve daha da önemlisi debug işlemleri için build etmeliyiz. İşte kullanacağımız terminal komutları.

cargo new debugging
cd debugging
cargo clippy
cargo run

# kodun çalıştığından emin olduktan sonra build etmeliyiz
cargo build

Artık kodu adım adım debug etmeye başlayabiliriz. GDB aracının belli başlı komutları var. İzleyen terminal komutlarına ait yorum satırlarında kullanımlarına ait kısa bilgiler bulabilirsiniz.

# Programa ait binary'yi debug modda açalım
gdb debugging
# Çalıştığını görelim
run
# ve ilk satırından itibaren kod içeriğine bir bakalım
list

# ardından örneğin increase_level ve decrease_level fonksiyonlarına birer breakpoint koyalım
b increase_level
b decrease_level

# kodu çalıştıralım
r

# Artık breakpoint noktalarında bir takım bilgilere bakabiliriz.
# Örneğin o andaki local değişkenlere ve argümanlara bakalım
info locals
info args

# Kodu bir adım ilerletelim
n

# Aynı bilgilere tekrar bakalım ve hatta stack bellek bölgesine bir göz atalım.
bt
info locals
info args

# Hatta pointer olarak gelen değişkenlerin içeriklerini şöyle görebiliriz
print *p

# Bir sonraki breakpoint noktasına geçmek için c komutunu kullanırız
c

# stack üzerindeki scope'ları görmek için yine bt'den yararlanabiliriz
bt

# debugger'dan çıkmak içinse aşağıdaki komutu kullanırız.
q

# Bu arada minik bir ipucu bırakalım. Ekran çok kalabalıklaştığında
# muhtemelen silmek isteyeceksiniz. Ctrl + L işinizi görecektir.

Tabii bu komutları denerken ekran görüntüsü aşağıya doğru uzayıp gidebilir :) Neyse ki sağdaki dikey monitör bana epeyce yardımcı oldu. Yine de sonuçları iki parça halinde paylaşacağım. İlk kısımda gdb aracını başlatıp kodun içeriğini gösteriyoruz. Bu arada binary dosyanın olduğu klasöre gittiğimize dikkat edelim.

Devam eden kısımda ise kalan komutların verdiği sonuçlarını görmekteyiz.

Bu kısmı yorumlamak oldukça önemli. Kodumuzdaki fonksiyonlar Player verisini referans olarak ödünç alıp kullanmaktalar. Bu nedenle girdiğimiz fonksiyonlarda birer pointer görmekteyiz. Pointer adresi ve hatta kullandığı String değişkeninki değişmiyor elbette. Dikkat çekici bir diğer nokta da fonksiyonlara parametre olarak gelen Player nesnesinin işaret ettiği veri yapısı. Dikkat edileceği üzere String olarak tasarladığımız name değişkeni String veri yapısının tasarımı gereği heap bölgesindeki içeriği işaret etmekte. Diğer yandan String türünün kendisi esasında bir Smart Pointer'dır. Yani scope dışına çıkıldığı anda otomatik olarak heap içeriği deallocate edilir. GDB aracını kullanarak özellikle Smart Pointer gibi enstrümanların işleyişini anlamak çok daha kolaydır. Bunun için örneğimize aşağıdaki fonksiyonu eklediğimizi düşünelim.

fn change_level(p: &mut Player) {
    let level = Box::new(90);
    p.level = *level;
}

Fonksiyon kendisine referansı verilen Player nesnesinin yine level isimli değerini değiştirmekte. Ancak yeni level bilgisinin kasıtlı olarak bir Smart Pointer tarafından tutulduğuna dikkat edelim. Box türünden bu değişken heap alanında duracak şekilde ilkel bir tamsayı verisi taşımakta. İlgili fonksiyonu main içerisinde aşağıdaki gibi kullanabiliriz.

fn main() {
    let mut calderon = Player {
        id: 1,
        name: String::from("Hoze Kalderon"),
        level: 78,
    };
    increase_level(&mut calderon);
    dbg!(calderon.level);
    decrease_level(&mut calderon);
    dbg!(calderon.level);
    change_level(&mut calderon);
    dbg!(calderon.level);
}

Sadece konuyu değerlendirmek için level isimli bir smart pointer kullanıyoruz. Smart Pointer'lar scope sonlandığında otomatik olarak heap'ten atılırlar ki bu özellikle silmeyi unuttuğumuz pointer'ların oluşturacağı Memory Leak durumunun oluşmamasını garanti eder. Gerçekten böyle olup olmadığını anlamanın(yani fonksiyon sonlanıp scope dışına çıkıldığında pointer'ın işaret ettiği bellek bölgesinde bir değer kalmadığını görmenin) bir yolu kodu debug ederken fonksiyon çağrısı tamamlandıktan sonraki fotoğrafa bakmaktır. Öyleyse tekrardan terminale dönüp debug işlemlerine başlayalım.

gdb debugging
# breakpoint'i ekleyelim
b change_level
# programı çalıştıralım(run)
r
# Birkaç satır ilerleyelim
n
n
n
# change_level fonksiyonu içinde tanımlanan local değişkenlere bir bakalım
info locals
#pointer değerini okuyalım (Tabii siz denerken adres farklı olacaktır)
x /d 0x5555555a5af0

# Kodu ilerletip scope'u sonlandıralım. Yani fonksiyon işleyişini tamamlayalım.
n
n
# Şimdi tekrar aşağıdaki komutu çalıştıralım
x /d 0x5555555a5af0

# sonuç 0 olmalı. Bu Smart Pointer'ın söylediği üzere ilgili bellek bölgesinin kaldırıldığı anlamına gelir.

Çalışma zamanı sonuçları aşağıdaki gibidir.

Dikkat edileceği üzere fonksiyon dışına çıkıldığında ilgili adres değeri 0 olarak elde edilmiştir. Smart Pointer'ın çalıştığının bir nevi ispatı olarak düşünebiliriz. Tabii büyük projelerde ve kalabalık kod parçalarında GDB ile debug işlemleri çok kolay olmayabilir. Hatta sağlıklı loglar daha çok işe yarayabilir. Yine de iç dinamikleri öğrenme aşamasındayken bu debugger'ı kullanmak bence oldukça önemli. Böylece geldik Rust Pratiklerinde bir bölümün daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Rust Pratikleri - Lifetimes Mevzusu

$
0
0

Rust'ın özellikle Garbage Collector kullanan dillerden çok farklı olduğunu bellek yönetimi için getirdiği kurallardan dolayı biliyoruz. Ownership, borrowing gibi hususlar sayesinde güvenli bir bellek ortamını garanti etmek üzerine ihtisas yapmış bir dil olduğunu söylesek yeridir. Bunlar pek çok dilde otomatik yönetildiği için Rust'ı öğrenmek biraz zaman alabiliyor ve bu yolda karşımıza çıkacak zor konulardan birisi de Lifetimes mevzusu. Kısaca nesnelere yaşam süresini bilinçli olarak vermek diye ifade edebileceğimiz bu konuyu esasında sevgili Bora Kaşmer ile başladığımız 45 Byte sohbetlerinde dile getirmek istiyorum. Lakin çok basit bir örnek ile konuyu olabildiğince sade bir şekilde anlatmam gerekiyor ve String ile &str arasındaki fark bunun için ideal olabilir.

String gibi veri türleri, bulundukları tüm dillerde Heap ovasını dilediğince kullandığı için hem pratik hem de tehlikeli olabiliyor. Özellikle performans odaklı diller Heap konusunda çok hassas ve gereksiz kaynak tüketimlerini sevmiyorlar. Olayıların daha çok stack üstünde kalmasını tercih ediyorlar ve gerçekten gereken hallerde Heap'e çıkılmasını bekliyorlar. Bunu basit bir örnekle pekiştirelim. Diyelim ki TCP protokolü üstünden belli bir sokete gelen paketleri işliyoruz. Bu paketler uygulama tarafına buffer nesnesi olarak alınırlar. Bir buffer belleğe alındığında, onun içinden işe yarar bilgileri alıp örneğin bir struct'ın String türünden değerlerinde saklamak mümkündür. Hatta ilk aklımıza gelen yol budur. Böylece değiştirilebilir(mutable) bir veri modelini de tesis etmiş oluruz. Lakin söz konusu buffer içindeki verileri çektikten sonra değiştirmek gibi bir niyetimiz yoksa(bir başka deyişle onları sadece okunabilir olarak kullanacaksak) heap üstünde String veri türleri için ekstra alanlar açmak yerine buffer içindeki ilgili dilimleri işaret eden &str türlerini kullanabiliriz. Dolayısıyla ağ paketi olarak gelen veriyi alıp doğrudan kullanmak hem bellek üzerindeki operasyonu azaltır hem de performansı artırır.

Peki pratiğimize konu olan lifetimes mevzusu bunun neresinde? Sorun şu ki yukarıda anlattığımız senaryo için ikinci kullanımı tercih edersek(yani String yerine &str kullanırsak) buffer olarak tutulan veri kümesinin bellekten atılması(deallocate) sonrasında Struct nesnemizin &str tipindeki alanları tarafından referans edilen bölgeler geçersiz hale gelecektir. Bu Rust'ın istemediği bir durumdur nitekim Danling Pointer probleminin oluşmasına sebeptir.

Olay biraz karıştı farkındayım. Dilerseniz hiç vakit kaybetmeden proje iskeletini oluşturarak konumuza devam edelim.

cargo new viva_las_vegas

Program kodlarımızı ilk etapta aşağıdaki gibi yazabiliriz.

#[allow(dead_code)]

#[derive(Debug)]
struct Player {
    id: u32,
    nick: String,
    country: String,
    level: u16,
}

impl Player {
    fn new(id: u32, nick: String, country: String, level: u16) -> Self {
        Self {
            id,
            nick,
            country,
            level,
        }
    }
}

fn main() {
    let gonzi = Player::new(1, "Gonsalez".to_string(), "Brasil".to_string(), 88);
    println!("{:#?}", gonzi);
}

Senaryomuz bir oyuncuyu temsil eden veri modelinin tasarlanması ile başlıyor. Player isimli struct işaretsiz 32 bit tamsayı ve String veri türlerinden oluşan alanlara sahip. Kolayca oluşturmak için new isimli bir yapıcı metodumuz da var(constructor). Kod bu haliyle aşağıdaki ekran görüntüsündekine benzer şekilde sorunsuz çalışmakta.

Şimdi senaryomuza oyuncunun nickname bilgisini değiştirecek yeni bir fonksiyon daha dahil edelim. Çok gerekli değil ama tuzağı hazırlamamız için şart.

fn change_nickname(p: &mut Player, new_nickname: String) -> &Player {
    p.nick = new_nickname;
    p
}

Player yapısının nick değerini parametre olarak gelen new_nickname ile değiştirmek istiyoruz. Tabii nick değerini doğrudan da değiştirebiliriz lakin bu fonksiyon Player nesnesi için ayrı bir scope açacağından ve bu stack bellek bölgesinde yeni bir alan anlamına geldiğinden parametre olarak gelen nesne davranışlarını öğrenmek açısından gerekli. Fonksiyon dikkat edileceği üzere mutable bir Player referansı alıyor, kapsamında nick bilgisini değiştiriyor ve ilgili Player referansını tekrar geri döndürüyor. Tam anlamıyla Player değişkenini referans olarak ödünç alan ve geri veren bir fonksiyon olduğunu söyleyebiliriz. Yeni fonksiyonu kullanacak şekilde main içeriğini değiştirerek devam edelim.

fn main() {
    let mut gonzi = Player::new(1, "Gonsalez".to_string(), "Brasil".to_string(), 88);
    println!("{:#?}", gonzi);
    let ceremiya = change_nickname(&mut gonzi, "Ceremiya".to_string());
    println!("{:#?}", ceremiya);
}

Programı bu haliyle çalıştırdığımızda gonzi değişkeni ile temsil edilen Player nesnesine ait nick alanının başarılı şekilde değiştirildiğini görebiliriz.

Her şey yolunda ve aslında henüz kafamızdan duman çıkaracak bir şey olmadı. Aslında Player değişkenindeki String içerikler literal türden de tanımlanabilirler. String veri türü heap alanını kullanan ve genişleyebilen bir yapıdır. Stack üzerinde Heap'teki metinsel alanı işaret eden, pointer ve referans adresi gibi bilgileri tutar. Ayrıca işaret edilen alanın byte cinsinden uzunluğunu ve ayrılan kapasiteyi de saklar. Ancak en nihayetinde onun için bir allocation söz konusudur. str literal ise String veri türünün olduğu bellek bölgesinin bir parçasını(metnin bir kısmını ki slice olarak ifade edilir) referans eder ve sabit uzunluktadır. Yani bir String üstünden literal çekildikten sonra içeriği değiştirilemez. Metinsel bilginin değişmeyeceği durumlarda literal kullanmak oldukça mantıklıdır(Aynen yazının girişindeki senaryoda bahsettiğimiz gibi). O halde gelin Player veri yapısında yer alan nick ve country alanlarını literal string türüne çevirip devam edelim. Tabii bunu yaptığımız zaman new fonksiyonundaki parametreleri de String türünden &str türüne dönüştürmemiz gerekiyor.

#[derive(Debug)]
struct Player {
    id: u32,
    nick: &str,
    country: &str,
    level: u16,
}

impl Player {
    fn new(id: u32, nick: &str, country: &str, level: u16) -> Self {
        Self {
            id,
            nick,
            country,
            level,
        }
    }
}

Ardından programı clippy ile bir kontrol edelim.

cargo clippy

Upss!!! Bir sürü hata aldık :( nick ve country alanlarını literal string türünden değiştirdik. Ancak bu referanslar Player'ın kullanıldığı scope'lar düşünüldüğünde deallocate işlemi sonrası, var olmayan bellek alanlarını referans eder hale gelebilirler. Bu durumun oluşmaması için Rust söz konusu alanların ne kadar süre yaşayacağını bilmek istiyor. Böylece Dangling Pointer oluşmasının önüne geçmiş oluyoruz. Bunun üstüne ilk olarak derleyicinin de önerdiği üzere Player için gerekli lifetime parametrelerini ekleyerek evam edelim.

#[derive(Debug)]
struct Player<'a> {
    id: u32,
    nick: &'a str,
    country: &'a str,
    level: u16,
}

Lifetime parametrelerinde genel olarak a,b gibi harfler kullanılmakta ancak ideal olanı nesnelerin gireceği scope'ları düşünerek isimlendirmek. Örneğin buradaki değişkenler bir stream okuma sürecinde yer alsalardı buffer veya buf gibi bir isimlendirme daha doğru olabilirdi. Yazımına alışmak biraz zaman alsa da Rust derleyicisine yaşam ömrü ile ilgili bir ipucu vermiş oluyoruz. Bu değişiklikerden sonra programımıza clippy ile tekrar bakalım.

Hobaaa!!! İşler daha da kötüye gitti sanki :D Hata mesajlarını ve uyarıları okursak işimizin daha kolay olduğunu görebiliriz. Player nesnesindeki literal string alanları için lifetime belirttiğimiz anda, Player'ın kullanıldığı ne kadar scope varsa onlar için de kullanım sürelerini belirtmemiz gerekiyor. Bu örneğe göre new ve change_nickname fonksiyonları Player türü ile çalışıyor. Dolayısıyla bu fonksiyonların stack bellek bölgesinde kaldığı süre doğal olarak new ve change_nickname metotlarına ait parametreler için de geçerli olmalı. Öyle ki dışarıdaki bir Player nesnesi ne kadar süre yaşayacaksa parametreler de aynı süre hayatta kalsın, Player nesnesi deallocate edildiğinde onlar da yok edilsinler. Bu bilgiler ışığında kodları aşağıdaki gibi değiştirmeliyiz.

#[derive(Debug)]
struct Player<'a> {
    id: u32,
    nick: &'a str,
    country: &'a str,
    level: u16,
}

impl<'a> Player<'a> {
    fn new(id: u32, nick: &'a str, country: &'a str, level: u16) -> Self {
        Self {
            id,
            nick,
            country,
            level,
        }
    }
}

fn change_nickname<'a>(p: &'a mut Player<'a>, new_nickname: &'a str) -> &'a Player<'a> {
    p.nick = new_nickname;
    p
}

Dikkat edileceği üzere new ve change_nickname fonksiyonlarında lifetime bildirimleri kullanmakta. Buna göre new ile bir Player nesnesi örneklendiğinde derleyici onun için bir yaşam süresi biçiyor. change_nickname'e parametre olarak gelen Player değişkeni de bir lifetime bilgisi bekliyor. Hatta aynı lifetime bilgisi ikinci parametre olan new_nickname için de geçerli. Rust'ta kodlama yaparken çoğu zaman bu tip lifetime bildirimleri ile karşılaşmıyoruz. Nitekim gerek olmadığı hallerde biz açık bir şekilde belirtmesek bile Rust derleyicisi ilgili lifetime bildirimlerini eklemekte(Implicit Lifetimes)Şimdi kodumuzu tekrar clippy ile gözden geçirip çalışma sonuçlarına bir bakalım.

İstediğimiz sonuçları elde ettiğimizi ifade edebiliriz. Tekrar hatıralayım. String heap'de duran, UTF8 formatındaki bir veri tipidir ve bulunduğu yere erişebiliriz. Onun için heap'te bir yer ayrılır(allocation)&str ise bir parça dilimdir(slice type) Yani zaten var olan bir String'in bir parçasını işaret eder, çalışma zamanında herhangi bir allocation gerektirmez ve sabit uzunlukta olan &str yeniden boyutlandırılamaz. Performans açısından yeni String sahaları oluşturmak yerine dilimlerle çalışmak pek tabii daha iyidir lakin bunu yaptığımızda lifetime gibi konular da karşımıza çıkabilir. Özellikle stream şeklinde akan ağ paketlerinde içerikten üretilen metinsel alanlarda değişiklikler yapılması düşünülmüyorsa String kullanım maliyeti çok yüksek olacaktır. Böylece geldik bir Rust Pratiğinin daha sonuna. Her zaman olduğu gibi örnek kodlara github reposu üzerinden erişebilirsiniz. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Rust Pratikleri - Serde, Json ve Biraz Eğlence

$
0
0

Sanıyorum JSON veriler ile çalışmayan programlama dili veya ortam yoktur. Sonuç itibariyle bir takım verileri düzenli, standart ve insan gözüyle okunabilir bir formatta tutmanın en iyi yollarından birisi şüphesiz ki JSON. Öncesinden gelen XML formatına göre daha az yer tutması da cazibesini artırmaktadır. Tabii günümüzde BSON gibi sıkıştırılabilir ve çok daha hızlı yol alabilen seçenekler de mevcut ama rust dilini öğrenirken bunun pratiğini yapmadan olmaz. Bu noktada işimizi epey kolaylaştıran bir kütüphane olduğunu ifade edebilirim. Serde isimli çatı(ki framework olduğu vurgulanıyor JSON ile çalışma konusunda epey popüler. Hiç vakit kaybetmeden örnek bir uygulama üstünden ilerleyelim.

Senaryomuzda basit olması nedeniyle sıklıkla tercih ettiğim bir terminal uygulaması kullanacağız. Programı komut satırından argüman vererek çalıştırabileceğiz. Dolayısıyla yürütülebilir bir rust programına komut satırından argüman nasıl yollanır öğreneceğiz. JSON dosya içeriğinde türlü türlü ipuçları olacak. Uygulamamızdan rustgele veya belli bir konu başlığında ipucu isteyebileceğiz. İpuçlarını tutan JSON dosyasında ise örnek olarak aşağıdaki gibi bilgiler saklayacağız.

[
  {
  "id": 1,
  "category": "Rust",
  "description": "Veri türleri varsayılan olarak immutable'dır."
  },
  {
    "id": 2,
    "category": "Rust",
    "description": "Kullanıcı tanımlı veri türü oluşturmanın bir yolu struct kullanmaktır."
  },
...

Dosyanın tamamı ve örneğe ait kodlar için github reposuna uğrayabilirsiniz. İlk olarak aşağıdaki terminal komutu ile projemizi oluşturalım.

cargo new gettip

serde paketini kullanacağız ancak rastgele bir ipucu getirilmesi de elbette güzel olur. Bu amaçla rand kütüphanesini kullanabiliriz. Dolayısıyla toml dosyasındaki dependencies kısmında ilgili kütüphane bildirimlerini eklemek gerekiyor. Aynen aşağıda olduğu gibi.

[package]
name = "gettip"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0.133", features = ["derive"] }
serde_json="1.0.74"
rand="0.8.4"

serde paketi için birde derive özelliğini kullanmak istediğimizi belirttik. Kod tarafında JSON dosyasındaki bir ipucunu işaret edebilecek bir struct tanımlayacağız. Bu veri modelinin JSON serileştirme davranışlarını otomatik olarak kazanması için deserialize trait'ini derive niteliği üstünden kullandıracağız. Nitekim dosya içeriğini bizim tanımladığımız bir veri yapısına ters serileştirme ile almamız gerekiyor. features içerisinde  tanımlanan derive bildirimi bu fonksiyonelliği uygulamamıza kazandıracak.

Gelelim kod tarafına. main.rs dosya içeriğini aşağıdaki gibi yazarak devam edelim.

use rand::{thread_rng, Rng};
use serde::{Deserialize};
use std::env;
use std::fmt::{Display, Formatter};
use std::fs::File;
use std::io::BufReader;

fn main() {
    let args: Vec<String> = env::args().collect();
    let tips = load_tips();

    match args.len() {
        2 => {
            let command = &args[1];
            if command == "r" {
                println!("{}", get_random_tip(&tips));
            } else {
                println!("r girerek deneyin.");
            }
        }
        3 => {
            let category = &args[2];
            let sub_tips: Vec<Tip> = tips
                .into_iter()
                .filter(|t| t.category == *category)
                .collect();
            if !sub_tips.is_empty() {
                let tip = get_random_tip(&sub_tips);
                println!("{}", tip);
            } else {
                println!("{} için hiçbir ipucu yok.", category);
            }
        }
        _ => {
            println!("Rustgele bir ipucu için `r` ile\nBelli bir kategoride rustgele ipucu için `r rust` ile \ndeneyin lütfen;)");
        }
    };
}

fn load_tips() -> Vec<Tip> {
    let f = File::open("tips.json").expect("Dosya açılırken hata");
    let reader = BufReader::new(f);
    let tips: Vec<Tip> = serde_json::from_reader(reader).expect("json okumada hata");
    tips
}

fn get_random_tip(tips: &[Tip]) -> String {
    let mut rng = thread_rng();
    let number = rng.gen_range(0..tips.len());
    tips[number].to_string()
}

#[derive(Deserialize)]
pub struct Tip {
    pub id: i32,
    pub category: String,
    pub description: String,
}

impl Display for Tip {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} -> {}", self.category, self.description)
    }
}

Bir ipucu Tip isimli struct ile temsil edilmekte. Bu veri modeli json içerisindeki nitelikleri karşılayan alanlar içermekte. id, category ve description. Bir ipucunu yazılabilir formatta servis etmek içinse Display trait'ini uyguluyoruz. Böylece to_string fonksiyonuna karşılık vermekteyiz. load_tips isimli fonksiyon adından da anlaşılacağı üzere tips.json dosya içeriğini okuyup, Tip nesnelerinden oluşan bir vector olarak geriye döndürmekte. get_random_tip fonksiyonu ise Tip türünden diziyi referans alıp rastgele üretilen bir değeri kullanarak metinsel formatta bilgi döndürmekte. 

Program başında env modülünden hareketle terminalden girilen argümanları yakalamaktayız. İki veya üç argümanla çalışacağımız için bu durumu kontrol altına alan bir match ifadesi söz konusu. Daha az veya daha çok argüman gelmesi halinde bir bilgi mesajı vererek kullanıcıyı uyarıyoruz. İki parametre gelmesi halinde ilgili anahtarın r olup olmadığına göre kod akışı değişiyor. Eğer üç argüman girilmişse son argümanın kategori olduğunu düşünerek hareket ediyoruz. Burada da Higher Order Function'lardan yararlanarak basit bir filtreleme yaptığımızı görebilirsiniz.

Örnekte hata yönetimi konusunda çok radikal işler yapmadığımızı ifade edebilirim. Result<T,Err> döndüren birkaç operasyonda expect fonksiyonunu kullanarak olası panik durumunda ek bilgi verip uygulamanın sonlanmasına müsaade ettik. 

Artık uygulamayı deneyebilir ve sonuçlara bakabiliriz.

cargo run r
cargo run r C#
cargo run r Rust
cargo run r Arch
cargo run r none

Kendi sistemimdeki çalışma zamanına ait çıktıyı aşağıda görebilirsiniz.

Tabii örneğimizi yürütülebilir bir binary olarak hazırlamakta yarar var. Bunun için build işlemini aşağıdaki gibi icra edip yine gerekli denemelerimizi yapabiliriz.

cargo build --release
cd target/release
./gettip r rust
./gettip r

Ancak şöyle bir hatırlatma yapalım. Program, tips.json dosyası ile çalıştığından onu da binary'nin olduğu klasörle birlikte dağıtmalıyız. En azından siz daha iyi bir çözüm bulana kadar böyle. Şimdi cargo paketine gereksinim duymadan binary'yi yürütebiliriz. İşte birkaç örnek.

Rust programlama dilinde başlangıç seviyesini tamamlamış herkesin yapabileceği türden bir örnek. Bizim için işleri kolaylaştıran serde ve rand kütüphanelerini kullandık. Biraz pattern matching, biraz dosya okuma, komut satırından argüman alma, fonksiyon tanımlama, vector, struct ve trait uyarlaması gibi konuları değerlendirmiş olduk. Elbette örnek daha da geliştirilebilir ve eksik yönleri de yok değil. Örneğin JSON dosya içeriği çok büyük olursa uygulama performansı bundan nasıl etkilenir? Ya da ipuçlarını bir JSON dosyasından değil de herhangi bir servisten alsak güzel olmaz mı? Kendinizi güçlü gördüğünüz bir programlama dili ile pekala REST tabanlı bir servis yazıp bu terminal uygulamasından çağırmayı deneyebilirsiniz. JSON dosyasını binary ile birlikte taşımanın daha kolay bir yolu var mıdır? Örnekte sadece dosyadan json veri okuyup ters serileştirme ile bir vektor dizisine nasıl alınacağına baktık. Peki komut satırından bu dosyaya yeni bir ipucu eklemek istersek nasıl bir yol izleriz? İşte bana ve size pek güzel sorular :) 

Böylece geldik bir rust pratiğimizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Viewing all 351 articles
Browse latest View live