การเขียนภาษา Dart ที่มีประสิทธิภาพ

สวัสดีครับ ทุกท่าน วันสองวันนี้เป็นช่วงเวลาว่างๆ เลยหาอะไรมาอ่านเล่น ผมไปเจอกับบทความใน guides ของเว็บไซต์ dart.dev ที่มีเนื้อหาเกี่ยวกับแนวทางการเขียน Dart อ่านแล้วก็น่าสนใจดี เพราะช่วงแรกๆของผมนั้น ผมชอบเขียนปะปนกับ Java style ใน dart เสมอๆ เลยนำมาสรุปไว้ในบล็อกนี้ โดยผมเลือกมาเฉพาะส่วนหลักๆที่ผมคิดว่าสำคัญกับการทำงานร่วมกันกับคนอื่นๆ
โดยขอย้ำว่า นี่เป็นเพียงแนวทางเท่านั้นนะ ไม่จำเป็นต้องทำตามทั้งหมดก็ได้ สุดท้ายแล้วก็อยู่ที่การตกลงกันภายในทีม
สามารถอ่านฉบับเต็มได้ที่นี่เลย
https://dart.dev/guides/language/effective-dart
ไม่ใช้ “new” keyword
ตั้งแต่ dart2 คำว่า “new” ที่ใช้เวลาเราสร้าง instance นั้น ได้กำหนดเป็น optional สิ่งนี้ช่วยลด code ที่ซ้ำๆไปได้เยอะเลย ดังนั้นเลิกใช้มันได้เลย
/*new*/ Text('Increment');
บรรทัดละ 80 ตัวอักษร
Dart บอกว่า อ้างอิงจากผลการศึกษาที่ว่าสายตาคนเราจะกวาดตาอ่านได้ดี คือราวๆ 80 ตัวอักษรในหนึ่งบรรทัด นั่นเป็นเหตุผลว่าทำไมหนังสือพิมพ์ถึงมีหลายๆคอลัมน์
ตัวอย่าง code ที่มี 80 ตัวอักษร
Navigator.push(context, MaterialPageRoute(builder: (_) => CreditCardScreen()));
สำหรับใน Android Studio สามารถปรับค่าของการขึ้นบรรทัดใหม่อัตโนมัติ หรือเรียกว่า Hard Wrap ได้ที่ Setting และค้นหาคำว่า “hard wrap” ปรับได้ตามใจเลย

Classes , Enum , Type , Extension
ชื่อ Class จะใช้การตั้งชื่อแบบ UpperCamelCase รวมถึง enum , typedef ด้วย
// CLass
class Employee { ... }
// Enum
enum DisplayMode {GRID , LIST}
// Typedef
typedef Predicate<T> = bool Function(T value);
//Extension
extension MyCustomList<T> on List<T> { ... }
ชื่อไฟล์ Dart และ Package
ชื่อไฟล์ dart , directory, library, ชื่อ package ต่างๆ จะใช้การตั้งชื่อแบบ lowercase_with_underscores
import 'splash_screen.dart';
Import prefixes
การตั้งชื่อให้กับ package ที่ import เข้ามา จะใช้ keyword “as” โดย prefix ที่ว่านี้ แนะนำให้ตั้งชื่อแบบ lowercase_with_underscores
import 'package:date_picker/date_components' as date_components;
ลำดับของ Import
Dart แนะนำให้จัดลำดับการ import จะแบ่งเป็น 4 กลุ่ม เรียงตามลำดับคือ
1. กลุ่ม dart หรือ Flutter ที่เป็น core package
2. กลุ่ม package แบบชื่อเต็ม ที่เป็น library
3. กลุ่ม package แบบ relative path ในโปรเจคของเรา
4. กลุ่ม export สำหรับทำเป็น external package
จากนั้นก็เรียงตามลำดับตัวอักษรในแต่ละกลุ่ม
// Dart or Flutter
import 'dart:async';
import 'dart:html';
// Packages
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';
// Relative imports
import 'util.dart';
// Export section
export 'src/error.dart';
สำหรับไฟล์ dart ภายในโปรเจคของเรา จะสามารถ import แบบ package: หรือ relative path ก็ได้ ซึ่ง Dart แนะนำให้ใช้แบบ relative path
เช่น
my_package
└─ lib
├─ src
│ └─ utils.dart
└─ api.dart
import 'src/utils.dart';
แต่ใน Android Studio หลายๆครั้ง มักจะแนะนำให้ใช้แบบ package: ซะอย่างนั้น แม้จะเป็น dart ในโปรเจคของเราเองก็ตาม

