Design Pattern
디자인 패턴의 이유와 방법, 언제 어떤 디자인 패턴을 써서 문제를 해결하는지에 대해 살펴보자.
Design 패턴
디자인 공통 언어, 전문가와 소통 도움, 문서화, 가독성( 한번 패턴 만들면 그 디자인을 잘 설명한다)
Architecture Vs Design pattern
Architecture
high-level 프레임워크: 시스템을 component와 interaction으로 정의
Design 패턴
architecture보다 lower-level. Reusable Collaboration
Ex) 시스템 X 와 그 subsystem Y를 decouple 하는 방법은?
디자인 패턴의 용도
- Object oriented reuse를 높은 추상화레벨에서 돕는다.
- Object-oriented implementation을 가이드하고 제한하는 프레임워크의 역할.
디자인 패턴의 4가지 필수 요소
Name, problem, solution, consequence
- Name: 패턴을 식별
- Problem: 언제 해당 디자인 패턴을 사용할지
- Solution: 디자인을 구성하는 요소, relationship, responsibility, collaboration.
- Consequence: 해당 패턴을 사용할 때의 결과와 tradeoff
구분
목적: creational, structural, Behavioral 패턴
Scope: class, object
Creational 패턴: 객체 생성에 관련된 패턴이다. 객체의 생성과 조합을 캡슐화해 특정 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 크게 받지 않도록 유연성을 제공한다.
Abstract factory, Singleton, …
Various Design Pattern
Factory 패턴
일반적인 생성자는
- object가 언제, 어떻게 생성되는지 control 할 때 좋지 않다.
- 언어적 한계가 존재한다.
- polymorphic type hide 문제가 존재한다.
Ex) Problem: polymorphic object를 create, use 해야 하지만, client는 모르게해야한다.
Solution: 우리가 원하는 타입의 객체를 생성하지만, 그 객체의 base class를 리턴하는 function을 작성하자.
Llama_factory 에 인자로 이름, type 전달 => type 파라미터 가지고 원하는 타입 객체 생성 후
Llama (base class) 로 리턴
실전 활용 예제
Q) 게임 설정 난이도에 따라 다른 몬스터 (그냥 굼바, 스파이크 굼바) 나오게 만들기
=> 적을 생성시마다 게임 난이도를 체크해야 해서 좋지 못한 설계이다. 이를 극복하기 위해 Abstract Factory 패턴을 적용할 수 있다.
AbstractFactory 패턴
난이도에 맞는 굼바 객체를 최초에 생성하면, 1번만 알맞게 생성해도 끝이다!
Singleton 패턴
아래와 같은 요구가 있다면 어떻게 접근해야할까?
Q) 전역적으로 사용가능한 state를 만들 되, 어떻게 data가 access, update 되는지 control해야할 때
Bad solution: 전역 변수로 전부 >> 전부 접근 가능
Less bad solution: 모든 상태를 class에 넣고, 그것의 전역 instance를 만들기.
=> 어디서나 접근가능해야 하지만, parameter로 전달하는 것은 어수선하다.
하지만 그렇다고 parameter로 전달하는 양을 줄이기 위해 전역변수를 늘리는 건 최악.
State를 database, web API 등 program 외부에 쓰면?
여전히 둘 모두 좋은 해결책이 아니다.
이를 해결하기 위한 좋은 방식이“Singleton 패턴” 이다.
Singleton 패턴이란?
각 class가 오직 1개의 instance만 가지게하고, 전역적으로 접근 가능하게 한다.
위 main 문 결과는 어떻게 될까?
(2번째 get_instance()에서는 새로 생성 x 처음 생성한 Singleton.instance 값을 받아와서 앞의 변경사항 유지됨) >> 이런식으로 singleton을 통해 전역변수 처럼 값을 유지한다.
문제점: Singleton.get_insatnce가 호출될 때마다 항상 같은 object를 반환한다는 보증이 없다.
그렇다면, 아래와 같은 상황에선 어떻게 해야할까?
Q) 카드게임 로직을 만드는데, 이 카드게임은 1게임 할 때마다 exit 된다.
위와 같은 상황에선, 싱글톤이 어울리지 않는다. “single instance”를 요구하는 명세 없으면 최대한 사용하지 않는 것이 좋다.
singleton은 전역으로 만들기 위해 사용되는 것이 아니다!
Singleton: 메모리를 최초 instance 생성시 1번만 사용, 데이터 공유도 good( 1개 instance로). 싱글톤은 이와 같은 특징이 주력이기에, 단순 전역을 만들기 위해 싱글톤을 형성하는 것은 Cost Waste다!
Structural Pattern
클래스나 객체를 조합해 더 큰 구조를 만드는 패턴으로 예를 들어 서로 다른 인터페이스를 지닌 2개의 객체를 묶어 단일 인터페이스를 제공하거나 객체들을 서로 묶어 새로운 기능을 제공하는 패턴이다.
Adapter 패턴
특정 인터페이스를 지원하지 않는 대상 객체를 인터페이스를 지원하는 Adapter에 집어넣어서 사용하는 방법이라 할 수 있다.
(원하는 특정 인터페이스를 상속 받는 adapter를 통해 내가 가진 대상 객체를 원하는
인터페이스로 변환)
Problem: 우리가 필요한 기능을 가진 Object를 가지고 있지만, 우리가 사용하고 싶은 방식대로 가지고 있지 않은 경우.
여기서 Iterator<String> 타입인 itr은 불가하다. ( Iterable 타입이어야한다… )
Adapter 패턴으로 중간다리를 놓아준다.
Iterator<String> => 요구되는 Iterable<String>을 상속받는 adapter 안에 집어넣어서 사용한다. ( 변환기 같은 것)
그 외
composite: client가 개별적인 object와 object 그룹들을 평등하게 대한다.
Proxy: 다른 객체의 control에 접근 가능한 대리인 제공
Behavior Pattern
객체나 클래스 사이의 알고리즘, 책임 분배에 관련된 패턴으로, 한 객체가 혼자 수행할 수 없는 작업을 여러 개의 객체로 어떻게 분배하는지, 또 그렇게 하면서도 객체 사이의 결합도(coupling)를 최소화하는 것에 중점을 둔다.
Ex) iterator 패턴: 어떻게 implement되는지 관계 없이 container를 탐색할 수 있게 하는
Uniform한 인터페이스
아래와 같은 상황에는 어떻게 설계해야할까?
Q) 이동과 관계없이 lock-on을 통해 적을 자동 조준. 조준한 적이 패배 시, 해당 적을 조준 멈춤
적이 죽으면 play객체의 release lock을 통해 자신에 대한 lock-on 해제된다는 명세
=> player와 enemy사이에 너무 강한 coupling(결합도)
Ex
player class 바꾸면, enemy도 영향을 받는다. (의존성!!)
Player가 여럿이거나, 적을 처치시마다 player의 score을 올려야 하거나 하면 성능상 문제가 된다.
* 적이 죽을 때마다 새로운 feature가 생기게 만들면, enemy class를 무조건 업데이트하고, 새로운 feature과 coupling 해야한다.
이때 사용할만한 것이, Observer 패턴이다.
Observer 패턴
Observer 패턴은 Listener가 주시하는 대상의 변화를 스스로 감지하여 변화 상황에서 유연하게 대처할 수 있다.
이를 Reactive 패러다임의 근간이 되기도 한다.이에 관심이 있다면 아래 글을 참고하자.
흔히 Pub-Sub 구조로 이뤄진다.
Observer 패턴의 형태
Object 간에 1대 다 의존성을 만들어서, object가 변경 시, 자동으로 변경사항을 알리고 업데이트한다.
Class 다이어그램
Subject(옵저버 관리자 클래스)와 Observer(데이터 변경을 통보 받는 인터페이스)로 일반화하여
Concrete subject (enemy,변경관리 대상 데이터 가진 객체) 와 Concrete Observer(player,enemy로부터 변경된 데이터를 통보 받는 클래스)간의 의존성을 제거한다.
- 옵저버 관리 클래스 <1 n > 옵저버 인터페이스 1대 다 관계.
- enemy가 변경사항( 사망) 등을 통보 시, 옵저버 관리 클래스가 해당하는 observer 인터페이스에 변경사항을 주고, 그걸 상속받는 player 가 해당 정보 반영한다.
이제 이를 활용해서, 어떻게 이전의 명세를 구성할 수 있는지 알아보자.
- Lock-on feature에 대한 옵저버 관리자는 옵저버를 생성한다.
- Enemy(Subject)가 on-death() 하면, 옵저버 관리자가 안에 들어있는 유저( 해당 enemy 처치한 유저들 모음) 들에 대한 update_enemy_defeated() 메소드를 호출한다.
Enemy는 publisher로써, 자신이 죽으면 자신의 변화된 상태를 자신을 SubScribe하는 Subscriber(옵저버 인터페이스 상속받는 Player)들에게 알린다.
Observer가 직접 fetch하는 pull 방식보다(player가 죽은 적의 정보 가져오기), 데이터를 update 함수의 parameter로 넘기는 push가 좋다. (enemy가 죽은 직후, 직접 player에게정보를 Push)
Template Method 패턴
아래의 상황은 어떻게 처리하면 좋을까?
Q) 다양한 적들과 싸우는 게임, plater나 적이 맞으면, 데미지 받는다.
- 피가 0이되면 사망, player가 죽으면 게임 end. 적이 죽으면, item 드랍.
- 적과 플레이어 모두 데미지 받고 피가 0이 되지 않으면 소리와 함께 넉백 된다.
- 둘을 포함하는 상위의 Actor가 가진 receive_hit으로 둘 다 데미지 받는다.
- Plater와 enemy가 피가 0이되면 서로 다른 특성 가지기에, Override 통해 해당 부분을 다르게 구현해야한다.
하지만, 이를 단순히 Override를 통해 변경이 필요한 부분을 일일이 구현하는 것은, 너무 많은 중복 코드를 양산하는 문제가 존재한다.
위와 같이, 데미지를 받는 특성은 공통인데, 중복 구현이 발생하게 된다.
이럴 때 사용하는 것이 Template Method 패턴이다.
Template Method 패턴이란?
⦁ 템플릿 메소드 패턴(template method pattern)은 소프트웨어 공학에서 동작 상의 알고리즘의 프로그램 뼈대를 정의하는 행위 디자인 패턴이다. 알고리즘의 구조를 변경하지 않고 알고리즘의 특정 단계들을 다시 정의할 수 있게 해준다
- 상위 추상 클래스에서 구조를 만들고, 하위 클래스에서 해당 구조를 구현하는 방식.
- Actor은 추상 클래스. 공통사항은 Actor(추상 클래스) 에서 정의하고( public 영역)
- 개별사항은 하위 클래스에서 구현한다( protected 영역)
위와 같이, 공통되는 부분은 상위에서 구현해 중복 줄이고, 차별되는 부분만 따로 구현하면 된다. ( 추상클래스로 하위에서 필요한걸 가져다 쓰는 느낌)
이제, Template 메소드로 위와 같이 구현했기에, 아래와 같은 새로운 명세가 추가되어도 유동적으로 변경되는 부분만 수정해서 명세를 충족할 수 있다.
위의 명세에서, 데미지 받아도 넉백 x 인 Turrent Enemy를 추가하라
코드 예시
기존
Virtual: 하위에서 Override 가능
- Appy_knockback()이 Override 가능하게 한 뒤,
- Enemy의 하위에 TurrentEnemy를 만들고, TurrentEnemy가 모든 enemy의 속성을 받아오지만
- Apply_knockback()을 비어 있게 Override해서 넉백 되지 않게 한다.
'소프트웨어 공학(rebooting now)' 카테고리의 다른 글
[Software Engineering] Version Control System [Git] (0) | 2023.01.09 |
---|---|
[Software Engineering] Object Oriented Analysis&Design (0) | 2023.01.09 |