แนวทางสถาปัตยกรรมที่ดีที่สุดสำหรับการสร้างแอปพลิเคชั่นเครือข่าย iOS (ไคลเอนต์ REST)


323

ฉันเป็นนักพัฒนา iOS ที่มีประสบการณ์และคำถามนี้น่าสนใจสำหรับฉันจริงๆ ฉันเห็นแหล่งข้อมูลและวัสดุต่าง ๆ มากมายในหัวข้อนี้ แต่อย่างไรก็ตามฉันก็ยังสับสนอยู่ สถาปัตยกรรมที่ดีที่สุดสำหรับแอปพลิเคชั่นเครือข่าย iOS คืออะไร ฉันหมายถึงกรอบนามธรรมพื้นฐาน, รูปแบบ, ซึ่งจะเหมาะกับทุก ๆ แอปพลิเคชั่นเครือข่ายไม่ว่าจะเป็นแอพเล็ก ๆ , ซึ่งมีเพียงไม่กี่คำขอของเซิร์ฟเวอร์หรือไคลเอนต์ REST ที่ซับซ้อน Apple แนะนำให้ใช้MVCเป็นสถาปัตยกรรมพื้นฐานสำหรับแอปพลิเคชั่น iOS ทุกตัว แต่รูปแบบที่MVCทันสมัยกว่านั้นไม่ได้MVVMอธิบายว่าจะวางรหัสตรรกะเครือข่ายไว้ที่ใดและจะจัดระเบียบอย่างไรโดยทั่วไป

ฉันจำเป็นต้องพัฒนาบางสิ่งเช่นMVCS( SสำหรับService) และในServiceเลเยอร์นี้ใส่APIคำขอทั้งหมดและตรรกะเครือข่ายอื่น ๆ ซึ่งในมุมมองอาจซับซ้อนจริงๆ? หลังจากทำวิจัยฉันพบวิธีพื้นฐานสองประการสำหรับเรื่องนี้ ที่นี่ขอแนะนำให้สร้างคลาสแยกต่างหากสำหรับทุกคำขอเครือข่ายไปยังบริการเว็บAPI(เช่นLoginRequestคลาสหรือPostCommentRequestคลาสเป็นต้น) ซึ่งสืบทอดมาจากคลาสฐานคำขอนามธรรมAbstractBaseRequestและนอกจากนี้เพื่อสร้างผู้จัดการเครือข่ายทั่วโลกบางส่วนซึ่งห่อหุ้มรหัสเครือข่ายทั่วไปและ การตั้งค่าอื่น ๆ (อาจเป็นการAFNetworkingปรับแต่งหรือRestKitการปรับแต่งถ้าเรามีการแมปวัตถุที่ซับซ้อนและการคงอยู่หรือแม้กระทั่งการดำเนินการสื่อสารเครือข่ายของตัวเองด้วย API มาตรฐาน) แต่วิธีนี้ดูเหมือนจะเป็นค่าใช้จ่ายสำหรับฉัน อีกวิธีหนึ่งคือการมีบางเดี่ยวAPIมอบหมายงานหรือระดับผู้จัดการในขณะที่วิธีการแรกแต่ไม่สามารถสร้างชั้นเรียนสำหรับทุกคำขอและแทนที่จะแค็ปซูลคำขอเป็นวิธีการเช่นประชาชนในระดับนี้ผู้จัดการเหมือนทุกfetchContacts, loginUserวิธีการอื่น ๆ ดังนั้นสิ่งที่ เป็นวิธีที่ดีที่สุดและถูกต้อง? มีวิธีการที่น่าสนใจอื่น ๆ ที่ฉันยังไม่รู้หรือไม่?

และฉันควรสร้างเลเยอร์อื่นสำหรับสิ่งที่เกี่ยวกับเครือข่ายทั้งหมดนี้เช่นServiceหรือNetworkProviderเลเยอร์หรืออะไรก็ตามที่อยู่ด้านบนสุดของMVCสถาปัตยกรรมของฉันหรือเลเยอร์นี้ควรรวม (ฉีด) เข้ากับMVCเลเยอร์ที่มีอยู่เช่นModel?

ฉันรู้ว่ามีวิธีการที่สวยงามอยู่แล้วหรืออย่างไรแล้วสัตว์ประหลาดมือถือเช่นลูกค้า Facebook หรือลูกค้า LinkedIn จัดการกับความซับซ้อนที่เพิ่มขึ้นอย่างทวีคูณของตรรกะเครือข่าย?

ฉันรู้ว่าไม่มีคำตอบที่แน่นอนและเป็นทางการกับปัญหา เป้าหมายของคำถามนี้คือการรวบรวมวิธีการที่น่าสนใจมากที่สุดจากนักพัฒนา iOS ที่มีประสบการณ์ วิธีการที่แนะนำที่ดีที่สุดจะถูกทำเครื่องหมายว่าเป็นที่ยอมรับและได้รับรางวัลด้วยชื่อเสียงที่มีชื่อเสียง มันเป็นคำถามเชิงทฤษฎีและการวิจัยเป็นส่วนใหญ่ ฉันต้องการเข้าใจวิธีการพื้นฐานที่เป็นนามธรรมและถูกต้องสำหรับการใช้งานเครือข่ายใน iOS ฉันหวังว่าจะได้รับคำอธิบายโดยละเอียดจากนักพัฒนาที่มีประสบการณ์


14
นี่ไม่ใช่คำถาม "รายการช็อปปิ้ง" ใช่หรือไม่ ฉันเพิ่งมีคำถามลงคะแนนให้นรกและปิดเพราะมันระบุว่า "สิ่งที่ดีที่สุด" คำถามประเภทจุดประกายการอภิปรายที่ไม่มีเหตุผลมากเกินไป อะไรทำให้คำถามรายการช้อปปิ้งนี้เป็นคำถามที่ดีที่ควรค่าแก่การ upvotes และความโปรดปรานในขณะที่คนอื่น ๆ ปิดตัวลง
อัลวิน ธ อมป์สัน

1
โดยทั่วไปแล้วเครือข่ายลอจิกจะเข้าไปในคอนโทรลเลอร์ซึ่งจะเปลี่ยนโมเดลวัตถุและแจ้งผู้แทนหรือผู้สังเกตการณ์ใด ๆ
ระงับ

1
คำถามและคำตอบที่น่าสนใจมาก หลังจากการเข้ารหัส iOS 4 ปีและพยายามหาวิธีที่สวยที่สุดในการเพิ่มเลเยอร์เครือข่ายลงในแอป คลาสใดควรมีหน้าที่รับผิดชอบในการจัดการคำขอเครือข่าย คำตอบด้านล่างมีความเกี่ยวข้องจริงๆ ขอบคุณ
darksider

@ JoeBlow นี้ไม่เป็นความจริง อุตสาหกรรมแอพมือถือยังคงพึ่งพาการสื่อสารกับเซิร์ฟเวอร์เป็นอย่างมาก
scord

คำตอบ:


