分裂とルール。Objective-CおよびSwiftでのモジュラーモノリスアプリケーション





こんにちは、Habr!私の名前はVasilyKozlovです。DeliveryClubのiOS技術リーダーであり、プロジェクトはモノリシックな形で見つかりました。私はこの記事が捧げられている戦いに参加したことを告白しますが、私は悔い改め、プロジェクトとともに意識を変えました。



Objective-CとSwiftの既存のプロジェクトを別々のモジュール(フレームワーク)に分割する方法を説明したいと思います。Appleよれば、フレームワークは特定の構造のディレクトリです。



最初に、ユーザーサポートのためにチャット機能を実装するコードを分離し、ビルド時間を短縮するという目標を設定しました。これは、習慣なしでは追跡するのが難しく、1つのプロジェクトのモノリシックな世界に存在する有用な結果につながりました。



突然、悪名高いSOLIDの原則具体化し始め、最も重要なことは、問題の定式化そのものが、それらに従ってコードを編成することを余儀なくされました。エンティティを別のモジュールに移動すると、そのすべての依存関係に自動的に遭遇します。これらの依存関係は、このモジュールには含まれておらず、メインアプリケーションプロジェクトにも複製されています。したがって、共通の機能を備えた追加のモジュールを編成するという問題は熟しています。 1つのエンティティが1つの目的を持つ必要がある場合、これは単一の責任の原則ではありませんか?



2つの言語と大きな遺産を持つプロジェクトをモジュールに分割することの複雑さは、一見怖がらせることができます。これは私に起こりましたが、新しいタスクへの関心が優勢でした。



以前に見つかった記事で、著者は約束しました新しいプロジェクトに典型的なシンプルで明確なステップを備えた雲ひとつない未来。しかし、最初の基本クラスを共通コードのモジュールに移動すると、非常に多くの非自明な依存関係が明らかになり、Xcodeで非常に多くのコード行が赤で覆われていたため、続行したくありませんでした。



プロジェクトには、多くのレガシーコード、Objective-CとSwiftのクラスへの相互依存、iOS開発に関するさまざまなターゲット、CocoaPodsの印象的なリストが含まれていました。このモノリスから少し離れると、プロジェクトがXcodeでのビルドを停止し、最も予期しない場所でエラーが見つかることがありました。



したがって、私はそのようなプロジェクトの所有者の生活を楽にするために私が取った一連の行動を書き留めることにしました。



最初のステップ



それらは明白であり、多くの記事がそれらについて書かれています。 Appleは、それらを可能な限りユーザーフレンドリーにするように努めました。



1.最初のモジュールを作成します。[ファイル]→[新しいプロジェクト]→[CocoaTouch Framework]



2。モジュールをプロジェクトワークスペースに追加します。3











メインプロジェクトのモジュールへの依存関係を作成し、[埋め込みバイナリ]セクションで後者を指定します。プロジェクトに複数のターゲットがある場合、モジュールは、それに依存する各ターゲットの埋め込みバイナリセクションに含まれている必要があります。



私自身からのコメントを1つだけ追加します。急いではいけません。



このモジュールに何が配置されるか、モジュールはどのような基準で分割されるかを知っていますか?私のバージョンでは、UIViewControllerテーブルやセルとチャットするため。チャット付きのCocoapodをモジュールに接続する必要があります。しかし、それは少し異なった結果になりました。チャットのUIViewControllerプレゼンターとセルの両方が、新しいモジュールが何も知らない基本クラスとプロトコルに基づいていたため、チャットの実装を延期する必要がありました



モジュールを強調表示する方法は?最も論理的なアプローチ-「ficham」(機能)、つまり一部のユーザータスク。たとえば、技術サポート、登録/ログイン画面、メイン画面設定の下部シートとチャットします。さらに、ほとんどの場合、機能ではなく、UI要素、基本クラスなどのセットのみである、ある種の基本的な機能が必要になります。この機能は、有名なUtilsファイルと同様の共通モジュールに移動する必要があります..。このモジュールも分割することを恐れないでください。キューブが小さいほど、本館に簡単に収まります。これが、もう1つのSOLID原則を定式化する方法であるように思われます私が使用しなかったモジュールに分割するための既製のヒント



