어떤 클래스를 변경해야 하는 이유는 오직 단 하나 뿐이어야 한다.
(A class should have one, and only one, reason to change.)
class Album:
def __init__(self, name, artist, songs) -> None:
self.name = name
self.artist = artist
self.songs = songs
def add_song(self, song):
self.songs.append(song)
def remove_song(self, song):
self.songs.remove(song)
def __str__(self) -> str:
return f"Album {self.name} by {self.artist}\nTracklist:\n{self.songs}"
# breaks the SRP
def search_album_by_artist(self):
""" Searching the database for other albums by same artist """
pass
Album
클래스는 앨범명, 아티스트, 트랙 리스트를 저장하고 곡 추가/제거와 같은 조작을 함# instead:
class AlbumBrowser:
""" Class for browsing the Albums database"""
def search_album_by_artist(self, albums, artist):
pass
def search_album_starting_with_letter(self, albums, letter):
pass
소프트웨어 개체(패키지, 모듈, 함수 등)은 확장에 대해선 개방적이되 변경에 대해서는 폐쇄적이어야 한다.
(Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
if-else
구문이 있는 경우에 OCP가 위반되는 경우가 많음class Album:
def __init__(self, name, artist, songs, genre):
self.name = name
self.artist = artist
self.songs = songs
self.genre = genre
#before
class AlbumBrowser:
def search_album_by_artist(self, albums, artist):
return [album for album in albums if album.artist == artist]
def search_album_by_genre(self, albums, genre):
return [album for album in albums if album.genre == genre]
#after
class SearchBy:
def is_matched(self, album):
pass
class SearchByGenre(SearchBy):
def __init__(self, genre):
self.genre = genre
def is_matched(self, album):
return 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 [album for album in albums if searchby.is_matched(album)]
__and__
를 사용하면 됨#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):
return self.searchby1.is_matched(album) and self.searchby2.is_matched(album)
&
으로 묶을 수 있음LAWoman = Album(
name="L.A. Woman",
artist="The Doors",
songs=["Riders on the Storm"],
genre="Rock",
)
Trash = Album(
name="Trash",
artist="Alice Cooper",
songs=["Poison"],
genre="Rock",
)
albums = [LAWoman, Trash]
# this creates the AndSearchBy object
my_search_criteria = SearchByGenre(genre="Rock") & SearchByArtist(
artist="The Doors"
)
browser = AlbumBrowser()
assert browser.browse(albums=albums, searchby=my_search_criteria) == [LAWoman]
# yay we found our album
를 자료형 의 객체 에 대해 증명할 수 있는 속성이라고 하자. 그렇다면 가 의 하위형이라면 는 자료형 의 객체 에 대해 증명할 수 있어야 한다.
(Let be a property provable about objects of type . Then should be true for objects of type where is a subtype of .)
class Rectangle:
def __init__(self, height, width):
self._height = height
self._width = width
@property
def width(self):
return self._width
@width.setter
def width(self, value):
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
self._height = value
def get_area(self):
return self._width * self._height
class Square(Rectangle):
def __init__(self, size):
Rectangle.__init__(self, size, size)
@Rectangle.width.setter
def width(self, value):
self._width = value
self._height = value
@Rectangle.height.setter
def height(self, value):
self._width = value
self._height = value
def get_squashed_height_area(Rectangle):
Rectangle.height = 1
area = Rectangle.get_area()
return area
rectangle = Rectangle(5, 5)
square = Square(5)
assert get_squashed_height_area(rectangle) == 5 # expected 5
assert get_squashed_height_area(square) == 1 # expected 5
Square
가 Rectangle
을 상속받고 있지만 LSP를 위반하고 있음클라이언트는 자신이 사용하지 않은 인터페이스에 의존 관계를 가지면 안된다.
(Clients should not be forced to depend upon interfaces that they do not use.)
class PlaySongs:
def __init__(self, title):
self.title = title
def play_drums(self):
print("Ba-dum ts")
def play_guitar(self):
print("*Soul-moving guitar solo*")
def sing_lyrics(self):
print("NaNaNaNa")
# This class is fine, just changing the guitar and lyrics
class PlayRockSongs(PlaySongs):
def play_guitar(self):
print("*Very metal guitar solo*")
def sing_lyrics(self):
print("I wanna rock and roll all night")
# This breaks the ISP, we don't have lyrics
class PlayInstrumentalSongs(PlaySongs):
def sing_lyrics(self):
raise Exception("No lyrics for instrumental songs")
PlayInstrumentalSongs
클래스는 sing_lyrics
메서드가 필요가 없으나 부모 클래스인 PlaySongs
을 상속 받아 불필요한 메서드를 갖게 됨class PlaySongsLyrics:
@abstractmethod
def sing_lyrics(self, title):
pass
class PlaySongsMusic:
@abstractmethod
def play_guitar(self, title):
pass
@abstractmethod
def play_drums(self, title):
pass
class PlayInstrumentalSong(PlaySongsMusic):
def play_drums(self, title):
print("Ba-dum ts")
def play_guitar(self, title):
print("*Soul-moving guitar solo*")
class PlayRockSong(PlaySongsMusic, PlaySongsLyrics):
def play_guitar(self):
print("*Very metal guitar solo*")
def sing_lyrics(self):
print("I wanna rock and roll all night")
def play_drums(self, title):
print("Ba-dum ts")
고레벨 모듈은 저레벨 모듈에 의존하여선 안된다. 두 모듈 모두 추상화된 것에 의존해야 한다. (인터페이스 등)
(High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).)
추상화된 것은 구체적인 것에 의존하여선 안된다. 구체적인 것이 추상화된 것에 의존해야 한다.
Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.*
class AlbumStore:
albums = []
def add_album(self, name, artist, genre):
self.albums.append((name, artist, genre))
class ViewRockAlbums:
def __init__(self, album_store):
for album in album_store.albums:
if album[2] == "Rock":
print(f"We have {album[0]} in store.")
AlbumStore
클래스에서 앨범을 추가할 때 튜플의 순서를 바꾸면 전체 코드는 작동하지 않게 됨ViewRockAlbums
클래스에서 제대로 된 앨범명을 출력할 수 없음class GeneralAlbumStore:
@abstractmethod
def filter_by_genre(self, genre):
pass
class MyAlbumStore(GeneralAlbumStore):
albums = []
def add_album(self, name, artist, genre):
self.albums.append((name, artist, genre))
def filter_by_genre(self, genre):
if album[2] == genre:
yield album[0]
class ViewRockAlbums:
def __init__(self, album_store):
for album_name in album_store.filter_by_genre("Rock"):
print(f"We have {album_name} in store.")
MyAlbumStore
가 GeneralAlbumStore
를 상속받도록 하고, MyAlbumStore
는 클래스만의 탐색 메서드를 추가하여 DIP를 만족 시킴https://towardsdatascience.com/5-principles-to-write-solid-code-examples-in-python-9062272e6bdc