327

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 ยังเป็นแบบป้องกันและในกรณีส่วนใหญ่ (ยกเว้นของหายาก) เป็นวิธีการแก้ปัญหาที่ผิด ควรพิจารณาซิงเกิลตันเฉพาะเมื่อตรงตามเกณฑ์ทั้งสามข้อต่อไปนี้:

  1. ไม่สามารถมอบหมายความเป็นเจ้าของอินสแตนซ์เดี่ยวได้อย่างสมเหตุสมผล
  2. การเริ่มต้น Lazy เป็นที่พึงปรารถนา
  3. การเข้าถึงทั่วโลกไม่ได้มีไว้สำหรับ

ในกรณีที่เราเป็นเจ้าของอินสแตนซ์เดียวไม่ใช่ปัญหาและเราไม่ต้องการการเข้าถึงทั่วโลกหลังจากที่เราแบ่งผู้จัดการเทพเจ้าของเราเป็นบริการเพราะตอนนี้มีเพียงหนึ่งหรือหลายตัวควบคุมเฉพาะต้องการบริการเฉพาะ (เช่น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ดังนั้น มันเป็นคำแนะนำที่ดีที่สุดที่ฉันสามารถให้คุณได้


ยังเป็นวิธีที่น่าสนใจและมั่นคงขอบคุณ
MainstreamDeveloper00

1
@darksider ตามที่ฉันได้เขียนไปแล้วในคำตอบของฉัน: "` ฉันไม่เคยใช้ singletons, God APIManager ชั้นใด ๆ หรือสิ่งที่ผิดอื่น ๆ เพราะ singleton เป็นรูปแบบการต่อต้านและในกรณีส่วนใหญ่ (ยกเว้นคนที่หายาก) เป็นทางออกที่ผิด". I don't like singletons. I have an opinion that if you decided to use singletons in your project you should have at least three criteria why you do this (I edited my answer). So I inject them (lazy of course and not each time, but ครั้งเดียว `) ในทุก ๆ ตัวควบคุม
Oleksandr Karaberov

14
สวัสดี @alexander คุณมีโครงการตัวอย่างใน GitHub หรือไม่ คุณอธิบายวิธีการที่น่าสนใจมาก ขอบคุณ แต่ฉันเป็นผู้เริ่มต้นในการพัฒนา Objective-C และสำหรับฉันนั้นยากที่จะเข้าใจบางแง่มุม บางทีคุณสามารถอัปโหลดโครงการทดสอบบางอย่างใน GitHub และให้ลิงก์ได้หรือไม่
เดนิส

1
สวัสดี @AlexanderKaraberov ฉันสับสนเล็กน้อยเกี่ยวกับคำอธิบายร้านค้าที่คุณให้ สมมติว่าฉันมี 5 รุ่นสำหรับแต่ละฉันมี 2 คลาสซึ่งหนึ่งในนั้นรักษาการเชื่อมต่อเครือข่ายและแคชวัตถุอื่น ๆ ตอนนี้ฉันควรจะมีชั้นเก็บแยกสำหรับแต่ละรุ่นที่เรียกใช้ฟังก์ชั่นของเครือข่ายและคลาสแคชหรือชั้นเก็บเดียวที่มีฟังก์ชั่นทั้งหมดสำหรับแต่ละรุ่นดังนั้นตัวควบคุมจะเข้าถึงไฟล์เดียวสำหรับข้อมูล
อุกกาบาต

1
@icodebuster โครงการสาธิตนี้ช่วยให้ฉันเข้าใจแนวคิดหลายอย่างที่อธิบายไว้ที่นี่: github.com/darthpelo/NetworkLayerExample

31

ตามเป้าหมายของคำถามนี้ฉันต้องการอธิบายวิธีการสถาปัตยกรรมของเรา

วิธีการสถาปัตยกรรม

สถาปัตยกรรมของโปรแกรมประยุกต์ iOS ของเราทั่วไปยืนอยู่บนรูปแบบต่อไปนี้: ชั้นบริการ , MVVM , ข้อมูล UI ผูกพัน , การพึ่งพาการฉีด ; และกระบวนทัศน์การเขียนโปรแกรมปฏิกิริยาโต้ตอบ

เราสามารถแบ่งแอปพลิเคชันสำหรับผู้ใช้ทั่วไปที่หันเข้าหาเลเยอร์ตรรกะดังต่อไปนี้:

  • การชุมนุม
  • แบบ
  • บริการ
  • การเก็บรักษา
  • ผู้จัดการ
  • ผู้ประสานงาน
  • UI
  • โครงสร้างพื้นฐาน

แอสเซมบลีเลเยอร์เป็นจุดเริ่มต้นของแอปพลิเคชันของเรา มันมีคอนเทนเนอร์การอ้างอิงการพึ่งพาและการประกาศวัตถุของแอปพลิเคชันและการอ้างอิงของพวกเขา เลเยอร์นี้อาจมีการกำหนดค่าของแอปพลิเคชัน (URL, คีย์บริการของบุคคลที่สามและอื่น ๆ ) เพื่อจุดประสงค์นี้เราใช้ห้องสมุดไต้ฝุ่น

เลเยอร์โมเดลมีคลาสโมเดลโดเมนการตรวจสอบความถูกต้องการแม็พ เราใช้Mantle ไลบรารี่สำหรับทำแผนที่โมเดลของเรา: มันรองรับ serialization / deserialization ในJSONรูปแบบและNSManagedObjectโมเดล สำหรับการตรวจสอบและรูปแบบของการเป็นตัวแทนของรุ่นของเราเราใช้FXFormsและFXModelValidationห้องสมุด

บริการเลเยอร์ประกาศบริการที่เราใช้สำหรับการโต้ตอบกับระบบภายนอกเพื่อส่งหรือรับข้อมูลซึ่งแสดงในรูปแบบโดเมนของเรา ดังนั้นโดยปกติแล้วเรามีบริการสำหรับการสื่อสารกับเซิร์ฟเวอร์ API (ต่อเอนทิตี), บริการส่งข้อความ (เช่นPubNub ), บริการจัดเก็บข้อมูล (เช่น Amazon S3), ฯลฯ โดยทั่วไปบริการห่อวัตถุโดย SDK (เช่น PubNub SDK) หรือใช้การสื่อสารของตนเอง ตรรกะ. สำหรับเครือข่ายทั่วไปเราใช้ไลบรารีAFNetworking

จุดประสงค์ของเลเยอร์พื้นที่เก็บข้อมูลคือเพื่อจัดระเบียบที่เก็บข้อมูลในเครื่องบนอุปกรณ์ เราใช้ Core Data หรือRealmสำหรับเรื่องนี้ (ทั้งคู่มีข้อดีข้อเสียการตัดสินใจว่าจะใช้อะไรขึ้นอยู่กับข้อมูลจำเพาะที่เป็นรูปธรรม) สำหรับการตั้งค่า Core Data เราใช้ไลบรารีMDMCoreDataและเครือข่ายคลาส - คลังข้อมูล - (คล้ายกับบริการ) ที่ให้การเข้าถึงหน่วยเก็บข้อมูลภายในสำหรับทุกหน่วยงาน สำหรับอาณาจักรเราเพิ่งใช้คลังเก็บที่คล้ายกันเพื่อเข้าถึงหน่วยเก็บข้อมูลท้องถิ่น

เลเยอร์ผู้จัดการเป็นสถานที่ที่ abstractions / wrappers ของเราอาศัยอยู่

ในบทบาทผู้จัดการอาจเป็น:

  • ตัวจัดการข้อมูลรับรองที่มีการใช้งานที่แตกต่างกัน (keychain, NSDefaults, ... )
  • ผู้จัดการเซสชันปัจจุบันซึ่งรู้วิธีการเก็บรักษาและจัดเตรียมเซสชันผู้ใช้ปัจจุบัน
  • ไปป์ไลน์ซึ่งให้การเข้าถึงอุปกรณ์สื่อ (การบันทึกวิดีโอเสียงการถ่ายภาพ)
  • BLE Manager ซึ่งให้การเข้าถึงบริการบลูทู ธ และอุปกรณ์ต่อพ่วง
  • ผู้จัดการสถานที่ตั้งทางภูมิศาสตร์
  • ...

ดังนั้นในบทบาทของผู้จัดการอาจเป็นวัตถุใด ๆ ที่ใช้ตรรกะของลักษณะเฉพาะหรือข้อกังวลที่จำเป็นสำหรับการทำงานของแอปพลิเคชัน

เราพยายามหลีกเลี่ยง Singletons แต่เลเยอร์นี้เป็นสถานที่ที่พวกเขาอาศัยอยู่หากพวกเขาต้องการ

เลเยอร์ของผู้ประสานงานให้วัตถุที่ขึ้นอยู่กับวัตถุจากเลเยอร์อื่น ๆ (บริการ, ที่เก็บ, รุ่น) เพื่อรวมตรรกะของพวกเขาเป็นหนึ่งในลำดับการทำงานที่จำเป็นสำหรับโมดูลบางอย่าง (คุณสมบัติหน้าจอเรื่องราวของผู้ใช้หรือประสบการณ์ของผู้ใช้) มันมักเชื่อมโยงการดำเนินงานแบบอะซิงโครนัสและรู้วิธีตอบสนองต่อความสำเร็จและความล้มเหลวของกรณี ตัวอย่างเช่นคุณสามารถจินตนาการถึงคุณสมบัติการส่งข้อความและMessagingCoordinatorวัตถุที่เกี่ยวข้อง การจัดการการส่งข้อความอาจมีลักษณะเช่นนี้:

  1. ตรวจสอบข้อความ (โมเดลเลเยอร์)
  2. บันทึกข้อความในเครื่อง (ที่เก็บข้อความ)
  3. อัปโหลดไฟล์แนบข้อความ (บริการ amazon s3)
  4. อัปเดตสถานะข้อความและไฟล์แนบ URL และบันทึกข้อความไว้ในเครื่อง (ที่เก็บข้อความ)
  5. จัดลำดับข้อความเป็นรูปแบบ JSON (เลเยอร์โมเดล)
  6. เผยแพร่ข้อความไปยัง PubNub (บริการ PubNub)
  7. อัปเดตสถานะข้อความและคุณสมบัติและบันทึกไว้ในเครื่อง (ที่เก็บข้อความ)

ในแต่ละขั้นตอนข้างต้นข้อผิดพลาดจะถูกจัดการอย่างสอดคล้องกัน

เลเยอร์ UIประกอบด้วยเลเยอร์ย่อยต่อไปนี้:

  1. ViewModels
  2. ViewControllers
  3. เข้าชม

เพื่อหลีกเลี่ยง Massive View Controllers เราใช้รูปแบบ MVVM และใช้ตรรกะที่จำเป็นสำหรับการนำเสนอ UI ใน ViewModels ViewModel มักจะมีผู้ประสานงานและผู้จัดการเป็นอ้างอิง ViewModels ที่ใช้โดย ViewControllers และ Views บางชนิด (เช่นเซลล์มุมมองตาราง) กาวระหว่าง ViewControllers และ ViewModels คือ Data Binding และรูปแบบคำสั่ง เพื่อให้เป็นไปได้ที่จะมีกาวที่เราใช้ห้องสมุดReactiveCocoa

นอกจากนี้เรายังใช้ ReactiveCocoa และRACSignalแนวคิดเป็นอินเทอร์เฟซและคืนค่าชนิดของผู้ประสานงานบริการวิธีการเก็บรักษาทั้งหมด สิ่งนี้ทำให้เราสามารถเชื่อมโยงการดำเนินงานเรียกใช้งานแบบขนานหรือแบบอนุกรมและสิ่งที่มีประโยชน์อื่น ๆ อีกมากมายที่จัดทำโดย ReactiveCocoa

เราพยายามที่จะใช้พฤติกรรม UI ของเราในทางที่เปิดเผย การผูกข้อมูลและเค้าโครงอัตโนมัติช่วยให้บรรลุเป้าหมายนี้ได้อย่างมากมาย

เลเยอร์โครงสร้างพื้นฐานประกอบด้วยผู้ช่วยเหลือส่วนขยายโปรแกรมอรรถประโยชน์ที่จำเป็นสำหรับงานแอปพลิเคชันทั้งหมด


วิธีการนี้ใช้งานได้ดีสำหรับเราและแอพประเภทที่เรามักจะสร้าง แต่คุณควรเข้าใจว่านี่เป็นเพียงวิธีการอัตนัยที่ควรปรับเปลี่ยน / เพื่อวัตถุประสงค์ของทีมที่เป็นรูปธรรม

หวังว่านี่จะช่วยคุณได้!

นอกจากนี้คุณสามารถค้นหาข้อมูลเพิ่มเติมเกี่ยวกับกระบวนการพัฒนา iOS ในบล็อกนี้โพสต์การพัฒนา iOS เป็นบริการ


เริ่มชอบสถาปัตยกรรมนี้เมื่อไม่กี่เดือนที่ผ่านมาขอบคุณอเล็กซ์ที่แบ่งปันมัน! ฉันต้องการลองกับ RxSwift ในอนาคตอันใกล้นี้!
ingaham

18

เนื่องจากแอพ iOS ทั้งหมดแตกต่างกันฉันคิดว่ามีวิธีการที่แตกต่างกันในการพิจารณา แต่โดยทั่วไปฉันจะดำเนินการดังนี้:
สร้างคลาส central manager (singleton) เพื่อจัดการคำขอ API ทั้งหมด (โดยปกติจะเรียกว่า APICommunicator) . และมีวิธีกลาง (ไม่ใช่สาธารณะ) หนึ่งวิธี:

-(RACSignal *)sendGetToServerToSubPath:(NSString *)path withParameters:(NSDictionary *)params;

สำหรับบันทึกฉันใช้ไลบรารี่ / เฟรมเวิร์กหลัก 2 รายการคือ ReactiveCocoa และ AFNetworking ReactiveCocoa จัดการการตอบสนองเครือข่าย async อย่างสมบูรณ์แบบคุณสามารถทำได้ (sendNext :, sendError:, ฯลฯ )
วิธีนี้เรียก API รับผลลัพธ์และส่งผ่าน RAC ในรูปแบบ 'raw' (เช่น NSArray ที่ AFNetworking ส่งคืน)
จากนั้นวิธีการgetStuffList:ที่เรียกว่าวิธีการข้างต้นบอกรับสัญญาณเป็นตัววิเคราะห์ข้อมูลดิบลงในวัตถุ (ด้วยบางอย่างเช่น Motis) และส่งวัตถุทีละหนึ่งไปยังผู้โทร ( getStuffList:และวิธีการที่คล้ายกันก็ส่งสัญญาณที่ผู้ควบคุมสามารถสมัคร )
ตัวควบคุมที่สมัครเป็นสมาชิกได้รับวัตถุโดยsubscribeNext:บล็อกของและจัดการพวกเขา

ฉันลองใช้หลายวิธีในแอพที่แตกต่างกัน แต่อันนี้ใช้งานได้ดีที่สุดจากทั้งหมดดังนั้นฉันจึงใช้มันในไม่กี่แอพเมื่อเร็ว ๆ นี้มันเหมาะกับทั้งโครงการขนาดเล็กและใหญ่และมันง่ายต่อการขยายและดูแลรักษาหากจำเป็นต้องแก้ไข
หวังว่าสิ่งนี้จะช่วยได้ฉันอยากได้ยินความคิดเห็นของผู้อื่นเกี่ยวกับวิธีการของฉันและบางทีคนอื่น ๆ คิดว่านี่อาจจะดีขึ้น


2
ขอบคุณสำหรับคำตอบ +1 วิธีการที่ดี ฉันทิ้งคำถามไว้ อาจเป็นไปได้ว่าเราจะมีแนวทางอื่นจากนักพัฒนาอื่น ๆ
MainstreamDeveloper00

1
ฉันชอบความหลากหลายของวิธีการนี้ - ฉันใช้ตัวจัดการ API กลางซึ่งดูแลกลไกการสื่อสารกับ API อย่างไรก็ตามฉันพยายามทำให้ฟังก์ชั่นการใช้งานทั้งหมดเปิดเผยในวัตถุจำลองของฉัน รุ่นจะให้วิธีการเช่น+ (void)getAllUsersWithSuccess:(void(^)(NSArray*))success failure:(void(^)(NSError*))failure;และ- (void)postWithSuccess:(void(^)(instancetype))success failure:(void(^)(NSError*))failure;ที่ทำเตรียมการที่จำเป็นแล้วโทรผ่านไปยังผู้จัดการของ API
jsadler

1
วิธีการนี้ตรงไปตรงมา แต่เมื่อจำนวน API เพิ่มขึ้นมันก็ยากที่จะรักษาผู้จัดการ API แบบซิงเกิล และ API ที่เพิ่มเข้ามาใหม่จะเกี่ยวข้องกับผู้จัดการไม่ว่าจะเป็นโมดูลใด API นี้ ลองใช้ github.com/kevin0571/STNetTaskQueueเพื่อจัดการคำขอ API
Kevin

นอกเหนือจากสาเหตุที่คุณโฆษณาห้องสมุดของคุณซึ่งไกลที่สุดเท่าที่จะทำได้จากวิธีแก้ปัญหาของฉันและซับซ้อนกว่านี้ฉันได้ลองใช้วิธีนี้กับโครงการที่นับไม่ถ้วนทั้งขนาดเล็กและใหญ่ตามที่กล่าวไว้และฉันใช้มันอย่างแท้จริง เหมือนกันตั้งแต่ฉันเขียนคำตอบนี้ ด้วยอนุสัญญาการตั้งชื่อที่ฉลาดมันไม่ยากเลยที่จะรักษา
Rickye

8

ในสถานการณ์ของฉันฉันมักจะใช้ห้องสมุดResKitเพื่อตั้งค่าเลเยอร์เครือข่าย มันมีการแยกวิเคราะห์ที่ง่ายต่อการใช้งาน มันลดความพยายามของฉันในการตั้งค่าการแมปสำหรับการตอบสนองและสิ่งต่าง ๆ

ฉันเพิ่มรหัสเพื่อตั้งค่าการจับคู่โดยอัตโนมัติ ฉันกำหนดคลาสพื้นฐานสำหรับโมเดลของฉัน (ไม่ใช่โปรโตคอลเนื่องจากมีโค้ดจำนวนมากเพื่อตรวจสอบว่ามีการใช้วิธีการบางอย่างหรือไม่และใช้โค้ดน้อยกว่าในโมเดลเอง):

MappableEntry.h

@interface MappableEntity : NSObject

+ (NSArray*)pathPatterns;
+ (NSArray*)keyPathes;
+ (NSArray*)fieldsArrayForMapping;
+ (NSDictionary*)fieldsDictionaryForMapping;
+ (NSArray*)relationships;

@end

MappableEntry.m

@implementation MappableEntity

+(NSArray*)pathPatterns {
    return @[];
}

+(NSArray*)keyPathes {
    return nil;
}

+(NSArray*)fieldsArrayForMapping {
    return @[];
}

+(NSDictionary*)fieldsDictionaryForMapping {
    return @{};
}

+(NSArray*)relationships {
    return @[];
}

@end

ความสัมพันธ์เป็นวัตถุที่แสดงวัตถุซ้อนกันในการตอบสนอง:

RelationshipObject.h

@interface RelationshipObject : NSObject

@property (nonatomic,copy) NSString* source;
@property (nonatomic,copy) NSString* destination;
@property (nonatomic) Class mappingClass;

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass;
+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass;

@end

RelationshipObject.m

@implementation RelationshipObject

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = key;
    object.destination = key;
    object.mappingClass = mappingClass;
    return object;
}

