2. 디자인 패턴 - 구조 패턴
구조 패턴
종류: Pattern
1. 구조 - Bridge
빌더패턴은 구현부에서 추상층을 분리해서 각자 독립적으로 변형할 수 있게 해주는 패턴이다!
위의 예시처럼 구성하면 새로운 모양이나 색이 추가하면 기하급수적으로 많은 클래스를 추가해야함!!
이런 식으로 모양과 색을 분리해준다면 각 속성을 독립적으로 수정 및 확장이 가능해짐!
구조
- abstraction
- client가 사용하는 최상위 타입
- implementation을 참조하고 일을 위임
- implementation
- abstraction의 기능을 구현하기 위해 인터페이스를 정의
2. 구조 - Decorator
주어진 상황 및 용도에 따라 객체에 책임을 덧붙이는 패턴으로, 서브클래싱 대신에 기능확장을 쉽게 해줄 수 있다!
- 마트로시카마냥 계속 자기자신을 감싼다라고 생각하면 됨!
문제상황
ex) SNS 알림 라이브러리를 만들었다.
- 상속으로 해결했지만 사람들은 SMS, Facebook를 같이 알림을 받고 싶다는 피드백이 왔다!
→ 이걸 또 상속으로 한다면… 계속해서 서브클래싱이 무한대로 발산해버린다 ㅠㅠ
⇒ 그렇다면 상속이 아닌 집합을 쓰는 건 어떨까??
class Uploader {
private func setUp() {
/*...*/
}
func upload() {
setUp()
}
}
class DecoratedUploader: Uploader {
var uploader: Uploader? // 데코레이터가 내부 uploader를 가지기에 랩핑하게 됨!!
init(_ uploader: Uploader? = nil) {
self.uploader = uploader
}
override func upload() {
super.upload()
uploader?.upload()
}
}
class TextUploader: DecoratedUploader {
override func upload() {
super.upload()
print("uploading text")
}
}
class ImageUploader: DecoratedUploader {
override func upload() {
super.upload()
print("uploading image")
}
}
class VideoUploader: DecoratedUploader {
override func upload() {
super.upload()
print("uploading video")
}
}
class FileUploader: DecoratedUploader {
override func upload() {
super.upload()
print("uploading file")
}
}
let textAndImageAndVideoUploader = TextUploader(ImageUploader(VideoUploader()))
let imageAndVideoUploader = ImageUploader(VideoUploader())
let everythingUplodaer = TextUploader(ImageUploader(VideoUploader(FileUploader())))
3. 구조 - Facade
퍼사드는 건물의 정면, 외관을 의미함. 즉, 어떤 소프트웨어의 커다란 코드에 대한 간략화된 인터페이스라고 보면 됨!
- 퍼서드는 여러 인스턴스를 소유하여 사용해야하는 타입이 있는 경우, 간단한 인터페이스를 통해 각 인스턴스들이 일하게 할 때 유용!
protocol Facade {
func work()
}
struct CPU {
func work(with memory: Memory) { }
}
struct Memory {
func input(from Devices: [Device]) { }
func output(to Devices: [Device]) { }
}
class Device { ... }
class InputDevice: Device { ... }
class OutputDevice: Device { ... }
class Keyboard: InputDevice { ... }
class Monitor: OutputDevice { ... }
class TouchBar: Device { ... }
struct Computer: Facade {
private let cpu = CPU()
private let memory = Memory()
private let keyboard = Keyboard()
private let monitor = Monitor()
private let touchBar = TouchBar()
func work() {
memory.input(from: [keyboard, touchBar])
cpu.work(with: memory)
memory.output(to: [monitor, touchBar])
}
}
// My code
let computer = Computer()
computer.work()
4. 구조 - Flyweight
동일하거나 유사한 객체 사이에서 가능한 많은 데이터를 공유하게 해서 메모리 사용량을 최소화하는 패턴!
- flyweight: 공유되는 데이터에 대한 인터페이스
- flyweightFactory: flyweigfht객체들을 가지며 관리
ex) 몬스터들이 대량으로 이동하면서 사라지고 다시 생성되는 경우
import Foundation
import CoreGraphics
// flyweight
protocol Monster {
func createAtCurrentLocation(at location: CGPoint)
func deleteAtCurrentLocation(at location: CGPoint)
func recreateAtOtherLocation(at location: CGPoint)
func attack()
}
// flyweight 구현체
class LowerLevelMonster: Monster {
func createAtCurrentLocation(at: CGPoint) {
// some code
}
func deleteAtCurrentLocation(at: CGPoint) {
// some code
}
func recreateAtOtherLocation(at: CGPoint) {
// some code
}
func attack() {
// some code
}
}
class MonsterClient {
let monster: Monster
var currentLocation: CGPoint
init(monster: Monster, currentLocation: CGPoint) {
self.monster = monster
self.currentLocation = currentLocation
}
func createMonster(currentLocation: CGPoint) {
monster.createAtCurrentLocation(at: currentLocation)
}
func recreateMonster(currentLocation: CGPoint, at location: CGPoint) {
monster.deleteAtCurrentLocation(at: currentLocation)
monster.recreateAtOtherLocation(at: location)
}
}
// MARK: Factory: flyweight들을 관리하는 객체
class MonsterFactory {
static let shared = MonsterFactory()
private init() { }
enum MonsterLevel {
case lower
}
private var createdMonster = [MonsterLevel: Monster]()
private func createMonster(_ level: MonsterLevel) -> Monster {
switch level {
case .lower:
let lowerLevelMonster = LowerLevelMonster()
createdMonster[level] = lowerLevelMonster
return lowerLevelMonster
}
}
func monster(level: MonsterLevel) -> Monster {
if let monster = createdMonster[level] {
return monster
} else {
let monster = createMonster(level)
return monster
}
}
}
let lowerLevelMonster = MonsterFactory.shared.monster(level: .lower)
let lowerLevelMonsterClient = MonsterClient(monster: lowerLevelMonster, currentLocation: CGPoint(x: 10, y: 20))
lowerLevelMonsterClient.createMonster(currentLocation: CGPoint(x: 10, y: 20))
lowerLevelMonsterClient.recreateMonster(currentLocation: CGPoint(x: 10, y: 20), at: CGPoint(x: 100, y: 110))
- 몬스터의 인스턴스를 가져올 때 새로운 인스턴스를 가져오는게 아니라 딕셔너리에 있는 값을 캐시해서 가져올 수 있게 됨!
5. 구조 - Proxy
프록시 대리라는 뜻으로 다른 누군가를 대신해서 그 역할을 수행하는 존재 자신이 할 수 있는 최선의 일을 한 후에 범위를 벗어나면 진짜 일을 하는 사람에게 요청하는 패턴
프록시의 종류
- remote proxy
- 요청을 처리하고 서비스 객체에 이를 전달하는 역할 담당
- virtual proxy
- 서비스 객체에 대한 정보를 캐싱하여 접근을 연기
- protection proxy
- 특정 작업을 요청한 객체가 해당 작업을 수행할 권한을 가지고 있는지 확인
구조
- Proxy: 대리 역할
- RealSubject: 실제의 주체
- Subject: 대리와 실제 역할을 동일시하기 위한 프로토콜
// subject
protocol YouTubeDownloadSubject {
func downloadYoutubeVideos() async -> [String]
}
final class RealSubject: YouTubeDownloadSubject {
func downloadYoutubeVideos() async -> [String] {
//Todo: 유튜브 서버에서 비디오를 다운로드해오는 부분을 구현한다.
}
}
final class Proxy: YouTubeDownloadSubject {
//진짜 요청을 받아서 처리하는 개체, 정말로 사용할때만 초기화하기 위하여 lazy키워드 사용
private lazy var realSubject = RealSubject()
//캐싱구현
private var videoCache = [String]()
//클라이언트 권한 받음
private var client: Clinet
init(_ client: Clinet) {
self.client = client
}
func downloadYoutubeVideos() async -> [String] {
//클라이언트 권한에 따라 제어를 할 수도 있다.
guard client.auth == .owner else {
print("유튜브 비디오를 다운로드할 권한이 없습니다.")
return []
}
//비디오 캐시가 비어있으면 실제 realSubject에 데이터를 요청함.
if videoCache.isEmpty {
videoCache = await realSubject.downloadYoutubeVideos()
return videoCache
} else {
//비디오 캐시에 데이터 있으면 그거 리턴해줌.
return videoCache
}
}
}
let client = Clinet(.owner)
let proxy = Proxy(client)
//프로토콜 타입을 받습니다.
func loadYouTubeVideo(_ service: YouTubeDownloadSubject) {
service.downloadYoutubeVideos()
}
loadYouTubeVideo(proxy)
장점
- realsubject가 아주 큰 인스턴스일 때 proxy를 이용해서 최대한 지연시킬 수 있음
- proxy는 real이 준비되지 않거나 사용할 수 없는 경우에도 동작
단점
- proxy를 도입해야하므로 코드가 복잡해짐
6. 구조 - Composite
객체들을 트리 구조로 구성하여 부분, 전체 계층을 표현하는 패턴 사용자가 단일 객체와 복합 객체 모두 동일하게 다루도록 한다
재귀적으로 최하단까지 실행하고 싶을 경우 유용하게 사용 가능
- component: 공통적인 기능을 하는 프로토콜
- leaf, composite: 모두 component를 구현하지만, compoiste는 자식 component들을 추가적으로 가지고 있음
import Foundation
// Component
protocol Military {
var unitName: String { get set }
func attack()
}
// Leafs
struct AirForce: Military {
var unitName: String
func attack() {
print("\\(unitName) 공격 🔫")
}
}
struct Navy: Military {
var unitName: String
func attack() {
print("\\(unitName) 공격 🔫")
}
}
struct Army: Military {
var unitName: String
func attack() {
print("\\(unitName) 공격 🔫")
}
}
// Composite
struct MilitaryGroup: Military {
var unitName: String
var group: [Military]
func attack() {
print("-----\\(unitName) 예하에 있는 부대에 공격 명령을 하달한다.-----")
group.forEach { unit in
unit.attack()
}
}
}
let navy627 = Navy(unitName: "해군 627대대")
let navy625 = Navy(unitName: "해군 625대대")
let army653 = Army(unitName: "육군 653대대")
let army669 = Army(unitName: "육군 669대대")
let airForce257 = AirForce(unitName: "공군 257대대")
let airForce239 = AirForce(unitName: "공군 239대대")
let navy1 = MilitaryGroup(unitName: "해군 1사단", group: [navy627, navy625])
let army1 = MilitaryGroup(unitName: "육군 1사단", group: [army653, army669])
let airForce1 = MilitaryGroup(unitName: "공군 1사단", group: [airForce257, airForce239])
let thirdROKArmy = MilitaryGroup(unitName: "3군 사령부", group: [navy1, army1, airForce1])
thirdROKArmy.attack()
7. 구조 - Adapter
클래스의 인터페이스를 사용자가 원하는 다른 인터페이스로 변환하는 패턴
구조
- client: 기존 비즈니스 로직을 포함하는 클래스
- target interface: 다른 클래스
- adapter: adaptee를 wrapping해서 구현
- adaptee: 호환되지 않은 클래스
**// MARK: - import 라이브러리의 인터페이스 (Adaptee)**
class AppleAuthorization {
func presentAuth() {
// 애플 로그인 내부구현
}
}
class GoogleAuthorization {
func tryLogin() {
// 구글 로그인 내부구현
}
}
**// Mark: - Target Interface**
protocol AuthorizationService {
func login(completion: (Bool) -> ())
}
// MARK: - Adapter
class Authorization: AuthorizationService {
func submitUserInfo(_ userInfo: Token, completion: (Bool) -> ()) {
// 사용자 정보를 서버로 요청하는 동작
}
func login(completion: (Bool) -> ()) {
// 로그인 동작
let exampleInfo = Token(id: "사용자 아이디", password: "사용자 비밀번호")
submitUserInfo(exampleInfo) { result in
completion(result)
}
}
}
struct AppleAuthorizationAdapter: AuthorizationService {
let adaptee = AppleAuthorization()
func login(completion: (Bool) -> ()) {
adaptee.presentAuth()
}
}
struct GoogleAuthorizationAdapter: AuthorizationService {
let adaptee = GoogleAuthorization()
func login(completion: (Bool) -> ()) {
adaptee.tryLogin()
}
}
// Client
enum AuthorizationPlatform {
case basic
case apple
case google
}
func presentAuthorization(_ platform: AuthorizationPlatform, completion: (Bool) -> ()) {
switch platform {
case .basic:
let basicAuth = Authorization()
basicAuth.login { result in
// 로그인 성공/실패
}
case .apple:
let appleAuth = AppleAuthorizationAdapter()
appleAuth.login { result in
// 로그인 성공/실패
}
case .google:
let googleAuth = GoogleAuthorizationAdapter()
googleAuth.login { result in
// 로그인 성공/실패
}
}
}