ตัวแปรต่างๆ (Variable)
ตัวแปรไม่ว่าจะเป็น members ของ Class หรือที่อยู่ top-level รวมทั้งเป็น parameters หรือ named parameters จะใช้การตั้งชื่อแบบ lowerCamelCase
var data;
CoffeeProvider coffeProvider;
void start(bool force){
...
}
การตั้งชื่อตัวแปรที่ไม่ใช่ประเภท boolean แนะนำให้ใช้คำนาม และหลีกเลี่ยงการใช้คำที่มาจากตัวย่อ
pageCount
list.length
context.lineWidth
// numPages "num" เป็นตัวย่อของคำว่า number(of)
ค่าคงที่ (Constant)
ค่าคงที่ต่างๆจะใช้ keyword ว่า “const” หรือ “final” และจะใช้การตั้งชื่อแบบ lowerCamelCase
// Constant variable
const pi = 3.14;
const defaultTimeout = 1000;
// Constant
class Dice {
static final numberGenerator = Random();
}
คำย่อ (acronyms)
การตั้งชื่อ Class ที่มาจากคำเต็มๆแล้วต่อย่อให้สั้น จะแบ่งเป็น 2 กรณี คือ กรณีที่ย่อแล้วมี 2 ตัวอักษร และกรณีที่ยาวกว่า 2 ตัวอักษร
2 ตัวอักษร
เช่น
input/output = IO
User Interface = UI
การตั้งชื่อ Class จะใช้ตัวใหญ่ทั้งหมด (Fully Capitalized) และใช้ lowerCamelCase สำหรับชื่อตัวแปร
class IO {}
class UIHandler{}
var uiHandler = UIHandler();
มากกว่า 2 ตัวอักษร
เช่น
HTTP = Hypertext Transfer Protocol
ใช้การตั้งชื่อขึ้นต้นตัวใหญ่ตามด้วยตัวเล็ก (Capitalized)
class HttpConnection {}
ตัวย่อ (abbreviations)
ตัวย่อคือการตัดเอาตัวอักษรบางส่วนของคำมาใช้เรียกสั้นๆ
เช่น
id = identifier
ชื่อ Class จะใช้ Capitailzed เช่น Id
ส่วนชื่อตัวแปรจะใช้ lowerCamelCase
class UserId { ... }
UserId userId = UserId();
Method
method โดยทั่วไปแนะนำให้ตั้งชื่อที่เมื่ออ่านแล้วมีลักษณะเป็นประโยค จะทำให้สามารถเข้าใจได้ง่าย
// "If errors is empty..."
if (errors.isEmpty)
หากมี return type เป็น boolean ให้ใช้ชื่อเป็นคำคุณศัพท์หรือคำกริยา
if (window.closeable) ... // Adjective.
if (window.canClose) ... // Verb.
แนะนำว่าให้ใช้ชื่อที่เป็นไปในทางบวก
เช่น isConnected แทน isNotConnected
if (socket.isConnected && database.hasData) {
...
}
หาก method นั้น ทำงานลักษณะ copy ไปเป็น state ใหม่ให้ใช้คำว่า to ขึ้นต้น
DaTeim local = dateTime.toLocal();
หาก method นั้น ทำงานลักษณะเปลี่ยน object เป็น type ที่ต่างออกไป ให้ใช้คำว่า as ขึ้นต้น
var map = table.asMap();
แล้วก็ในชื่อ method ไม่จำเป็นต้องอธิบาย argument ว่าทำอะไร เพราะยังไง ก็สามารถดูจาก definetion ได้ และอาจจะยิ่งทำให้สับสน
list.add(element);
map.remove(key);
// list.addElement(element);
// map.removeKey(key);
Callback parameters ที่ไม่ใช้
หลายครั้งจะมี callback ที่มี parameter แต่เราไม่ได้ใช้ ให้ตั้งชื่อมันด้วย _ (underscore) หรือ ถ้ามีหลาย parameter จะใช้ __ , ___ ไปเรื่อยๆ
futureMethod.then((_) {
print('Done.');
});
futureMethod.then((_,__) {
print('Done.');
});
Prefix letters
การตั้งชื่อตัวแปรโดยใช้ตัวอักษรนำหน้าเพื่อระบุประเภทอะไรบางอย่าง ใน dart จะไม่แนะนำให้ใช้
แต่ว่าใน Flutter Repository นั้นจะมีใช้ prefix แบบนี้บ้าง เช่น จะใช้ “k” เป็น prefix สำหรับ ค่าคงที่ที่เป็น global
// Dart
const double paragraphSpacing = 1.5;
// Flutter repo
const double kParagraphSpacing = 1.5;
เรื่องของ space
เว้น space ระหว่าง operator เพื่อให้อ่านง่าย
average = (a + b) / 2;
largest = a > b ? a : b;
เว้น space หลัง , และ : เสมอ
function(a, b, named: c)
[some, list, literal]
{map: literal}
การเขียน for loop ต้องเว้น space แบบนี้
for (var i = 0; i < 100; i++) { ... }
for (final item in collection) { ... }
และให้เว้น space ก่อน { ของ method body
getEmptyFn(a) {
...
}
การเขียน if
อย่างที่รู้กันว่า if-else มี style เขียนหลายแบบ เคยเป็นที่ถกเถียงกันในกลุ่มโปรแกรมเมอร์กันด้วย ว่า style ไหนคือดีที่สุด ส่วนใน dart แนะนำว่าให้ใช้แบบนี้
class Foo {
method() {
if (someCondition) {
// ...
} else {
// ...
}
}
}
กรณีไม่มี else จะเขียนแบบบรรทัดเดียว แบบที่ไม่มี { } ก็ได้ แต่ถ้าขึ้นบรรทัดใหม่ให้ใช้ { } เสมอ
if (arg == null) return defaultValue;
if (overflowChars != other.overflowChars) {
return overflowChars < other.overflowChars;
}
Switch case
การเขียน switch case จะมี indent และเว้นบรรทัดระหว่าง case ทุกครั้ง
switch (fruit) {
case 'apple':
print('delish');
break;
case 'durian':
print('stinky');
break;
}
Constructor
ส่วนกำหนดค่าเริ่มต้นใน constructor ให้เขียนแยกบรรทัดในแต่ละ field
MyClass()
: firstField = 'some value',
secondField = 'another',
thirdField = 'last' {
// ...
}
Comments
comment ที่เราเห็นกันบ่อยๆจะมี 3 แบบ คือ
//
/* */
///
// คือ comment โดยทั่วไปจะใช้เพื่ออธิบายเพิ่มเติมว่ากำลังทำอะไร
greet(name) {
// Assume we have a valid name.
print('Hi, $name!');
}
ส่วน /* */ หรือเรียกว่า Block comment จะใช้สำหรับ comment code ส่วนที่ไม่ใช้
greet(name) {
// Assume we have a valid name.
print('Hi, $name!');
}
/*
hello() {
print('Hello');
}
*/
และ comment /// จะใช้สำหรับเป็นคำอธิบายแบบทางการ สามารถนำไป generate เป็น document ได้
/// The number of characters in this chunk when unsplit.
int get length => ...
อย่างไรก็ตาม การ comment แบบ /// นั้นมาจาก C# แต่ว่าใน dart ยังรองรับ /** */ ของ Java โดยทั้งสองแบบเป็น comment doc สามารถใช้ได้ทั้งคู่ แต่ dart แนะนำแบบ C# มากกว่า เพราะอ่านง่ายกว่า
forEach
แนะนำให้ใช้ for (var .. in ..) สำหรับคำสั่งหลาย statement ส่วน .foreach() จะใช้สำหรับกำหนดชื่อ method ลงไปเป็น parameter
for (var person in people) {
...
}
// people.forEach((person) {
// ...
// });
people.forEach(print);
ไม่ต้องเขียน type ถ้าไม่จำเป็น
.map(T) เป็น method ที่น่าจะใช้กันบ่อยๆ โดย Dart แนะนำว่าไม่ต้องเขียนชื่อ type ใน parameter ของ map()
var names = people.map((person) => person.name);
// var names = people.map((Person person) => person.name);
type arguments ของ Generic ก็แนะนำว่าเขียน type เพียงจุดเดียวก็พอ
Set<String> things = Set();
var things = Set<String>();
//Set<String> things = Set<String>();
List<String> list = List();
//List<String> list = List<String>();
ใช้ => สำหรับ method ง่ายๆ
method ไหนที่ทำงานง่ายๆ หรือเป็น getter ถ้าเป็นไปได้ให้ใช้ => expression
double get area => (right - left) * (bottom - top);
// double get(){
// return (right - left) * (bottom - top);
// }
Dart Analysis !
ทั้งหมดที่ว่าไปนี้ มีบางส่วนอยู่ใน Android studio เรียบร้อยแล้ว เช่น naming ตัวแปร โดยมันติดมากับ dart plugin นั่นเอง ทำให้เวลาเราเขียน Flutter จะมีหน้าต่าง Dart Analysis คอยเช็คให้ พร้อมกับแสดง warning

เราสามารถไป ignore ได้
// ignore: non_constant_identifier_names
String KEY_LANGUAGE = "language";
แต่ถ้าไม่สนใจจะเขียนตาม Guide นี้ ก็ยังสามารถ compile ได้ตามปกติ อย่างผมเองเขียนไปเขียนมา ใน dart analysis ก็มี warning 400 กว่าจุดเลย (แฮะๆ)

หวังว่าบทความจะมีประโยชน์กับผู้อ่านครับ (: