Nesne Yönelimli Programlama Kavramlarına Gerçek Senaryolar ile Bakış

Yeni bir ev tasarlayacak bir mimar olduğunuzu hayal edin. Çalışmak için boş bir tuvaliniz var ve onu nasıl mükemmel bir ev haline getireceğinize dair fikirleriniz var. Peki nereden başlarsınız? Tüm bu fikirleri nasıl organize eder ve somut bir şeye dönüştürürsünüz?

Photo by Daniel K Cheung on Unsplash

Giriş

OOP’nin (Nesne Yönelimli Programlama) burada devreye girip yazılım geliştirme sürecinin karmaşıklığını azaltır. Nesne yönelimli programlama (Object-Oriented Programming) (OOP), tıpkı bir mimarın bir eve şekil, yapı mimari getirdiği gibi, kodunuza mimari getirmenize yardımcı olabilecek güçlü bir araçtır. Programınızı, nesneler (object) adı verilen net sorumluluklara sahip daha küçük, yönetilebilir ayrı parçalara bölerek, kullanıcılarınızın ihtiyaçlarını karşılayan daha verimli, esnek ve ölçeklenebilir yazılımlar oluşturabilirsiniz. Gelin OOP konseptlerini projelerinize nasıl uygulayabileceğinize beraber bakalım ☕🍫.

https://media.giphy.com/media/iMBEgyXkFBtdCFS93i/giphy.gif

Nesne Yönelimli Programlama (OOP) Konseptleri Nelerdir❓

Nesne Yönelimli Programlama (OOP) Konseptleri, OOP paradigmasını tanımlayan temel fikir ve ilkelerdir. Bu konseptler encapsulation🔴, inheritance 🟢, polymorphism 🔵 veabstraction’dır 🟠. Karmaşık uygulamaların oluşturulmasını, bakımınının kolaylaştırılmasını sağlarlar.

Encapsulation Nedir🔴❓

Encapsulation, bir sınıfın çalışma-kod ayrıntılarını dış dünyadan gizleme ve nesneyle etkileşim için basit bir arayüz sağlama işlemidir. İlgili verilerin (değişkenler) ve fonksiyonların gruplandırılmasını,private, public, veprotected gibi visibility modifierları aracılığıyla bunlara erişimin kontrol edilmesini içerir.

Visibility Modifer nedir❓

Visibility modifierlar nesneler, fonksiyonlar ve iç sınıflar (inner class) gibi sınıf üyelerine erişim düzeyini kontrol etmek için kullanılır. Çoğu OOP dilinde üç tür görünürlük değiştirici vardır: public, private, ve protected.

public; bir sınıf üyesine programdaki herhangi bir koddan erişilebileceği anlamına gelir. Sınıfın bir nesnesine (instance) sahip olan veya statik bir nesneye referansı olan herhangi bir kod tarafından erişilebilir.

private; bir sınıf üyesine yalnızca aynı sınıf içinden erişilebileceği anlamına gelir. Sınıfın dışındaki kodlar, hatta alt sınıflardan bile erişilemez.

protected; bir sınıf üyesine aynı sınıf ve onun alt sınıflarından erişilebileceği anlamına gelir. (Inheritance bölümünde alt sınıflardan bahsedeceğiz.)

https://media.giphy.com/media/e7yNPQmGUozyU/giphy.gif

Encapsulation Örnekleri👨‍💻

class User {
private var name: String = ""

fun getName(): String {
return name
}

fun setName(userName: String) {
name = userName
}
}

Bu örnekte, nameadlı privatebir değişkeni olan Useradlı bir sınıfımız var. Değişkeni privateolarak işaretleyerek, name değişkenine sınıfın dışından doğrudan erişimi engelleyerek verileri encapsulate ediyoruz.

Sınıf, namedeğişkenine erişilmesi veya değiştirilmesi için getName() ve setName() adlı iki fonksiyon sağlar. getName() fonksiyonu, namedeğişkeninin değerini döndürür ve setName() fonksiyonu, namedeğişkenini değiştirmemize imkan sağlar.

Hadi başka bir örneğe bakalım 🧐