+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = source;
    object.destination = destination;
    object.mappingClass = mappingClass;
    return object;
}

@end

จากนั้นฉันกำลังตั้งค่าการทำแผนที่สำหรับ RestKit ดังนี้:

ObjectMappingInitializer.h

@interface ObjectMappingInitializer : NSObject

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager;

@end

ObjectMappingInitializer.m

@interface ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses;

@end

@implementation ObjectMappingInitializer

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager {

    NSMutableDictionary *mappingObjects = [NSMutableDictionary dictionary];

    // Creating mappings for classes
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *newMapping = [RKObjectMapping mappingForClass:mappableClass];
        [newMapping addAttributeMappingsFromArray:[mappableClass fieldsArrayForMapping]];
        [newMapping addAttributeMappingsFromDictionary:[mappableClass fieldsDictionaryForMapping]];
        [mappingObjects setObject:newMapping forKey:[mappableClass description]];
    }

    // Creating relations for mappings
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *mapping = [mappingObjects objectForKey:[mappableClass description]];
        for (RelationshipObject *relation in [mappableClass relationships]) {
            [mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relation.source toKeyPath:relation.destination withMapping:[mappingObjects objectForKey:[relation.mappingClass description]]]];
        }
    }

    // Creating response descriptors with mappings
    for (Class mappableClass in [self mappableClasses]) {
        for (NSString* pathPattern in [mappableClass pathPatterns]) {
            if ([mappableClass keyPathes]) {
                for (NSString* keyPath in [mappableClass keyPathes]) {
                    [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:keyPath statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
                }
            } else {
                [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
            }
        }
    }

    // Error Mapping
    RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[Error class]];
    [errorMapping addAttributeMappingsFromArray:[Error fieldsArrayForMapping]];
    for (NSString *pathPattern in Error.pathPatterns) {
        [[RKObjectManager sharedManager] addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:errorMapping method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError)]];
    }
}

