Önceki Parçalar:
Kodumuzu temiz, esnek ve sürdürülebilir hale getirmeye yönelik yaklaşımları değerlendirmeye devam ediyoruz. Şimdi temiz mimari çözümleri araştırmaya başlayalım.
SOLID ilkeleri Robert C. Martin tarafından ortaya atılmıştır ve nesne yönelimli programlama ve yazılım tasarımında en iyi uygulamalar olarak kabul edilmektedir.
Bu makalede, ilk üç SOLID ilkesine bir göz atacağız: Tek Sorumluluk İlkesi, Açık/Kapalı İlkesi ve Liskov İkame İlkesi'ni pratik örneklerle.
Bir sınıfın değişmek için tek bir nedeni olmalıdır
Başka bir deyişle, bir sınıfın tek bir sorumluluğu veya işi olmalıdır. Bu önemlidir çünkü tek sorumluluğa sahip bir sınıfın anlaşılması, değiştirilmesi ve bakımı daha kolaydır. Kodun bir alanındaki değişiklikler tüm sisteme yayılmayacak ve hata oluşma riskini azaltacaktır.
Pratik bir örneğe ve tek sorumluluk ilkesini takip etmenin yollarına bir göz atalım:
// Bad class UserSettingsService { constructor(user: IUser) { this.user = user; } changeSettings(settings: IUserSettings): void { if (this.isUserValidated()) { // ... } } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): Promise<boolean> { const userInfo = await this.getUserInfo(); // ... } }
Bu örnekte sınıfımız farklı yönlerde eylemler gerçekleştiriyor: bağlamı kuruyor, değiştiriyor ve doğruluyor.
SRP'yi takip etmek için bu farklı sorumlulukları bölmemiz gerekir.
// Better class UserAuth { constructor(user: IUser) { this.user = user; } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): boolean { const userInfo = await this.getUserInfo(); // ... } } class UserSettings { constructor(user: IUser) { this.user = user; this.auth = new UserAuth(user); } changeSettings(settings: IUserSettings): void { if (this.auth.isUserValidated()) { // ... } } }
SRP, kod modülerliğini geliştirmeyi ve karşılıklı bağımlılıklardan kaynaklanan zorlukları en aza indirmeyi amaçlamaktadır. Kodu işlevler, ayrılmış sınıflar halinde düzenleyerek ve modülerliği teşvik ederek, daha yeniden kullanılabilir hale gelir ve aksi takdirde mevcut işlevselliği yeniden kodlamak için harcanabilecek zamandan tasarruf sağlar.
Yazılım varlıkları (sınıflar, modüller, işlevler) genişletmeye açık, değişiklik yapmaya kapalı olmalıdır
Mevcut kodu değiştirmeden yeni işlevler ekleyebilmelisiniz. Bu önemlidir çünkü bu prensibe bağlı kalarak, mevcut kodun kararlılığını riske atmadan sisteminize yeni özellikler veya bileşenler katabilirsiniz.
Kodun yeniden kullanılabilirliğini artırır ve işlevselliği genişletirken kapsamlı değişiklik ihtiyacını azaltır.
// Bad class Product { id: number; name: string[]; price: number; protected constructor(id: number, name: string[], price: number) { this.id = id; this.name = name; this.price = price; } } class Ananas extends Product { constructor(id: number, name: string[], price: number) { super(id, name, price); } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } } class HttpRequestCost { constructor(product: Product) { this.product = product; } getDeliveryCost(): number { if (product instanceOf Ananas) { return requestAnanas(url).then(...); } if (product instanceOf Banana) { return requestBanana(url).then(...); } } } function requestAnanas(url: string): Promise<ICost> { // logic for ananas } function requestBanana(url: string): Promise<ICost> { // logic for bananas }
Bu örnekte sorun HttpRequestCost
sınıfındadır, yöntemde getDeliveryCost
farklı ürün türlerinin hesaplanmasına yönelik koşullar içerir ve her ürün türü için ayrı yöntemler kullanırız. Dolayısıyla, yeni bir ürün türü eklememiz gerekiyorsa HttpRequestCost
sınıfını değiştirmeliyiz ve bu güvenli değildir; beklenmedik sonuçlar elde edebiliriz.
Bunu önlemek için, Product
sınıfında gerçekleştirmeler olmadan soyut bir yöntem request
oluşturmalıyız. Özel farkındalık şu sınıfları miras alacaktır: Ananas ve Muz. İsteği kendileri gerçekleştirecekler.
HttpRequestCost
Product
sınıfı arayüzünü takip ederek product
parametresini alacak ve HttpRequestCost
belirli bağımlılıkları aktardığımızda zaten kendisi için request
yöntemini gerçekleştirecektir.
// Better abstract class Product { id: number; name: string[]; price: string; constructor(id: number, name: string[], price: string) { this.id = id; this.name = name; this.price = price; } abstract request(url: string): void; } class Ananas extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for ananas } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for bananas } } class HttpRequestCost { constructor(product: Product) { this.product = product; } request(): Promise<void> { return this.product.request(url).then(...); } }
Bu prensibi takip ederek kod eşleşmesini azaltacak ve sistemi öngörülemeyen davranışlardan kurtaracaksınız.
Bir üst sınıfın nesneleri, uygulamayı bozmadan alt sınıflarının nesneleri ile değiştirilebilir olmalıdır.
Bu prensibi anlamak için şu örneğe bir göz atalım:
// Bad class Worker { work(): void {/../} access(): void { console.log('Have an access to closed perimeter'); } } class Programmer extends Worker { createDatabase(): void {/../} } class Seller extends Worker { sale(): void {/../} } class Designer extends Worker { access(): void { throwError('No access'); } }
Bu örnekte Contractor
sınıfıyla ilgili bir sorunumuz var. Designer
, Programmer
ve Seller
tümü Workers'tır ve Worker
üst sınıfından miras alırlar. Ancak aynı zamanda Tasarımcılar Çalışan değil Yüklenici oldukları için kapalı çevreye erişimleri yoktur. access
yöntemini geçersiz kıldık ve Liskov Değiştirme İlkesini çiğnedik.
Bu prensip bize, Worker
üst sınıfını alt sınıfıyla (örneğin Designer
sınıfı) değiştirirsek işlevselliğin bozulmaması gerektiğini söyler. Ancak bunu yaparsak, Programmer
sınıfının işlevselliği bozulacaktır; access
yöntemi, Designer
sınıfından beklenmeyen gerçekleşmelere sahip olacaktır.
Liskov İkame Prensibini takip ederek, alt sınıflardaki gerçekleşmeleri yeniden yazmamalıyız, ancak her bir soyutlama türü için belirli gerçekleşmeleri tanımladığımız yeni soyutlama katmanları yaratmamız gerekir.
Düzeltelim:
// Better class Worker { work(): void {/../} } class Employee extends Worker { access(): void { console.log('Have an access to closed perimeter'); } } class Contractor extends Worker { addNewContract(): void {/../} } class Programmer extends Employee { createDatabase(): void {/../} } class Saler extends Employee { sale(): void {/../} } class Designer extends Contractor { makeDesign(): void {/../} }
Employee
ve Contractor
için yeni soyutlama katmanları oluşturduk ve access
yöntemini Employee
sınıfına taşıdık ve belirli gerçekleştirmeyi tanımladık. Worker
sınıfını Contractor
alt sınıfıyla değiştirirsek Worker
işlevselliği bozulmayacaktır.
LSP'ye bağlı kalırsanız, temel sınıf örneğinin beklendiği yerde herhangi bir alt sınıf örneğini kullanabilirsiniz; program yine de amaçlandığı gibi çalışmaya devam etmelidir. Bu, kodun yeniden kullanımını ve modülerliğini destekler ve kodunuzu değişikliklere karşı daha dayanıklı hale getirir.
Bir sonraki yazımızda Arayüz Ayrımı ve Bağımlılığı Ters Çevirme SOLID prensiplerine göz atacağız.