[iOS Dev] Objective-C Style Guide


สิ่งสำคัญเวลาจะเขียน Code ภาษาต่างๆ สิ่งแรกที่ควรอ่านคือเขาเขียน Code ยังไง
นอกจากเรื่องความสวยงามแล้ว  Code Convention ที่ดีจะสามารถแก้ไขโดยผู้อื่น

Style Guide ที่เขียนใส่ Blog นี้ถูกรวบรวมมาจากที่นี่นะ (แล้วก็มีที่เขียนเพิ่มโดยผมเองนิดหน่อย)
บางอย่างก็ไม่ได้หยิบมา อยากอ่านฉบับเต็มก็เชิญได้ที่ต้นฉบับเลยครับ :)

https://github.com/NYTimes/objective-c-style-guide
https://github.com/github/objective-c-conventions


Dot-Notation Syntax

Dot-Notation Syntax จะใช้ทุกครั้งที่เข้าถึงหรือเปลี่ยนแปลง Property ต่างๆ ของวัตถุใดๆ ส่วน Bracket Notation จะใช้ในกรณีอื่นๆ โดยมากแล้วจะเป็นการเรียก method ต่างๆ (Class method, Instance method)

ตัวอย่าง

view.backgroundColor = [UIColor orangeColor];
[UIApplication sharedApplication].delegate;

ไม่ควร

[view setBackgroundColor:[UIColor orangeColor]];
UIApplication.sharedApplication.delegate;

Pragma mark
ใช้ #pragma mark ในการแบ่งกลุ่มของ method ที่ทำหน้าต่างกัน เพื่อให้เกิดความเป็นระเบียบเวลาเราใช้ method jumper (ชื่อนี้หรือปล่าวไม่แน่ใจ) ใน Xcode ตัวอย่าง #pragma mark ที่ผมใช้อยู่บ่อยๆ นะ

ที่เห็นนี่คือหลักๆ ข้างล่างของ #pragma mark พวกนี้ก็จะเป็น Datasource และ Delegate ต่างๆ ซึ่งจะเขียนแยกกันอย่างชัดเจนเลย

Spacing
  • การย่อหน้า (Indent) ควรใช้ 4 spaces ไม่ควรใช้ Tab หรือผสมกัน ซึ่งถ้าถนัดกด Tab มากกว่าสามารถตั้งค่าให้ Tab เป็น Indent (Soft Tab) ได้ใน Perfernece ใน Xcode
  • วงเล็กปีกกาสำหรับ method หรือวงเล็ปปีกกาอื่นๆ (if / else / switch / while) จะเปิดในบรรทัดเดียวกันกับชื่อ method หรือเงื่อนไขที่ควรเป็นจริง และปิดวงเล็บด้วยการขึ้นบรรทัดใหม่

ตัวอย่าง