data class Item(val id: Long, val price: Double, val name: String, val stockCount: Int, val quantity: Int )

class ShoppingCart(private val items: MutableList<Item>) {
private var totalPrice: Double = 0.0

fun addItem(item: Item): Boolean {
if (item.quantity > item.stockCount) {
return false // Don't add the item to cart
}

items.add(item)
totalPrice += (item.price * item.quantity)
return true
}

fun removeItem(item: Item) {
items.remove(item)
totalPrice -= (item.price * item.quantity)
}

fun getTotalPrice(): Double {
if (isTotalPriceReachedFreeCargoThreshold()) {
return totalPrice
}

return totalPrice + getCargoPrice()
}

private fun getCargoPrice(): Double = 33.0

private fun isTotalPriceReachedFreeCargoThreshold(): Boolean {
return totalPrice >= 100
}

}

Bu örnekte, Itemnesnelerinin bir listesini encapsulate eden ShoppingCartadında bir sınıfımız var. Sınıfın içinde Itemlistesini saklayan itemsadlı private bir değişkeni var. Alışveriş sepetindeki tüm ürünlerin toplam fiyatını saklayan totalPriceadlı private bir değişkeni var.

addItem(item: Item): Boolean fonksiyonu, sepete bir ürün eklemek için kullanılır. Parametre olarak bir ürün alır. Talep edilen miktar, ürünün stok sayısından fazlaysa, fonksiyon, ürünün sepete eklenmediğini belirten falsedöndürür. Aksi takdirde ürünü sepete ekler ve totalPricedeğişkenini günceller. Eklenen ürünün fiyatını toplam fiyata eklemiş olur. Bunu yaparak sepet öğelerini encapsulate ettik. Kendi ihtiyacımıza göre, yeni bir ürün eklendiğinde gerekli yerleri güncelledik ettik. Bunların güncellenmesini dışarıya bırakmayıp iş kurallarımızın doğru çalışmasını garantiledik.

getTotalPrice() fonksiyonu, varsa kargo ücretini de dahil olmak üzere sepetteki tüm öğelerin toplam fiyatını hesaplar. Sepetin toplam fiyatı, ücretsiz gönderim eşiğinden (bu durumda 100) büyük veya eşitse, fonksiyon yalnızca totalPrice(ürünlerin toplam fiyatı) değişkenini döndürür. Aksi takdirde, kargo ücretini totalPricedeğişkenine ekler ve sonucu döndürür. Bu şekilde de toplam fiyatın ne olacağı ile ilgili iş kurallarımızı (bussiness logic) sınıfın içinde yerine getirerek fiyatın doğru hesaplanmasını garantiledik. Dışarıya açık olsaydı, fiyat hesaplanacak her yerde ücretsiz kargo durumu gözetilmesi gerekirdi. Bu hem kod kalabalığı hem de hataya açık olmak (unutulabilir) demek. Encapsulation ile bunlardan kurtulmuş oluyoruz.

ShoppingCartsınıfı, sepet ürünleri ilgili tüm yönetimi ve kuralları kapsar ve bu süreçlerde herhangi bir harici değişiklik yapılmasını engellemiş olur.

Encapsulation’ın faydaları

İlk olarak, ShoppingCartsınıfının içindeki bilgilerin, kuralların dışardaki koddan gizlenmesini sağlayarak yanlışla değişiklik yapılmasını önler. Bu, kod kararlılığını artırır.

https://media.giphy.com/media/hpF9R9M1PHN5e5liSx/giphy.gif

İkinci olarak, ShoppingCartsınıfının içindeki kuralların, onu kullanan harici kodu etkilemeden değişiklikler yapılabileceğinden, encapsulation kodu korumayı ve güncellemeyi kolaylaştırır. Bu, modülerliği destekler ve geliştirme süresini kısaltmaya yardımcı olabilir.

Üçüncüsü, hassas veriler veya iş kuralları gizli tutulabileceğinden ve yetkisiz kodlara erişilemeyeceğinden, encapsulation kodun güvenliğini artırmaya yardımcı olabilir.

Inheritance Nedir 🟢❓

https://media.giphy.com/media/l0HlKYxenTClHlOV2/giphy.gif