@end

@implementation ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses {
    return @[
        [FruiosPaginationResults class],
        [FruioItem class],
        [Pagination class],
        [ContactInfo class],
        [Credentials class],
        [User class]
    ];
}

@end

ตัวอย่างของการใช้ MappableEntry:

User.h

@interface User : MappableEntity

@property (nonatomic) long userId;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *password;
@property (nonatomic, copy) NSString *token;

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password;

- (NSDictionary*)registrationData;

@end

User.m

@implementation User

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password {
    if (self = [super init]) {
        self.username = username;
        self.email = email;
        self.password = password;
    }
    return self;
}

- (NSDictionary*)registrationData {
    return @{
        @"username": self.username,
        @"email": self.email,
        @"password": self.password
    };
}

+ (NSArray*)pathPatterns {
    return @[
        [NSString stringWithFormat:@"/api/%@/users/register", APIVersionString],
        [NSString stringWithFormat:@"/api/%@/users/login", APIVersionString]
    ];
}

+ (NSArray*)fieldsArrayForMapping {
    return @[ @"username", @"email", @"password", @"token" ];
}

+ (NSDictionary*)fieldsDictionaryForMapping {
    return @{ @"id": @"userId" };
}

@end

ตอนนี้เกี่ยวกับการห่อคำขอ:

