Solid Kodu nasıl yazılır?
Solid Kodu nasıl yazılır?
Solid Kodu nasıl yazılır?
Solid Kodu nasıl yazılır? Python örnekleriyle gösterilen SOLID Tasarım İlkelerinin yardımıyla daha iyi kod yazmak için kısa bir kılavuz.
Yakın zamanda resmi bir Bilgisayar Bilimi geçmişi olmayan bir Yazılım Mühendisi olarak çalışmaya başlayan biri olarak, makul düşük seviyeli tasarımlar bulmak ve kodu doğru şekilde yapılandırmak için çok uğraştım. Başlangıçta, bu yazıda sizlerle paylaşacağım, takip etmem gereken 5 ilkeden oluşan bir kontrol listesi düşünmeme çok yardımcı oldu.
SOLID Tasarım Prensipleri
SOLID, ilk olarak yaklaşık 20 yıl önce Robert C. Martin tarafından kavramsallaştırılan 5 nesne yönelimli tasarım ilkesi koleksiyonunun kısaltmasıdır ve bugün yazılım yazma şeklimizi şekillendirmişlerdir.
Bunlar daha basit, daha kolay anlaşılır, bakımı yapılabilir ve genişletilebilir kod oluşturmaya yardımcı olmayı amaçlarlar. Bu, büyük bir grup insan için, sürekli büyüyen ve gelişen, genellikle yüz binlerce (milyonlarca değilse) kod satırından oluşan kod tabanları üzerinde çalışırken çok önemli hale gelir. İlkeler, iyi uygulamayı sürdürmenin ve daha kaliteli kod yazmanın yolunu gösteriyor.
Harfler şu anlama gelir:
- Tek Sorumluluk İlkesi
- Açık / Kapalı Prensibi
- Liskov İkame İlkesi
- Arayüz Ayrıştırma Prensibi
- Bağımlılık Ters Çevirme Prensibi
Hepsi anlaşılması kolay basit kavramlardır, ancak endüstri standardı kod yazarken gerçekten değerlidir.
1. Tek Sorumluluk İlkesi
Bir sınıfın değişmesi için bir ve yalnızca bir nedeni olmalıdır.
Bu muhtemelen en sezgisel ilkedir ve yazılım bileşenleri veya mikro hizmetler için de geçerlidir. “Değişmek için tek bir nedene” sahip olmak, “tek bir sorumluluğu” olduğu şeklinde yeniden ifade edilebilir. Bu, kodu daha sağlam ve esnek hale getirir, bir başkası için anlaşılmasını kolaylaştırır ve mevcut kodu değiştirirken bazı beklenmedik yan etkilerden kaçınırsınız. Ayrıca daha az değişiklik yapmanız gerekecek: Bir sınıfın değişmesi için ne kadar bağımsız neden varsa, o kadar sık değişmesi gerekir. Birbirinize bağlı çok sayıda sınıfınız varsa, yapmanız gereken değişiklik sayısı katlanarak artabilir. Sınıflarınız ne kadar karmaşıksa, beklenmedik sonuçlar olmadan onları değiştirmek o kadar zor olur.
class Albüm:
def __init __ (öz, ad, sanatçı, şarkılar) -> Yok:
self.name = ad
self.artist = sanatçı
self.songs = şarkılar def add_song (self, şarkı):
self.songs.append (şarkı) def remove_song (self, şarkı):
self.songs.remove (şarkı) def __str __ (self) -> str:
return f "{self.artist} tarafından {self.name} albümü \ nTracklist: \ n {self.songs}" # SRP
def search_album_by_artist (self):
"" "Veritabanında aynı sanatçıya göre diğer albümler için arama" ""
geçişini bozar
Yukarıdaki örnekte bir sınıf oluşturdum Album
. Bu, albüm adını, sanatçıyı ve parça listesini depolar ve şarkı ekleme veya silme gibi albüm içeriğini değiştirebilir. Şimdi, aynı sanatçının albümlerini aramak için bir işlev eklersem, Tek Sorumluluk İlkesini bozarım. Albümleri farklı bir şekilde saklamaya karar verirsem sınıfımın değişmesi gerekir (örneğin, kayıt etiketini ekleyerek veya parça listesini parça adı ve uzunluğunun sözlüğü olarak kaydederek) ve değiştirirsem sınıfımın da değişmesi gerekir. bu albümleri sakladığım veritabanı (örneğin, bir Excel sayfasından çevrimiçi bir veritabanına geçiyorum). Bunların iki ayrı sorumluluk olduğu açıktır.
Bunun yerine, Albümler veritabanıyla etkileşim için bir sınıf oluşturmalıyım. Bu, albümleri arayarak, başlangıç harfi, parça sayısı vb. İle genişletilebilir (tam olarak nasıl yapılacağıyla ilgili sonraki ilkeye bakın)
# bunun yerine:
class AlbumBrowser:
"" "Albümler veritabanına göz atmak için sınıf" ""
def search_album_by_artist (self, albums, artist):
pass
def search_album_starting_with_letter (self, albums, letter):
pass
Bir uyarı: Sınıfları aşırı derecede basit hale getirmek, kodun okunması kadar zorlaştırır, çünkü birinin birbirine geçen uzun bir nesne zincirini takip etmesi gerekir ve tek yöntemli sınıflarla parçalanmış bir kod tabanına yol açabilir. Bu ilke, her sınıfın tek bir yöntemde olduğu gibi tek bir şey yapması gerektiği anlamına gelmez, tek bir kavramdır .
2. Açık-Kapalı Prensibi
Yazılım varlıkları (sınıflar, modüller, işlevler vb.) Genişletme için açık, ancak değişiklik için kapalı olmalıdır.
Bu, mevcut kod yapımı değiştirmeden, bunun yerine yeni kod ekleyerek yeni işlevler ekleyebilmem gerektiği anlamına geliyor. Amaç, hataları önlemek için mevcut, test edilmiş kodu olabildiğince az değiştirmek ve her şeyi yeniden test etmek zorunda kalmaktır. Bu ilkeye uyulmazsa, sonuç, bağlı sınıflardaki değişikliklerin uzun bir listesi, mevcut özelliklerde gerileme ve gereksiz test saatleri olabilir.
Bu, aşağıdaki örnekle gösterilmiştir:
class Albüm:
def __init __ (öz, ad, sanatçı, şarkılar, tür):
self.name = ad
self.artist = sanatçı
self.songs = şarkılar
self.genre = tür#before
class AlbumBrowser:
def search_album_by_artist (self, albums, artist):
return [album.artist == artist ise albümlerdeki albüm için albüm] def search_album_by_genre (kişi, albümler, tür):
return [album.genre == tür ise albümlerdeki albüm için albüm]
Şimdi sanatçıya ve türe göre aramak istersem ne olur ? Yayın yılı eklersem ne olur ? Her seferinde yeni bir fonksiyon yazmam gerekecek (kesin olmak için toplamda (2 ^ n) -1) ve sayı üssel olarak artıyor.
Bunun yerine, belirtimim için ortak bir arabirime sahip bir temel sınıf tanımlamalı ve ardından bu arabirimi temel sınıftan miras alan her bir belirtim türü için alt sınıflar tanımlamalıyım:
#After
sınıfı SearchBy:
def is_matched (öz, albüm):
geçişli
sınıfı SearchByGenre (SearchBy):
def __init __ (self, tür):
self.genre = tarz def is_matched (self, album):
album.genre == self.genre
class SearchByArtist (SearchBy):
def __init __ (self, artist):
self.artist = artist def is_matched (self, album):
return album.artist == self.artist
class AlbumBrowser:
def browse (self, albums, searchby):
return [searchby.is_matched (album) ise albümlerdeki albüm için albüm]
Bu, aramaları istediğimizde başka bir sınıfla genişletmemizi sağlar (örneğin, yayın tarihine göre). Herhangi bir yeni arama sınıfının Searchby tarafından tanımlanan arayüzü karşılaması gerekecek, bu nedenle mevcut kodumuzla etkileşimde bulunurken sürprizlerle karşılaşmayacağız. Kriterlere göre göz atmak için, şimdi önce bir SearchBy nesnesi oluşturmalı ve bunu AlbumBrowser’a aktarmalıyız.
Peki ya birden çok kriter? Bu Tasarım Desenleri Udemy Kursunda gördüğüm bu çözümü gerçekten beğendim . Bu, aşağıdakiler tarafından birleştirilecek göz atma kriterlerine katılmanıza izin verir &
:
#add __and__:
class SearchBy:
def is_matched (self, album):
pass
def __and __ (self, other):
return AndSearchBy (self, other)class AndSearchBy (SearchBy):
def __init __ (self, searchby1, searchby2):
self.searchby1 = searchby1
self.searchby2 = searchby2 def is_matched (self, album):
self.searchby1.is_matched (albüm) ve self.searchby2.is_matched (albüm) döndür
Bu &
yöntem biraz kafa karıştırıcı olabilir, bu nedenle aşağıdaki örnek kullanımı göstermektedir:
LAWoman = Albüm (
isim = "LA Woman",
artist = "The Doors",
şarkılar = ["Fırtınadaki Biniciler"],
tür = "Rock",
)Çöp Kutusu = Albüm (
ad = "Çöp Kutusu",
sanatçı = "Alice Cooper",
şarkılar = ["Zehir"],
tür = "Rock",
)
albümler = [LAWoman, Çöp Kutusu]# bu AndSearchBy nesnesini oluşturur
my_search_criteria = SearchByGenre (genre = "Rock") & SearchByArtist (
artist = "The Doors"
)browser = AlbumBrowser ()
assert browser.browse (albums = albums, searchby = my_search_criteria) == [LAWoman]
# yay albümümüzü bulduk
3. Liskov İkame Prensibi
Bu ilke, ilkesini çok resmen formüle eden Barbara Liskov’a aittir:
“Φ (x), T türündeki x nesneleri hakkında kanıtlanabilir bir özellik olsun. Bu durumda S (y), S’nin T’nin bir alt türü olduğu S türündeki y nesneleri için doğru olmalıdır.”
Bu, eğer bir T taban sınıfımız ve S alt sınıfımız varsa, ana sınıf T’yi, kodu kırmadan alt sınıf S ile değiştirebilmeniz gerektiği anlamına gelir. Bir alt sınıfın arabirimi, temel sınıfın arabirimiyle aynı olmalı ve alt sınıf, temel sınıfla aynı şekilde davranmalıdır.
T’de S’de geçersiz kılınan bir yönteminiz varsa, o zaman her iki yöntem de aynı girdileri almalı ve aynı çıktı türünü döndürmelidir. Alt sınıf, temel sınıfın dönüş değerlerinin yalnızca bir alt kümesini döndürebilir, ancak temel sınıfın yaptığı tüm girdileri kabul etmelidir.
Dikdörtgenler ve kareler içeren klasik örnekte, genişlik ve yükseklik ayarlayıcıları olan bir Rectangle sınıfı oluşturuyoruz. Bir kareniz varsa, genişlik ayarlayıcının kare özelliğini korumak için yüksekliği de yeniden boyutlandırması gerekir ve bunun tersi de geçerlidir. Bu bizi bir seçim yapmaya zorlar: Ya Rectangle sınıfının uygulamasını saklarız, ama sonra üzerinde ayarlayıcıyı kullandığınızda Square bir kare olmayı durdurur ya da ayarlayıcıları, kareler için yükseklik ve genişliği aynı yapacak şekilde değiştirirsiniz. Şeklinizin yüksekliğini yeniden boyutlandıran bir işleviniz varsa, bu bazı beklenmedik davranışlara yol açabilir.
class Dikdörtgen:
def __init __ (
self._height = height
self._width = width) @ özelliği
def genişliği (kendi kendine)
dönüş self._width @ width.setter
def width (self, deger):
self._width = deger @ özelliği
def yüksekliği (öz):
return self._height @
yükseklik ayarlayıcı def yükseklik (öz, değer):
self._height = değer def get_area (self):
return self._width * self._height
sınıf Kare (Dikdörtgen):
def __init __ (öz, boyut):
Dikdörtgen .__ init __ (öz, boyut, boyut) @ Rectangle.width.setter
def genişlik (öz, değer):
self._width = değer
self._height = değer @ Rectangle.height.setter
def height (self, value):
self._width = değer
self._height = değer
def get_squashed_height_area (Rectangle):
Rectangle.height = 1
area = Rectangle.get_area ()
dönüş alanı
dikdörtgen = Dikdörtgen (5, 5)
kare = Kare (5)get_squashed_height_area (dikdörtgen) == 5 # bekleniyor 5
get_squashed_height_area (kare) == 1 # beklenen 5 iddia
Bu çok önemli görünmese de (kesinlikle sqaure’un genişliği de değiştirdiğini hatırlayabilirsiniz ?!), bu, işlevler daha karmaşık olduğunda veya başka birinin kodunu kullandığınızda daha büyük bir sorun haline gelir ve alt sınıfın şu şekilde davrandığını varsayın. aynı.
Circle-elips problemli Wiki makalesinden gerçekten hoşlandığım kısa ama sezgisel bir örnek :
sınıf Kişi ():
def walkNorth (metre):
geçişli
walkSouth def (metre):
geçişlisınıf Mahkum (Kişi):
def yürüme Kuzey (metre): def yürüme
geçmek
Güney (metre):
geçmek
Açıktır ki, keyfi yönlerde keyfi mesafeleri yürümek özgür olmadıkları için mahkumlara yürüme yöntemlerini uygulayamayız. Sınıfta yürüme yöntemlerini çağırmamıza izin verilmemeli, arayüz yanlış. Bu da bizi bir sonraki ilkemize götürür …
4. Arayüz Ayrıştırma Prensibi
“İstemciler, kullanmadıkları arayüzlere bağımlı olmaya zorlanmamalıdır.”
Eğer birçok metodu olan bir temel sınıfınız varsa, muhtemelen tüm alt sınıflarınız bunlara ihtiyaç duymayacaktır, belki sadece birkaçına. Ancak kalıtım nedeniyle, bu yöntemleri tüm alt sınıflarda, hatta ihtiyaç duymayanlarda bile çağırabileceksiniz. Bu, kullanılmayan, gereksiz ve yanlışlıkla arandıklarında hatalara neden olacak birçok arayüz anlamına gelir.
Bu ilke, bunun olmasını önlemek içindir. Arayüzleri olabildiğince küçük yapmalıyız, böylece ihtiyacımız olmayan işlevleri uygulamaya ihtiyacımız kalmasın. Büyük bir temel sınıf yerine, onları birden çok sınıfa ayırmalıyız. Yalnızca her biri için anlamlı olan yöntemlere sahip olmalı ve alt sınıflarımızın onlardan miras almasını sağlamalıdırlar.
Bir sonraki örnekte soyut yöntemler kullanacağız. Soyut yöntemler, temel sınıfta uygulaması olmayan, ancak temel sınıftan miras alan her alt sınıfta uygulanmaya zorlanan bir arabirim oluşturur. Soyut yöntemler esasen bir arayüzü zorlar.
sınıf PlaySongs:
def __init __ (öz, başlık):
self.title = title def play_drums (self):
print ("Ba-dum ts") def play_guitar (self):
print ("* Soul-hareket eden gitar solosu *") def sing_lyrics (self):
print ("NaNaNaNa")# Bu ders iyi, sadece gitar ve şarkı sözü
sınıfını değiştirmek için PlayRockSongs (PlaySongs):
def play_guitar (self):
print ("* Very metal guitar solo *") def sing_lyrics (self):
print ("Bütün gece rock and roll yapmak istiyorum")# Bu ISP'yi bozuyor, şarkı sözü
sınıfımız yok PlayInstrumentalSongs (PlaySongs):
def sing_lyrics (self):
Exception'ı artır ("Enstrümantal şarkılar için şarkı sözü yok")
Bunun yerine, şarkı ve müzik için ayrı ayrı bir sınıfımız olabilir (bizim durumumuzda gitar ve davulların her zaman birlikte olduğunu varsayarak, aksi takdirde onları daha da, belki de enstrümanla bölmemiz gerekir.) Bu şekilde, sadece bizim arayüzlere sahibiz. gerek, enstrümantal şarkılarda söz söyleyemeyiz.
abc ithal ABCMeta'dan
class PlaySongsLyrics:
@abstractmethod
def sing_lyrics (self, title):
pass
class PlaySongsMusic:
@abstractmethod
def play_guitar (self, title):
pass @abstractmethod
def play_drums (self, title):
geçmek
class PlayInstrumentalSong (PlaySongsMusic):
def play_drums (self, title):
print ("Ba-dum ts") def play_guitar (self, title):
print ("* Soul-hareket eden gitar solosu *")
class PlayRockSong (PlaySongsMusic, PlaySongsLyrics):
def play_guitar (self):
print ("* Very metal guitar solo *") def sing_lyrics (self):
print ("Bütün gece rock and roll yapmak istiyorum") def play_drums (self, title):
print ("Ba-dum ts")
5. Bağımlılığı Ters Çevirme İlkesi
Son ilke diyor ki
Yüksek seviyeli modüller, düşük seviyeli modüllere bağlı olmamalıdır. Her ikisi de soyutlamalara bağlı olmalıdır (örneğin arayüzler).
Soyutlamalar ayrıntılara bağlı olmamalıdır. Ayrıntılar (somut uygulamalar) soyutlamalara bağlı olmalıdır
Kodunuz iyi tanımlanmış soyut arayüzlere sahipse, bir sınıfın dahili uygulamasını değiştirmek kodunuzu bozmamalıdır. Etkileşim kurduğu bir sınıf, diğer sınıfın iç işleyişi hakkında bilgi sahibi olmamalı ve arayüzler aynı olduğu sürece etkilenmemelidir. Bir örnek, kullandığınız veritabanı türünü (SQL veya NoSQL) değiştirmek veya verilerinizi sakladığınız veri yapısını (sözlük veya liste) değiştirmek olabilir.
Bu, ViewRockAlbums öğelerinin açıkça albümlerin AlbumStore içinde belirli bir sırayla bir demet içinde depolandığı gerçeğine bağlı olduğu aşağıdaki örnekte gösterilmiştir. Albumstore’un iç yapısı hakkında bilgi sahibi olmamalıdır. Şimdi albümdeki tuplelardaki sıralamayı değiştirirsek, kodumuz kırılırdı.
class AlbumStore:
albums = [] def add_album (öz, ad, sanatçı, tür):
self.albums.append ((ad, sanatçı, tür))
class ViewRockAlbums:
def __init __ (self, album_store):
album_store.albums içindeki albüm için:
eğer album [2] == "Rock":
print (f "Mağazada {album [0]} var.")
Bunun yerine, diğer sınıflar tarafından çağrılabilen ayrıntıları gizlemek için AlbumStore’a soyut bir arayüz eklememiz gerekiyor. Bu, Açık-Kapalı Prensibindeki örnekte olduğu gibi yapılmalıdır, ancak başka herhangi bir şeye göre filtrelemeyi önemsemediğimizi varsayarsak, sadece bir filter_by_genre yöntemi ekleyeceğim. Şimdi, albümü farklı şekilde saklamaya karar veren başka bir AlbumStore türüne sahip olsaydık, ViewRockAlbums’ların çalışması için filter_by_genre için aynı arayüzü uygulaması gerekirdi.
class GeneralAlbumStore:
@abstractmethod
def filter_by_genre (self, genre)
passclass MyAlbumStore (GeneralAlbumStore):
albums = [] def add_album (öz, ad, sanatçı, tür):
self.albums.append ((ad, sanatçı, tür)) def filter_by_genre (öz, tür):
eğer albüm [2] == "Rock":
albüm getir [0]class ViewRockAlbums:
def __init __ (self, album_store): album_store.filter_by_genre'deki
album_name için ("Rock"):
print (f "Mağazada {album_name} var.")
Sonuç
SOLID tasarım ilkeleri, bakımı yapılabilir, genişletilebilir ve anlaşılması kolay kodlar yazmak için bir kılavuz olma amacını taşır. Bir dahaki sefere bir tasarım düşündüğünüzde, SOLID kodu yazmak onları akılda tutmaya değer. Her birinin ne anlama geldiğini hatırlayarak sadece zihninizdeki harfleri gözden geçirin:
- Tek Sorumluluk İlkesi
- Açık / Kapalı Prensibi
- Liskov İkame İlkesi
- Arayüz Ayrıştırma Prensibi
- Bağımlılık Ters Çevirme Prensibi
Şimdi gidin ve dünyayı kod tabanına göre daha iyi bir yer kod tabanı yapın!
Tüm yazılarımızı buradan okuyabilirsiniz.