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언어는 다중 상속을 지원하지 않음.
몇가지 실험들
기능 확장을 위해 프로토콜이 프로토콜은 한번 더 채택한 경우에 나타나는 현상들
- 프로토콜 확장을 위해 아래 이미지 처럼 준비해서 테스트 할 예정
1. 두개 모두 채택한 상황에서 isUsernameValid를 호출하면
- SubscriptionValidatesUsername이 불림
2. 명시적으로 캐스팅을 해보면
- 캐스팅 1, 2 여부에 상관없이 무조건 SubscriptionValidatesUsername이 불림
위 실험은 전략패턴을 해당 형태로 처리 가능한지에 대한 의문을 풀고자 시도.
언어가 동작하는 형태에서 해당 형태를 지원하지 않는 것 같은데, swift.org에 공유해 봐야겠음.
(참고)
https://machinethink.net/blog/mixins-and-traits-in-swift-2.0/
https://ios-development.tistory.com/806
'apple > iOS, UIKit, Documentation' 카테고리의 다른 글
[Swift] Timer + RunLoop, backgroundQueue (swift-corelibs-foundation) (3) | 2024.10.15 |
---|---|
[Swift] NSCache (swift-corelibs-foundation) (9) | 2024.10.11 |
UICollectionView Sticky Header (0) | 2024.09.04 |
Library vs Framework 정리 (1) | 2024.08.29 |
Swift @TaskLocal (3) | 2024.08.28 |