ฉันมีไฟล์ส่วนหัวพร้อมคำจำกัดความของบล็อกเพื่อลดความยาวบรรทัดในคลาส APIRequest ทั้งหมด:

APICallbacks.h

typedef void(^SuccessCallback)();
typedef void(^SuccessCallbackWithObjects)(NSArray *objects);
typedef void(^ErrorCallback)(NSError *error);
typedef void(^ProgressBlock)(float progress);

และตัวอย่างของคลาส APIRequest ของฉันที่ฉันใช้:

LoginAPI.h

@interface LoginAPI : NSObject

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError;

@end

LoginAPI.m

@implementation LoginAPI

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError {
    [[RKObjectManager sharedManager] postObject:nil path:[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString] parameters:[credentials credentialsData] success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
        onSuccess(mappingResult.array);
    } failure:^(RKObjectRequestOperation *operation, NSError *error) {
        onError(error);
    }];
}

@end

และสิ่งที่คุณต้องทำในรหัสเพียงแค่เริ่มต้นวัตถุ API และเรียกมันเมื่อใดก็ตามที่คุณต้องการ:

SomeViewController.m

@implementation SomeViewController {
    LoginAPI *_loginAPI;
    // ...
}

- (void)viewDidLoad {
    [super viewDidLoad];

    _loginAPI = [[LoginAPI alloc] init];
    // ...
}

// ...

- (IBAction)signIn:(id)sender {
    [_loginAPI loginWithCredentials:_credentials onSuccess:^(NSArray *objects) {
        // Success Block
    } onError:^(NSError *error) {
        // Error Block
    }];
}

// ...

@end

รหัสของฉันไม่สมบูรณ์แบบ แต่ตั้งค่าได้ง่ายครั้งเดียวและใช้สำหรับโครงการต่าง ๆ ถ้าเป็นเรื่องที่น่าสนใจสำหรับทุกคน mb ฉันสามารถใช้เวลาและทำสิ่งที่เป็นสากลสำหรับที่ใดที่หนึ่งใน GitHub และ CocoaPods


7

ในใจของฉันสถาปัตยกรรมซอฟต์แวร์ทั้งหมดขับเคลื่อนด้วยความต้องการ หากนี่คือเพื่อการเรียนรู้หรือเพื่อวัตถุประสงค์ส่วนตัวให้ตัดสินใจเป้าหมายหลักและผลักดันให้มีสถาปัตยกรรม หากนี่คืองานให้เช่าความต้องการทางธุรกิจนั้นยิ่งใหญ่ เคล็ดลับคือการไม่ปล่อยให้สิ่งที่เป็นเงาล่อใจคุณจากความต้องการที่แท้จริง ฉันพบว่ามันยากที่จะทำ มีสิ่งใหม่ที่ปรากฎในธุรกิจนี้อยู่เสมอและสิ่งเหล่านั้นมากมายไม่มีประโยชน์ แต่คุณไม่สามารถบอกได้ล่วงหน้า มุ่งเน้นไปที่ความต้องการและเต็มใจละทิ้งทางเลือกที่ไม่ดีหากทำได้

ตัวอย่างเช่นเมื่อเร็ว ๆ นี้ฉันได้สร้างต้นแบบอย่างรวดเร็วของแอพแบ่งปันภาพถ่ายสำหรับธุรกิจในท้องถิ่น เนื่องจากความต้องการทางธุรกิจคือการทำสิ่งที่รวดเร็วและสกปรกสถาปัตยกรรมจึงกลายเป็นรหัส iOS เพื่อเปิดกล้องและรหัสเครือข่ายบางส่วนที่ติดอยู่กับปุ่มส่งที่อัปโหลดภาพไปยังร้านค้า S3 และเขียนไปยังโดเมน SimpleDB รหัสนั้นเล็กน้อยและค่าใช้จ่ายน้อยที่สุดและลูกค้ามีคอลเลกชันภาพถ่ายที่ปรับขนาดได้ที่สามารถเข้าถึงได้ผ่านทางเว็บด้วยการเรียกใช้ REST แอพราคาประหยัดและไร้เดียงสามีข้อบกพร่องมากมายและจะล็อค UI ในบางโอกาส แต่มันจะเป็นการเสียเปล่าที่จะทำสิ่งที่มากกว่าสำหรับต้นแบบและมันช่วยให้พวกเขาสามารถนำไปใช้กับพนักงานของพวกเขาและสร้างภาพทดสอบนับพันได้อย่างง่ายดาย ความกังวล สถาปัตยกรรมเส็งเคร็ง แต่มันพอดีกับความต้องการและค่าใช้จ่ายอย่างสมบูรณ์แบบ

