Design Patterns & Golang: Singleton Pattern

Arda Coşar
4 min readJun 20, 2023

--

Giriş

Selamlar, design pattern yazı serimde özellikle gerçek hayattan basit örnekler seçmeye çalışıyorum. Çünkü sektörün içinden olsun diye seçilen karmaşık örnekler, hakim olmadığınız terimleri barındırabileceğinden ana fikrin anlaşılmasını zorlaştırabiliyor. Ayrıca sektördeki kullanım alanlarını vermenin kişiyi, pattern’i anlamaktan çok ezbere ittiğini düşünüyorum.

Hedefim, patternleri kullanmanız gerektiği yerlerde problemi tanımanız ve çözümü kendi şartlarınıza göre uyarlayabilecek seviyeye gelmeniz olacak.

Hikaye

Gül apartmanında yaşayan bir daire sahibisiniz. Maalesef apartmanınız çok eski ve yaşadığınız ilde yakın zamanda bir deprem bekleniyor. Bu durum artık herkesin canını sıkmaya başladı. Apartman sakinleri olarak hepiniz kentsel dönüşüm talebi için gerekli yerlere başvurma kararı aldınız.

Önce herkes kendince dilekçe belgesinin bir kopyasını çıkartarak imzaladı. Ancak etkili olabilmesi için bütün imzaların aynı belgede olması gerekiyordu. Bu bilgiyi öğrenen kişiler imzaların ortak bir belgede toplanması için harekete geçti ama bunun kimde olduğunu bilmiyorlardı. Birinin bu işe el atması gerekiyordu.

Apartman yöneticisi sorumluluğu aldı ve bir dilekçe hazırlayacağını söyledi. Müsait olduğunuzda imzalamak için dilekçe kağıdını hepinizin bildiği odamda bulabilirsiniz diye de ekledi. Herkesin aynı dilekçeye imza atması gerekiyordu.

Herkes sırayla geldiğinde sorun olmuyordu ama bazı apartman sakinleri sabırsızlık edip dilekçeye aynı anda imza atmaya çalıştı. Sonuç olarak da kağıt yırtıldı. Apartman yöneticisi yeni bir kağıt hazırladı ve bu sefer herkesi zorla sıraya sokması için birisine görev verdi. Artık aynı anda imzalamaları mümkün değildi. Dilekçe başarıyla hazırlandı. Gül apartmanı sakinleri sonsuza kadar mutlu oldu.(ama sert tutumundan dolayı yöneticiyi değiştirmeyi düşünüyorlar)

Problem

  • Herkesin kendince dilekçe hazırlamaya çalışması
  • Ortak dilekçeye karar verildikten sonra insanların imzalayacağı dilekçenin nerede olduğunu bilememesi
  • Dilekçenin aynı anda imzalanmaya çalışılması

Çözüm

  • Tek bir dilekçe olması
  • Bu dilekçenin yerinin herkesçe biliniyor olması
  • Aynı anda imzalamaya çalışanlar kağıdı yırtabildiği için sıraya sokulması

Teknik Giriş

Öncelikle belirtmek isterim ki bence bu pattern’nin uygulanmasının en rahat olduğu dillerden birisi Golang olarak karşımıza çıkıyor. Singleton’nın asıl belası hep concurrency olmuştur. Thread-safe yapmaya çalıştıkça uğraştırır durur.

Peki nedir bu Concurrency ve Thread-safe? Özetleyecek olursam, concurrency eşzamanlılık demektir. Bu da aynı anda iş yapılması anlamına gelir. Peki singleton ile alakası nedir? Singleton tek bir obje olduğu için iki thread aynı anda erişmeye çalıştığında sıkıntılar çıkabiliyor. Thread-safe dediğimiz yapı da eş zamanlılığa uygun şekilde tasarlanması anlamına geliyor. Neyseki Golang bu konuda oldukça esnek ve gayet yeterli çözümler sunuyor.

Ön bilgilendirme faslını da bitirdiğimize göre örneğimizi modellemeye Dilekçe objemizin yapısını kurarak başlayabiliriz.

type Dilekce struct {
imzalar []string
}

var gorevli sync.Mutex

func (d *Dilekce) Imzala(imza string) {
gorevli.Lock()
defer gorevli.Unlock()

d.imzalar = append(d.imzalar, imza)
fmt.Println("Dilekce imzalandi:", imza)
}

Dilekçe objesi, içerisinde imzalar isimli bir string listesi barındırıyor. Ek olarak kendisine parametre olarak geçilen imzayı sahip olduğu diziye ekleyen bir fonksiyona sahip.

İçindeki gorevli objesi ilginizi çekmiş olabilir. Kendisinin buradaki görevi eşzamanlı olarak gelen istekleri sıraya sokmak ve oluşabilecek hataların önüne geçilmesidir.

gorevli.Lock() kodu çalıştığında ilgili goroutine işini bitirene kadar burayı kilitliyor ve defer gorevli.Unlock() komutu ile işi bittiğinde kilidi kaldırıyor. Eğer ki bu zaman zarfında başka bir goroutine bu fonksiyona gelirse bloklanacak ve kilit kalktığı andan itibaren kendisi kilitleyip işlemini yapmaya başlayacaktır. Şimdi bir de dilekçemize nasıl erişeceğimize bakalım.

var dilekce *Dilekce

