【翻訳】10 Tricks To Avoid Spaghetti Code in iOS Development

2023/07/26に公開

プロダクトオーナーやデベロッパーとして、私たちは優れたデザインと完全な機能を備えた素晴らしいアプリを作りたいと考えています。しかし、高度なUIデザインとフローを持つ複雑なアプリは、時として保守が非常に困難な悲惨なコードを書くことになりかねません。

スパゲッティ・コード(Spaghetti Code)は、開発者の間ではよく知られた言葉です。これは、構造化されておらず、理解や保守が困難なプロジェクトのことを指します。ほとんどのスパゲッティ・コード・プロジェクトは、関数の冗長性や一貫性のない命名など、ソフトウェア開発における不適切な慣行が原因となっています。

開発者の中には、短いタイムライン、多数の機能リクエスト、開発者のバックグラウンドから、これは避けられないことだと主張する人もいるかもしれません。しかし、優れた開発者である以上、タスクを達成するために、より良い方法を常に追求すべきなのです。

この記事では、コードをよりすっきりさせ、メンテナンスしやすくするために使える方法について説明します。

"コードが動くだけでは十分ではない"- ロバート・C・マーティン『クリーン・コードアジャイルソフトウェアクラフトマンシップのハンドブック

なぜクリーン・コードなのか?

コードをクリーンに保つべき主な理由は3つあります。

1.バグを追跡しやすい

完全に堅牢なプロジェクトを開発するのは難しいですが、追跡不可能なバグを解決するのはもっと難しいです。それは全ての開発者にとって悪夢となりえます。しかし、よく書かれたコードであれば、発生したバグやエラーを素早く追跡し、修正することができるはずです。

2.拡張性

どんな製品やアプリにも、将来の機能追加のためのロードマップがあります。新しく作成された iOS プロジェクトは、頻繁に大きな変更を加えることなく、新しいモジュールを追加して拡張できるよう、十分に準備されている必要があります。

3.理解しやすく、引き渡しやすい

醜いコードを他の開発者が綺麗にするために残してはいけません。読みやすく、できるだけシンプルにする責任があります。自分だけでなく、他の開発者にとっても理解しやすいものでなければなりません。

スパゲッティ・コードを避けるには

本題に戻りましょう。これを実現する方法は数多くありますが、この記事では iOS 開発における10のポイントだけを取り上げます。

1.よく計画し、より良いコードを書く

開発者として、簡単だとわかっていても計画段階をスキップしてはいけません。アプリが大きくなり、より多くの機能を持つようになることを念頭に置くべきです。

簡単なリサーチを終えたら、特にプロジェクトが複雑で経験があまりない場合は、常に先輩や友人、経験豊富な開発者からアドバイスを受けること。実際、時間とお金の節約になります。

2.コードの可読性

他人が理解できる1000行のコードを書くことは、開発者にとって重要なスキルです。以下は、コードを書く際に採用できるいくつかの方法です。

1. Swift のドキュメントで言及されているように、命名のための適切な設計ガイドラインに従います。

変数名とメソッド名は、それらを表すものでなければなりません。

// Improper
var string = "Hello World!"
let usr = User()
var list = [User]()
users.forEach { x in
    //
}

func move(x: CGPoint, y: CGPoint) {
    //
}

// Proper naming
var greetings = "Hello World!"
let user = User()
var users = [User]()
users.forEach { user in
    //
}

func move(start: CGPoint, end: CGPoint) {
    //
}

move(start: CGPoint(x: 0, y: 0), end: CGPoint(x: 100, y: 100))

2. より良い変数名とメソッド名のための文法チェッカーを有効にします。 Edit Format > Spelling and Grammar

3. プロジェクト全体で同じスタイルと記述ルールを強制するために SwiftLint を使います。

4. コメントは開発者がコードを素早く学ぶために重要です。適切なガイドラインはこちらをチェックしてください。また、最近の WWDC21 で、 Apple は DocC と呼ばれる新しい Xcode ドキュメントツールを紹介しました。

5. コードの書き方に一貫性を持たせます。例えば、全ての view controller が画面遷移に segue を使っている場合、他の view controller は同じアプローチに従うべきで、 pushViewController() を使うべきではありません。このように、画面遷移に関する問題は、 segue 呼び出しと prepareForSegue を見るだけで簡単にデバッグできます。

3.変更可能なグローバル変数は避ける

グローバル変数を使うのは、変数の状態を簡単に変更する最も一般的な方法で、プロジェクト内のどこからでも値を設定したり読み取ったりできます。この方法の問題点は、現在の値がどこから更新されているのかを追跡するのが難しく、テストが難しいことです。

変数を view controller の変数として渡すことで、リスクを最小限に抑えましょう。
Singleton パターンを使って、適切な方法で状態を保持しましょう。

4.UserDefault の適切な管理

