在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
开源软件名称:refactoring-challenge/reversi-ios开源软件地址:https://github.com/refactoring-challenge/reversi-ios开源编程语言:Swift 100.0%开源软件介绍:リファクタリング・チャレンジ (リバーシ編) iOS版本チャレンジは、 Fat View Controller として実装されたリバーシアプリをリファクタリングし、どれだけクリーンな設計とコードを実現できるかというコンペティションです(ジャッジが優劣を判定するわけではなく、設計の技を競い合うのが目的です)。 はじめにFat View Controller は iOS アプリ開発におけるアンチパターンとしてよく知られています。 Fat View Contoller を作ると、 UI とロジックが分離されず、モジュール性が低く、テストしづらいコードができあがります。状態管理が複雑になり、修正時の影響範囲が見通しづらく、メンテナンス性が低下します。 試行錯誤の結果、自分なりに Fat View Controller との戦い方を確立した人も多いでしょう。勉強会やカンファレンスなどで知見を共有することで、コミュニティとしての戦い方も進化してきました。また、このような問題は iOS アプリに限った話ではありません。アプリケーション開発においてよりクリーンな設計・コードを実現するために、様々なアーキテクチャパターンが考案されてきました。さらに、それらをサポートするフレームワークも多数開発されています。 しかし、そのような戦い方は抽象的な説明ではなかなか伝わりません。具体的な説明も、ボリュームの関係で極度に単純化された例になってしまいがちです。そのため、実際のプロジェクトに適用しようとしても、説明の意図通りに適用できているのかわからないというケースも多いのではないでしょうか。一方で、より現実的な例で説明しようとしても、業務で扱うような巨大で複雑なコードベースは、説明の題材には不適切です。戦い方を理解する前に、コードの挙動を理解するだけで多大な時間を必要とします。 もし、適切な複雑さとボリュームを持った題材があれば、 Fat View Controller との戦い方をより具体的な形で学べるはずです。また、共通の題材を用いて比較すれば、どの方法が何をどのように解決するのか、また何をカバーしてい ない のかを可視化し、論じやすくなります。 そこで、誰もが仕様を知っており、かつ、適度な複雑さを持った対象として、リバーシ(オセロとも呼ばれますが、オセロはメガハウス社の登録商標のため、ここではリバーシと呼びます)を選びました。このコンペティションでは、用意された Fat View Controller をどれだけクリーンにリファクタリングできるかを競います。 しかし、この Fat View Controller のコードは、どうしようもないスパゲッティコードなわけではありません。 UI とロジックが分離されていないという問題がありますが、コード自体はそれなりに整理されています。下手に Fat View Controller を解消しようとすると、不必要にコードを複雑化してしまうでしょう。そうならないように腕が問われます。是非、あなたの技を駆使して、理想の設計とコードを実現して下さい! なぜリバーシなのかリバーシは、次の点で題材として優れていると考えています。
また、一般的なアプリ開発において問題となりやすい事項を扱えるように、本アプリの仕様として次のような特徴も備えています。
チャレンジのしかた本リポジトリを clone し、 Xcode で Reversi.xcodeproj を開いて下さい。本アプリは Fat View Controller として実装されており、一部のビューコンポーネントや基本的なデータ型を除いて、すべてのコードが ViewController.swift に書かれています。この リファクタリングなので、 アプリの挙動が変化しないようにして下さい 。挙動を維持したまま、どれだけコードをクリーンにできるかというチャレンジです。なお、リファクタリングというタームは、ここでは挙動を変更せずにコードを変更するという意味で使っています。通常リファクタリングに求められるような段階的な修正を期待しているわけではありません。仕様を理解した上で理想的な設計を考え、ほぼスクラッチで再実装するような大胆な変更も問題ありません。もちろん、通常のリファクタリングと同じように、段階的に修正を行っても構いません。 なお、 ViewController.swift 以外のコード(リバーシの盤やディスクを表示するビューコンポーネントやディスクの裏表を表すデータ型)は変更を加えずにそのまま利用できるように作られています。たとえば、ディスクを裏返すアニメーションは 修正のポイントリバーシは明確なルールを持ったゲームです。ルールを正しく実装できているか単体テストで検証できることが望ましいです。現状では、ディスクを置いてディスクを裏返す処理とアニメーションの制御、ビューの更新(ディスクの枚数の表示)などが関連し合っており、リバーシのロジックだけを単体テストするのが困難です。リバーシのロジックを切り離して、単体テストできるようにしましょう。 さらに、リバーシというゲーム自体が持つロジックとは別に、アプリケーションに紐付いたロジックも存在します。たとえば、 "Manual" の(人間が操作する)プレイヤーは UI だけでなく、ファイル I/O やデータベースへのアクセス、ネットワークを介した通信など、純粋なロジックではなく外部環境が関係する処理も単体テストがしづらい部分です。本チャレンジでは、ゲームの状態をセーブおよびロードする処理を扱います。そのような処理を実際の I/O から分離し、セーブ・ロードを伴う処理を単体テストできると良いでしょう。 また、現状ではゲームの状態(黒・白どちらの番か、勝敗はついたのか)や非同期処理の制御などの状態管理のコードが その他の方向性として、お気に入りのライブラリやフレームワークを使って、冗長なコードと戦うこともできます。たとえば、現状では非同期処理のコードは主にコールバック関数をベースにして書かれています。ライブラリを導入してコードをシンプルにすることもできます。 なお、本リポジトリの実装には、コーナーケースでのみ発生する既知のバグが存在します。テストを導入するなどしてバグを発見し、修正できると望ましいです。 上記のすべてを行わないといけないわけではありませんし、他に取り組むべき問題もあるでしょう。ここで挙げた内容は参考程度にとどめ、理想の設計によるクリーンなコードを実現して下さい。 詳細仕様アプリが満たすべき仕様を説明します。シンプルなアプリなので、おおまかな仕様は実際に実行して操作してみることで把握可能です。ここでは、より細かい、注意すべき仕様について説明します。 プレイヤーモード本アプリには人間が操作する "Manual" と、 AI が操作する "Computer" の 2 種類のプレイヤーモードが存在します。 ユーザーは、黒・白ともいつでもプレイヤーモードを切り替えることができます。 "Manual" は盤のセルをタップすることでディスクを配置し、無効な手となるセルのタップ(ディスクを 1 枚も裏返せない、すでにディスクが置かれているなど)や、入力可能な状態でない( "Computer" の思考中、ディスクが裏返されるアニメーションの途中など)場合、入力は無視されます。 "Computer" の思考は非同期的に行われ、その思考中もユーザーの入力はブロックされません。 "Computer" の思考中は、そのことを示すインジケータ(上図右端)を表示します。 注意事項"Computer" の思考中にプレイヤーモードが "Manual" に切り替えられた場合、 "Computer" の思考を停止し、インジケータを隠さなければなりません。 "Computer" の思考ロジック自体は本チャレンジの課題ではありません。本リポジトリの実装では、有効な手からランダムに一つを選択し、 2 秒後に結果を返します。独自のロジックを実装しても構いませんが、本チャレンジの評価対象には含まれません。 ディスクのアニメーションディスクを置く・裏返す処理は非同期のアニメーションを伴い、一枚ずつ順番に処理されます。 アニメーションの途中でもユーザーの入力はブロックされません。 アニメーションの順番は次のように決められています。
たとえば、
このとき、下図の
すると、ディスクを置く・裏返すアニメーションは次の順番に実行されます。
その結果、最終的に盤の状態は次のようになります。
なお、あるセルのディスクを置いたり裏返したりするアニメーションは ディスクの枚数の表示黒・白それぞれのディスクの枚数を画面上に表示します。 ディスクが置かれ、ディスクの枚数が更新される場合、すべてのディスクが裏返された後でまとめて枚数の表示を更新します。何枚のディスクが裏返されても、 1 手につき一度だけ枚数の表示が更新されます。 注意事項ディスクを 1 枚裏返す度に枚数の表示を更新してはいけません(実際にそのような仕様を試してみたところ、見づらかったため現仕様となりました)。盤の状態の変化を検知し、自動的に枚数の表示を更新するようなコードでは、この仕様をうまく扱えません。一連の処理をまとめて扱う必要があります。 "Reset" ボタンユーザーは "Reset" ボタンを押すことで、いつでもゲームを初期化することができます。初期化の内容は次の通りです。
なお、盤の初期状態は前述の表記を用いると次のとおりです。
誤操作によってゲームが初期化されてしまわないように、 "Reset" ボタンが押されると次のようなアラートを表示して、ユーザーの意思を確認します。 "Cancel" が選択された場合には、ゲームの初期化は行われません。 注意事項ディスクを裏返すアニメーションの途中や "Computer" の思考中も "Reset" ボタンは有効です。それらの非同期処理を停止してただちにゲームを初期化する必要があります。 "Reset" ボタンによってプレイヤーモードも初期化されるということは、プレイヤーモードはユーザー入力だけでなくプログラムからも変更され得るということです。 メッセージエリア今、黒のターンなのか、白のターンなのか、それともゲームの勝敗が着いた状態なのかをメッセージエリアに表示します。 黒または白のターンの場合はメッセージエリアに "●'s turn" と表示します。 "●" の部分には黒または白の円形が入ります。本リポジトリの実装では、"●" の部分の表示に ゲームの勝敗が着いた場合は、メッセージエリアに "● won" と表示します。 "●" の取り扱いは "●'s turn" のときと同じです。ただし、引き分けの場合には "Tied" と表示します。 注意事項"Tied" のときは "●" が表示されず、 "Tied" が水平方向中央に表示されます。そのようなレイアウトは本リポジトリの実装をそのまま踏襲して構いません。 UI のレイアウトは本チャレンジの課題ではありません。 パス有効な手が存在しない場合、そのプレイヤーのターンはパスされます。 パスされる場合、次のようなアラートを表示します。 "Dismiss" ボタンが押されるとパスの処理が行われ、次のプレイヤーのターンに移ります。 パスするプレイヤーが "Computer" であっても、アラートの表示は必要です。その場合、 "Computer" の思考は行わずにただちにアラートが表示されます。 両プレイヤーとも有効な手が存在しない場合には、パスではなくゲーム終了となります。パスのアラートも表示しません。 アラートを表示し "Dismiss" ボタンが押されるまでの間は、メッセージエリアに "●'s turn" と、そのパスするプレイヤーのターンであることを表示しなければなりません。 注意事項パスの判定は UI を介在させずにロジックだけで完結できますが、アラートとメッセージエリアのターン表示が求められることで、パスの処理と UI のインタラクションを連携させなければなりません。単純にパスの処理をロジックで完結させてしまうと、それらの表示が省略されてしまいます。 セーブ/ロードゲームの途中でアプリが強制終了されてしまっても同じ状況からゲームを再開できるように、自動的にゲームの状態が保存されます。アプリ開始時には、最後に保存されたゲームの状態が読み込まれます。 保存される項目は次の通りです。
保存は、 1 手ごと、またはプレイヤーモードが変更された場合に自動的に行われます。 注意事項本リポジトリの実装では、ディスクを裏返すアニメーションが完了してから保存が行われます。これは、 UI とロジックが分離されていないため、 UI の更新が行われないと状態の変更が完了しないからです。リファクタリングの結果として、アニメーションの完了を待たずに状態の変更を先取りして保存しても構いません。 コード概要本リポジトリの課題用コードについて説明します。 意味のあるコードが書かれているのは次の 5 ファイルです。
基本的に ViewController.swift 以外には手を加える必要はありません 。 以下、一つずつ説明します( CellView.swift は省略します)。ここで紹介する API についてはドキュメンテーションコメントが書かれているので、 Xcode 上で確認することもできます。 Disk.swiftディスクが黒( dark )か白( light )かを表す次のような public enum Disk {
case dark
case light
}
DiskView.swiftディスクを表すビュー class DiskView: UIView
特に、メッセージエリアについては "●'s turn" か "○'s turn" かを切り替える必要があります。 // 表示されるディスクの色を黒にする
diskView.disk = .dark
// 表示されるディスクの色を反転する
diskView.disk.flip()
BoardView.swiftリバーシの盤を表すビュー class BoardView: UIView
protocol BoardViewDelegate: AnyObject
// 3 列目・ 4 行目のセルの状態を取得
let disk: Disk? = boardView.diskAt(x: 3, y: 4)
// 3 列目・ 4 行目のセルを黒のディスクが置かれている状態に変更
boardView.setDisk(.dark, atX: 3, y: 4, animated: false) 列・行はそれぞれ
どのアニメーションが適用されるかは自動的に決定されるため、この API の利用者がアニメーションの種類を選択する必要はありません。
boardView.setDisk(.dark, atX: 3, y: 4, animated: true) { isFinished in
// アニメーション完了時に呼ばれる
} 特に複数枚のディスクを順番に連続して裏返す場合などは、このコールバックを用いてタイミングを制御することが重要になります。
なお、 その他にも、
extension ViewController: BoardViewDelegate {
func boardView(_ boardView: BoardView, didSelectCellAtX x: Int, y: Int) {
// x 列目・ y 列目のセルがタップされたときに呼ばれる
}
} これは class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
boardView.delegate = self
}
}
ViewController.swift本課題の対象となる Fat View Controller class ViewController: UIViewController
全体の処理の流れは "Game management" で管理されており、
を繰り返すことでゲームが進行します。最初は 他にわかりづらい点としては、
結果一覧チャレンジの結果一覧です。掲載を希望される方は、下記の表に行を追加する Pull Request をお送り下さい。
全部评论
专题导读
上一篇:cloudinary/cloudinary_ios: Cloudinary iOS SDK发布时间:2022-06-21下一篇:calebd/SimpleAuth: Simple social authentication for iOS.发布时间:2022-06-21热门推荐
热门话题
阅读排行榜
|
请发表评论