apple/iOS, UIKit, Documentation

Swift Mixin and Trait

lgvv 2024. 11. 18. 01:39

Swift Mixin and Trait

 

iOS 프로그래밍에서 주로 사용되는 언어는 Swift로 다중 상속을 지원하지 않음.

Swift에서는 인터페이스(Interface)를 프로토콜(protocol)로 사용하고 있어서 프로토콜이라는 용어와 인터페이스의 의미는 같음.

 

목차

  • 배경
  • mixin이란?
  • interface(protocol), mixin, trait
  • 예제를 통해 알아보기 1
  • 예제를 통해 알아보기 2
  • 예제를 통해 알아보기 3
  • swift 다중 상속 컴파일 오류
  • 몇가지 실험들
    • 둘 다 채택한 경우
    • 명시적 캐스팅

배경

객체지향 프로그래밍에서 상속의 사용은 코드의 결합도를 크게 증가시킴. 이로 인하여 많은 문제점들이 발생.

  • 상속을 코드 중복을 해결하기 위한 수단으로 사용하면 안됨.
  • 상속은 부모와 자식간의 높은 결합도를 가지게 되어 코드의 유연성, 확장성 등 여러 문제를 초래함.
  • 부모에서 메서드 추가시 필요하지 않는 메서드를 자식이 가지게 되므로, 불필요한 코드가 추가될 뿐만 아니라 프로토콜 등과 같이 제약할 수 있는 수단이 없는 경우 사이드 발생 가능.
  • 상속을 사용할 경우 클래스 폭발 문제 등 여러 문제 및 다이아몬드 문제도 존재함.

따라서 상속보다는 합성을 통해 설계하는 것이 더 나은 설계가 될 수 있음.

  • 그렇다고 상속을 사용하지 말라는 것은 아니라, 문제 해결을 위한 방법으로써 적절한 것을 선택하는 것이 중요함.
    • 다시한번 언급하지만 코드 중복을 해결하기 위한 수단으로 사용하면 안됨.
  • 서비스가 안정화 되어 문서화가 잘 되어 있다면, 기능을 추가하는데 합성보다는 상속이 나은 경우도 존재.
  • 구현체를 상속받는 대신 프로토콜을 따라 프로그래밍을 하여 더 나은 구현을 할 수 있음.

 

mixin이란?

객체를 생성할 때 코드 일부를 객체 안에 섞어 넣어 재사용하는 기법을 가리키는 용어

  • 상속(Inheritance) 합성(Composition)의 특성을 모두 보유하고 있는 독특한 코드 재사용 방법
  • 상속은 is-a 관계를 만들기 위한 것이지만 mixin은 코드를 섞는 기법으로 포함(has-a 관계)에 속함.
  • 또한 Swift 언어는 다중 상속을 지원하지 않기 때문에 이를 해결하는 용도로도 사용 가능함.

 

interface(protocol), mixin, trait

세가지 의미에 대해서 짚고 넘어가기. 

  • interface(protocol): 메서드 시그니처나 프로퍼티 정의를 가짐
  • trait : 상태(stored property) 없이 순수하게 메서드들만 존재하는 형태
  • mixin : 상태(stored property)와 메서드를 가진 형태

서칭해 본 결과 mixin과 trait은 크게 구분 안하는 걸로 보임.

  • Swift 언어에서 인터페이스의 기본 구현체가 연산 프로퍼티를 통해 상태(computed property) 값을 가질 수 있음.
  • 다만, 상태가 저장 프로퍼티를 의미한다면 Swift 언어에서는 Mixin을 사용하기가 어려움

 

/* ? */ 지점이 무한반복되어야 함.

예제를 통해 알아보기 1

전통적인 MVC 구현에서는 아래의 예제를 따름.

  • 다른 영역에서 로그인과 관련한 기능이 필요하다면?
  • 코드를 복사해서 사용할 경우 보일러 플레이트가 늘어나며, 응집도 또한 낮아지게 됨.
  • 코드 재사용을 위한 상속은 다른 문제를 야기할 수 있어서, 기능 분리가 필요.
import UIKit

private class LoginViewController: UIViewController {
    private func isUsernameValid(username: String?) -> Bool {
        if let username = username, username.count > 4 {
            return true
        } else {
            return false
        }
    }
    
    func isPasswordValid(password: String?) -> Bool {
        if let password = password, password.count > 4 {
            return true
        } else {
            return false
        }
    }
    
    @objc
    private func didTapLoginButton(sender: UIButton) {
        let isUsernameValid = isUsernameValid(username: usernameTextField.text)
        let isPasswordValid = isPasswordValid(password: passwordTextField.text)
        
        if isUsernameValid && isPasswordValid {
            // proceed with login
        } else {
            // show alert with error message
        }
    }
    
    // MARK: - UIComponent
    
    private let usernameTextField = UITextField()
    private let passwordTextField = UITextField()
    private lazy var loginButton: UIButton = {
        let btn = UIButton()
        btn.addTarget(
            self,
            action: #selector(didTapLoginButton),
            for: .touchUpInside
        )
        return btn
    }()
}

 

예제를 통해 알아보기 2