โครงการอื่นที่เกี่ยวข้องกับการใช้ฐานข้อมูลความปลอดภัยในพื้นที่ซึ่งประสานกับระบบของ บริษัท ในพื้นหลังเมื่อเครือข่ายพร้อมใช้งาน ฉันสร้างซิงโครไนซ์พื้นหลังที่ใช้ RestKit เนื่องจากดูเหมือนว่ามีทุกสิ่งที่ฉันต้องการ แต่ฉันต้องเขียนโค้ดที่กำหนดเองมากมายสำหรับ RestKit เพื่อจัดการกับ JSON ที่มีนิสัยแปลก ๆ ซึ่งฉันสามารถทำได้เร็วขึ้นโดยการเขียนการแปลง JSON ของตัวเองเป็น CoreData อย่างไรก็ตามลูกค้าต้องการนำแอพนี้เข้ามาในบ้านและฉันรู้สึกว่า RestKit นั้นจะคล้ายกับเฟรมเวิร์กที่ใช้บนแพลตฟอร์มอื่น ฉันรอดูว่าเป็นการตัดสินใจที่ดีหรือไม่

อีกครั้งปัญหาสำหรับฉันคือการมุ่งเน้นไปที่ความต้องการและให้ที่กำหนดสถาปัตยกรรม ฉันพยายามที่จะหลีกเลี่ยงการใช้แพคเกจของบุคคลที่สามเพราะพวกเขานำค่าใช้จ่ายที่จะปรากฏขึ้นหลังจากที่แอปอยู่ในสนามมาระยะหนึ่งแล้ว ฉันพยายามหลีกเลี่ยงการทำลำดับชั้นของชั้นเรียนเนื่องจากไม่ค่อยจ่าย ถ้าฉันสามารถเขียนอะไรบางอย่างในช่วงเวลาที่เหมาะสมแทนที่จะใช้แพ็คเกจที่ไม่เข้ากันอย่างสมบูรณ์แบบฉันก็ทำได้ รหัสของฉันมีโครงสร้างที่ดีสำหรับการดีบักและแสดงความคิดเห็นอย่างเหมาะสม แต่แพ็คเกจบุคคลที่สามไม่ค่อยมี จากที่กล่าวมาฉันพบว่าระบบเครือข่าย AF มีประโยชน์เกินกว่าที่จะเพิกเฉยและมีโครงสร้างที่ดีมีความคิดเห็นดีและได้รับการดูแลรักษาและฉันใช้มันบ่อยมาก! RestKit ครอบคลุมกรณีทั่วไปจำนวนมาก แต่ฉันรู้สึกเหมือนฉันเคยต่อสู้เมื่อฉันใช้มัน และแหล่งข้อมูลส่วนใหญ่ที่ฉันพบเต็มไปด้วยนิสัยใจคอและปัญหาที่จัดการได้ดีที่สุดด้วยรหัสที่กำหนดเอง ในแอพพลิเคชั่นล่าสุดของฉันฉันใช้ตัวแปลง JSON ในตัวและเขียนวิธีการอรรถประโยชน์บางอย่าง

รูปแบบหนึ่งที่ฉันใช้อยู่เสมอคือให้เครือข่ายโทรออกจากเธรดหลัก แอป 4-5 รายการสุดท้ายที่ฉันได้ตั้งค่าตัวจับเวลาพื้นหลังโดยใช้ dispatch_source_create ที่จะปลุกทุกครั้งและทำงานเครือข่ายตามที่ต้องการ คุณต้องทำงานด้านความปลอดภัยของเธรดและให้แน่ใจว่ารหัสการแก้ไข UI ได้รับการส่งไปยังเธรดหลัก นอกจากนี้ยังช่วยในการทำ onboarding / initialization ในแบบที่ผู้ใช้จะไม่รู้สึกเป็นภาระหรือล่าช้า จนถึงตอนนี้มันใช้งานได้ค่อนข้างดี ฉันขอแนะนำให้ดูสิ่งเหล่านี้

ในที่สุดฉันคิดว่าเมื่อเราทำงานมากขึ้นและเมื่อระบบปฏิบัติการวิวัฒนาการเราก็มีแนวโน้มที่จะพัฒนาวิธีแก้ปัญหาที่ดีกว่า ฉันใช้เวลาหลายปีกว่าจะเชื่อว่าฉันต้องทำตามรูปแบบและการออกแบบที่คนอื่นอ้างว่าจำเป็น ถ้าฉันทำงานในบริบทที่เป็นส่วนหนึ่งของศาสนาอะแฮ่มฉันหมายถึงหลักปฏิบัติทางวิศวกรรมที่ดีที่สุดของแผนกจากนั้นฉันทำตามธรรมเนียมปฏิบัติกับจดหมายนั่นคือสิ่งที่พวกเขาจ่ายให้ฉัน แต่ฉันไม่ค่อยพบว่าการออกแบบและรูปแบบเก่า ๆ ต่อไปนี้เป็นทางออกที่ดีที่สุด ฉันพยายามดูวิธีแก้ปัญหาผ่านทางปริซึมของความต้องการทางธุรกิจและสร้างสถาปัตยกรรมเพื่อจับคู่และทำให้สิ่งต่าง ๆ เรียบง่ายที่สุดเท่าที่จะทำได้ เมื่อฉันรู้สึกว่ามีไม่เพียงพอที่นั่น แต่ทุกอย่างทำงานได้อย่างถูกต้องแล้วฉันจะติดตามที่ถูกต้อง


4

ผมใช้วิธีการที่ผมเคยได้จากที่นี่: https://github.com/Constantine-Fry/Foursquare-API-v2 ฉันได้เขียนไลบรารีนั้นอีกครั้งใน Swift และคุณสามารถดูแนวทางสถาปัตยกรรมจากส่วนต่าง ๆ ของรหัสนี้:

typealias OpertaionCallback = (success: Bool, result: AnyObject?) -> ()

class Foursquare{
    var authorizationCallback: OperationCallback?
    var operationQueue: NSOperationQueue
    var callbackQueue: dispatch_queue_t?

    init(){
        operationQueue = NSOperationQueue()
        operationQueue.maxConcurrentOperationCount = 7;
        callbackQueue = dispatch_get_main_queue();
    }

    func checkIn(venueID: String, shout: String, callback: OperationCallback) -> NSOperation {
        let parameters: Dictionary <String, String> = [
            "venueId":venueID,
            "shout":shout,
            "broadcast":"public"]
        return self.sendRequest("checkins/add", parameters: parameters, httpMethod: "POST", callback: callback)
    }

    func sendRequest(path: String, parameters: Dictionary <String, String>, httpMethod: String, callback:OperationCallback) -> NSOperation{
        let url = self.constructURL(path, parameters: parameters)
        var request = NSMutableURLRequest(URL: url)
        request.HTTPMethod = httpMethod
        let operation = Operation(request: request, callbackBlock: callback, callbackQueue: self.callbackQueue!)
        self.operationQueue.addOperation(operation)
        return operation
    }