があります。そのため、私は非常に多くのコピーを壊し、苦痛なものについて話すことにしました。しかし、このアプローチ(最初に行動し、次に考える)は、モノリシックプロジェクトの依存コードの恐ろしさに目を開かせました。旅の始めにいるとき、依存関係を排除するために必要となる変更の全量を把握することは困難です。



したがって、クラスをあるモジュールから別のモジュールに移動し、Xcodeで何が赤面しているかを確認し、依存関係を把握してみてください。 Xcode 10には注意が必要です。ファイルへのリンクをあるモジュールから別のモジュールに移動すると、ファイルは同じ場所に残ります。したがって、次のステップは次のようになります...



4.ファイルマネージャでファイルを移動し、Xcodeで古いリンクを削除して、新しいモジュールにファイルを再度追加します。一度に1つのクラスを実行すると、依存関係に巻き込まれないようになります。



モジュールの外部からすべての切り離されたエンティティを利用できるようにするには、SwiftとObjective-Cの特性を考慮する必要があります。



5. Swiftでは、すべてのクラス、列挙、およびプロトコルにアクセス修飾子を付ける必要がありますpublicその後、モジュールの外部からアクセスできます。基本クラスを別のフレームワークに移動する場合は、修飾子openマークする必要があります。そうしないと、基本クラスから子孫クラスを作成できません。



Swiftのアクセスレベルをすぐに覚えて(または初めて学習して)、利益を得る必要があります。







移植されたクラスのアクセスレベルを変更する場合、Xcodeでは、オーバーライドされたすべてのメソッドのアクセスレベルを同じに変更する必要があります。







次に、新しいフレームワークのインポートを、選択した機能が使用されるSwiftファイルといくつかのUIKitに追加する必要があります。その後、Xcodeのエラーが少なくなるはずです。



import UIKit
import FeatureOne
import FeatureTwo

class ViewController: UIViewController {
//..
}


Objective-Cを使用すると、シーケンスはもう少し複雑になります。また、ブリッジヘッダーを使用してObjective-CクラスをSwiftにインポートすることは、フレームワークではサポートされていません。







したがって、フレームワーク設定では、Objective-Cブリッジヘッダーフィールドを空にする必要があります。







この状況から抜け出す方法があり、なぜそうなるのかは別の研究のトピックです。



6.各フレームワークには独自のアンブレラヘッダーファイルがあり、これを介してすべてのパブリックObjective-Cインターフェイスが外の世界を調べます。



このアンブレラヘッダーで他のすべてのヘッダーファイルのインポートを指定すると、それらはSwiftで使用できるようになります。







import UIKit
import FeatureOne
import FeatureTwo

class ViewController: UIViewController {    
    var vc: Obj2ViewController?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }


Objective-Cでは、モジュールの外部のクラスにアクセスするには、その設定を試してみる必要があります。ヘッダーファイルを公開します。







7.すべてのファイルが1つずつ別のモジュールに転送されたら、Cocoapodsを忘れないでください。一部の機能が別のフレームワークで終了する場合は、Podfileを再編成する必要があります。これが私にとってのやり方でした。グラフィカルなインジケーターを備えたポッドを一般的なフレームワークに組み込む必要があり、チャット(新しいポッド)は独自のフレームワークに含まれていました。



プロジェクトが単なるプロジェクトではなく、サブプロジェクトを含むワークスペースであることを明示的に示す必要があります。



workspace 'myFrameworkTest'


フレームワークに共通の依存関係は、たとえば、次のように別々の変数に移動する必要がnetworkPodsありuiPodsます。



def networkPods
     pod 'Alamofire'
end



 def uiPods
     pod 'GoogleMaps'
 end


次に、メインプロジェクトの依存関係を次のように説明します。



target 'myFrameworkTest' do
project 'myFrameworkTest'
    networkPods
    uiPods
    target 'myFrameworkTestTests' do
    end
end 


チャットとのフレームワークの依存関係-このように:



target 'FeatureOne' do
    project 'FeatureOne/FeatureOne'
    uiPods
    pod 'ChatThatMustNotBeNamed'
end


水中の岩



おそらくこれは終了する可能性がありますが、後で私はいくつかの暗黙の問題を発見しました。これについても言及したいと思います。