Kalıtım (Inheritance), mevcut bir sınıfa dayalı olarak yeni bir sınıf oluşturmanıza olanak tanır. Alt sınıf (subclass) olarak bilinen yeni sınıf, süper sınıf/ana sınıf (super class/parent class) olarak bilinen mevcut sınıfın tüm değişkenlerini ve fonksiyonlarını devralır ve ayrıca kendi yeni değişkenlerini ve fonksiyonlarını ekleyebilir.

Kalıtım, “is-a” ilişkisi fikrine dayanır, yani bir alt sınıf, üst sınıfının “is-a” türüdür (-dır, -dir). Örneğin, “bir araba bir araçtır”. Böylece Araç üst sınıfından miras alan bir Araba alt sınıfı oluşturabilirsiniz.

Inheritance Örnek👨‍💻

Elektronik, giyim ve ev aletleri gibi çeşitli türde ürünlerin satıldığı bir alışveriş platformunu düşünelim. Her ürünün ad, açıklama ve fiyat gibi bazı ortak özellikleri vardır. Bunun yanı sıra ürünün türü, boyutu, rengi ve malzemesi gibi ürüne özel özelliklerde var olabilir.

Bunu Kotlin’de kalıtım kullanarak yapmak için, ortak özellikleri / değişkenleri içeren Productadında bir üst sınıfı open kelimesi kullanarak oluşturabiliriz. Diğer dillerde genellikle open gibi bir anahtar sözcük kullanmaya gerek yoktur.

open class Product(val name: String, val description: String, val price: Double) {
// common methods and properties for all products
}

Ardından, Productsınıfından inherit (miras) alan her ürün türü için : kullanarak alt sınıflar (child class) oluşturabilir ve bunların belirli özelliklerini/değişkenlerini ekleyebiliriz. Diğer dillerde genellikle extends anahtar kelimesi kullanılır “:” yerine.

class Electronics(val brand: String, val model: String, name: String, description: String, price: Double,) 
: Product(name, description, price) {
// additional methods and properties for electronic products
}

class Clothing(val size: String, val color: String, name: String, description: String, price: Double,)
: Product(name, description, price) {
// additional methods and properties for clothing products
}

Gördüğünüz gibi, her bir alt sınıf, üst Productsınıfından ortak değişkenleri miras alır ve kendi özel niteliklerini ekler. Bu şekilde, farklı ürün türleri arasında ortak özellikler için kodun tekrarlanmasını önleyebiliriz.

Aşağıdaki kod parçacığında bu sınıfları nasıl kullanabileceğimizi inceleyebilirsiniz.

// Usage of classes and variables
fun main() {
val computer = Electronics(
brand = "Apple",model = "M2 Air",
name = "Apple Computer", description = "", price = 1999.99
)

val tShirt = Clothing(
size = "S", color = "Green",
name = "Basic T-Shirt", description = "", price = 29.99
)

println("Common variable ${computer.name} Specific variable ${computer.model}")
println("Common variable ${tShirt.name} Specific variable ${tShirt.size}")

}

Inheritance (Kalıtım, Miras Alma) faydaları

Kodun Yeniden Kullanılabilirliği: Üst (parent) sınıf (Product), alt sınıflar (Electronics ve Clothing) tarafından paylaşılan ortak özellikleri ve fonksiyonları içerir. Alt sınıflar, üst sınıftan miras alarak, yazılan kodu yeniden kullanabilir.

Modülerlik: Kalıtım, modüler kodun oluşturulmasın sağlar. Ortak özellikleri ve fonksiyonları bir üst sınıfa ayırarak kodun bakımını yapmak ve güncellemek kolaylaşır. Bu aynı zamanda hataları önlemeye ve kod okunabilirliğini arttırmaya da yardımcı olur. Ayrıca, paylaşılan özelliklere ve davranışlara göre bir sınıf hiyerarşisi oluşturmanıza olanak tanır.

Polymorphism (Çok biçimlilik): Kalıtım, polimorfizme imkan tanır; bu, alt sınıfların nesnelerine, üst sınıfın nesneleri gibi davranılabileceği anlamına gelir. Bu, kod tasarımında daha fazla esneklik sağlar ve genel kod yazmayı kolaylaştırır.

