Clean Code - 객체와 자료구조
1. 개요
- 변수를 private으로 정의하는 이유는 외부 클래스가 클래스 내부 변수에 의존하지 않고 변경의 유연함을 얻기 위함이다.
- 멤버 변수의 사용 용도에 따라
a. read only -> getter() 제공
b. write only -> setter() 제공
c. read/write -> getter(), setter() 제공
2. 자료 추상화
class Wafer():
def __init__(self):
self.id = None
self.exposure_start_time = None
self.exposure_end_time = None
self.exposure_time = None
- 클래스 내부 데이터를 public으로 선언하고 외부에서 직접 접근하는 것은 올바른 방식이 아니다.
class Wafer():
def __init__(self):
self.__id = None
self.__exposure_start_time = None
self.__exposure_end_time = None
self.__exposure_time = None
@property
def id(self):
return self.__id
@id.setter
def id(self, id):
self.__id = id
@property
def exposure_start_time(self):
return self.__expoure_start_time
@exposure_start_time.setter
def exposure_start_time(self, time):
self.__exposure_start_time = time
@property
def exposure_end_time(self):
return self.__exposure_end_time
@exposure_end_time.setter
def exposure_end_time(self, time):
self.__exposure_end_time = time
@property
def exposure_time(self):
return self.__exposure_time
@exposure_time.setter
def exposure_time(self, time):
self.__exposure_time = time
- 멤버 변수를 private으로 선언하고 getter, setter를 정의한다고 해서 완벽한 캡슐화가 아니다.
- 결과적으로 getter, setter를 통해 클래스 내부 구조를 알 수 있고, 직접 접근할 수 있기 때문이다.
- 변수 사이에 함수라는 게층을 넣는다고 구현이 저절로 감춰지는 것은 아니다.
class Wafer():
@abstractmethod
def get_exposure_start_time(self) -> pd.Timestamp:
pass
@abstractmethod
def get_exposure_start_time(self) -> pd.Timestamp:
pass
@abstractmethod
def get_exposure_time(self) -> pd.Timestamp:
pass
@abstractmethod
def set_exposure_time(self, start_time, end_time):
pass
- 추상 클래스를 제공해 사용자가 구현을 모른 채, 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스라고 할 수 있다.
3. 데이터/객체 비대칭
- 객체는 추상화 뒤로 데이터를 숨긴 채 데이터를 다루는 함수만 공개한다.
- 자료 구조는 데이터를 그대로 공개하며 별다른 함수는 제공하지 않는다.
- 두 정의는 본질적으로 상반된다.
1) 절차지향적 코드
class CleanFactory():
def __init__(self, total_wafer, produced_wafer, execution_time):
self.total_wafer = total_wafer
self.produced_wafer = produced_wafer
self.execution_time = execution_time
class CMPFactory():
def __init__(self, total_wafer, produced_wafer, execution_time):
self.total_wafer = total_wafer
self.produced_wafer = produced_wafer
self.execution_time = execution_time
class EtchFactory():
def __init__(self, total_wafer, produced_wafer, execution_time):
self.total_wafer = total_wafer
self.produced_wafer = produced_wafer
self.execution_time = execution_time
class LossClassification():
@classmethod
def calculate_wafer_per_day(cls, Factory):
if type(Factory) == CleanFactory:
# do something
return wafer_per_day
if type(Factory) == CMPFactory:
# do something
return wafer_per_day
if type(Factory) == EtchFactory:
# do something
return wafer_per_day
raise NoneFactory("[%s]", type(Factory))
# 새로운 기능 추가
@classmethod
def analysis_loss(cls, Factory):
if type(Factory) == CleanFactory:
# do something
return analysis_result
if type(Factory) == CMPFactory:
# do something
return analysis_result
if type(Factory) == EtchFactory:
# do something
return analysis_result
raise NoneFactory("[%s]", type(Factory))
- 절차지향적 코드에서 각 공정을 나타내는 클래스는 단순히 공정정보를 담고있는 자료구조의 역할로써 존재한다.
- 절차지향적 코드는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.
- 절차지향적 코드는 새로운 자료 구조를 추가하기 어렵다. 사용하는 클라이언트 코드에서 모든 함수를 수정해야 한다.
2) 객체 지향적 코드
class Factory():
@abstractmethod
def calculate_wafer_per_day(self):
pass
@abstractmethod
def analysis_loss(self):
pass
class CleanFactory(Factory):
def __init__(self, total_wafer, produced_wafer, execution_time):
self.__total_wafer = total_wafer
self.__produced_wafer = produced_wafer
self.__execution_time = execution_time
def calculate_wafer_per_day(self):
# do something
return wafer_per_day
def analysis_loss(self):
# do something
return analysis_result
class CMPFactory():
def __init__(self, total_wafer, produced_wafer, execution_time):
self.__total_wafer = total_wafer
self.__produced_wafer = produced_wafer
self.__execution_time = execution_time
def calculate_wafer_per_day(self):
# do something
return wafer_per_day
def analysis_loss(self):
# do something
return analysis_result
class EtchFactory():
def __init__(self, total_wafer, produced_wafer, execution_time):
self.__total_wafer = total_wafer
self.__produced_wafer = produced_wafer
self.__execution_time = execution_time
def calculate_wafer_per_day(self):
# do something
return wafer_per_day
def analysis_loss(self):
# do something
return analysis_result
class LossClassification():
@classmethod
def calculate_wafer_per_day(cls, Factory):
return Factory.calcaute_wafer_per_day()
# 새로운 기능 추가
@classmethod
def analysis_loss(cls, Factory):
return Factory.analysis_loss()
- 객체 지향적 코드에서는 데이터를 클래스 내부로 감추고 인터페이스만을 제공해서 클라이언트 코드와 메시지를 주고 받는다.
- 객체 지향적 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.
- 객체 지향적 코드는 새로운 함수를 추가하기 위해서 모든 클래스를 고쳐야 하기 때문에 어렵다.
4. 디미터의 법칙
- 모듈은 자신이 조작하는 객체의 내부를 몰라야 한다.
- 클래스 (A)의 메서드 (b)는 다음과 같은 객체의 메서드만을 호출해야 한다.
a. 클래스 (A)
b. (b)가 생성한 객체
c. (b) 인수로 넘어온 객체
d. (A) 인스턴스 변수에 저장된 객체
- 구조체 구조 + 객체 구조 형태는 피하도록 하자
5. 자료 전달 객체 (DTO, Data Transfer Object)
- DTO는 public 변수가 있거나 private 변수에 getter/setter가 있는 객체를 말한다.
- save(), findById()와 같은 검색 함수도 제공할 수 있다.
- 활성 레코드는 데이터베이스 테이블이나 다른 소스에서 자료를 직접 변환한 결과이다.
- 활성 레코드는 비즈니스 로직 메서드를 추가해 자료구조 + 객체 구조의 형식을 띄게 된다.
- 활성 레코드는 자료구조로 취급하고 비즈니스 로직을 담으면서 내부 자료를 숨기는 객체를 따로 생성하도록 한다.
6. 정리
- 객체는 동작을 공개하고 내부 데이터는 숨겨 새 객체 타입을 추가하기는 쉽지만, 기존 객체에 새 동작을 추가하기는 어렵다.
- 자료 구조는 별다른 동작없이 내부 데이터를 노출하므로 새 동작을 추가하기는 쉽지만, 기존 함수에 새 자료 구조를 추가하는 것은 어렵다.
- 소프트웨어를 개발할 때, 새로운 자료 타입을 추가하는 유연성이 필요하다면 객체가 더 적합하다.
- 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합하다.
- 두 패러다임 사이에 편견 없이 문제 해결에 적합한 패러다임을 선택하도록 하자.
'Architecture' 카테고리의 다른 글
[Clean Code 정리] 경계 (0) | 2022.05.29 |
---|---|
[Clean Code 정리] 오류 처리 (0) | 2022.05.29 |
[Clean Code 정리] 형식 맞추기 (0) | 2022.05.29 |
[Clean Code 정리] 주석 (0) | 2022.05.28 |
[Clean Code 정리] 함수 (0) | 2022.05.28 |
댓글