if (user.isHappy) {
//Do something
}
else {
//Do something else
}

    • เวลาเขียน Math Expression ควรเว้นวรรคระหว่าง 1st Operand, Operator, 2nd Operand ด้วย ยกเว้นเครื่องหมาย Increment (++, --) เช่น b = a + c, c * b * a, (a++ + ++b) 
    • ในการเขียน method มากกว่า 1 method ให้เว้น 1 บรรทัดสำหรับแต่ละ method ด้วยเพื่อให้มองดูแยกกันชัดเจน
    • @synthesize และ @dynamic ควรใช้ 1 บรรทัดต่อ 1 property

    Conditionals

    ในการเขียนเงื่อนไข if แม้จะเป็น if ที่มีเนื้อหาเพียงบรรทัดเดียว ก็ควรจะมี {} ครอบไว้เพื่อกำหนดขอบเขตให้ชัดเจน และใช้กฎของการครอบปีกกาด้านบน

    ตัวอย่าง

    if (!error) {
        return success;
    }
    
    ไม่ควร

    if (!error)
        return success;
    
    หรือ 
    if (!error) return success;

    Ternary Operator 

    Ternary Operator (?:) จะใช้ต่อเมื่อต้องการให้โค้ดเคลียร์ และเป็นระเบียบเรียบร้อยขึ้น โดยมากแล้วจะใช้กับการตรวจสอบแบบ "เงื่อนไขเดียว" เท่านั้น

    ตัวอย่าง

    result = a > b ? x : y;

    ไม่ควร

    result = a > b ? x = c > d ? c : d : y;

    Methods

    ในการประกาศ method ควรจะใส่ spac ไปด้านหลังสัญลักษณ์บอกขอบเขต (สัญลักษณ์ -/+) และใช้ space ในการแยกส่วนต่างๆ ของชื่อ method (ถ้าเป็นไปได้ ลองหาคำเชื่อมประโยคที่เหมาะสมอย่างเช่น with, and มาใช้จะทำให้อ่านง่ายขึ้น)

    ตัวอย่าง

    - (void)setExampleText:(NSString *)text image:(UIImage *)image;
    


    Variables

    ชื่อตัวแปรควรจะตั้งให้สื่อความหมายมากที่สุดเท่าที่จะเป็นไปได้ว่าตัวแปรนี้ชี้ไปหาอะไร ชื่อตัวแปรอักษรเดียวไม่ควรใช้เป็นอันขาดนอกเสียจากในคำสั่งทำซ้ำ for()

    เครื่องหมาย Asterisks (*) เป็นตัวบ่งบอก Pointer ของตัวแปรนั้นๆ ควรจะเขียนให้เครื่องหมาย * ติดกับชื่อตัวแปร เช่น NSString *text ไม่ใช่ NSString* text หรือ NSString * text (อาจารย์ผมท่านเขียนแบบ NSString* text ท่านอธิบายไว้ว่า นี่เป็น Pointer ที่ชี้ไปหาตัวแปรชนิด NSString ก็ถือว่าเข้าใจได้นะ)

    ควรใช้ Property ในการประกาศ Instance Variable เนื่องจากไม่ควรเข้าถึงตัวแปรตรงๆ (ผ่าน _variableName) ยกเว้นใน method การตั้งค่าเริ่มต้นต่างๆ (init, initWithCoder: และอื่นๆ), method ในการเคลียร์หน่วยความจำ (dealloc) และ method สำหรับการเข้าถึง หรือเปลี่ยนแปลงตัวแปรที่เขียนขึ้นเอง

    ตัวอย่าง

    @interface NYTSection: NSObject
    
    @property (nonatomic) NSString *headline;
    
    @end

    ไม่ควร

    @interface NYTSection : NSObject {
        NSString *headline;
    }
    


    Naming

    ตามคำแนะนำในการตั้งชื่อของ Apple ชื่อ method หรือตัวแปรที่ยาว และบรรยายตัวเองได้เป็นเรื่องที่ดีมาก ส่วนของรูปแบบโดยมากแล้วเวลาที่ผมตั้งชื่อจะใช้หลักการแบบนี้

    • lower camel-case => ชื่อตัวแปร, ชื่อค่าคงที่ในกรณีที่ใช้ static const (เช่น bookArray)
    • upper camel-case => ชื่อ Class  (เช่น ScifiBook)
    • upper under score => ค่าคงที่ในกรณีใช้ #define (BOOK_AMOUNT) 

    ตัวอย่าง

    UIButton *settingsButton;

    ไม่ควร

    UIButton *setBut;

    ตัวอักษร 3 ตัว (หรือ 2 ตัว) นำหน้าเรียกว่า Class Prefix จะใช้กับชื่อค่าคงที่ หรือชื่อ Class เท่านั้น ค่าคงที่นั้นควรจะตั้งชื่อให้สัมพันธ์กับชื่อ Class และมีลักษณะของ camel-case

    ตัวอย่าง

    static const NSTimeInterval NYTArticleViewControllerNavigationFadeAnimationDuration = 0.3;

    ไม่ควร

    static const NSTimeInterval fadetime = 1.7;

    Property ต่างๆ ควรตั้งชื่อด้วยลักษณะ camel-case ที่นำหน้าด้วยอักษรตัวเล็ก (thisIsABook) โดย Xcode 4.5 ขึ้นจะทำ synthesize ให้กับตัวแปรที่ประกาศขึ้นให้อัตโนมัติ ในรูปแบบ

    @synthesize descriptiveVariableName = _descriptiveVariableName;


    Underscores

    ในการเข้าถึงหรือเปลี่ยนแปลงค่าของ property ต่างๆ ควรใช้ self. นั่นหมายถึงจะเข้าถึง property ตัวนั้นผ่าน getter, setter ที่มี (จากการทำ synthesize หรือเขียนขึ้นมาเอง) ไม่ใช่เข้าถึงตัวแปรตรงๆ การเข้าถึงตัวแปรตรงๆ (เมื่อ synthesize ด้วยรูปแบบด้านบนจะเข้าถึงด้วย _variableName) จะใช้ในกรณีตั้งค่าเริ่มต้น หรือมีการเข้าถึงใน Initialize method, dealloc method เท่านั้น

    Literals

    NSString, NSDictionary, NSArray และ NSNumber มีรูปแบบในการสร้างแบบใหม่เมื่อเราต้องการสร้างวัตถุที่ไม่สามารถแก้ไขได้ และควรระวัง อย่ากำหนดค่า NSArray หรือ NSDictionary ด้วยค่า nil (มีผลทำให้ App Crash)

    ตัวอย่าง

    NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
    NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"};
    NSNumber *shouldUseLiterals = @YES;
    NSNumber *buildingZIPCode = @10018;

    ไม่ควร

    NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul", nil];
    NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill", @"Mobile Web", nil];
    NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];
    NSNumber *ZIPCode = [NSNumber numberWithInteger:10018];


    CGRect Functions

    เมื่อต้องการเข้าถึง x, y, width หรือ height ของ CGRect ควรจะใช้ CGGeometry Functions แทนที่จะเข้าถึงค่านั้นๆ ด้วย struct member

    ตัวอย่าง

    CGRect frame = self.view.frame;
    
    CGFloat x = CGRectGetMinX(frame);
    CGFloat y = CGRectGetMinY(frame);
    CGFloat width = CGRectGetWidth(frame);
    CGFloat height = CGRectGetHeight(frame);

    ไม่ควร
    CGRect frame = self.view.frame;
    
    CGFloat x = frame.origin.x;
    CGFloat y = frame.origin.y;
    CGFloat width = frame.size.width;
    CGFloat height = frame.size.height;
    

    Constants

    ควรจะใช้ static const แทน macro (#define)

    ตัวอย่าง
    static NSString * const NYTAboutViewControllerCompanyName = @"The New York Times Company";  
    
    static const CGFloat NYTImageThumbnailHeight = 50.0;
    
    ไม่ควร

    #define CompanyName @"The New York Times Company"
    
    #define thumbnailHeight 2
    


    Enumerated Types

    เมื่อมีการใช้ enum แนะนำให้ใช้การประกาศ NS_Enum เนื่องจากมีคุณสมบัติ strong type checking และ code completion

    ตัวอย่าง

    typedef NS_ENUM(NSInteger, NYTAdRequestState) {
        NYTAdRequestStateInactive,
        NYTAdRequestStateLoading
    };


    Private Properties

    property ที่ต้องการให้เป็นส่วนตัวใช้เฉพาะใน class นั้นๆ ให้ประกาศ private interface ขึ้นใน implementation file และไม่จำเป็นต้องตั้งชื่อให้สื่อความหมายว่านี่เป็น private property แต่อย่างใด

    ตัวอย่าง in .m file


    @interface NYTAdvertisement ()
    
    @property (nonatomic, strong) GADBannerView *googleAdView;
    @property (nonatomic, strong) ADBannerView *iAdView;
    @property (nonatomic, strong) UIWebView *adXWebView;
    
    @end

    Booleans

    เนื่องจาก nil หมายถึง NO ได้เช่นกัน ดังนั้นจึงไม่จำเป็นต้องเปรียบเทียบ NO ในเงื่อนไข ในกรณี YES ก็เช่นกัน

    ตัวอย่าง
    if (!someObject) {
    }
    
    ไม่ควร
    if (someObject == nil) {
    }
    
    ตัวอย่างสำหรับ BOOL

    if (isAwesome)
    if (![someObject boolValue])

    ไม่ควร
    if ([someObject boolValue] == NO)
    if (isAwesome == YES) // Never do this.
    
    ในการตั้งชื่อให้ตัวแปรชนิด BOOL นั้นจะตั้งชื่อโดยใช้คำว่า "is" นำหน้า แต่ในกรณีที่ property ตัวนั้นมีชื่อเป็น adjective จะไม่ใช้ "is" นำหน้าแต่ใส่ไว้หน้าชื่อ getter method ตัวอย่างเช่น

    @property (assign, getter=isEditable) BOOL editable;


    Singletons

    ในการเขียน Shared Singleton Object ให้ใช้ Thread-safe pattern 


    + (instancetype)sharedInstance {
       static id sharedInstance = nil;
    
       static dispatch_once_t onceToken;
       dispatch_once(&onceToken, ^{
          sharedInstance = [[self alloc] init];
       });
    
       return sharedInstance;
    }

    และอย่างหนึ่งสำหรับการปิดท้ายคือ นี่คือบทความที่บอกถึงสิ่งที่ "ควร" จะเป็นในการเขียนโค้ด ไม่ใช่ข้อบังคับ ดังนั้น จะเขียนยังไงก็ได้ โค้ดบางรูปแบบ เราอาจจะเขียนอีกแบบ และก็มีเหตุผลรองรับในการเขียนของตัวเอง ฉันเข้าใจแบบนี้ ไม่ใช่เรื่องผิดครับ ลองตั้ง Protocol สำหรับตัวเองหรือ Team ดูนะครับ เวลาที่เราต้อง Maintain Code คนอื่น หรือคนอื่นต้องมาเขียนต่อเรา จะได้ไม่ลำบาก :)

    สามารถแนะนำมาได้นะครับ ใครใช้ Style ไหนที่น่าสนใจอีก ... ผมจะได้เอามาเพิ่มเติมให้ใน Blog
    เดี๋ยวผมเจออะไรที่สวยกว่า หรือน่าสนใจจะเอามาอัพเดทให้ที่นี่เหมือนกัน ^^

    Popular posts from this blog

    12 วิธี การบริการและดูแลลูกค้าในร้าน Starbucks

    "อีสุกอีใส" ประสบการณ์เมื่อต้องมาเป็นตอนอายุ 22

    [Android Dev] การติดตั้ง Eclipse+AndroidSDK เพื่อพัฒนาโปรแกรมบน Android