Polymorphism Nedir?🔵❓

https://media.giphy.com/media/T6V1Cy3CwEi88/giphy.gif

Polimorfizm (Çok biçimlilik), nesnelerin birden çok şekilde davranabilme yeteneğini ifade eder. Polimorfizm, birden çok farklı türü temsil etmek için tek bir ad kullanma işlemidir.

OOP’de iki ana polimorfizm türü vardır: derleme zamanı polimorfizmi ( compile-time polymorphism) (overloading olarak da bilinir) ve çalışma zamanı polimorfizmi (runtime polymorphism) (overriding olarak da bilinir).

Compile-time Polymorphism (Overloading)

Derleme zamanı polimorfizmi, bir sınıfta aynı isme sahip ancak farklı parametrelere sahip birden çok fonksiyonun olmasıdır.

fun blablaFun() {}
fun blablaFun(number: Int) {}
fun blablaFun(number: Double) {}
fun blablaFun(number: Int, number2: Float) {}

Derleme sırasında, çağrılacak fonksiyon, iletilen değişkenlerin sayısına ve türüne göre belirlenir.

Compile-time Polymorphism (Overloading) Örnek👨‍💻

fun showDialog(context: Context, title: String, message: String) {
val builder = AlertDialog.Builder(context)
builder.setTitle(title)
builder.setMessage(message)
builder.show()
}

fun showDialog(
context: Context, title: String, message: String,
positiveText: String, negativeText: String,
onPositiveClicked: () -> Unit, onNegativeClicked: () -> Unit
) {
val builder = AlertDialog.Builder(context)
builder.setTitle(title)
builder.setMessage(message)
builder.setPositiveButton(positiveText) { dialog, which ->
onPositiveClicked()
}
builder.setNegativeButton(negativeText) { dialog, which ->
onNegativeClicked()
}
builder.show()
}

İlk fonksiyon üç parametre alır: context, title vemessage. Gönderilen başlık ve mesajla bir AlertDialog oluşturur ve bunu builder.show() fonksiyonu kullanarak gösterilir.

İkinci fonksiyon ek parametreler alır: positiveText, negativeText, onPositiveClicked veonNegativeClicked. Bu fonksiyon, positiveTextve negativeTextbutonları ve bunlara karşılık gelen onPositiveClicked ve onNegativeClickedhigh-order fonksiyonları ile daha karmaşık bir AlertDialog oluşturur. Kullanıcı herhangi bir butona tıkladığında, karşılık gelen high-order fonksiyon tetiklenir.

İkinci fonksiyon, birinci fonksiyondan daha fazla parametreye sahip olduğu için, ikinci fonksiyonun birinci fonksiyonun aşırı yüklenmiş (overloaded) bir versiyonu olduğunu söyleyebiliriz. showDialog fonksiyonunu üç parametre ile çağırdığımızda üstteki ilk fonksiyon, ek parametreler ile çağırdığımızda ise alttaki fonksiyon çağrılacaktır. Bu yaklaşım, geliştiricilerin aynı fonksiyonların farklı sürümlerini farklı parametrelerle çağırarak daha esnek ve yeniden kullanılabilir fonksiyonlar oluşturmasına olanak tanır.

Not: Kotlin’deki default ve named argumentleri kullanarak, birden fazla fonksiyon yazmaya gerek duyamadan polimorfizmi sağlayabilirsiniz.

Runtime Polymorphism (Overriding) 👨‍💻

https://media.giphy.com/media/13AN8X7jBIm15m/giphy.gif

Çalışma zamanı polimorfizmi, bir alt sınıfta, üst sınıfındaki bir fonksiyonla aynı imzaya (yani, ad ve parametreler) sahip bir fonksiyona sahip olarak elde edilir. Çağrılacak fonksiyonun hangisi olacağı çalışma zamanında belirlenir.

Runtime Polymorphism (Overriding) Örnek 👨‍💻

open class PaymentMethod {
open val type: String = ""

open fun pay(amount: Double) {
println("Payment completed with the default payment method.")
}
}