UserDefault は、様々なデータ型で永続的なデータを保持するのに非常に便利です。

・ まず、 enum や SwiftyUserDefaults のようなサードパーティライブラリを使うことで、ハードコードされたキーを避けましょう。

・ 追跡されない値の変更(グローバル変数と同様の問題)を避けます。値の更新を管理するヘルパー・クラスを作成します。したがって、プロジェクト内のあらゆる場所で値を変更するのではなく、ワンストップのクラスを使ってのみ全ての値を変更できるようになります。

5.再利用性

物事をシンプルに保ち、できるだけコードを書かないようにします。冗長なコードを避けるために、エクステンション、サブクラス化、ヘルパー・クラスを使うことができます。

例えば、複数の ViewController で使われている背景スタイルを変更するコードが数行あるとします。最も単純な解決策は、 UIViewController 拡張モジュールに新しい関数を追加することです。将来的にスタイルを変更する必要が生じた場合は、その拡張モジュール内の関数を更新すればよいのです。

簡単でしょう?私のお勧めは、将来の柔軟性のためにコードを適切に構造化することにもっと時間をかけることです。

加えて、私は常に UIViewController を複数の UIView コンポーネントに分割してシンプルに保つようにしています。このプロセスを説明するチュートリアルを作りました。

6.コールバック地獄を避ける

ネストされた複数のコールバック関数を適切に呼び出すには、いくつかの方法があります。

・ コールバックデータをきれいに読み込むために Result 型 を使う。

・ 最近 WWDC21 で発表されたように、 Apple は Swift 5.5 で async/await 機能を導入しました。それ以外にも、 PromiseKit や Then のような promise ライブラリを使うこともできます。素晴らしい!

firstly {
    authenticate()
}.then {
    when(fulfilled: fetchUser, sendStatistics, updateUserData)
}.done { user, context in
    refreshUI()
}.ensure {
    UIApplication.shared.isNetworkActivityIndicatorVisible = false
}.catch { error in
    self.show(UIAlertController(for: error), sender: self)
}

上記以外の簡単な方法(ただし少し面倒)は、コールバック・メソッドの呼び出しとデータ処理を同時に行わないようにすることです。その代わりに、メソッド呼び出しと処理を分けるようにします。下の例の2つのコードを見比べてください。

func loadData() {
    APIService.authenticate { result in
        switch result {
        case .success(let data):
            // ..
            // do something
            APIService.fetchUser { result in
                // ..
                // process result
                APIService.sendStatistics (userId: user.id) { result in
                    // process result
                    // ..
                    APIService.updateUserData(userId: user.id) { result in
                        // process result
                    }
                }
            }
        case .failure(let error):
            print(error)
        }
    }
}
func loadData() {
    APIService.authenticate { result in
        switch result {
        case .success(let data):
            // ..
            // do something
            fetchUser()
        case .failure(let error):
            print(error)
        }
    }
}

func fetchUser() {
    APIService.fetchUser { result in
        // ..
        // process result
        sendStats()
    }
}

func sendStats() {
    APIService.sendStatistics (userId: user.id) { result in
        // process result
        APIService.updateUserData(userId: user.id) { result in
            // process result
        }
    }
}

7.常にCodableを使う

Swift の標準ライブラリである Codable は、 WWDC17 で導入され、 JSON データを自動的に解析し、エンコードおよびデコード可能なプロトコルを使用してエンコードすることで、開発者を可能にします。

JSON データのプロパティを読み取るために、 [String: Any] や Dictionary 型は絶対に使わないでください。コードが複雑になり、バックエンドが新しいプロパティや構造を更新したときに更新しにくくなります。 decodable を使用することで、 JSON データを定義されたオブジェクトモデルにマッピングするためのサードパーティライブラリが不要になります。詳しくは、こちらの記事を参照してください。

/* Sample JSON data
{
  "id": "A000012444",
  "name" : "John Doe",
  "email": "john@example.my"
  ...
  ...
}
*/

// Define Model based on JSON data
struct User: Decodable {
    let id: String
    let name: String
    let email: String?
}

// Decoding
do {
    let data = try JSONSerialization.data(jsonData: , options: [])
    let decoder = JSONDecoder()
    let user = try decoder.decode(User.self, from: data)
    print(user)
} catch {
    print(error.localizedDescription)
}

8.アクセスコントロール

全ての人がアクセス・コントロールを使うわけではないことは理解しています。しかし、アクセス・コントロールを使うメリットを考えれば、より良いコードを書くことができます。実際、コンパイル時間やアプリのパフォーマンスが向上します。

例えば、ある変数に private ・アクセスを指定した場合、他のクラスはその変数にアクセスできません。同じクラス内でのみ設定できます。その結果、外部から更新する必要がある場合、その変数に対して特定の更新を行う public 関数を書くことができます。主な目的は、変数の変更を追跡して、エラーやバグを簡単に特定できるようにすることです。