기능 분리가 필요하므로 기존 구현된 코드에서 기능을 객체 단위로 분리

  • 상속은 지양하는 방향으로 has-a 관계로 정의되었음.
  • 또한, 객체의 구현이 분리된 영역으로 숨겨져 있기 때문에 나쁘지 않은 방법
  • 하지만, 이는 인터페이스가 아닌 여전히 구현체 자체를 바라보고 있어서 인터페이스를 통한 구현으로 변경하고자 함.

 

import UIKit

private class UsernameValidator {
    func isUsernameValid(username: String?) -> Bool {
        if let username = username, username.count > 4 {
            return true
        } else {
            return false
        }
    }
}

private class PasswordValidator {
    func isPasswordValid(password: String?) -> Bool {
        if let password = password, password.count > 4 {
            return true
        } else {
            return false
        }
    }
}

private class LoginViewController: UIViewController {
    let usernameValidator = UsernameValidator()
    let passwordValidator = PasswordValidator()
    
    @objc
    private func didTapLoginButton(sender: UIButton) {
        let isUsernameValid = usernameValidator.isUsernameValid(username: usernameTextField.text)
        let isPasswordValid = passwordValidator.isPasswordValid(password: passwordTextField.text)
        
        if isUsernameValid && isPasswordValid {
            // proceed with login
        } else {
            // show alert with error message
        }
    }
    
    // MARK: - UIComponent
    
    private let usernameTextField = UITextField()
    private let passwordTextField = UITextField()
    private lazy var loginButton: UIButton = {
        let btn = UIButton()
        btn.addTarget(
            self,
            action: #selector(didTapLoginButton),
            for: .touchUpInside
        )
        return btn
    }()
}

 

예제를 통해 알아보기 3

프로토콜의 extension을 통해 사용하는 객체 내부에 불필요한 프로퍼티를 없애면서 동일한 동작을 제공할 수 있음.

  • LoginViewController에서 Validate 객체가 사라짐.
import UIKit

private protocol ValidatesUsername {
    func isUsernameValid(username: String?) -> Bool
}

extension ValidatesUsername {
    func isUsernameValid(username: String?) -> Bool {
        if let username = username, username.count > 4 {
            return true
        } else {
            return false
        }
    }
}

private protocol ValidatesPassword {
    func isPasswordValid(password: String?) -> Bool
}

extension ValidatesPassword {
    func isPasswordValid(password: String?) -> Bool {
        if let password = password, password.count > 4 {
            return true
        } else {
            return false
        }
    }
}

private class LoginViewController: UIViewController, ValidatesUsername, ValidatesPassword {
    
    @objc
    private func didTapLoginButton(sender: UIButton) {
        let isUsernameValid = isUsernameValid(username: usernameTextField.text)
        let isPasswordValid = isPasswordValid(password: passwordTextField.text)
        
        if isUsernameValid && isPasswordValid {
            // proceed with login
        } else {
            // show alert with error message
        }
    }
    
    // MARK: - UIComponent
    
    private let usernameTextField = UITextField()
    private let passwordTextField = UITextField()
    private lazy var loginButton: UIButton = {
        let btn = UIButton()
        btn.addTarget(
            self,
            action: #selector(didTapLoginButton),
            for: .touchUpInside
        )
        return btn
    }()
}

 

Swift 다중 상속 컴파일 오류

Swift언어는 다중 상속을 지원하지 않음.

 

다중상속 X

 

 

몇가지 실험들

기능 확장을 위해 프로토콜이 프로토콜은 한번 더 채택한 경우에 나타나는 현상들

  • 프로토콜 확장을 위해 아래 이미지 처럼 준비해서 테스트 할 예정

 

상황은 두개 모두 채택한 상황

 

 

1. 두개 모두 채택한 상황에서 isUsernameValid를 호출하면 

  • SubscriptionValidatesUsername이 불림

 

2. 명시적으로 캐스팅을 해보면

  • 캐스팅 1, 2 여부에 상관없이 무조건 SubscriptionValidatesUsername이 불림

 

 

위 실험은 전략패턴을 해당 형태로 처리 가능한지에 대한 의문을 풀고자 시도.

언어가 동작하는 형태에서 해당 형태를 지원하지 않는 것 같은데, swift.org에 공유해 봐야겠음.

 

 

 

 

 

 

 

 

 

(참고)

https://machinethink.net/blog/mixins-and-traits-in-swift-2.0/

 

Mixins and traits in Swift 2.0

This is a transcription of a talk I gave at the Dutch CocoaHeads meetup in Rotterdam in July, 2015. Some of the code is out-of-date now that we have Swift 3. However, the ideas are still valid. If you paid attention to this year’s WWDC, you’ve probably

machinethink.net

https://ios-development.tistory.com/806

 

[iOS - swift] Mixin 패턴(mix-in), Traits 패턴

Mixin, Traits 패턴 Mixin, Traits 패턴: 특정 클래스에서 어떤 기능이 필요할 때, 이 기능을 interface로 정의하여 이 interface만 준수하면 바로 기능을 사용할 수 있도록 설계된 패턴 상속이 아닌 포함 코드

ios-development.tistory.com

https://stonzeteam.github.io/%EC%BD%94%EB%93%9C-%EC%9E%AC%EC%82%AC%EC%9A%A9%EC%9D%84-%EC%9C%84%ED%95%9C-Mixin/

 

코드 재사용을 위한 Mixin

코드 재사용 패턴들에 대해 알아봅시다.

stonzeteam.github.io