SOLID prinsipləri — Pythonla

Kamran Mammadov
Pragmatech
Published in
6 min readApr 21, 2020

Əgər S.O.L.I.D. priniplərini anlaşıqlı dildə oxumaq istəyirsinizsə və bu günə qədər hər bir prinsipin Pythonda tətbiqi ilə bağlı məqalə tapmaqda mənim kimi İnformasiya Texnologiyalarının zülmünü çəkmisinizsə, “oxumadan keçməyin” deyirəm.

İdeyası ilk dəfə Amerikalı *Software Engineer Robert Martin tərəfindən irəli sürülən, SOLID deyə adlandırılan dizayn prinsipi əsasən Obyekt Yönümlü yanaşma zamanı tətbiq olunur. Proyektə özündə beş yanaşmanı saxlayan bu prinsiplə yanaşmanın ümumi məqsədi genişləndirilə bilən, məntiqli oxunması və anlaşılması rahat kod yazmaqdır.

*Məqalə daxilində proqramlaşdırma ilə bağlı bir sıra terminlər azərbaycanca qarşılığı olmadığından ingiliscə ifadə olunub.

S.O.L.I.D. yuxarıda sözügedən beş yanaşmanın baş hərflərindən yaranan abbreviaturadır və bu prinsiplər aşağıdakı kimi təsnifata ayrılır:

1. Single Responsibility Principle

Hərfi tərcümədə “tək məsuliyyətlilik prinsipi” kimi tərcümə olunan ilk prinsip istifadə olunan classlardan yalnız və yalnız bir görməsini tələb edir. Səbəb isə bir məsuliyyəti olan classın testingi üçün az case olması, az funksionallığı olan classın az asılı olması (dependency) və lazım olduqda böyük proyekt içərisində rahat tapılmasıdır.

Aşağıdakı kod nümunəsinə baxaq:

class Book:

def __init__(self, title, author, year, content):
self.title = title
self.author = author
self.year = year
self.content = content
def replaceWordInContent(word: str) -> str:
content = content.replace(content, word)
return content

Burada bəsit bir Book classı və kitabın hər hansı hissəsindəki bir kontenti verilən sözlə əvəz edən replaceWordInContent metodunu görürük. Fərz edək ki, bu metodu işə saldıq, hər hansı bir kontenti dəyişdik və indi kitabın yeni halını ekranda çap etdirmək lazımdır. Onun üçün bir də aşağıdakı kimi bəsit printBook metodunu əlavə edək:

class Book(object):    def __init__(self, title, author, year, content):
self.title = title
self.author = author
self.year = year
self.content = content

def replaceWordInContent(self, word: str) -> str:
self.content = self.content.replace(self.content, word)
return self.content

def printBook(self):
print(self.content)

Yuxarıdakı kodları oxuyub, başa düşüb məğzini anladıqsa, yadınızda heç vaxt belə kod yazmamağı saxlayın. Çünki bu class artıq özündə ikinci — printBook məsuliyyətini saxlayaraq single responsibility prinsipini tapdamış olur. Problemi həll etmək üçün aşağıdakı kimi bir həll yolunu izləyirik:

class Book(object):    def __init__(self, title, author, year, content):
self.title = title
self.author = author
self.year = year
self.content = content

def replaceWordInContent(self, word: str) -> str:
self.content = self.content.replace(self.content, word)
return self.content
class Printer: def __init__(self, book: Book):
self.book = book
def printBook(self):
print(self.book.content)

2. Open/Closed Principle

Hərfi tərcümədə “açmaq-qapamaq prinsipi” kimi tərcümə olunan ikinci prinsip istifadə olunan kodun genişlənməyə açıq, dəyişikliyə qapalı olmasını tələb edir. Genişlənməyə açıq olmaq yeni imkanların, funksiyaların, classların rahatlıqla əlavə oluna bilməsi anlamına gəldiyi halda, dəyişikliyə qapalı olmaq yeni funksionallığın əlavə edildiyi təqdirdə orijinal classlardan heç birinin modifikasiyaya məcbur qalmamalı olduğu məğzini daşıyır. Nümunə üçün aşağıdakı məsələyə baxaq:

Fərz edək ki, yeni fəaliyyətə başlamış onlayn mağazamız var və hər müştəriyə hər alışda 10% endirim edirik. Bir də mağazamızda “gold” kateqoriyalı müştərilər olsun, onlara da 50% endirim edək. Endirimi obyekt kimi (burda hər şey obyektdir, bro) ayrı bir classa çıxaraq:

