概要
実装やデータの流れが統一できるクリーンアーキテクチャとRx+MVVMに魅了されて日々研究をしています。
30半ばの古参者でデザパタ大好き人間だったがデザパタの使い所がなかなかないので
無理やり、タイトルにもある通り「デザインパターンをRx+MVVM+DDDクリーンアーキテクチャで利用できるか」を検討してみたいと思います。
逐次更新していきます。
rxSwift/rxCocoaを前提に考察しますが、読み替えればすべての言語で対応可能になるように考察します。
生成に関するパターン
Singleton
Singleton パターンとは、そのクラスのインスタンスが1つしか生成されないことを保証するデザインパターンのことである。ロケールやルック・アンド・フィールなど、絶対にアプリケーション全体で統一しなければならない仕組みの実装に使用される[1]。
実装
CoreLocationを利用し、位置情報を表示する
CoreLocationの処理を行うRxLocationをSingletonにしている
import CoreLocation
import RxCocoa
import RxSwift
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
vm.latitude.asDriver().drive(latitude.rx.text).disposed(by: vm.disposeBag)
vm.longitude.asDriver().drive(longitude.rx.text).disposed(by: vm.disposeBag)
startButton.rx.tap.asDriver()
.drive(vm.startUpdateLocationCommand)
.disposed(by: vm.disposeBag)
}
let vm = LocationViewModel()
@IBOutlet weak var startButton: UIButton!
@IBOutlet weak var latitude: UILabel!
@IBOutlet weak var longitude: UILabel!
}
class LocationViewModel {
let usecase = LocationUsecase()
let location = Variable<CLLocation?>(nil)
let latitude: Driver<String>!
let longitude: Driver<String>!
let disposeBag = DisposeBag()
init() {
usecase.getObservable().bind(to: location).disposed(by: disposeBag)
latitude = location.asDriver().map { $0?.coordinate.latitude.description ?? "" }
longitude = location.asDriver().map { $0?.coordinate.longitude.description ?? "" }
startUpdateLocationCommand.asObserver().bind(onNext: usecase.startUpdatingLocation).disposed(by: disposeBag)
}
let startUpdateLocationCommand = PublishSubject<Void>()
}
class LocationUsecase {
private var repository = LocationRepository()
func getObservable() -> Observable<CLLocation> {
return repository.getObservalbe()
}
func startUpdatingLocation() {
repository.startUpdatingLocation()
}
}
class LocationRepository {
private let location: RxLocation = RxLocation.shared
func getObservalbe() -> Observable<CLLocation> {
return location.Observable
}
func startUpdatingLocation() {
location.startUpdatingLocation()
}
}
class RxLocation: NSObject {
static let shared = RxLocation()
let locationManager = CLLocationManager()
private override init() {
super.init()
auth()
}
func auth() {
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.distanceFilter = 10
locationManager.requestLocation()
}
}
func requestLocation() {
locationManager.requestLocation()
}
func startUpdatingLocation() {
locationManager.startUpdatingLocation()
}
func stopUpdatingLocation() {
locationManager.stopUpdatingLocation()
}
private let observer = PublishSubject<CLLocation>()
var Observable: Observable<CLLocation> {
return observer
}
}
extension RxLocation: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
observer.onNext(location)
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
break
case .restricted, .denied:
break
case .authorizedAlways, .authorizedWhenInUse:
break
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
}
考察
位置情報をSingletonでObservableで公開することで常に同じObservableをsubscribeできる。
※コード上はsubscribeを使わずにbindとdriveでObserverを追加している
Abstract Factory
wikipedia Abstract Factory パターン
関連するインスタンス群を生成するための API を集約することによって、複数のモジュール群の再利用を効率化することを目的とする。日本語では「抽象的な工場」と翻訳される事が多い。
考察
よく使われるインスタンスの生成を3パターン用意しました。
- Factoryは具象クラス ProductはProtocol
- Factoryは具象クラス ProductはProtocol
- Factoryなし ProductはProtocol
let factory: Factory = Factory()
let product = factory.create(someKind)
product.doSomething()
let product:Product = Factory.create(someKind)
product.doSomething()
let product:Product = Product.create(someKind)
product.doSomething()
どのパターンでもcreateの中身はsomeKindを判定し、適切なProductを返します。
単体テストのためにProductをMockにしたい、という場合
FactoryをMockに置き換えないといけない。
2, 3 は staticなcreateメソッドのためモックに置き換えるのは難しい。
1のFactoryをMockに置き換えてcreateメソッドでMockProductを返すようにする。
Usecaseのインスタンス生成をAbstractFactoryに任せてみる
class ViewModel {
let some1Usecase: Some1Usecase
let some2Usecase: Some2Usecase
init(usecaseFactory: UsecaseFactory = UsecaseFactoryImpl()) {
self.some1Usecase = usecaseFactory.createSome1Usecase()
self.some2Usecase = usecaseFactory.createSome2Usecase()
}
}
protocol Some1Usecase {}
class Some1UsecaseImpl: Some1Usecase {}
protocol Some2Usecase {}
class Some2UsecaseImpl: Some2Usecase{}
protocol UsecaseFactory {
func createSome1Usecase() -> Some1Usecase
func createSome2Usecase() -> Some2Usecase
}
class UsecaseFactoryImpl: UsecaseFactory {
func createSome1Usecase() -> Some1Usecase {
return Some1UsecaseImpl()
}
func createSome2Usecase() -> Some2Usecase {
return Some2UsecaseImpl()
}
}
直接Usecaseを渡す場合
Factoryを使わない場合(ViewModel2)とどっちが楽だろうか。
class ViewModel2 {
let some1Usecase: Some1Usecase
let some2Usecase: Some2Usecase
init(some1: Some1Usecase = Some1UsecaseImpl(),
some2:Some2Usecase = Some2UsecaseImpl()) {
self.some1Usecase = some1
self.some2Usecase = some2
}
}
結論
Mockの入れ替えをする上でイニシャライザ(コンストラクタ)の引数が減るという意味では有用かもしれない。ただし必須だとは思わない。
- アプリ内のすべてのusecaseを同一のFactoryでcreateすることによって、全ViewModelのイニシャライザを統一できる、というメリットはあるかもしれない。
その場合、Mock生成も統一されたものになりテストコードの可読性が上がるかもしれない。
導入するなら導入し、すべてのクラスが同じ実装になるようにすべきではないかと思う。
一部だけ取り入れるのは後からコードを見る人間にとって混乱を招き害悪でしかない。
一部だけやってみた のであれば「やってみた」 というコメントを残しておいて欲しい。
Factory Method
Factory Method パターンは、他のクラスのコンストラクタをサブクラスで上書き可能な自分のメソッドに置き換えることで、 アプリケーションに特化したオブジェクトの生成をサブクラスに追い出し、クラスの再利用性を高めることを目的とする。
考察
abstract protected でcreateメソッドを定義しサブクラスで生成を切り替える。
TemplateMethodパターンの1メソッド(または複数)をFactoryにしただけと想定。
どうやってDDD+CleanArchitectureに取り入れようか。
最近、多態性=単体テストのMockっていう思考回路になってきていて
それはそれで正しいんだけど、プロダクトコードの処理を切り替えたかったんだよ。GoFは。
でも、継承を多用すると継承がガッチガッチになるのでやっぱりMock切り替えで使ってみます。
ViewModelにて Usecaseの生成をFactory Methodで実装してみる
class ViewModel{
var usecase: Usecase!
init() {
usecase = createUsecase()
}
func createUsecase() -> Usecase {
return UsecaseImpl()
}
}
protocol Usecase {}
class UsecaseImpl: Usecase {}
ViewModelTestで継承でUsecaseを切り替えてみる
class ViewModelTest: ViewModel {
override func createUsecase() -> Usecase {
return UsecaseMock()
}
}
class UsecaseMock: Usecase {}
結論
Testできるけど、なんか違うような?違うよね?いや、使える?!
Builder
オブジェクトの生成過程を抽象化することによって、動的なオブジェクトの生成を可能にする。
考察
Andoridのライブラリ(java/kotlin)では多用されているイメージがある。
OKHttpとか。
通信やDBアクセスや、デバイスアクセスといった外部リソースの通信ライブラリはDataレイヤーに閉じ込められる。
repositoryはどういったI/Fとして公開するかを考えれば良く、個別のライブラリの影響は受けないので、DDD+CleanArchitectureという中では利用することはないのではと思う。
Prototype
生成されるオブジェクトの種別がプロトタイプ(典型)的なインスタンスであるときに使用され、このプロトタイプを複製して新しいオブジェクトを生成する。
このパターンは、Abstract Factory パターンでなされるように、クライアント・アプリケーションにおいてオブジェクトの生成者をサブクラスにすることを回避する
標準的な方法(例えば'new')で新しいオブジェクトを作ることによる固有のコストが所与のアプリケーションにとって高すぎる時にそれを回避する
ために用いられる。
考察
既に最近の言語では一般的になってしまったデザインパターンの一つである。
Clonableというprotocolだったりinterfaceだったり、基本的なライブラリとして提供されているため、ここでは言及する必要はないのかもしれない。
#構造に関するパターン
DDD+CleanArchitectureにおいては
Presenter/Domain/Dataの各層とそれに属する各クラスが決まっているため
構造に関するパターンを適用することはほぼないのではと考えられる。
※亜種はあるが、だいたい同じ思想である。
Adapter
Adapter パターンを用いると、既存のクラスに対して修正を加えることなく、インタフェースを変更することができる。Adapter パターンを実現するための手法として継承を利用した手法と委譲を利用した手法が存在する。
考察
CleanArchitectureにおけるRepositoryの考え方がAdapterそのものだ。
ただし、委譲はするが継承はしない。
Bridge
「橋渡し」のクラスを用意することによって、クラスを複数の方向に拡張させることを目的とする。
考察
特にDDD+CleanArchitectureで利用しなくてもいいかも。
以下に、復習としてBridgeをまとめます。
車でいうと 車種、カラーという二つの要素を全パターン網羅しようとすると
車種の数 かけることの カラーの数 という組み合わせが生じてしまう。
例 車種: {BMW, ベンツ}
カラー { 赤、青、黒}
組み合わせ {赤BMW, 青BMW, 黒BMW, 赤ベンツ、青ベンツ、黒ベンツ}
さらにカーナビの種類、MT/ATなど、カスタマイズを入れると莫大な組み合わせになる。
それらをサブクラスで表現にしないで委譲を使いましょうという思想です。
class Car {
let color: Color
let type: CarType
}
Bridgeしないと以下になる。こんな実装ありえない。。。
protocol Car {}
class RedBMW: Car {}
class BlueBMW: Car {}
class BlackBMW: Car {}
ゲームだと
装備としての武器、鎧、兜、小手
アバターの髪の毛、顔パーツとか。
CM「組み合わせは1億通り!君だけのキャラを作れ!(サブクラス1億個!)」