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?
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 ☕🍫.
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.)
Encapsulation Örnekleri👨💻
class User {
private var name: String = ""
fun getName(): String {
return name
}
fun setName(userName: String) {
name = userName
}
}
Bu örnekte, name
adlı private
bir değişkeni olan User
adlı bir sınıfımız var. Değişkeni private
olarak işaretleyerek, name değişkenine sınıfın dışından doğrudan erişimi engelleyerek verileri encapsulate ediyoruz.
Sınıf, name
değişkenine erişilmesi veya değiştirilmesi için getName()
ve setName()
adlı iki fonksiyon sağlar. getName()
fonksiyonu, name
değişkeninin değerini döndürür ve setName()
fonksiyonu, name
değ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, Item
nesnelerinin bir listesini encapsulate eden ShoppingCart
adında bir sınıfımız var. Sınıfın içinde Item
listesini saklayan items
adlı private bir değişkeni var. Alışveriş sepetindeki tüm ürünlerin toplam fiyatını saklayan totalPrice
adlı 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 false
döndürür. Aksi takdirde ürünü sepete ekler ve totalPrice
değ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 totalPrice
değ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.
ShoppingCart
sı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, ShoppingCart
sı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.
İkinci olarak, ShoppingCart
sı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 🟢❓
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 Product
adı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, Product
sı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 Product
sı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?🔵❓
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, positiveText
ve negativeText
butonları ve bunlara karşılık gelen onPositiveClicked
ve onNegativeClicked
high-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) 👨💻
Ç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, processPayment
fonskiyonuna sahip bir Order
sınıfımız var. processPayment
fonskiyonu bir miktar alır ve ödemeyi başlatmak için PaymentMethod
nesnesindeki pay
yöntemini çağırır.
PaymentMethod
sınıfının, ödemenin nasıl gerçekleşeceğine ilişkin kendi kurallarını belirlemek için pay
yöntemini override eden CreditCard
ve BankTransfer
adlı 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 Order
nesnesine iletilir. processPayment
fonksiyonu çağrıldığında, paslanan ödeme yöntemi nesnesinin içindeki pay
fonksiyonu çağrılır. Bu pay
fonksiyonu o ödeme yöntemine özel işlemler yapar.
Hangi pay
fonksiyonunun çağrılacağı nesnenin türüne göre çalışma zamanında belirlenir. CreditCard
türündeki bir nesnede pay
fonksiyonunu çağırdığımızda CreditCard
sınıfının içindeki kodlar/kurallar çalıştırılacak. BankTransfer
türündeki bir nesnedepay
fonksiyonunu çağırdığımızda BankTransfer
sınıfının içindeki kodlar/kurallar çalıştırılacak.
Abstraction Nedir⚪❓
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 Product
sı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 calculateTotalCost
adlı 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 calculateTotalCost
fonksiyonun 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 Product
sı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, Product
sı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, Product
sınıfı, Book
ve Clothing
gibi 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, Product
sınıfı, bir ürünün uygulama/kod ayrıntılarını gizler. Sistemin diğer bölümlerini etkilemeden Product
sı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 Product
sınıfı, Book
ve Clothing
gibi 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.
Keyifli Kodlamalar 👩💻.Okuduğunuz için teşekkürler🤗. Sonraki yazılarda görüşmek üzere👋.