Cocoa Style for Objective-C: Part II

จาก Cocoa Style for Objective-C ตอนแรก ได้พูดการตั้งชื่อคลาส เมธอด และตัวแปรไปแล้วนั้น ตอนนี้ก็จะพูดถึงรายละเอียดที่มากขึ้นไปอีกในการตั้งชื่อที่มากไปกว่าครั้งก่อน เพราะด้วยไวยกรณ์และลักษณะของภาษา Objective-C ที่แปลกออกไปจากหลายๆ ภาษาอื่น เช่น C/C++, Java หรือ PHP และด้วยวิถีของ Apple ที่วางเอาไว้ว่าการกำหนดชื่อส่วนประกอบต้องอ่านแล้วเข้าใจได้ง่าย อธิบายได้ด้วยตัวของมันเอง

บล็อกนี้เรียบเรียงเนื้อหามาจาก Cocoa Dev Central : Cocoa Style for Objective-C Part I, Part II

ชื่อเมธอด: ออบเจ็คที่คืนกลับมา

การเข้าถึงข้อมูลภายในออบเจ็คผ่าน accessor, class method (ภาษาทั่วไปคือ static method), หรือเมธอด ออบเจ็คที่คืนกลับมานั้นก็จะขึ้นอยู่กับเงื่อนไขต่างๆ ภายในเมธอดหรือข้อมูลที่ใสเข้าไป ซึ่งจะมีรูปแบบคือ

// Pattern
[object/class thing+condition];
[object/class thing+input:input];
[object/class thing+identifier:input];

thing ในที่นี้นั่นก็คือรูปแบบข้อมูลที่จะคืนออกมา ซึ่งค่าที่คืนออกมานั้นผ่านการคิดด้วย เงื่อนไข (condition), ข้อมูลที่ส่งให้ (input, identifier) ที่ระบุถัดมา ยกตัวอย่าง เช่น

// 'thing' = string, and condition is 'DeletingLastPahtComponent'
pathToFile = [fullPathOfFile stringByDeletingLastPathComponent];

// 'thing' = string, input = 'Encoding' = NSUnicodeStringEncoding
readablePath = [pathInURLEncoded stringByReplacingPercentEscapesUsingEncoding:NSUnicodeStringEncoding];
object = [array objectAtIndex:2];

// class methods
newString = [NSString stringWithFormat:@"%@/newfile.tex", pathToFile];
newArray = [NSArray arrayWithObjects:yesterdayMessage, todayMessage, tomorrowMessage, nil];

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

// Pattern
[object adjective+thing];
[object adjective+thing+condition];
[object adjective+thing+input:input];

ยกตัวอย่างเช่น

// Usage
capitalizedUsername = [username capitalizedString];
newString = [string decomposedStringWithCanonicalMapping];
subarray = [array subarrayWithRange:segment];

หลีกเลี่ยงการใช้คำที่ให้ความหมายไม่ชัดเจน

ข้อผิดพลาดในโปรแกรม (Bug) ที่เกิดขึ้นส่วนหนึ่งก็เพราะการใช้คำพูดที่กำกวนในชื่อคลาสหรือเมธอดที่ตั้ง ตัวอย่างเช่น

// Ambiguous Messages
-sortInfo
-refreshTimer
-update
-fetchInfo:

ถ้ายืดตามการตั้งชื่อของ Cocoa แล้วก็จะได้ว่า

  • sortInfo จะคืนข้อมูลที่เป็น sort info (อาจมองได้ว่าเป็น getter method) หรือเปล่า หรือคืนการจัดเรียง (sort) บางอย่างที่เรียกว่า “info”
  • refreshTimer คืน “Timer” สำหรับ refresh หรือจะ refresh ตัว Timer
  • update อัพเดตอะไร ?
  • fetchInfo: จะ fetch ตัวแปร Info หรือเป็นเพียงการส่ง info ของการ fetch

ทางแก้

  • currentSortInfo ให้ความหมายได้ดีกว่าชื่อเดิม
  • refreshDefaultTimer คำว่า refresh ในชื่อใหม่นี่จะอธิบายความได้ชัดกว่าชื่อเดิม นอกจากนั้นยังไม่โดนมองเป็น getter method ด้วย
  • updateMenuItem บ่งชี้ไปว่าจะอัพเดตอะไร
  • infoForFetch: เมธอดนี้คืน Info สำหรับ fetch ที่ส่งให้

Global C Functions

สำหรับ Global C Function นั้นจะใช้รูปแบบทั่วไปดังนี้

// Global C Functions Pattern
Prefix + Value ()
Prefix + Value + With/From/For + Input ()
Prefix + Action ()
Prefix + Action + Type ()

จำเห็นได้ว่าการหลักการตั้งชื่อจะเหมือนกับชื่อคลาสที่ต้องเริ่มต้นด้วย Prefix เพื่อหลีกเลี่ยงชื่อที่ซ้ำซ้อนกันตัวอย่างเช่น

// Global C Functions Example
NSHomeDirectory()
NSHomeDirectoryForUser()
NSClassFromString()
NSBeginAlertSheet()
NSDrawGrayBezel()