class CreditCard : PaymentMethod() {
override val type: String = "CreditCard"
override fun pay(amount: Double) {
// code to process the credit card payment goes here
// take credit card number, name, CVV etc then go checkout
println("Payment completed with credit card.")
}
}

class BankTransfer : PaymentMethod() {
override val type: String = "BankTransfer"
override fun pay(amount: Double) {
// code to process the bank transfer payment goes here
// show your IBAN number
println("Payment completed with bank transfer.")
}
}

class Order(private val paymentMethod: PaymentMethod) {
fun processPayment(amount: Double) {
paymentMethod.pay(amount)
}
}

Bu örnekte, processPaymentfonskiyonuna sahip bir Ordersınıfımız var. processPaymentfonskiyonu bir miktar alır ve ödemeyi başlatmak için PaymentMethodnesnesindeki payyöntemini çağırır.

PaymentMethodsınıfının, ödemenin nasıl gerçekleşeceğine ilişkin kendi kurallarını belirlemek için payyöntemini override eden CreditCardve BankTransferadlı iki alt sınıfımız var.

Ödeme sırasında, kullanıcı bir ödeme yöntemi seçtiğinde, uygun alt sınıfın bir nesnesi (örn. CreditCard veyaBankTransfer) oluşturulur ve Ordernesnesine iletilir. processPaymentfonksiyonu çağrıldığında, paslanan ödeme yöntemi nesnesinin içindeki payfonksiyonu çağrılır. Bu pay fonksiyonu o ödeme yöntemine özel işlemler yapar.

Hangi payfonksiyonunun çağrılacağı nesnenin türüne göre çalışma zamanında belirlenir. CreditCardtüründeki bir nesnede payfonksiyonunu çağırdığımızda CreditCardsınıfının içindeki kodlar/kurallar çalıştırılacak. BankTransfertüründeki bir nesnedepayfonksiyonunu çağırdığımızda BankTransfersınıfının içindeki kodlar/kurallar çalıştırılacak.

Abstraction Nedir⚪❓

http://objectorientedthought.blogspot.com/2015/09/abstraction-in-oop.html

Abstraction (Soyutlama), karmaşık sistemleri veya fikirleri basitleştirilmiş bir şekilde göstermeyi ifade eder. Soyutlama, ayrıntıları ve karmaşıklığı gizleyip bir nesnenin veya sistemin asıl özelliklerine odaklanmamızı sağlar.

OOP’de soyutlama, abstract (soyut) sınıflar ve interfaceler (arayüz) kullanılarak gerçekleştirilir.

Soyut bir sınıf, detaylı bir şekilde kodlanmamış sınıflar için bir taslak görevi gören bir sınıftır. Alt sınıf tarafından override edilmesi gereken, asıl işlev kodları yazılmamış fonksiyonları içerir. Alt sınıf içinde olması gereken özelliklerin çerçevesini belirler.

Arayüzler (interface) ise, bir sınıfın sağlaması gereken davranışları/özellikleri tanımlayan soyut fonksiyon koleksiyonlarıdır. Bir interface’i extend eden bir sınıf interface içindeki her fonksiyonun kodunu kendi içinde yazmak zorundadır.

Abstraction Örnek 👨‍💻

abstract class Product(val name: String, val price: Double) {
// Abstract method to calculate the total cost of the product
abstract fun calculateTotalCost(quantity: Int): Double
}

class Book(name: String, price: Double, val author: String) : Product(name, price) {
// Implementation of the abstract method for books
override fun calculateTotalCost(quantity: Int): Double {
return price * quantity
}
}

class Clothing(name: String, price: Double, val size: String) : Product(name, price) {
// Implementation of the abstract method for t-shirts
override fun calculateTotalCost(quantity: Int): Double {
val basePrice = price * quantity
return if (size == "XL") {
basePrice + 9.99 // XL t-shirts cost 9.99₺ extra
} else {
basePrice
}
}
}

Bu örnekte, genel bir ürünü temsil eden soyut bir Productsınıfımız var. Bu sınıfın iki değişkeni vardır: name veprice . Ayrıca bir adet bilgisi alan ve ürünün toplam maliyetini döndüren calculateTotalCostadlı bir soyut (abstract) fonksiyonumuz var.