また、同じクラス内でしかアクセスできない場合は、すべての関数を private にする必要があるかもしれません。以下に簡単な例を示します。

class HeaderView: UIView {
    private lazy var titleLabel = UILabel()
    private lazy var subtitleLabel = UILabel()
    
    private var post = Post() {
        didSet {
            titleLabel.text = post.title
            subtitleLabel.text = post.subtitle
        }
    }
    
    // ...
    // ...
    
    func updatePost(_ post: Post) {
        self.post = post
    }
}

class DemoViewController: UIViewController {
    let post = Post()
    
    lazy var headerView = HeaderView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // after fetch data
        headerView.updatePost(post)
    }
}

この例では、 HeaderView クラスは updatePost() 関数以外の全ての変数を非表示にしています。これは、 post オブジェクトをその関数からしか設定できないようにするための良い習慣です。

他の有用な例は、クラスが他のサブクラスとして使用されない(継承されない)ことを確認する場合、クラスに final を使用することです。

9.依存性注入

依存性注入(DI)は、前項で説明したアクセス制御機能を補完するものです。 DI は複雑なパターンのように見えますが、そうではありません。単に、オブジェクトの生成時に依存オブジェクトの受け渡しを強制するパターンです。

"依存性注入 "とは、オブジェクトにインスタンス変数を与えることです。本当にそうです。それだけです。「ジェームズ・ショア」

以下の単純な例では、初期化メソッドで Profile オブジェクトが渡されない限り、 SettingViewController のインスタンスが生成されないため、エラーが発生します。超クールですね。

import UIKit

class DemoViewController: UIViewController {
    private let userProfile = Profile()
    // ...
    // ...
    private func showSettingPage() {
        // inject the profile into Setting VC
        let settingVC = SettingViewController(profile: userProfile)
        self.navigationController?.pushViewController(settingVC, animated: true)
    }
}

class SettingViewController: UIViewController {
    private var profile: Profile!
    
    // Profile needed
    init(profile: Profile) {
        self.profile = profile
        super.init(nibName: nil, bundle: nil)
    }
    
    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // ...
    // ...
}

10.ピアコードレビュー

優れた開発チームは、本番環境に送る前に良いコードを書くことを確実にするために、ワークフローにこのプロセスを含めるべきです。これは、完成したソースコードが承認され、メインの作業用ソースコードにマージされる前に、開発者がお互いにレビューし、フィードバックを提供するプロセスです。

今日では、誰もが GitHub や Bitbucket などのソース管理システムを利用しています。そのため、プル・リクエスト(PR)という言葉には慣れているはずです。モジュールを完成させた開発者は、(別のブランチにある)プライマリ/メインブランチに PR を提出します。そして、そのコードを他の開発者がレビューします。レビュアーが承認すれば、そのコードは対象のブランチに直接マージされます。レビュアーは、改善のためのフィードバックを提供することで承認を保留することもできます。

最近、アップルは WWDC21 で Xcode 13 のコラボレーションツールの強化を発表し、開発者が Xcode 内でインタラクティブなコードレビューやプルリクエストを行えるようにしました。

正直なところ、私は新しいプロジェクトや会社に参加しようとするとき、良いチームで働くことで自分の開発スキルを向上させる機会を増やすために、ワークフローにコードレビューのプロセスがあるかどうかを尋ねることにしています。

次は?

素晴らしいことに、コードをクリーンで保守しやすい状態に保つための重要なキーポイントを全て網羅しました。あなたのプロジェクトにこの10項目を少しずつ実装してみて、開発フローと開発時間がどのように改善されるかを見てみましょう。

例えば、クリーンなフレームワークを採用する、デザインパターン(Singleton、Coordinatorなど)を実装する、アーキテクチャパターン(MVVM、VIPERなど)を使用する、ユニットテストを追加する、などです。

ただし、プロジェクトが複雑な構造になって理解しづらくなるような過剰なエンジニアリングは避けるべきだということを覚えておいてほしい。

この記事があなたの開発スキルの向上に役立つことを願っています。シェアしてください。フィードバックはとてもありがたい。楽しいコーディングを!

参考記事

https://swift.org/documentation/api-design-guidelines/#naming

https://github.com/realm/SwiftLint

https://levelup.gitconnected.com/alternatives-to-global-variables-34982becfcc

https://developer.apple.com/documentation/dispatch/dispatch_group

https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html

https://developer.apple.com/videos/play/wwdc2021/10132/

https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html

https://medium.com/@roykronenfeld/semaphores-in-swift-e296ea80f860

https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests

https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52

【翻訳元の記事】

10 Tricks To Avoid Spaghetti Code in iOS Development
https://medium.com/better-programming/10-tricks-to-avoid-spaghetti-code-in-ios-development-3f5a0ab2f46f

Discussion