Global Symbols อื่นๆ

นอกจากฟังก์ชั่นด้านบนแล้ว ก็ยังมี Symbol อื่นๆ มากกว่านั้น ตัวอย่างเช่น

  • Constants
  • Typedef’d structs
  • Typedef’d enums
  • Individual enum values
  • Objective-C Protocols

ซึ่งทั้งหมดนี้ก็มีวิธีการตั้งชื่อเช่นเดียวกันกับคลาสหรือ C functions

// Structs
NSPoint       point; // struct
NSRange       range; // struct
NSRectArray  *rects; // c-style array of structs

สำหรับ Cocoa นั้น enums มักจะใช้ “mode” สำหรับ methods

range = [string rangeOfString:@"find me" options:NSLiteralSearch];

โดยทั้ง constants และ enums นั้นจะมี suffix ที่บ่งชี้ประเภทของตัวเอง เช่น

// Constants & Enums

// search modes (enums)
NSLiteralSearch
NSCaseInsensitiveSearch

// exception names (constants)
NSMallocException
NSInvalidArgumentException

// notification names (constants)
NSTaskDidTerminateNotification
NSWindowWillMoveNotification

สำหรับ notification นั้น จะมีรูปแบบกันตั้งชื่อที่ต่างไปคือ

Class of Affected Object + Did/Will + Action + "Notification"

Dynamic Typing

Objective-C เป็นภาษาที่ยืดหยุ่น (dynamically-typed) กล่าวคือไม่ต้องบอกว่าคอมไพล์เลอร์ว่าตอนนี้เรากำลังใช้ออบเจคนี้ด้วยชนิด (type) อะไรขณะคอมไพล์ การประกาศตัวแปรแบบนี้เป็นเพียงแค่การสัญญาว่าจะมีการเปลี่ยนแปลงชนิดนี้ในส่วนใดส่วนหนึ่งของโค้ดเท่านั้น สำหรับใน Objective-C จะใช้ id เพื่อแทนออบเจคของ Objective-C

// Dynamically-Typed Variables
id hostName;
id ipAddress;
id keyedAccountNames;
id theObject;

// the compiler is fine with this
theObject = [NSString string];
theObject = [NSNumber numberWithInt:1];
theObject = [NSDictionary dictionary];

มี 3 เหตุผลที่ต้องระบุชนิดของตัวแปรทั้งหมด คือ

  1. เพื่อความชัดเจน เพื่อทำให้เกิดความชัดเจนในสิ่งที่จะทำกับตัวแปรนั้น
  2. หลีกเลี่ยงคำเตือนต่างๆ ที่ไม่จำเป็นเท่าไหร่ เช่น การเตือนให้ส่งเมสเซจที่ไม่จำเป็นไปยังออบเจคนั้น
  3. ได้คำเตือนที่เป็นประโยชน์จากคอมไพล์เลอร์ ในกรณีที่ใช้เมสเซจผิด คอมไพล์เลอร์ก็จะเตือนเรา เช่น ใช้ -string ใน NSArray

สำหรับการใช้งาน id สำหรับ dynamic type เหมาะสำหรับการใช้งานประเภท

  1. delegate หรือ datasource
  2. notification ของออบเจค
  3. content ใน container
  4. ออบเจคที่ทำหน้าที่เกี่ยวกับ target หรือ action

การตั้งชื่อพารามิเตอร์

การตั้งชื่อพารามิเตอร์นั่นมักจะนำหน้าด้วย 'the', 'an' หรือ 'new'

- (void)       setTitle:           (NSString *)   aTitle;
- (void)       setName:            (NSString *)   newName;
- (id)         keyForOption:       (CDCOption *)  anOption;
- (NSArray *)  emailsForMailbox:   (CDCMailbox *) theMailbox;
- (CDCEmail *) emailForRecipients: (NSArray *)    theRecipients;

นอกจากนั้นแล้วก็สำหรับการตั้งชื่อตัวแปรใน loop นั้น มักจากนำหน้าตัวแปรที่อยู่ใน loop นั้นด้วย 'one' หรือ 'a/an' เพื่ออ้างถึงตัวแปรในที่อยู่ในลักษณะเดี่ยวๆ เช่น

for ( i = 0; i < count; i++ ) {
    oneObject = [allObjects objectAtIndex: i ];
    NSLog( @"oneObject: %@", oneObject );
}

NSEnumerator *e = [allObjects objectEnumerator];
id item;

while( item = [e nextObject] )
    NSLog( @"item: %@", item );

Odds and Ends

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

// NSColor
color = [NSColor colorWithCalibratedHue: 0.10
                             saturation: 0.82
                             brightness: 0.89
                                  alpha: 1.00];

สำหรับคลาสที่มีการใช้งานแบบ Singleton ชื่อของเมธอดจำนำหน้าด้วย shared

// Singleton
@implementation SLManagedSemesterContext

+ (id) sharedSemesterContext
{
    static SLManagedSemesterContext *shared = nil;
    if ( ! shared )
        shared = [[self alloc] init];

    return shared;
}