class Discount:
def __init__(self, customer, price):
self.customer = customer
self.price = price
def give_discount(self):
return self.price * 0.1
if self.customer == 'gold':
return self.price * 0.5

Hər şey işləkdir, hər şey olmalı olduğu kimi hesablanır. Bəs “O.” ? Fikir verdiksə yeni fuksionallıq əlavə etmək üçün orijinal classı modifikasiya etdik, bu isə ikinci, yəni Open-Closed Principle-a ziddir. Bu prinsipə uyğun olaraq cari məsələni aşağıdakı kimi həll edəcəyik:

class Discount:
def __init__(self, customer, price):
self.customer = customer
self.price = price
def get_discount(self):
return self.price * 0.1
class DiscountForGoldUsers(Discount):
def get_discount(self):
return super().get_discount() * 5

Bir nümunə daha vermək lazımdırsa, şahmat fiqurlarını ağlımıza gətirək. Deyək ki, bir ümumi figure classımız var və bu fiqurdan törəyəcək olan 6 fərqli fiqur olacaq və hər birinin hərəkət istiqamətini qeyd etmək üçün move metodu olacaq. Əgər yuxarıda sözü gedən prinsipə tabe olmasaq, figure classımızın içərisində bir move metoduna 6 if şərti yazmalıyıq. Digər üsul isə bu prinsipə tabe olaraq, əsas fiqur classından 6 classa inherit etdirmək və hər bir fiqurun hərəkət istiqamətini öz classının içərisində qeyd etməkdir. Fərqindəyiksə, bu prinsip bizi polymorphysm/override etməyə şövq edir.

Qeyd: Sözlə ifadə etdiyim son şahmat məsələsini kodla yazmağa çalışın.

3. Liskov Substitution Principle

Bu prinsip 1987-ci ildə ilk dəfə bu yanaşmanı öz “Data abtraction” konfransında dilə gətirən komputer alimi Barbara Liskovun adı ilə bağlıdır. Prinsipin fəlsəfəsi ondan ibarətdir ki, əgər B A-nın subclassıdırsa, A-nın B-ni əvəzləməsi zamanı proqramın davranışında bir dəyişiklik baş verməməlidir. Yəni, base class istifadə etdiyimiz istənilən yerdə onun child classını da istifadə edə bilməliyik. Gəlin, anlaması digərlərindən daha kompleks sayılan bu prinsipi özümüz üçün əyləncəli bir məsələ ilə aydınlaşdıraq.

Fərz edək ki, bir Animal classımız var və ondan iki tip canlı — it və pişik törədək və uyğun olaraq hər bir canlının sadəcə bir — sevdiyi qida attributu olsun:

class Animal:
def __init__(self, food):
self.food = food

class Dog(Animal):
def __init__(self, food):
super().__init__(food)
self.food = food
class Cat(Animal):
def __init__(self, food):
super().__init__(food)
self.food = food

İndi isə sevgi dolu insanlıq naminə bu canlıların hər birini yedirməyi özümüzə borc bilərək, onları qidalandırmaq üçün qlobal bir funksiya yazaq:

def FeedTheAnimal(animal: Animal):
message = "You feed the " + str(type(animal).__name__ ) + " with " + animal.food
print(message)

Parametrin annotasiyasına fikir verdinizsə, bu funksiya əsas, yəni base class (Animal) tipindən dəyər qəbul etməlidir. İndi bu funksiyanı işə salmazdan öncə hər canlıdan bir nümunə yaradaq:

toplan = Dog("bones")
mestan = Cat("whiskas")

Bu canlılar hər biri gördüyümüz kimi subclassdan, yəni child classdan törədi. İndi xatırlayaq Liskov nə deyirdi ? Əgər, Dog və ya Cat Animal-ın subclassıdırsa, Animalın Dog və ya Cat-i əvəzləməsi zamanı bir xəta yaşanmamalıdır. Parametrin almalı olduğu dəyər base classdır, test üçün child class ötürüb nəticəyə baxaq:

FeedTheAnimal(toplan)
FeedTheAnimal(mestan)

Bu sətirlərin outputu bu cür olacaq:

You feed the Dog with bones
You feed the Cat with whiskas

Deməli, bu kod blokumuz Liskov prinsipinin haqqını vermiş oldu :)

4. Interface Segregation

