SOLID prensipleri yazı serisinde daha öncesnde SOLID nedir ve Single Responsibility konularından bahsetmiştik. Şimdi ise sırada SOLID'in "O" su olan Open-Closed Principle. Bu prensip 1988 yılında fransız akademist Bertrand Meyer tarafından ortaya atılıp şu şekilde tanımlandı;
"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification."
Peki ne demek bu "gelişime açık değişime kapalı" sözü ? Şu bir gerçek ki bir kod yazacaksınız ve ilelebet hep aynı kalacak.. tamamen palavra. Hiç bir kod yıllar içinde değişime uğramadan kalamaz, sürekli olarak yeni bir feature ekleme durumu söz konusudur ve müşteri sürekli olarak "şunu da ekleyelim, bunu da ekleyelim, şu da olsun.." vs. vs. istekler hiçbir zaman bitmeyecektir. Bu tarz durumlar için Open-Closed bize yeni gelen değişiklik isteklerini projenize eklerken yazmış olduğunuz sınıfların genel davranışlarını değiştirmeden sadece birkaç ufak dokunuşla bu geliştirmeleri ekleyebileceğiniz class lar yazmamızı söylüyor. Yani Core koda gerekirse hiç müdahale etmeden yeni feature'ları ekleyin diyor. Sebebi ise mevcut koda yapılacak olan her müdahale yeni bug'ların çıkmasına neden olabilir ve buda yeniden test-analiz süreci demektir ki bunu hiçbir firma veya çalışan istemez.
Örnek bir proje üzerinden ilerleyelim. Bad Example ve Better Example diye Open-Closed'u iki şekilde ele alalım.
Bad Example
Örneğimiz şu şekilde olacak; Araba ve Otobüs üretimi yapan bir fabrikamız var diyelim. Bunun için bir tane aşağıdaki gibi Vehicle adında bir class'ımız olsun. Bu class içerisinde VehicleType isminde bir enum ve bu enum'ında Car-Bus diye 2 değeri olsun. Daha sonra Vehicle class'ından inherit olmuş Car ve Bus isminde 2 class oluşturalım ve constructor'larında gerekli enum değerlerini verelim.
Vehicle.cs
namespace BadExample
{
public class Vehicle
{
public VehicleType VType { get; set; }
}
public enum VehicleType
{
Car,
Bus
}
public class Car : Vehicle
{
public Car()
{
this.VType = VehicleType.Car;
}
}
public class Bus : Vehicle
{
public Bus()
{
this.VType = VehicleType.Bus;
}
}
}
Şimdi ise bu araçları üretme kısmına geldi. VehicleFactory adında bir class oluşturalım ve bu class içerisinde ProduceVehicle isminde Vehicle objesini parametre olarak alan bir metod olsun ve bu metoda verilen objedeki vehicleType enum değerine göre ilgili aracı üreten fonksiyonları çağırsın.
VehicleFactory.cs
namespace BadExample
{
public class VehicleFactory
{
public void ProduceVehicle(Vehicle vehicle)
{
switch (vehicle.VType)
{
case VehicleType.Car:
ProduceCar((Car)vehicle);
break;
case VehicleType.Bus:
ProduceBus((Bus)vehicle);
break;
}
}
private void ProduceCar(Car car)
{
Console.WriteLine("Car Produced\n");
}
private void ProduceBus(Bus car)
{
Console.WriteLine("Bus Produced\n");
}
}
}
Şimdi sırada yazmış olduğumuz class'ları kullanarak araçları üretme işlemi var. Bunun için Program.cs class'ı aşağıdaki gibi olacak
Program.cs
namespace BadExample
{
class Program
{
static void Main(string[] args)
{
VehicleFactory vf1 = new VehicleFactory();
vf1.ProduceVehicle(new Car());
VehicleFactory vf2 = new VehicleFactory();
vf2.ProduceVehicle(new Bus());
Console.ReadLine();
}
}
}
Uygulamamızın Ekran çıktısı
Ne güzel araçlarımız ürettik kodumuz tıkır tıkır çalışıyor ve fabrika haftada 300 car-bus üretiyor. Peki patron çıldırdı dedi ki "Arkadaşlar 2 ay sonra kamyon üretmeye başlıyoruz, bütün altyapıyı hazır edin..". Hiç sorun değil nasıl olsa kodumuzu yazdık arabayı otobüsü nasıl ürettiysek kamyonu da öyle üretiriz deyip kamyon için aşağıdaki gibi kodlarımızı modify edip değişiklikleri yapalım.
1- İlk olarak => VehicleType enum'ına Truck diye yeni bir alan eklemeliyiz
public enum VehicleType
{
Car,
Bus,
Truck//Truck enum değerini ekledik
}
2- İkinci olarak => Vehicle class'ına Truck objesini oluşturma. Nasıl Car ve Bus için Vehicle'dan inherit olan class lar yazdık aynı şekilde Truck içinde bunu yapıyoruz
namespace BadExample
{
public class Vehicle
{
public VehicleType VType { get; set; }
}
public enum VehicleType
{
Car,
Bus,
Truck
}
public class Car : Vehicle
{
public Car()
{
this.VType = VehicleType.Car;
}
}
public class Bus : Vehicle
{
public Bus()
{
this.VType = VehicleType.Bus;
}
}
public class Truck : Vehicle//Truck objesini tanımladık
{
public Truck()
{
this.VType = VehicleType.Truck;
}
}
}
3- Üçüncü olarak => VehicleFactory class'ına ProduceTruck class'ını ekleyip ProduceVehicle metoduna Truck için gerekli kontrolleri ekleyeceğiz
using System;
namespace BadExample
{
public class VehicleFactory
{
public void ProduceVehicle(Vehicle vehicle)
{
switch (vehicle.VType)
{
case VehicleType.Car:
ProduceCar((Car)vehicle);
break;
case VehicleType.Bus:
ProduceBus((Bus)vehicle);
break;
case VehicleType.Truck://truck üretimi için logic kısmını yazdık
ProduceTruck((Truck)vehicle);
break;
}
}
private void ProduceCar(Car car)
{
Console.WriteLine("Car Produced\n");
}
private void ProduceBus(Bus car)
{
Console.WriteLine("Bus Produced\n");
}
private void ProduceTruck(Truck truck) //truck üretimi yapan metodu ekledik
{
Console.WriteLine("Truck Produced\n");
}
}
}
4- Dördüncü olarak => Yazmış olduğumuz kodları kullanma vakti, Main fonksiyonda da Truck üretimi için gerekli metod çağrılarını yapacağız.
namespace BadExample
{
class Program
{
static void Main(string[] args)
{
VehicleFactory vf1 = new VehicleFactory();
vf1.ProduceVehicle(new Car());
VehicleFactory vf2 = new VehicleFactory();
vf2.ProduceVehicle(new Bus());
VehicleFactory vf3 = new VehicleFactory();
vf3.ProduceVehicle(new Truck());
Console.ReadLine();
}
}
}
Uygulamamızın Ekran çıktısı
Kamyonumuzu ürettikkk. Patron mutlu alan mutlu satan mutlu.. Peki ama yeni bir araç yani Kamyon türünde bir Vehicle üretmek için neler yaptık öyle... Elimizi atmadığımız class kalmadı. Bu işlemi tam 4 farklı adımda halledebildik. Bu peki istediğimiz bir şey mi ?.. Ne diyordu Open-Closed prensibi "gelişime açık değişime kapalı" Peki biz ne yaptık; projede ki logic kısmı dahil her yere müdahale edip bir takım değişiklikler yaptık yani prensibe uygun hareket etmedik.
Peki nasıl olması gerekirdi ? Sadece Main fonksiyonunda bir parça kod ekleyip sorunu çözecek halimiz yok tabiki de ancak patron yarın tekrardan gelip "Motorsiklet üretmeye başlıyoruz, Bisiklet üretmeye başlıyoruz.." diye devam etme ihtimaline karşı daha generic ve core koda yani logic'in bulunduğu kod kısımlarına çok fazla dokunmadan bir kaç extended class yazarak yeni üretim işlemine başlıyor olmamız gerekirdi.
Better Example
Şimdi gelin Truck üretim işlemimizi biraz daha "Better than the previous example" sloganıyla yazalım
İlk olarak Vehicle.cs ile başlayalım. Bu sefer Vehicle class'ımız abstract içerisinde bir adet abstract olarak tanımlı Produce() metodu var ve Car, Bus ve Truck objeleri de yine Vehicle class'ını implemente edip ve Produce metodunu kullanacaklar.
using System;
namespace GoodExample.MyFolder
{
public abstract class Vehicle
{
public abstract void Produce();
}
public class Car : Vehicle
{
public override void Produce()
{
Console.WriteLine("Car Produced\n");
}
}
public class Bus : Vehicle
{
public override void Produce()
{
Console.WriteLine("Bus Produced\n");
}
}
public class Truck : Vehicle
{
public override void Produce()
{
Console.WriteLine("Truck Produced\n");
}
}
}
VehicleFactory.cs class'ımızda artık hiçbir if/else yada switch/case condition'ı bulunmicak. Bu class ta yine Vehicle tipinde parametre alan ProduceVehicle() adında bir metod olacak ve tek görevi parametre olarak aldığı abstract Vehicle'dan türeyen objeyi alıp Produce() metodunu çağırmak.
namespace GoodExample.MyFolder
{
public class VehicleFactory
{
public void ProduceVehicle(Vehicle vehicle)
{
vehicle.Produce();
}
}
}
Program.cs'ın diğer örnekte olduğu gibi aynısını kullanıcaz yani üretmek istediğimiz objenin instance'ını alıp üretime başlicaz.
using System;
namespace GoodExample
{
class Program
{
static void Main(string[] args)
{
VehicleFactory vf1 = new VehicleFactory();
vf1.ProduceVehicle(new Car());
VehicleFactory vf2 = new VehicleFactory();
vf2.ProduceVehicle(new Bus());
VehicleFactory vf3 = new VehicleFactory();
vf3.ProduceVehicle(new Truck());
Console.ReadLine();
}
}
}
Uygulamamızın Ekran çıktısı
Şimdi içimizden şu soruyu sorabiliriz.. "Eeee noldu şimdi aynı işi yaptık, hedefimiz Truck üretimi yapmaktı Bad Example'da da yaptık Better Example'da da..". Cevabı açık ve net arkadaşlar
- Projemizi daha generic hale getirdip hiçbir condition'a bağımlı tutmadık,
- Abstraction kullanarak projedeki bazı yerleri daha kolay geliştirebilir hale getirdik,
- Core taraftaki kodu gelişime açık çok büyük köklü değişimlere kapalı hale getirdik,
- Yeni bir üretim işlemi başlasın Vehicle objesinden inherit olan class'ımızı olluşturup Produce metodunun içerisini doldurduktan sonra kolayca üretime başlayabilecek hale geldik,
- Yani kısaca projemizi Open-Closed prensibine uygun hale getirdik
BadExample'da yaptığımız gibi yeni bir araç üretimine başlamak için yapmış olduğumuz o 4-5 farklı değişiklikten kurtulup BetterExample'da olduğu gibi sadece üretilecek olan nesneyi Vehicle objesinden inherit edilmiş şekilde tanımlayıp Main fonksiyonunda Üretim işlemini başlatmak yeterli olacaktır.
Hadi son olarak Motorcycle üretelim. İlk olarak nesnemizi tanımlıyoruz
public class Motorcycle : Vehicle
{
public override void Produce()
{
Console.WriteLine("Motorcycle Produced\n");
}
}
Sonrasında Main fonksiyonunda üretime başla diyoruz
using System;
namespace GoodExample
{
class Program
{
static void Main(string[] args)
{
VehicleFactory vf1 = new VehicleFactory();
vf1.ProduceVehicle(new Motorcycle());
Console.ReadLine();
}
}
}
That's it :)