    func constructURL(path: String, parameters: Dictionary <String, String>) -> NSURL {
        var parametersString = kFSBaseURL+path
        var firstItem = true
        for key in parameters.keys {
            let string = parameters[key]
            let mark = (firstItem ? "?" : "&")
            parametersString += "\(mark)\(key)=\(string)"
            firstItem = false
        }
    return NSURL(string: parametersString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding))
    }
}

class Operation: NSOperation {
    var callbackBlock: OpertaionCallback
    var request: NSURLRequest
    var callbackQueue: dispatch_queue_t

    init(request: NSURLRequest, callbackBlock: OpertaionCallback, callbackQueue: dispatch_queue_t) {
        self.request = request
        self.callbackBlock = callbackBlock
        self.callbackQueue = callbackQueue
    }

    override func main() {
        var error: NSError?
        var result: AnyObject?
        var response: NSURLResponse?

        var recievedData: NSData? = NSURLConnection.sendSynchronousRequest(self.request, returningResponse: &response, error: &error)

        if self.cancelled {return}

        if recievedData{
            result = NSJSONSerialization.JSONObjectWithData(recievedData, options: nil, error: &error)
            if result != nil {
                if result!.isKindOfClass(NSClassFromString("NSError")){
                    error = result as? NSError
            }
        }

        if self.cancelled {return}

        dispatch_async(self.callbackQueue, {
            if (error) {
                self.callbackBlock(success: false, result: error!);
            } else {
                self.callbackBlock(success: true, result: result!);
            }
            })
    }

    override var concurrent:Bool {get {return true}}
}

โดยทั่วไปมีคลาสย่อย NSOperation ที่ทำให้ NSURLRequest แยกวิเคราะห์การตอบสนอง JSON และเพิ่มบล็อกการติดต่อกลับพร้อมผลลัพธ์ในคิว คลาส API หลักสร้าง NSURLRequest เริ่มต้นคลาสย่อย NSOperation และเพิ่มลงในคิว


3

เราใช้วิธีการบางอย่างขึ้นอยู่กับสถานการณ์ สำหรับสิ่งที่ AFNetworking ส่วนใหญ่เป็นวิธีที่ง่ายและมีประสิทธิภาพที่สุดในการที่คุณสามารถตั้งค่าส่วนหัวอัปโหลดข้อมูลหลายส่วนใช้ GET, POST, PUT & DELETE และมีหมวดหมู่เพิ่มเติมสำหรับ UIKit ซึ่งช่วยให้คุณสามารถตั้งค่ารูปภาพจาก URL ในแอพที่ซับซ้อนที่มีการโทรจำนวนมากบางครั้งเราสรุปเรื่องนี้เป็นวิธีการอำนวยความสะดวกของเราเองซึ่งจะเป็นสิ่งที่ต้องการ:

-(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;

มีบางสถานการณ์ที่ AFNetworking ไม่เหมาะสมอย่างไรก็ตามเช่นที่คุณกำลังสร้างเฟรมเวิร์กหรือส่วนประกอบไลบรารีอื่น ๆ เนื่องจาก AFNetworking อาจอยู่ในฐานรหัสอื่นอยู่แล้ว ในสถานการณ์นี้คุณจะใช้ NSMutableURLRequest ทั้งแบบอินไลน์หากคุณทำการโทรเพียงครั้งเดียวหรือเป็นนามธรรมลงในคลาสคำขอ / ตอบกลับ


สำหรับฉันนี่คือคำตอบที่ดีที่สุดและชัดเจนที่สุดไชโย "มันง่ายมาก" @martin ส่วนตัวเราแค่ใช้ NSMutableURLRequest ตลอดเวลา มีเหตุผลจริงที่จะใช้ AFNetworking หรือไม่
Fattie

เครือข่าย AFN สะดวกสบายจริงๆ สำหรับฉันความสำเร็จและการบล็อกที่ล้มเหลวทำให้คุ้มค่าในขณะที่มันทำให้การจัดการโค้ดง่ายขึ้น ฉันยอมรับว่าบางครั้งมันเกินความจริงทั้งหมด
Martin

จุดที่ยอดเยี่ยมบนบล็อคขอบคุณสำหรับสิ่งนั้น ฉันเดาว่าลักษณะเฉพาะของสิ่งนี้จะเปลี่ยนแปลงด้วย Swift
Fattie

2

ฉันหลีกเลี่ยงซิงเกิลตันเมื่อออกแบบแอปพลิเคชันของฉัน มันเป็นเรื่องปกติสำหรับคนจำนวนมาก แต่ฉันคิดว่าคุณสามารถหาคำตอบที่สง่างามกว่าที่อื่น โดยทั่วไปสิ่งที่ฉันทำคือการสร้างเอนทิตีของฉันใน CoreData แล้วใส่รหัส REST ของฉันในหมวดหมู่ NSManagedObject ถ้าเช่นฉันต้องการสร้างและโพสต์ผู้ใช้ใหม่ฉันจะทำ:

User* newUser = [User createInManagedObjectContext:managedObjectContext];
[newUser postOnSuccess:^(...) { ... } onFailure:^(...) { ... }];

ฉันใช้ RESTKit สำหรับการแมปวัตถุและเริ่มต้นเมื่อเริ่มต้น ฉันพบว่าการโอนสายทั้งหมดของคุณผ่านซิงเกิลตันเป็นการเสียเวลาและเพิ่มจำนวนสำเร็จรูปที่ไม่จำเป็น

ใน NSManagedObject + Extensions.m:

+ (instancetype)createInContext:(NSManagedObjectContext*)context
{
    NSAssert(context.persistentStoreCoordinator.managedObjectModel.entitiesByName[[self entityName]] != nil, @"Entity with name %@ not found in model. Is your class name the same as your entity name?", [self entityName]);
    return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:context];
}

ใน NSManagedObject + Networking.m:

- (void)getOnSuccess:(RESTSuccess)onSuccess onFailure:(RESTFailure)onFailure blockInput:(BOOL)blockInput
{
    [[RKObjectManager sharedManager] getObject:self path:nil parameters:nil success:onSuccess failure:onFailure];
    [self handleInputBlocking:blockInput];
}

ทำไมจึงต้องเพิ่มคลาสตัวช่วยพิเศษเมื่อคุณสามารถขยายการทำงานของคลาสพื้นฐานทั่วไปผ่านหมวดหมู่ได้

หากคุณสนใจข้อมูลรายละเอียดเพิ่มเติมเกี่ยวกับโซลูชันของฉันแจ้งให้เราทราบ ฉันมีความสุขที่จะแบ่งปัน


3
จะมีความสนใจในการอ่านเกี่ยวกับวิธีการนี้อย่างละเอียดในโพสต์บล็อก
Danyal Aytekin


0

จากมุมมองการออกแบบอย่างหมดจดคุณจะมีลักษณะดังนี้:

  • ตัวควบคุมมุมมองของคุณควบคุมมุมมองอย่างน้อยหนึ่งมุมมอง
  • คลาสโมเดลข้อมูล - มันขึ้นอยู่กับจำนวนเอนทิตีที่แตกต่างกันจริงที่คุณกำลังเผชิญและความเกี่ยวข้อง