var apartmanYoneticisi sync.Once

func DilekceyiGetir() *Dilekce {
if dilekce == nil {
fmt.Println("Dilekçe boş, yaratılmak için harekete geçiliyor")
apartmanYoneticisi.Do(
func() {
fmt.Println("Dilekçe ilk kez yazıldı")
dilekce = &Dilekce{}
},
)
} else {
fmt.Println("Dilekçe zaten var")
}

return dilekce
}

Dilekçemize sadece DilekceyiGetir() fonksiyonunu kullanarak erişebiliriz. Bu fonksiyonun görevi; eğer daha önce üretilmemişse üretilmesi, daha önce üretildiyse de tekrar üretilmemesini garanti altına almak. Apartman yöneticimiz burada bir kez yaratılacağını garanti ediyor. Buraya aynı anda istekler gelse bile içerideki fonksiyon sadece bir kez çalıştırılacak.

Örnek senaryomuza geçmeden önce Daire sahipleri için nasıl bir obje yarattığımıza da göz atalım.

type DaireSahibi struct {
imza string
}

func (ds DaireSahibi) Imzala(wait *sync.WaitGroup) {
defer wait.Done()

dilekce = DilekceyiGetir()
dilekce.Imzala(ds.imza)
}

Burada aslında oldukça sade bir yapı var. Dilekçeyi eline alıyor ve imzalıyor. Fonksiyonun içerisindeki defer wait.Done() satırı dikkatinizi çekmiş olabilir. Bunu bütün goroutinler işlerini bitirmeden program durdurulmasın diye yerleştirdim. Bu terimlerin çok ayrıntısına girmeden özetleyerek geçmeye çalışıyorum çünkü kendileri bizzat makale konusu olabilecek derinlikte oluyor.

Her şeyi konuştuğumuza göre örnek artık kodumuza geçebiliriz.

var wait sync.WaitGroup

func OrnekCalistir() {
var apartmanSakinleri []DaireSahibi
kisiSayisi := 5

for i := 1; i <= kisiSayisi; i++ {
apartmanSakinleri = append(apartmanSakinleri,
DaireSahibi{
imza: fmt.Sprintf("imza-%d", i),
})
}
wait.Add(kisiSayisi)

for _, daireSahibi := range apartmanSakinleri {
go daireSahibi.Imzala(&wait)
}

wait.Wait()

fmt.Println("İmzalar:", dilekce.imzalar)
fmt.Println("İmza sayısı:", len(dilekce.imzalar))
}

Programın başında farklı imzalara sahip 5 apartman görevlisi yaratıyoruz. Daha sonrasında wait.Add(kisiSayisi) satırı ile bitmesini bekleyeceğimiz goroutine sayısını bildiriyoruz. Hemen altındaki for döngüsüyle de eşzamanlı olarak aynı dilekçeyi imzalamaya çalışıyoruz.

wait.Wait() komutu bütün işlemler bitene kadar programımızın kapanmasını engelleyecektir. Özetle wait.Done() fonsiyonu, wait.Add() ile belirtilen adetler kadar çalışıncaya dek programın senkron olarak akışı durdurulacak. Bütün işlemler bittikten sonra da kaç imza atıldığını ve hepsinin listeye eklenip eklenmediğini kontrol edeceğiz. Hadi o zaman programı çalıştırıp çıktısına bakalım:

Dilekçe boş, yaratılmak için harekete geçiliyor
Dilekçe ilk kez yazıldı
Dilekce imzalandi: imza-5
Dilekçe zaten var
Dilekce imzalandi: imza-3
Dilekçe zaten var
Dilekce imzalandi: imza-4
Dilekçe boş, yaratılmak için harekete geçiliyor
Dilekce imzalandi: imza-2
Dilekçe boş, yaratılmak için harekete geçiliyor
Dilekce imzalandi: imza-1
İmzalar: [imza-5 imza-3 imza-4 imza-2 imza-1]
İmza sayısı: 5

Sonuç

Artık bu dizaynın ne gibi problemlere çözüm getirdiğini biliyorsunuz. Ek olarak bu dizayn ile beraber gelen sorunların da nasıl çözüleceğini biliyorsunuz. Ama hala aklınızda bir soru olabilir. Belki de programın çıktısında gördüğünüz bir durum sizi huzursuz etti. “Dilekçe boş, yaratılmak için harekete geçiliyor” yazısı birden fazla kez yazılmış ancak “Dilekçe ilk kez yazıldı” yazısı sadece bir kere basılmış. Peki bu nasıl olabilir?

if dilekce == nil {
fmt.Println("Dilekçe boş, yaratılmak için harekete geçiliyor")
apartmanYoneticisi.Do(
func() {
fmt.Println("Dilekçe ilk kez yazıldı")
dilekce = &Dilekce{}
},
)
}

Bunun sebebi aynı veya çok yakın bir anda dilekçeyi elde etmeye çalışan goroutineler obje yaratılmamışken if bloğunun içine girebildiği için yaşanıyor. Her çalıştırdığınızda çıktıyı farklı görmeniz de muhtemeldir. Neyse ki içlerinden birisi ilk kez Do fonksiyonunu çalıştırdıktan sonra diğerlerinin dilekçeyi yeniden oluşturması Apartman Yöneticisi tarafından engellenmiş oluyor.

--

--