Bu prinsip birbaşa interfeysləri dəstəkləyən proqramlaşdırma dilləri ilə bağlıdır. Python abstract classları və multiple inheritance-ı dəstəklədiyi üçün bəzən iş prinsipini süni şəkildə simulyasiya etsək də, interface istifadəsinə ehtiyac duymur. Prinsipi məğzi ondan ibarətdir ki, böyük interfeyslər ən xırda halına qədər hissələrə parçalanmalıdır. Lakin bu prinsipin fəlsəfi yanaşmasını biz interfeysləri simulyasiya etdikdə də, adi və ya abstract classlarla işlədikdə də rahatlıqla tətbiq edə bilirik. Məsələn:

class IShape:
def draw_circle(): pass
def draw_rectangle(): pass
def draw_square(): pass

kod blokunda simulyasiya etdiyimiz IShape “interfeysi” (əslində classdır) üç metod saxlayır özündə. Bu prinsipə görə, bu kod bloku aşağıdakı kimi olmalıdır:

class IShape:
def draw(self):
raise NotImplementedError
class Circle(IShape):
def draw(self):
print("Draw circle")
class Rectagnle(IShape):
def draw(self):
print("Draw rectangle")
class Square(IShape):
def draw(self):
print("Draw square")

Eyni yanaşmanı abstract class istifadəsi zamanı da rahatlıqla edə bilərik. Alın, bu da sizə ikinci tapşırıq olsun :)

5. Dependency Inversion Principle

Bu prinsipin yanaşmasına görə yüksək səviyyəli modullar aşağı səviyyəli modullardan asılı olmamalıdır, həm aşağı, həm də yüksək səviyyəli classlar eyni abstraksiyalardan asılı olmalıdır, yəni yüksək səviyyəli modul və ya classlar aşağı səviyyəli classlardan deyil, onların abstraksiyasından asılı olmalıdır. Qarışıqdır, anlaması yuxarıdakı 4 prinsipdən daha çətindir, ona görə də uzatmadan nümunə kodlarımızı yazaq. Yenə də kodları oxumaq yorucu olmasın, əksinə işi əyləncəli hala gətirə bilim deyə, absurd bir nümunə ilə izah edəcəm bu prinsipi.

Fərz edək ki, istehsalat sənayesinə dair bir proyekt işləyirik və bir Production, bir də Bread classlarımız var:

class Production:
def produce(self):
bread = Bread()
bread.bake(True)

class Bread:
def bake(self, isTendir: bool):
print("Bread was baked in tendir")

Burada Production classı içərisindəki produce metodu Bread classı içərisindəki bake metodu ilə birbaşa bağlıdır. Bunun səbəbi də bake metodunun produce içərisində çağırılmasıdır. Belə ki, bake metodunda edilən bütün dəyişikliklər deməli, produce metoduna da təsir edəcək. Bu “bəladan” qurtulmaq və S.O.L.I.D. prinsiplərimizin sonuncusuna əməl etmək naminə aşağıdakı yolu izləyəcəyik:

# burada ortaq bir interfeys yaradırıq
class IFood:
def bake(self): pass

class Bread(IFood):
def bake(self):
print("Bread was baked")
# test üçün bir class daha əlavə edirəm, etməsək də olar
class Pastry(IFood):
def bake(self):
print("Pastry was baked")

class Production:
def __init__(self, food): # food burada IFood interfeysini implement edən hər hansı bir class olacaq
self.food = food

def produce(self):
self.food.bake()

Fikir verdinizsə, burada ən aşağı səviyyə (Bread) və onunla eyni səviyyədə əlavə etdiyim (Pastry) classlarını abstrakt səviyyəsinə gətirdik və yuxarı səviyyə classımızdakı (Production) aşağı səviyyə class ilə bağlılığı “qırmış” olduq. Yəni, hər üç classımızı səviyyəsindən asılı olmayaraq bir interfeysə bağladıq. Və bu səbəbdən Production classımızın produce metodu birbaşa implement olunan food-un bake metodunu çağırır. Aşağıdakı kod blokunu run etməklə nəticədən fəlsəfəni anlaya bilərik:

ProduceBread = Production(Bread())
ProduceBread.produce()
ProducePastry = Production(Pastry())
ProducePastry.produce()

Və, və, və… Beləliklə, 5-ci prinsipin də sonuna gəldik. Ümid edirəm ki, ən azından azərbaycanca oxuduğunuz ən dəyərli Pythonic məqalələrdən birinin müəllifiyəm artıq :)

Bir Kamranlar sözü deyir ki, “S.O.L.I.D.” prinsiplərilə yazılan kod солидный olur.

--

--