    ตัวอย่างเช่นหากคุณมีอาร์เรย์ของรายการที่จะแสดงในการแสดงสี่แบบที่แตกต่างกัน (รายการ, แผนภูมิ, กราฟ ฯลฯ ) คุณจะมีคลาสโมเดลข้อมูลหนึ่งรายการสำหรับรายการหนึ่งรายการเพิ่มเติม รายการระดับรายการจะใช้ร่วมกันโดยสี่ตัวควบคุมมุมมอง - เด็กทุกคนของตัวควบคุมแถบแท็บหรือตัวควบคุม nav

    คลาสโมเดลข้อมูลจะมีประโยชน์ไม่เพียง แต่แสดงข้อมูล แต่ยังทำให้เป็นอนุกรมซึ่งแต่ละคลาสสามารถแสดงรูปแบบการทำให้เป็นอนุกรมของตนเองผ่านวิธีการส่งออก JSON / XML / CSV (หรืออย่างอื่น)

  • สิ่งสำคัญคือต้องเข้าใจว่าคุณต้องการคลาสตัวสร้างคำขอ APIที่จับคู่โดยตรงกับจุดสิ้นสุด REST API ของคุณ สมมติว่าคุณมี API ที่บันทึกผู้ใช้ดังนั้นคลาสตัวสร้างล็อกอิน API ของคุณจะสร้างเพย์โหลด POST JSON สำหรับ API การเข้าสู่ระบบ ในอีกตัวอย่างหนึ่งคลาสตัวสร้างคำร้องขอ API สำหรับรายการไอเท็มแค็ตตาล็อก API จะสร้างสตริงเคียวรี GET สำหรับ API ที่สอดคล้องกันและดำเนินการเคียวรี REST GET

    คลาสตัวสร้างคำร้องขอ API เหล่านี้มักจะได้รับข้อมูลจากตัวควบคุมมุมมองและส่งข้อมูลเดียวกันกลับไปยังตัวควบคุมมุมมองสำหรับการอัปเดต UI / การดำเนินการอื่น ๆ ตัวควบคุมมุมมองจะตัดสินใจเลือกวิธีอัปเดตวัตถุตัวแบบข้อมูลด้วยข้อมูลนั้น

  • ในที่สุดหัวใจของไคลเอนต์ REST - คลาสตัวดึงข้อมูล APIซึ่งไม่คำนึงถึงคำขอ API ทุกประเภทที่แอปของคุณทำ คลาสนี้มีแนวโน้มที่จะเป็นซิงเกิล แต่ในขณะที่คนอื่นชี้ให้เห็นมันไม่จำเป็นต้องเป็นซิงเกิล

    โปรดทราบว่าลิงก์นี้เป็นเพียงการนำไปปฏิบัติโดยทั่วไปและไม่คำนึงถึงสถานการณ์ที่ต้องพิจารณาเช่นเซสชันคุกกี้ ฯลฯ แต่ก็เพียงพอที่จะให้คุณดำเนินการต่อโดยไม่ต้องใช้กรอบงานของบุคคลที่สามใด ๆ


0

คำถามนี้มีคำตอบที่ยอดเยี่ยมและครอบคลุมมากมาย แต่ฉันรู้สึกว่าฉันต้องพูดถึงมันเพราะไม่มีใครอื่น

Alamofire สำหรับ Swift https://github.com/Alamofire/Alamofire

มันถูกสร้างขึ้นโดยคนเดียวกับเครือข่าย AFN แต่ได้รับการออกแบบโดยตรงกับ Swift


0

ฉันคิดว่าตอนนี้โครงการขนาดกลางใช้สถาปัตยกรรม MVVM และโครงการใหญ่ใช้สถาปัตยกรรม VIPER และพยายามทำให้สำเร็จ

  • การเขียนโปรแกรมเชิงโปรโตคอล
  • รูปแบบการออกแบบซอฟต์แวร์
  • ขายหลักการ
  • การเขียนโปรแกรมทั่วไป
  • อย่าทำซ้ำตัวเอง (DRY)

และแนวทางสถาปัตยกรรมสำหรับการสร้างแอปพลิเคชั่นเครือข่าย iOS (ไคลเอนต์ REST)

ข้อกังวลเรื่องการแยกสำหรับโค้ดที่สะอาดและอ่านได้หลีกเลี่ยงการทำซ้ำ

import Foundation
enum DataResponseError: Error {
    case network
    case decoding

    var reason: String {
        switch self {
        case .network:
            return "An error occurred while fetching data"
        case .decoding:
            return "An error occurred while decoding data"
        }
    }
}

extension HTTPURLResponse {
    var hasSuccessStatusCode: Bool {
        return 200...299 ~= statusCode
    }
}

enum Result<T, U: Error> {
    case success(T)
    case failure(U)
}

ผกผันพึ่งพา

 protocol NHDataProvider {
        func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL, completion: @escaping (Result<Codable, DataResponseError>) -> Void)
    }

หลักรับผิดชอบ:

  final class NHClientHTTPNetworking : NHDataProvider {

        let session: URLSession

        init(session: URLSession = URLSession.shared) {
            self.session = session
        }

        func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL,
                             completion: @escaping (Result<Codable, DataResponseError>) -> Void) {
            let urlRequest = URLRequest(url: url)
            session.dataTask(with: urlRequest, completionHandler: { data, response, error in
                guard
                    let httpResponse = response as? HTTPURLResponse,
                    httpResponse.hasSuccessStatusCode,
                    let data = data
                    else {
                        completion(Result.failure(DataResponseError.network))
                        return
                }
                guard let decodedResponse = try? JSONDecoder().decode(Model.self, from: data) else {
                    completion(Result.failure(DataResponseError.decoding))
                    return
                }
                completion(Result.success(decodedResponse))
            }).resume()
        }
    }

คุณจะพบว่าที่นี่เป็นสถาปัตยกรรม GitHub MVVM พร้อมส่วนที่เหลือ API Swift Project


0

ในวิศวกรรมซอฟต์แวร์มือถือที่ใช้กันอย่างแพร่หลายที่สุดคือรูปแบบ Clean Architecture + MVVM และ Redux

Clean Architecture + MVVM ประกอบด้วย 3 เลเยอร์: โดเมน, การนำเสนอ, เลเยอร์ข้อมูล ตำแหน่งที่ Presentation Layer และ Data Repositories Layer ขึ้นอยู่กับ Domain Layer:

Presentation Layer -> Domain Layer <- Data Repositories Layer

และเลเยอร์การนำเสนอประกอบด้วย ViewModels และ Views (MVVM):

Presentation Layer (MVVM) = ViewModels + Views
Domain Layer = Entities + Use Cases + Repositories Interfaces
Data Repositories Layer = Repositories Implementations + API (Network) + Persistence DB

ในบทความนี้มีคำอธิบายโดยละเอียดเพิ่มเติมเกี่ยวกับ Clean Architecture + MVVM https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.