すべての一般的な依存関係は、1つの別個のフレームワーク、チャットに移動されます-別のフレームワークに移動され、コードは少しクリーンになり、プロジェクトはビルドされますが、起動時にクラッシュします。



最初の問題はチャットの実装にありました。広大なネットワークでは、この問題は他のポッドでも発生します。ライブラリがロードされていません:理由:画像が見つかりません」というグーグルだけです。転倒が起こったのはこのメッセージでした。 より洗練された解決策を見つけることができず、メインアプリケーションでチャットを使用してポッド接続を複製することを余儀なくされました。







target 'myFrameworkTest' do
    project 'myFrameworkTest'
    pod 'ChatThatMustNotBeNamed'
    networkPods
    uiPods
    target 'myFrameworkTestTests' do
    end
end


したがって、Cocoapodsを使用すると、アプリケーションは、起動時およびプロジェクトのコンパイル時に動的にリンクされたライブラリを確認できます。



もう一つの問題は、私が安全に忘れていて、この側面についての言及を覚えておくのを見たことがなかったリソースでした。セルxibファイルを登録しようとするとアプリケーションがクラッシュしました:「NIBをバンドルにロードできませんでした」デフォルトのクラス



コンストラクタは、メインアプリケーションモジュールでリソースを検索します。当然のことながら、モノリシックプロジェクトで開発が行われる場合、これについては何も知りません。 解決策は、リソースクラスが定義されているバンドルを指定するか、クラスコンストラクターを使用してコンパイラーにそれを実行させることです。init(nibName:bundle:)UINib



init(for:)Bundle..。そしてもちろん、これからは、リソースがすべてのモジュールに共通または1つのモジュールに固有になる可能性があることを忘れないでください。



モジュールがxibsを使用する場合、Xcodeは通常どおりボタンを提供しUIImageView、プロジェクト全体からグラフィックリソース選択しますが、実行時に他のモジュールにあるすべてのリソースが読み込まれるわけではありません。init(named:in:compatibleWith:)クラスのコンストラクターを使用してコードに画像をロードしましUIImageた。2番目のパラメーターBundleは画像ファイルの場所です。



細胞内UITableViewUICollectionView今も、同様にして登録する必要があります。また、文字列表現のSwiftクラスにはモジュールの名前も含まれていることを覚えておく必要がありNSClassFromString()、Objective-Cのメソッドnil、なので、文字列ではなくクラスを指定してセルを登録することをお勧めします。UITableView次のヘルパーメソッドを使用することができます。



@objc public extension UITableView {

    func registerClass(_ classType: AnyClass) {
        let bundle = Bundle(for: classType)
        let name = String(describing: classType)
        register(UINib(nibName: name, bundle: bundle), forCellReuseIdentifier: name)
    }
}


結論



各モジュールには独自のxcodeprojファイルがあるため、1つのプルリクエストに異なるモジュールで行われたプロジェクト構造の変更が含まれているかどうかを心配する必要はありません。プロジェクトファイルをまとめるのに数時間を費やす必要がないように、作業を分散できます。大規模で分散したチームにモジュラーアーキテクチャがあると便利です。結果として、開発速度は向上するはずですが、その逆も当てはまります。モノリス内でチャットを作成する場合よりも、最初のモジュールに多くの時間を費やしました。Apple



も指摘している明らかな利点のうち、-コードを再利用する機能。アプリケーションに異なるターゲット(アプリ拡張)がある場合、これが最もアクセスしやすいアプローチです。おそらくチャットは最良の例ではありません。ネットワークレイヤーをレイアウトすることから始めるべきでしたが、正直に言うと、これは非常に長く危険な道路であり、小さなセクションに分割するのが最適です。そして、ここ数年、これは技術サポートを組織するための2番目のサービスの導入であったため、導入せずに実装したいと思いました。3番目がすぐに表示されないという保証はどこにありますか?



モジュールを開発する際の自明でない効果の1つは、より思慮深く、よりクリーンなインターフェースです。開発者は、特定のプロパティとメソッドに外部からアクセスできるようにクラスを設計する必要があります。必然的に、モジュールを再び簡単に使用できるように、何を非表示にし、モジュールをどのように作成するかを考える必要があります。



All Articles