Ayrıca,Product ’ı inherit alan Book veClothing adlı iki alt sınıfına sahibiz. Bu sınıflar, Product sınıfını genişletir (miras alır) ve her ürünün belirli özelliklerine dayalı olarak kendi calculateTotalCostfonksiyonun kodunu yazar, o ürüne özel kurallarını uygular.

Örneğin, Book sınıfı, toplam maliyeti hesaplamak için fiyatı adetle çarpar, Giyim sınıfı ise XL beden giysiler için toplam maliyete 9,99₺ ekler.

Soyutlamayı bu şekilde kullanarak, her ürün tipi için yeniden kod yazmak zorunda kalmadan farklı ürün tiplerini işleyebilen modüler ve esnek bir sistem oluşturabiliriz. Sistemdeki tüm ürünler için tutarlı bir kontrat için soyut Productsınıfına güvenirken, Product’ın yeni alt sınıflarını oluşturabilir ve bunların kendi calculateTotalCost fonsiyonlarını uygulayabiliriz.

Abstraction’ın faydaları

Sistemi basitleştirme: Soyutlama, karmaşık bir sistemin yapılmasını basitleştirir. Verilen örnekte, Productsınıfı, ürünün uygulama/kod ayrıntılarını soyutlayarak, sistemin diğer bölümlerinin onunla etkileşim kurmasını kolaylaştırır. Sistemdeki diğer bölümlerin Product’ı inherit alan bir sınıfın içinde temel olarak ne olacağını bilmesini sağlar.

Kod Tekrarını Azaltma: Soyutlama, benzer sınıflar için ortak bir arayüz sağlayarak bir sistemdeki kod tekrarını azaltır. Verilen örnekte, Productsınıfı, Bookve Clothinggibi farklı ürün türleri için ortak bir arabirim sağlar.

Modülerliği Arttırma: Soyutlama, bir sistemi daha küçük, daha yönetilebilir parçalara bölerek modülerliği teşvik eder. Verilen örnekte, Productsınıfı, bir ürünün uygulama/kod ayrıntılarını gizler. Sistemin diğer bölümlerini etkilemeden Productsınıfının inherit alan ürünün detaylarının değiştirilmesini kolaylaştırır. Bu da sistemin bakımını ve zaman içinde değiştirilmesini kolaylaştırır.

Testi Kolaylaştırma: Soyutlama, soyut sınıf ile kod ayrıntılarını içeren alt sınıf arasında açık bir ayrım sağlayarak testi kolaylaştırır. Verilen örnekte Productsınıfı, Bookve Clothinggibi farklı uygulamalardan bağımsız olarak test edilebilir.

🔚 💚 👏 Sonuç

Çoğumuz mimarların evler tasarlayıp inşa ettiğini hayal ederiz. Ancak bir mimarın gerçek amacı bir sorunu çözmektir. İyi bir mimar, işlevsellik ve estetiği birleştirerek kullanıcıların ihtiyaçlarını karşılayacak bir yapı tasarlar. Yazılım mühendisliği de benzer bir yaklaşıma sahiptir. Nesne Yönelimli Programlama (OOP) ile yazılımcılar, kodu daha modüler, daha okunabilir ve daha yönetilebilir hale getirerek kullanıcılara daha iyi bir deneyim sağlamak için benzer şekilde kod tasarlar.

Makaleleri 50'ye kadar alkışlayabileceğinizi biliyor muydunuz? Eğer bu makaleyi yararlı ve eğlenceli bulduysanız alkış ikonuna basılı tutarak bu özelliği deneyebilirsiniz.

Source

Keyifli Kodlamalar 👩‍💻.Okuduğunuz için teşekkürler🤗. Sonraki yazılarda görüşmek üzere👋.

Referanslar

--

--

Cengiz Toru (🇵🇸 #FreePalestineFromGenocide)
Huawei Developers - Türkiye

(🇵🇸 #FreePalestineFromGenocide 🍉) | Muslim, Computer Engineer & Android Developer @ Hepsiburada (NASDAQ: HEPS ) , ex; Huawei, T-Soft, Arneca