こんにちは、@shogunzozoです。
これまで書いてきた記事は知識ベースな物でした。目的として、自身の知識の確認&定着という事があります。
また、初心者の方がみても分かりやすいと思って頂く事に重点を置いていたのでベーシック感は否めませんした。
今回は少し踏み込みます。
ネイティブやWEBに関係なく実装時は綺麗なコードを書きたいですよね。
そこで、綺麗なコードを具体的に説明できるかと自問自答しました。
最近、上司の方から腑に落ちるアドバイスを頂けたので皆さんにも共有したいと思い残す事にしました!!
今回はテーマにもある通り、綺麗なコードとは何か??について現状の自分の考えを以下に書いていきます。
何か間違えや、不自然な記述等あればご指摘いただけると幸いです!!
SOLIDの原則
初めに皆さんSOLIDの原則は聞いた事はありますか??
こちらの記事に詳しく書いてあるので参考にされても良いかもしれません。
以下簡単に説明していきます。とっつきにくい方は以下を参考にして頂ければ、現状は大丈夫であると思います。
S:SRP、単一責任の原則
O:OCP、解放閉鎖の原則
L:LSP、リスコフの置換原則
I:ISP、インタフェース分離の原則
D:DIP、依存性逆転の原則
単一責任原則とは??
あるClassがあった時に、そのClassが何をするためのClassなのか明確に分けなくてはいけません。
例えば、TableViewで使うCellに関するClassがあるとします。このClassの目的はCellの生成です。
こういった際は、Cellの生成に関する事のみを責務とする必要があります。
極端な話ですが、通信に関する処理をベタ書きする事は原則に反するという事です。その際は、他でも通信に関する処理を再利用などする可能性も考慮して別に専用ファイルを作りましょう。
解放閉鎖の原則
あるClassに対して何かロジック(処理)を加えるとします。
この時考えなくてはいけないのが以下2点です。
- そのロジックを追加する事によりClassの意味は変わってこないか??
- そのクラスを継承させていた場合その追加ロジックは必要なものか??
もし、上記2点を考えて不必要なロジックであれば継承先に意味のない処理を書く必要が生じるかもしれません。
こういったケースでは、extentionなどを使い拡張させましょう。それにより、変更に柔軟かつ強い一貫性のあるコードになります。
リスコフ置換の原則
個人的にはこの原則の理解がすっきりとしていませんが、以下の様に解釈しました。
例えば、Aという親クラス、Bというサブクラスがあったとします。
リスコフ置換の原則を見ると、基本クラスをサブクラスに入れ替えても問題なく動かなけらばならないとあります。
最初は??と思いましたが
①サブクラスの入力のパターンは、基本クラスの入力のパターンよりも緩い
②サブクラスの出力のパターンは、基本クラスの入力のパターンよりも厳しい
①②を考えて見ると少しは理解しやすくやすくなるのではないでしょうか??
サブクラスで何かしらの値を入力する時に、基本クラスよりも厳しいとどうなるでしょうか??
サブクラスを利用する度に、サブクラスが許容する値なのかどうか確認する必要があります。考えただけでも使いづらいですね...
一方、サブクラスの出力条件が基本クラスよりも緩い場合はどうでしょうか??
サブクラスを利用する際に想定外の出力をしないかどうか都度気にする必要があります。同様、都度考えるのは厳しいものがあります。
そこで、これらの不を解決すべくリスコフ置換の原則があるわけです。
インタフェース分離の原則
こちらは個人的にSOLIDの中で一番分かりやすいかなと思います。
上記で紹介させて頂いたこちらの記事にある例がわかりやすと思います。
原則が
顧客に特化した細粒度のインタフェースを作れ
顧客は自分たちが使わないインターフェースに依存することを強いられるべきではない
とあるので、リンク先もinterfaceを用いています。
ここで言いたい事は、無闇やたら一つのいinterfaceに書かずに機能毎に切り分けるという事だと思います。
もし、interfaceをimplementする時の様に不必要な処理を全てで書く必要があるとどうでしょうか??
変更に弱くなる上に、呼び出し元の定義している関数・クラスの責務とも乖離する事も起きるのではないでしょうか??
そのため、何か定義をする時は以下を心がけましょう。
今定義している関数・クラス・structなどは不必要な物を含んでいないか
ここに関しては後に例をあげて説明します。
依存性逆転の原則
実はここの考え方が、本Qiitaのテーマのコア部分になっていると自身では考えております。
コードの例などは上記リンクを参考にして下さい。
自身の場合は最初リンク先の同項目を見た際に直ぐ理解が出来無かったので、同じ様な状況の方向けに噛み砕いて説明します。
例えば、関数Aがあったとします。また関数A内で関数Bを直接読み込んでいるとします。
この時、もし関数Bに大きなバグが見つかり変更差分も大きいとします。もちろん、関数Aも調査範囲になりますよね。
問題は、関数Aも調査範囲になるという事です。
こう言った二次被害を防ぐために、関数Aから関数Bを直接見るのではなく何かしらのインターフェースを作ります。
そして、関数Aはそのインターフェースを見る様にする。
これが、依存性逆転の原則で言いたい事だと解釈しています。
SOLIDの原則をもとにコードの書き方を考えてみよう
前置きが長くなってしまいましたがここからが本題です。
SOLIDの原則がある程度腹落ちしている事が前提になりますが、私が導き出した綺麗のコードの書き方は以下2点です。
①依存性を考えて不必要なコードを書かない・継承しない。
②他でも再利用できる可能性があるものは切り出す。
これらについて下記で説明していきます!!
依存性を考えて不必要なコードを書かない・継承しない
以下のコードをご覧になって何か違和感を感じるでしょうか??
本来型定義はファイルを分けますが、説明の都合上ご了承下さい。
struct Example {
var text: String
var message: String
var number: Int
var lastName: String
var familyName: String
}
class ExampleTableViewCell: UITableViewCell {
@IBOutlet private var textLabel: UILabel!
@IBOutlet private var messageLabel: UILabel!
@IBOutlet private var numberLabel: UILabel!
func configure(withExample example: Example) {
}
}
ExampleClassで使用する必要のない要素まで、Exampleは持っている事がわかります。
これは必要でない要素まで直接的に継承しているので、依存度の観点から好ましくありません。
更にいうと、もしExample型がサーバーからデータを受け取る際の型であればクライアント側で利用する事も責務の観点から好ましくないでしょう。
それでは、この際どうすれば良いか??
classで状態を保持するstateを作ってあげて、必要なものだけをこのcellを生成するclassに保持させます。
コードのイメージは以下でしょうか??
struct Example {
var text: String
var message: String
var number: Int
var lastName: String
var familyName: String
}
class ExampleTableViewCell: UITableViewCell {
@IBOutlet private var textLabel: UILabel!
@IBOutlet private var messageLabel: UILabel!
@IBOutlet private var numberLabel: UILabel!
func configure(withExample example: ExampleTableViewCellState) {
}
}
extension ExampleTableViewCell {
struct ExampleTableViewCellState {
.init(withExample example: Example) {
self.textLabel = example.text
self.messageLabel = example.message
self.numberLabel = example.number
}
}
}
あくまで、目的は必要な物を必要なだけ持ってくる事です。
他でも再利用できる可能性があるものは切り出す
ここは上記のコードから想像して頂きたいのですが、ExampleTableViewCellに何か型を変換するための初期化処理があるとします。
それを他クラスから使うとなった場合、当然ExampleTableViewCellを見に来ます。
もう皆さん、違和感を感じたのではないでしょうか??
そうです、ExampleTableViewCellを見にくるクラスでは型を変換するための初期化処理を利用したいがためにExampleTableViewCellと依存関係ができてしまいました。
SOLIDの原則を学んだ方は違和感を感じる書き方ですね。
つまり、他でも使えそうな処理はextensionとして他に切り出しましょう。
まとめ
これまで例を挙げながら書いてきましたが、自身もレビューなどで指摘される毎日です。
しかし、変更のし易さや依存性を考える視点を持ちながらコードを書く事は自身のコーディング力向上というメリットを超えた大きなものがあると思っています。
それは
・可読性が上がる事による開発効率の上昇
・コードの保守性が上がる事から、プロダクトクオリティーの向上(不要なバグを回避できるという技術的な視点から)
の2点です。
いい事ずくしだと思うので、是非コードを書く際は考えながら書きたいと思います。