I want to understand basic, abstract and correct architectural approach for networking applications in iOS: ไม่มี "ดีที่สุด" หรือ "วิธีที่ถูกที่สุด" สำหรับการสร้างสถาปัตยกรรมแอปพลิเคชัน มันเป็นงานที่สร้างสรรค์มาก คุณควรเลือกสถาปัตยกรรมที่ตรงไปตรงมาที่สุดและขยายได้ซึ่งจะชัดเจนสำหรับนักพัฒนาใด ๆ ที่เริ่มทำงานในโครงการของคุณหรือสำหรับนักพัฒนาอื่น ๆ ในทีมของคุณ แต่ฉันยอมรับว่าอาจมี "ดี" และ "ไม่ดี" "สถาปัตยกรรม
คุณพูดว่า: collect the most interesting approaches from experienced iOS developersฉันไม่คิดว่าวิธีการของฉันน่าสนใจที่สุดหรือถูกต้อง แต่ฉันใช้มันในหลาย ๆ โครงการและพอใจกับมัน มันเป็นวิธีการผสมผสานของสิ่งที่คุณได้กล่าวถึงข้างต้นและยังมีการปรับปรุงจากความพยายามในการวิจัยของฉันเอง ฉันน่าสนใจในปัญหาของวิธีการสร้างซึ่งรวมหลายรูปแบบที่รู้จักกันดีและสำนวน ฉันคิดว่ารูปแบบองค์กรจำนวนมากของFowlerสามารถนำไปใช้กับแอปพลิเคชันมือถือได้สำเร็จ นี่คือรายการของสิ่งที่น่าสนใจที่สุดซึ่งเราสามารถนำไปใช้สำหรับการสร้างสถาปัตยกรรมแอปพลิเคชั่น iOS ( ในความคิดของฉัน ): Service Layer , Unit of Work , Remote Facade , Data Transfer Object ,เกตเวย์ , ชั้น supertype , คดีพิเศษ , Domain รุ่น คุณควรออกแบบเลเยอร์โมเดลอย่างถูกต้องและอย่าลืมเกี่ยวกับการคงอยู่ (ซึ่งสามารถเพิ่มประสิทธิภาพของแอพได้อย่างมาก) คุณสามารถใช้Core Dataสำหรับสิ่งนี้ แต่คุณไม่ควรลืมนั่นCore Dataไม่ใช่ ORM หรือฐานข้อมูล แต่เป็นตัวจัดการกราฟวัตถุที่มีอยู่เป็นตัวเลือกที่ดี ดังนั้นบ่อยครั้งมากที่จะCore Dataหนักเกินไปสำหรับความต้องการของคุณและคุณสามารถดูวิธีแก้ปัญหาใหม่ ๆ เช่นRealmและCouchbase Liteหรือสร้างการแมปวัตถุที่มีน้ำหนักเบา / เลเยอร์การติดตาของคุณขึ้นอยู่กับ SQLite หรือLevelDB ดิบ. นอกจากนี้ผมแนะนำให้คุณทำความคุ้นเคยกับการออกแบบขับเคลื่อนโดเมนและCQRS
ตอนแรกฉันคิดว่าเราควรสร้างเลเยอร์ใหม่สำหรับการสร้างเครือข่ายเพราะเราไม่ต้องการตัวควบคุมไขมันหรือโมเดลที่หนักหนาสาหัส ฉันไม่เชื่อในfat model, skinny controllerสิ่งเหล่านั้น แต่ฉันเชื่อในskinny everythingวิธีการเพราะชั้นเรียนไม่ควรอ้วนเลยทีเดียว เครือข่ายทั้งหมดสามารถแยกออกเป็นตรรกะทางธุรกิจโดยทั่วไปดังนั้นเราควรมีอีกชั้นหนึ่งที่เราสามารถวางไว้ได้ Service Layerคือสิ่งที่เราต้องการ:
It encapsulates the application's business logic, controlling transactions
and coordinating responses in the implementation of its operations.
ในMVCดินแดนของเราService Layerนั้นเป็นเหมือนคนกลางระหว่างโมเดลโดเมนและคอนโทรลเลอร์ มีความแตกต่างที่คล้ายกันของวิธีการนี้เรียกว่าMVCSโดยที่ a Storeเป็นServiceชั้นของเรา Storeอินสแตนซ์ของแบบจำลองและจัดการกับเครือข่ายแคช ฯลฯ ฉันต้องการพูดถึงว่าคุณไม่ควรเขียนเครือข่ายและตรรกะทางธุรกิจทั้งหมดของคุณในเลเยอร์บริการของคุณ นี่ก็ถือได้ว่าเป็นการออกแบบที่ไม่ดี สำหรับข้อมูลเพิ่มเติมดูที่โมเดลโดเมนAnemicและRich วิธีการบริการบางอย่างและตรรกะทางธุรกิจสามารถจัดการได้ในรูปแบบดังนั้นมันจะเป็นรูปแบบ "รวย" (พร้อมพฤติกรรม)
ฉันมักจะใช้อย่างกว้างขวางสองห้องสมุด: AFNetworking 2.0และReactiveCocoa ฉันคิดว่ามันเป็นสิ่งที่จำเป็นสำหรับแอพพลิเคชั่นที่ทันสมัยใด ๆ ที่โต้ตอบกับเครือข่ายและบริการบนเว็บหรือมีตรรกะ UI ที่ซับซ้อน
สถาปัตยกรรม
ตอนแรกผมสร้างทั่วไปAPIClientชั้นซึ่งเป็น subclass ของAFHTTPSessionManager นี่คือข้อผิดพลาดของการเชื่อมต่อเครือข่ายทั้งหมดในแอปพลิเคชัน: คลาสบริการทั้งหมดมอบหมายการร้องขอ REST จริงให้กับมัน มันมีการปรับแต่งทั้งหมดของไคลเอนต์ HTTP ซึ่งฉันต้องการในแอปพลิเคชันเฉพาะ: การปักหมุด SSL, การประมวลผลข้อผิดพลาดและการสร้างNSErrorวัตถุที่ตรงไปตรงมาด้วยเหตุผลความล้มเหลวโดยละเอียดและคำอธิบายของAPIข้อผิดพลาดทั้งหมดและการเชื่อมต่อ ผู้ใช้), การตั้งค่าคำขอและการตอบสนอง serializers, ส่วนหัว http และสิ่งอื่น ๆ ที่เกี่ยวข้องกับเครือข่าย แล้วฉันมีเหตุผลแบ่งทั้งหมดคำขอ API เข้า subservices หรือมากกว่าอย่างถูกต้องmicroservices : UserSerivces, CommonServices, SecurityServices,FriendsServicesและตามตรรกะทางธุรกิจที่ใช้ microservices เหล่านี้แต่ละคลาสแยกกัน Service Layerพวกเขาร่วมกันในรูปแบบ คลาสเหล่านี้มีเมธอดสำหรับแต่ละคำขอ API ประมวลผลโมเดลโดเมนและส่งคืน a RACSignalด้วยโมเดลการตอบกลับที่แจงหรือNSErrorไปยังผู้เรียกเสมอ
ฉันต้องการพูดถึงว่าถ้าคุณมีตรรกะการทำให้เป็นอันดับแบบซับซ้อน - แล้วสร้างอีกชั้นสำหรับมัน: สิ่งที่ชอบData Mapperแต่ทั่วไปมากขึ้นเช่น JSON / XML -> mapper แบบจำลอง หากคุณมีแคช: สร้างเป็นเลเยอร์ / บริการแยกต่างหากด้วย (คุณไม่ควรใช้ตรรกะทางธุรกิจกับการแคช) ทำไม? เพราะชั้นแคชที่ถูกต้องนั้นค่อนข้างซับซ้อนด้วย gotchas ของตัวเอง ผู้คนใช้ตรรกะที่ซับซ้อนเพื่อให้ได้การแคชที่ถูกต้องและคาดการณ์ได้เช่นการแคชแบบ monoidal ที่มีการคาดการณ์โดยอิงจากผู้ชำนาญการ คุณสามารถอ่านเกี่ยวกับห้องสมุดที่สวยงามแห่งนี้ชื่อCarlosเพื่อทำความเข้าใจเพิ่มเติม และอย่าลืมว่า Core Data สามารถช่วยคุณในการแคชปัญหาทั้งหมดและจะช่วยให้คุณเขียนตรรกะน้อยลง นอกจากนี้หากคุณมีตรรกะบางอย่างระหว่างNSManagedObjectContextและเซิร์ฟเวอร์ขอรุ่นคุณสามารถใช้รูปแบบพื้นที่เก็บข้อมูลซึ่งแยกตรรกะที่ดึงข้อมูลและแมปไปยังรูปแบบเอนทิตีจากตรรกะทางธุรกิจที่ทำหน้าที่ในรูปแบบ ดังนั้นฉันแนะนำให้ใช้รูปแบบ Repository แม้ว่าคุณจะมีสถาปัตยกรรมแบบ Core Data ก็ตาม พื้นที่เก็บข้อมูลสามารถสิ่งที่เป็นนามธรรมเช่นNSFetchRequest, NSEntityDescription, NSPredicateและอื่น ๆ เพื่อวิธีการธรรมดาเหมือนหรือ
getput
หลังจากการกระทำเหล่านี้ทั้งหมดในเลเยอร์บริการผู้เรียก (มุมมองตัวควบคุม) สามารถทำสิ่งอะซิงโครนัสที่ซับซ้อนบางอย่างด้วยการตอบสนอง: การจัดการสัญญาณการผูกมัดการแมป ฯลฯ ด้วยความช่วยเหลือของReactiveCocoaดั้งเดิมหรือเพียงแค่สมัครสมาชิก . ผมฉีดกับฉีดอยู่ในทุกชั้นเรียนบริการของฉันAPIClientซึ่งจะแปลบริการโทรเข้าโดยเฉพาะอย่างยิ่งที่เกี่ยวข้องGET, POST, PUT, DELETEฯลฯ การร้องขอไปยังปลายทางที่เหลือ ในกรณีนี้APIClientจะถูกส่งผ่านไปยังตัวควบคุมทั้งหมดโดยปริยายคุณสามารถทำให้สิ่งนี้ชัดเจนด้วยการกำหนดขอบเขตของAPIClientคลาสบริการ วิธีนี้เหมาะสมถ้าคุณต้องการใช้การปรับแต่งที่แตกต่างกันของAPIClientสำหรับคลาสบริการเฉพาะ แต่ถ้าคุณด้วยเหตุผลบางอย่างไม่ต้องการสำเนาเพิ่มเติมหรือคุณแน่ใจว่าคุณจะใช้หนึ่งอินสแตนซ์หนึ่งโดยเฉพาะ (โดยไม่มีการกำหนดเอง) ของAPIClient- ทำให้เป็นซิงเกิล แต่ไม่ต้องการโปรดดอน ไม่ทำให้คลาสบริการเป็นแบบซิงเกิล
จากนั้นแต่ละตัวควบคุมมุมมองอีกครั้งด้วย DI ฉีดคลาสบริการที่ต้องการเรียกวิธีการบริการที่เหมาะสมและรวบรวมผลลัพธ์ของพวกเขาด้วยตรรกะ UI สำหรับฉีดพึ่งพาผมชอบที่จะใช้BloodMagicหรือกรอบการทำงานที่มีประสิทธิภาพมากขึ้นไต้ฝุ่น ฉันไม่เคยใช้ซิงเกิลตันAPIManagerWhateverคลาสพระเจ้าหรือสิ่งผิดปกติอื่น ๆ เพราะถ้าคุณเรียกชั้นเรียนของคุณWhateverManagerนี้บ่งชี้กว่าที่คุณไม่ทราบว่าจุดประสงค์ของมันและมันก็เป็นทางเลือกการออกแบบที่ไม่ดี Singletons ยังเป็นแบบป้องกันและในกรณีส่วนใหญ่ (ยกเว้นของหายาก) เป็นวิธีการแก้ปัญหาที่ผิด ควรพิจารณาซิงเกิลตันเฉพาะเมื่อตรงตามเกณฑ์ทั้งสามข้อต่อไปนี้:
- ไม่สามารถมอบหมายความเป็นเจ้าของอินสแตนซ์เดี่ยวได้อย่างสมเหตุสมผล
- การเริ่มต้น Lazy เป็นที่พึงปรารถนา
- การเข้าถึงทั่วโลกไม่ได้มีไว้สำหรับ
ในกรณีที่เราเป็นเจ้าของอินสแตนซ์เดียวไม่ใช่ปัญหาและเราไม่ต้องการการเข้าถึงทั่วโลกหลังจากที่เราแบ่งผู้จัดการเทพเจ้าของเราเป็นบริการเพราะตอนนี้มีเพียงหนึ่งหรือหลายตัวควบคุมเฉพาะต้องการบริการเฉพาะ (เช่นUserProfileความต้องการควบคุมUserServicesและอื่น ๆ ) .
เราควรเคารพSหลักการในSOLIDเสมอและใช้การแยกข้อกังวลดังนั้นอย่าใช้วิธีการบริการและเครือข่ายทั้งหมดของคุณในชั้นเดียวเพราะมันบ้าโดยเฉพาะอย่างยิ่งถ้าคุณพัฒนาแอปพลิเคชันองค์กรขนาดใหญ่ นั่นเป็นเหตุผลที่เราควรพิจารณาการฉีดพึ่งพาและวิธีการบริการ ผมคิดว่าวิธีการนี้เป็นที่ทันสมัยและมีการโพสต์ OO ในกรณีนี้เราแบ่งแอปพลิเคชันของเราออกเป็นสองส่วน: ตรรกะการควบคุม (ตัวควบคุมและเหตุการณ์) และพารามิเตอร์
พารามิเตอร์ชนิดหนึ่งจะเป็นพารามิเตอร์ "ข้อมูล" ทั่วไป นั่นคือสิ่งที่เราผ่านฟังก์ชั่น, จัดการ, แก้ไข, คงอยู่, ฯลฯ สิ่งเหล่านี้คือเอนทิตี, มวลรวม, คอลเลกชัน, คลาสเคส อีกประเภทหนึ่งคือพารามิเตอร์ "บริการ" เหล่านี้เป็นคลาสที่แค็ปซูลตรรกะทางธุรกิจอนุญาตให้สื่อสารกับระบบภายนอกให้การเข้าถึงข้อมูล
นี่คือขั้นตอนการทำงานทั่วไปของสถาปัตยกรรมของฉันตามตัวอย่าง สมมุติว่าเรามีFriendsViewController, ซึ่งจะแสดงรายการเพื่อนของผู้ใช้และเรามีตัวเลือกให้ลบออกจากเพื่อน ฉันสร้างวิธีการในFriendsServicesชั้นเรียนของฉันที่เรียกว่า:
- (RACSignal *)removeFriend:(Friend * const)friend
โดยที่Friendเป็นโมเดล / โดเมนวัตถุ (หรือเป็นเพียงUserวัตถุหากมีคุณลักษณะที่คล้ายกัน) underhood วิธีนี้จะแยกวิเคราะห์FriendการNSDictionaryของพารามิเตอร์ JSON friend_id, name, surname, friend_request_idและอื่น ๆ ฉันมักจะใช้ห้องสมุดMantleสำหรับหม้อไอน้ำประเภทนี้และสำหรับเลเยอร์โมเดลของฉัน (แยกวิเคราะห์ไปข้างหน้าและไปข้างหน้าการจัดการลำดับชั้นวัตถุที่ซ้อนกันใน JSON และอื่น ๆ ) หลังจากแยกที่เรียกว่าAPIClient DELETEวิธีการที่จะทำการร้องขอ REST ที่เกิดขึ้นจริงและผลตอบแทนResponseในRACSignalการโทร ( FriendsViewControllerในกรณีของเรา) เพื่อแสดงข้อความที่เหมาะสมสำหรับผู้ใช้หรืออะไรก็ตาม
หากใบสมัครของเราใหญ่มากเราต้องแยกตรรกะของเราออกให้ชัดเจนยิ่งขึ้น เช่นมันไม่ดีเสมอไปในการผสมRepositoryหรือจำลองตรรกะกับสิ่งServiceใดสิ่งหนึ่ง เมื่อผมอธิบายวิธีการของฉันฉันได้กล่าวว่าremoveFriendวิธีการที่ควรจะอยู่ในServiceชั้น Repositoryแต่ถ้าเราจะมีความรู้มากขึ้นเราสามารถสังเกตเห็นว่ามันจะดีกว่าที่เป็น จำไว้ว่า Repository คืออะไร Eric Evans ให้คำอธิบายที่ชัดเจนในหนังสือของเขา [DDD]:
พื้นที่เก็บข้อมูลแสดงวัตถุทั้งหมดของบางประเภทเป็นชุดแนวคิด มันทำหน้าที่เหมือนคอลเลกชันยกเว้นด้วยความสามารถในการสอบถามที่ซับซ้อนมากขึ้น
ดังนั้น a Repositoryจึงเป็นส่วนหน้าซึ่งใช้ซีแมนทิกส์สไตล์การรวบรวม (เพิ่มอัปเดตลบ) เพื่อให้การเข้าถึงข้อมูล / วัตถุ นั่นเป็นเหตุผลที่เมื่อคุณมีสิ่งที่ชอบ: getFriendsList, getUserGroups, removeFriendคุณสามารถวางไว้ในRepositoryเพราะคอลเลกชันเหมือนความหมายสวยใสที่นี่ และรหัสเช่น:
- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;
เป็นตรรกะทางธุรกิจอย่างแน่นอนเนื่องจากอยู่นอกเหนือCRUDการดำเนินงานขั้นพื้นฐานและเชื่อมต่อสองวัตถุโดเมน ( FriendและRequest) นั่นคือเหตุผลที่ควรวางไว้ในServiceเลเยอร์ นอกจากนี้ผมต้องการที่จะแจ้งให้ทราบล่วงหน้า: ไม่ได้สร้างแนวคิดที่ไม่จำเป็น ใช้วิธีการเหล่านี้อย่างชาญฉลาด เพราะหากคุณต้องการเอาชนะแอ็พพลิเคชันของคุณด้วย abstractions สิ่งนี้จะเพิ่มความซับซ้อนโดยไม่ตั้งใจและความซับซ้อนทำให้เกิดปัญหาในระบบซอฟต์แวร์มากกว่าสิ่งอื่นใด
ฉันอธิบายคุณเป็นตัวอย่าง "Objective-C" แบบเก่า แต่วิธีนี้สามารถดัดแปลงได้ง่ายสำหรับภาษา Swift พร้อมการปรับปรุงที่มากขึ้นเพราะมันมีคุณสมบัติที่มีประโยชน์และน้ำตาลที่ใช้งานได้ ผมขอแนะนำให้ใช้ห้องสมุดนี้: Moya ช่วยให้คุณสร้างAPIClientเลเยอร์ที่หรูหรามากขึ้น(workhorse ของเราตามที่คุณจำได้) ตอนนี้APIClientผู้ให้บริการของเราจะเป็นประเภทค่า (enum) ที่มีส่วนขยายที่สอดคล้องกับโปรโตคอลและใช้ประโยชน์จากการจับคู่รูปแบบการทำลายล้าง Swift enums + การจับคู่รูปแบบช่วยให้เราสามารถสร้างประเภทข้อมูลพีชคณิตเช่นเดียวกับในการเขียนโปรแกรมการทำงานแบบคลาสสิก microservices ของเราจะใช้APIClientผู้ให้บริการที่ได้รับการปรับปรุงนี้เช่นเดียวกับใน Objective-C สำหรับเลเยอร์โมเดลแทนคุณMantleสามารถใช้ไลบรารี ObjectMapperหรือฉันชอบที่จะใช้ห้องสมุด
Argo ที่หรูหราและใช้งานได้ดีขึ้น
ดังนั้นฉันจึงอธิบายวิธีสถาปัตยกรรมทั่วไปซึ่งสามารถปรับให้เหมาะกับการใช้งานใด ๆ ฉันคิดว่า แน่นอนว่าอาจมีการปรับปรุงเพิ่มเติมมากมาย ฉันแนะนำให้คุณเรียนรู้การเขียนโปรแกรมที่ใช้งานได้เพราะคุณจะได้ประโยชน์จากมันมาก แต่อย่าไปไกลเกินไป การกำจัดสถานะที่ไม่แน่นอนทั่วโลกที่ใช้ร่วมกันมากเกินไปการสร้างรูปแบบโดเมนที่ไม่เปลี่ยนรูปแบบหรือการสร้างฟังก์ชั่นที่บริสุทธิ์โดยไม่มีผลข้างเคียงจากภายนอกคือโดยทั่วไปแล้วแนวปฏิบัติที่ดีและSwiftภาษาใหม่ๆ แต่โปรดจำไว้เสมอว่าการโอเวอร์โหลดโค้ดของคุณด้วยรูปแบบการใช้งานที่บริสุทธิ์อย่างหนักวิธีการตามหมวดหมู่เชิงทฤษฎีเป็นแนวคิดที่ไม่ดีเพราะนักพัฒนารายอื่นจะอ่านและสนับสนุนโค้ดของคุณและพวกเขาอาจหงุดหงิดหรือน่ากลัวprismatic profunctorsและสิ่งต่าง ๆ ในรูปแบบที่ไม่เปลี่ยนรูปของคุณ สิ่งเดียวกันกับReactiveCocoa: อย่าใช้RACifyรหัสของคุณมากเกินไปเพราะมันจะไม่สามารถอ่านได้อย่างรวดเร็วโดยเฉพาะอย่างยิ่งสำหรับมือใหม่ ใช้เมื่อมันสามารถทำให้เป้าหมายและตรรกะของคุณง่ายขึ้น
read a lot, mix, experiment, and try to pick up the best from different architectural approachesดังนั้น มันเป็นคำแนะนำที่ดีที่สุดที่ฉันสามารถให้คุณได้