전략 패턴(Strategy Pattern)
|
정의
알고리즘군을 정의하고 각각 캡슐화하여 교환해서 사용할 수 있도록 만들고 전략패턴을 활용하게되면 알고리즘을 사용하는
클라이언트와 독립적으로 알고리즘을 변경할 수 있다
사용하면 어떤 부분에서 좋을까?
상속으로 해결될 수 없는 코드 중복이나 객체의 실시간 알고리즘의 변경시에 유용합니다
추상객체(프로토콜-protocol / 익스텐션-extension)를 상속해서 알고리즘을 추가하여 확장이 가능합니다
그리고 알고리즘이 변경될 때 해당 알고리즘만 변경하면 되기 때문에 유지보수 측면에서도 용이하다고 볼 수 있습니다
예시
온라인 스토어 같은 경우 요즘 새벽배송 / 직진배송 / 일반배송 여러가지 배송이 있는데요
각 객체에서 공통된 역할이 존재합니다
배송과 시간이라는 점이죠
만약 배송 클래스에서 만약 새벽배송에 필요한 구문과 직진배송에 필요한 메서드가 들어가있다면
일반 배송에서는 의미없는 메서드로 변질될것이고
배송에서 만약 새로운 기능이 생성되었을 때 상속받는 객체들을 모두 수정해줘야하는 유지보수에 어려움이
발생하게 됩니다
장점
전략패턴을 사용하게되면 불필요한 코드의 작성을 남발하지 않아도 되기 때문에 코드의 반복사용성을 줄일 수 있어
재사용성이 용이하고 기능확장에 유연하게 대처할 수 있습니다
그리고 그 배송상품이 할인된 상품인지 아닌지 결제 또한 어떤방식으로 사용해서 결제할 건지 복합적으로 생각해본다면
각각 세부적으로 나눠서 관리하는게 용이하다라고 생각이 들죠?
우선 단순하게 배송관련 코드를 작성해보고자 합니다
예전에 어떤식으로 사용했냐?
게임적으로 설명하자면
각 몬스터나 캐릭터도 특징이 많습니다
움직이며 공격만 하는 캐릭터, 공중을 날라다니며 공격하는 캐릭터,
힐러 탱커 등등 아주 많은 특성을 가지고 있죠?
그걸 한 클래스에 넣어버리면 상상만해도 끔찍하겠죠?..
그래서 추상화나 인터페이스를 사용하여 (protocol, extension)
사용하게됩니다
//⭐️온라인 쇼핑몰을 생각한다면 아주 많은 기능이 있다
//✏️할인, 배송, 결제 등등
protocol Discount {
func applyDiscount(to price: Double) -> Double
}
enum DeliveryType {
case standard
case overnight
case sameday
case discussWithDriver
var description: String {
switch self {
case .standard:
return "보통 2-3일 그 이상 소요될 수 있습니다"
case .overnight:
return "다음 날 배송됩니다"
case .sameday:
return "당일 배송됩니다"
case .discussWithDriver:
return "기사님과 협의해야합니다"
}
}
}
protocol Shipping {
var calculateShippingCost: Double { get }
var deliveryType: DeliveryType { get }
}
class CODShipping: Shipping {
var calculateShippingCost: Double { return 30000 }
var deliveryType: DeliveryType { return .discussWithDriver }
}
class Freeshipping: Shipping {
var calculateShippingCost: Double { return 0.0 }
var deliveryType: DeliveryType { return .standard }
}
class Overnightshipping: Shipping {
var calculateShippingCost: Double { return 10000 }
var deliveryType: DeliveryType { return .overnight }
}
간단하게 이렇게 작성할 수도 있겠지만
조금만 응용을 하자면
주문 클래스를 적용할 수 있습니다
주문자 or 판매자가 적용한 배송방법을 보고
주문할 때 적용하는 방법입니다
class Product {
var name: String
var price: Double
init(name: String, price: Double) {
self.name = name
self.price = price
}
}
class Order {
var products: [Product]
var shipping: Shipping
init(products: [Product], shipping: Shipping) {
self.products = products
self.shipping = shipping
}
func calculatorCost() -> Double {
var cost = products.reduce(0.0) { $0 + $1.price }
cost += shipping.calculateShippingCost
return cost
}
}
여기서 개발이 완료후 퀵배송도 생각할 수 있고 온라인 문자로 전달 같은 부분도 기능을 추가한다고 했을때도 고려할 수 있고
심지어 배송전 배송 타입을 변경하고 싶을때 자유롭게 변경이 가능한 점도 있습니다
어떻게 보면 런타임에 자유롭게 변경이 가능한 점도 장점이라고 할 수 있습니다
import UIKit
class Product {
var name: String
var price: Double
var shippingCost: Double
init(name: String, price: Double, shippingCost: Double) {
self.name = name
self.price = price
self.shippingCost = shippingCost
}
}
protocol ShippingStrategy {
func calculateShippingCost(for products: [Product]) -> Double
}
class VariableShippingStrategy: ShippingStrategy {
func calculateShippingCost(for products: [Product]) -> Double {
let totalShippingCost = products.reduce(0.0) { $0 + $1.shippingCost }
return totalShippingCost
}
}
class Order {
var products: [Product]
var shipping: ShippingStrategy
init(products: [Product], shipping: ShippingStrategy) {
self.products = products
self.shipping = shipping
}
func calculateCost() -> Double {
let shippingCost = shipping.calculateShippingCost(for: products)
let productCost = products.reduce(0.0) { $0 + $1.price }
return shippingCost + productCost
}
}
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var totalPriceLabel: UILabel!
var products: [Product] = []
var order: Order?
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
loadProducts()
}
func setupUI() {
tableView.dataSource = self
tableView.delegate = self
}
func loadProducts() {
products = [
Product(name: "Product 1", price: 10.0, shippingCost: 5.0),
Product(name: "Product 2", price: 15.0, shippingCost: 7.0),
Product(name: "Product 3", price: 20.0, shippingCost: 6.0)
]
tableView.reloadData()
}
@IBAction func calculateCostButtonTapped(_ sender: Any) {
calculateCost()
}
func calculateCost() {
let shippingStrategy = VariableShippingStrategy()
order = Order(products: products, shipping: shippingStrategy)
let totalCost = order?.calculateCost() ?? 0.0
totalPriceLabel.text = "Total Cost: $\(totalCost)"
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return products.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath)
let product = products[indexPath.row]
cell.textLabel?.text = product.name
cell.detailTextLabel?.text = "$\(product.price)"
return cell
}
}
그리고 조건문을 사용하지 않고 사용할 수 있는것이 장점입니다
그리고 새로운 기능이 생긴다고해서 자식 클래스에서 추가하는것이 아닌 프로토콜을 하나 생성해서 필요한 부분에만 추가하는게
유지보수 측면이나 확장성을 생각했을때 훨씬 더 유연한 구조라고 생각할 수 있습니다
여기서 또 조금 자세히 생각해 볼 수 있는건
상품의 카테고리 / 상품 할인 등등 여러요건을 생각해 볼 수 있습니다
그리고 판매수량과 무관하게 계속 판매할 수 있는 경우도 있지만 미리 예약받은 한정 수량만 판매하는 경우는
미리 기재하면 좋겠죠? 공동구매라던가?.. 그런식으로 여러개 생각하며 작성하게된다면 전략패턴이
유용하다는걸 생각할 수 있을것 같습니다
예시2
만약 한 제품에서 다양한 옵션이 들어간 제품을 만들게 된다면??
슬라임도 요즘 여러가지 만들잖아요
비즈가 들어가는 슬라임
색깔이 투명인 슬라임 / 초코색인 슬라임 등등 여러개를 만들 수 있잖아요?
이런식으로 만들게 되면 프로토콜을 사용할 수 밖에 없는 상황이 옵니다
그렇게 되면 슬라임 하나를 생성한다고 할 때
투명색 / 비즈가 들어가고 / 촉감도 프로토콜로 만들어서 관리할 수 있겠죠?
변경하고 싶다면 실시간으로 변경도 가능하게 끔 메서드나 아님 계산 프로퍼티로 사용하여도 가능하구용
단점
클래스 증가가 됩니다 각 알고리즘마다 별도의 클래스를 만들어야하기 때문입니다
알고리즘 선택을 위해 추가적인 코드가 필요하기 때문에 코드의 복잡성이 증가할 수 있습니다
한마디
상속보다 유연하게 사용할 수 있는 방법은 많다
끝입니다
혹시 잘못된 부분이 있다면 꼭 알려주세요 감사합니다❤️
'DesignPattern' 카테고리의 다른 글
Swift 디자인패턴 | 옵저버 패턴(Observer Pattern) (0) | 2024.06.10 |
---|