リファクタリング
TOC
現状の問題点
- 開発スピードが遅い
- 意図しないバグが多い
なぜか?
- 意図の分かりづらいコードが多い
- ライブラリへの依存が多くの箇所にまたがっている
- ライブラリの破壊的なアップデートに対応できない
- 新しいライブラリへの変更が難しい
- 同じ処理が複数の箇所に散乱している
- 変更時の影響範囲を把握しづらい
開発スピードをあげるにはどうするか?
- 機能追加・変更を容易にする
- 変更に対する強度をあげる
- 環境の変化に対応する
なにができるか?
- テストの作成
- 継続的なリファクタリング
どう実践すればいいのか?
- テストの書きやすいコードを書く
- リファクタリングのしやすいコードを書く
テストを書く目的とは?(前フリ)
書いたコードにバグがないことを保証するため?
- テストでバグが見つかることは少ない
- テストコードを書いているエンジニアがテストしたいことしかテストできない
- プロダクションコードを書いているエンジニアがテストするのであれば、なおさら想定通りに動くコードを想定通りにテストしてしまう
「テストの書きやすいコードを書く」とは、テストを書くことが目的ではない
テストを書く目的とは?(本題)
-
将来のエンジニアが安心して、追加・変更・リファクタリングできるようにするため
-
実装者が想定している使い方のドキュメントとしての役割
追加・変更・リファクタリングが容易になっていれば、テストなしでも担保できる強度がある
-> 設計する
なぜ将来変更されるコードを設計をするのか
- 将来変更されるからこそ、設計する
- 変更していいこと・変更してはいけないことを設計する
- 設計を確認すれば、影響範囲や意図を知ることができる
- 変更ではなく、追加・削除が正しいという判断ができるかも
リファクタリング の目的とは?
-
ソフトウェア開発環境の変化に対応するため
- 変化に乗り遅れたプログラムはユーザーの期待に応えられない
- ex. iOS14 で追加されるウィジェット・App Clips
- 変化に乗り遅れたプログラムはユーザーの期待に応えられない
-
アーキテクチャを柔軟に変更するため
プログラムは完成しない
ソフトウェア開発環境は常に変化している
- 高度なビジネス要件
- 新しい技術・機能
- 新しいアーキテクチャ
- ライブラリサポートの終了
機能変更や追加があるかぎり、リファクタリングを続ける
-> リファクタリングを続けるためには、リファクタリングしやすいコードにしておくべき
リファクタリングしやすいコード
リファクタリングしやすいコードを書くためにいくつかの原則を知っておくとやりやすい
SOLID の原則
S: Single Pesponsibility Principle(単一責任の原則)
単一のアクター(ユーザー・クラス・データベース・etc...)に対してだけ機能を提供できるように分割する
変更する理由が同じものは集める、変更する理由が違うものは分ける。
参考: 単一責任原則
// NG: 飲み物・乗り物・書類形式が増えるたびに変更する
class Operator {
func serveTea() -> String { "Tea" }
func serveMilk() -> String { "Milk" }
func driveCar() {}
func printPDF() {}
}
// OK: 変更する理由が飲み物が増える時だけ
class DrinkServer {
func serveTea() -> String { "Tea" }
func serveMilk() -> String { "Milk" }
}
class Driver {
func driveCar() {}
}
class Printer {
func printPDF() {}
}
O: Open/Closed Principle(オープン/クロースドの原則)
拡張に対してオープン、変更に対してクローズド
主に機能追加時には、クラスの内部実装を変更するのではなく拡張する
// NG: 変更にオープン
class NGClass {
func print(type: SomeType) -> String {
switch type {
case aType: "a"
case bType: "b"
case cType: "c"
}
}
}
// OK: 拡張にオープン
protocol printable {
func print() -> String
}
// OK: 変更にクローズド
class AClass: printable {
func print() -> String { "a" }
}
class BClass: printable {
func print() -> String { "b" }
}
class CClass: printable {
func print() -> String { "c" }
}
L: Liskov Substitution Principle(リスコフの置換原則)
型 T と 派生型 S がある時、プログラム内で T 型のオブジェクトが使われている箇所は全て S 型のオブジェクトで置換可能
protocol T {
func call()
}
struct S: T {
func call()
}
class C {
func function(t: T) { t.call() }
}
// OK: 置換可能であること
class C {
func function(t: S) { t.call() }
}
T 型のオブジェクトが使われている箇所では T の振る舞いだけを許可する
protocol T {
func call()
}
struct S: T {
func call()
func aCall()
}
// NG: T の宣言箇所で S 固有のメソッドを使ってはいけない
class C {
func function(t: T) { (t as S).aCall() }
}
I: Interface Segregation Principle(インターフェース分離の原則)
インターフェースの利用者(実装クラス)にとって不要なメソッドに依存させてはいけない
protocol DrinkServerable {
func serveTea()
func serveMilk()
}
// NG: 不要なメソッドを実装させている
class TeaServer: DrinkServerable {
func serveTea() -> String { "Tea" }
func serveMilk() -> String { "" }
}
protocol TeaServerable {
func serveTea()
}
protocol MilkServerable {
func serveMilk()
}
// OK: 必要なメソッドのみを実装させる
class TeaServer: TeaServerable {
func serveTea() -> String { "Tea" }
}
D: Dependency Inversion Principle(依存性逆転の原則)
依存対象の実装を知るのではなく、抽象に依存する
// NG: Some の実装に依存している
class C {
func call() {
let some = Some()
some.aCall()
}
}
protocol Someable {
func aCall()
}
// OK: 抽象に依存している
class C {
func call(someImpl: Someable) {
someImpl.aCall()
}
}
オブジェクト指向プログラミング
クラスはデータとデータを扱うメソッドをもつ
カプセル化
class User {
private let firstName: String
private let lastName: String
var fullName: String { lastName + firstName }
init(first: String, last: String) {...}
}
let user = User("Aaa", "Bbb")
user.firstName = "Ccc" // NG: 外部からデータを変更させない
user.fullName
継承
class Parent {
func 暗黙的なメソッド() {}
}
class Chilld: Parent {
func child固有メソッド() {}
}
class Brother: Parent {}
let child = Chilld()
chlid.暗黙的なメソッド()
child.child固有メソッド()
let bro = Brother()
bro.暗黙的なメソッド()
多態性(ポリモーフィズム)
アドホック多相
class C {
func function(args: String)
func function(args: Int)
}
C().function(args: "Hello")
C().function(args: 1000)
パラメータ多相
class C {
func function<T>(args: T) {}
}
C().function(args: "Hello")
C().function(args: 1000)