web analytics

การเขียนภาษา 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 กว่าจุดเลย (แฮะๆ)

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

Reference
https://dart.dev/guides/language/effective-dart