พอดีได้อ่านหนังสือ Good Code, Bad Code ของ Manning แล้วมีส่วน 6 Pillars of code quality เลยคิดว่า เอามาลองเขียน Blog จดๆไว้ดีกว่า ในนี้ใส่ตัวอย่างทั้ง dotnet (C#) / spring / golang ตามความเข้าใจของผมนะครับ
Make code readable
ทำให้ Code อ่านง่ายขึ้น โดยอาจจะมีแนวคิดที่ส่งต่อกัน Function/Method ไม่ควรเกิน 1 หน้าจอ หรือ 20 บรรทัด และอย่างอื่น เช่น
📌 การตั้งชื่อของทุกอย่างเลย ตัวแปร / method หรือพวก ชื่อ deployment ด้วย ควรตั้งให้สื่อ กับ Business ที่มันไปตอบโจทย์ได้
- CalcA1 กับ CalcBondPriceThorFormular อ่านแล้วเข้าใจได้เลย
- หรือ ที่เจอหลังพวก Cloud VNET1 / APP1 แล้วคนมาดูต่อต้องไป Map เองอีกว่า 1 คือ Service ไหน Dev/ Prod
📌Method / Function ที่สั้น และทำงานอย่างหนึ่ง (Single Responsibility) ถ้ามันยาวเกินไปควร Extract ออกมาให้เล็กจะได้เขียน Unit Test ได้ง่าย
📌Comment ก็สำคัญนะ แต่ถ้า Code บอกได้ชัดเจน + มี Test อยู่แล้ว ลดการเขียนก็ได้นะ เพราะจากประสบการณ์ Code เปลี่ยน แต่ Comment ยังเหมือนเดิมตั้งแต่ปี 1 ? เคยเจอ 10 ปี มันขัดแย้งกับ Code แล้ว
ถ้าจะให้ดีที่สุดเขียน Code ให้มันสื่อไปเลย จะได้ลดความขัดแย้งของ Comment / Code ได้ครับ เช่น กำหนดชื่อ Method ให้ชัด เป็นต้น
ตัวอย่าง Code
- C#
// C# - readable example
public class Order
{
public int Id { get; init; }
public decimal Amount { get; init; }
}
public interface IPaymentGateway
{
bool Charge(decimal amount);
}
public class OrderService
{
private readonly IPaymentGateway _gateway;
public OrderService(IPaymentGateway gateway) => _gateway = gateway;
// ชื่อเมธอดชัดเจน: ProcessOrder => ทำอะไรบ้างเห็นได้ชัด
public bool ProcessOrder(Order? order)
{
if (order == null) throw new ArgumentNullException(nameof(order));
if (order.Amount <= 0) return false;
return _gateway.Charge(order.Amount);
}
}
- Spring
// Spring - readable example
@Service
public class OrderService {
private final PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
// ชื่อเมธอดและพารามิเตอร์ชัดเจน
public boolean processOrder(OrderDto order) {
if (order == null) throw new IllegalArgumentException("order required");
if (order.getAmount().compareTo(BigDecimal.ZERO) <= 0) return false;
return paymentGateway.charge(order.getAmount());
}
}
- Golang
// Go - readable example
package orders
type Order struct {
ID int
Amount float64
}
type PaymentGateway interface {
Charge(amount float64) bool
}
type Service struct {
gateway PaymentGateway
}
func NewService(g PaymentGateway) *Service {
return &Service{gateway: g}
}
func (s *Service) ProcessOrder(o *Order) bool {
if o == nil || o.Amount <= 0 {
return false
}
return s.gateway.Charge(o.Amount)
}
Avoid surprises
ทำให้ Code มีชัดเจน ส่ง Input เดิมไปทุกรอบ ผลลัพธ์ที่กลับมาต้องได้เท่าเดิม ไม่งั้นตอนทำ Test จะเกิด Flaky Test ได้ บางครั้งถูก บางครั้งก็ผิด รวมถึงการลดปัญหา side-effects ขอขยายความ side-effects ว่าเกิดจากอะไร
📌 การเปลี่ยนของ property ใน Class / DTO หรือ การเปลี่ยนค่าตัวแปร global รวมถึงเข้าใจ ByRef / ByValue
📌 การจัดการกับพวกเวลา ที่มันเปลี่ยนตลอดเวลา หรือตัวเลขแบบสุ่ม Date.now(), Math.random()
📌 การจัด nulls ถ้าได้มาควรจะคืนอะไรให้คนเรียกไม่พัง
📌 การจัดการพวก throw exception, process.env เปลี่ยนค่า
📌 การจัดการกับ External System พวก I/O: อ่าน/เขียนไฟล์, เรียก API, เขียนฐานข้อมูล
ตัวอย่าง Code
- C#
// Avoid surprises: do not mutate input and return clear result
public class StringUtils
{
// ไม่เปลี่ยน string ที่ส่งมา (immutable)
public static string Normalize(string? input)
{
if (input == null) return string.Empty;
return input.Trim().ToLowerInvariant();
}
}
- Spring
// Avoid surprises: return Optional instead of nulls, explicit transactional boundaries
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
@Service
@Transactional(readOnly = true)
public class UserService {
private final UserRepository repo;
public UserService(UserRepository repo) { this.repo = repo; }
public Optional<UserDto> findByEmail(String email) {
return repo.findByEmail(email).map(UserDto::fromEntity);
}
}
- Golang
// Avoid surprises: explicit error returns, no panics
func ParseAmount(s string) (float64, error) {
if s == "" {
return 0, fmt.Errorf("empty amount")
}
a, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, fmt.Errorf("invalid amount: %w", err)
}
return a, nil
}
Update: แถมอีกเคสนึง เพราะระหว่าง Review Code น้องในทีมเคส Sample เกี่ยวกับ DateTime พอดี เป็น C# แบบที่ผิด เพราะเราคุม State DateTime.Now ไม่ได้ และยัง Test ยากด้วย
// C#
public void UpdateStatus() { if (DateTime.Now.Hour > 17) globalStatus = "Closed"; }
// Golang
func UpdateStatus() { if time.Now().Hour() > 17 { globalStatus = "Closed" } }
ส่วนที่ปรับปรุงจะเป็นประมาณนี้ ย้ายให้มันมาเป็น parameter ซะ
- C#
public string GetStatus(DateTime currentTime) => currentTime.Hour > 17 ? "Closed" : "Open";
- Spring
public String getStatus(LocalDateTime currentTime) {
return currentTime.getHour() > 17 ? "Closed" : "Open";
}
- Golang
func GetStatus(currentTime time.Time) string {
if currentTime.Hour() > 17 {
return "Closed"
}
return "Open"
}
Make code hard to misuse
ออกแบบ API ให้ทนทานต่อการใช้งานผิดๆ API ในนี้พวก REST และ Method ต่างๆ เอาง่ายอะไรที่มีการเรียกใช้งาน โดยการทำให้ code ทนทานต่อการใช้งานผิดๆ มีดังนี้
📌 Make illegal states unrepresentable - บังคับจาก Types ลดการเกิดข้อผิดพลาด
📌 Fail fast & loudly - ตรวจการใช้งานผิดตั้งแต่ compile-time หรือ runtime - ทำ validation ด้วยข้อความชัดเจน
📌 Provide safe defaults - ตั้งค่าที่ปลอดภัยเมื่อผู้ใช้ไม่กำหนด
📌 Small surface area - ทำให้ Code ทำงานเพียงอย่างเดียว ให้การตีความมันชัดเจนในตัว ลดตัวตัวเลือกที่ผู้ใช้ต้องเรียนรู้ ลด Cognitive Load ด้วย การทำ factories/builders
📌 Explicit over implicit - type-safety
📌 Encapsulate mutability - ซ่อนการเปลี่ยนแปลง state และให้ API เป็น immutable/transactional
📌 Prefer types to docs - ให้ type/signature บอกวิธีใช้ มากกว่าอ่าน docs เท่านั้น
ตัวอย่าง Code
- C#
// Make misuse harder: immutable value object + factory
public class Email
{
public string Value { get; }
private Email(string value) => Value = value;
public static Email Create(string email)
{
if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("email empty");
if (!email.Contains("@")) throw new ArgumentException("invalid email");
return new Email(email.Trim().ToLowerInvariant());
}
}
- Spring
// Make misuse harder: DTO validation + @Validated service
public class CreateUserRequest {
@NotBlank
@Email
private String email;
// getters/setters
}
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public ResponseEntity<Void> createUser(@Valid @RequestBody CreateUserRequest req) {
// Spring will validate automatically; invalid payload -> 400
// ... create user
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
- Golang
// Make misuse harder: unexported fields + constructor that validates
type Config struct {
host string
port int
}
func NewConfig(host string, port int) (*Config, error) {
if host == "" { return nil, fmt.Errorf("host required") }
if port <= 0 { return nil, fmt.Errorf("invalid port") }
return &Config{host: host, port: port}, nil
}
// users outside package cannot set fields directly
Update อันนี้พี่ปุ๋ย Somkiat.cc มีมาแนะนำว่าพวก Java มี Type Record แล้ว จาก Post ใน สมาคมโปรแกรมเมอร์ไทย ผมเลยลองใช้ดู ผมเลยลองมาดู C# ก็มีเหมือนกัน เลยมาเพิ่มตัวอย่าง Bad เป็นของ C#
// Bad (Primitive Obsession) in C# / But Same in Java
public void ProcessPayment(decimal amount, string currency)
{
// Add Validation Check
if (amount < 0) throw new ArgumentException("Amount cannot be negative");
// ... logic ...
}
และสิ่งที่ปรับ มีทั้ง C# / Spring (Java) / Golang ด้วยครับ
- C#
public readonly record struct Money
{
public decimal Amount { get; init; }
public string Currency { get; init; }
public Money(decimal amount, string currency)
{
if (amount < 0) throw new ArgumentException("Money cannot be negative");
if (string.IsNullOrWhiteSpace(currency)) throw new ArgumentException("Currency is required");
Amount = amount;
Currency = currency;
}
// You can Encapsulation Logic Business Logic
public Money Add(Money other)
{
if (this.Currency != other.Currency)
throw new InvalidOperationException("Cannot add different currencies");
return new Money(this.Amount + other.Amount, this.Currency);
}
}
- Java
public record Money(BigDecimal amount, String currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
if (currency == null || currency.isBlank()) {
throw new IllegalArgumentException("Currency is required");
}
}
// You can Encapsulation Logic Business Logic
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalStateException("Currencies do not match");
}
return new Money(this.amount.add(other.amount), this.currency);
}
}
- Golang
package finance
import (
"errors"
"fmt"
)
// Money is a value object. Fields are unexported/private (lowercase)
type Money struct {
amount float64
currency string
}
// NewMoney acts as a "Fail Fast" factory to create a object from outside this package.
func NewMoney(amount float64, currency string) (Money, error) {
if amount < 0 {
return Money{}, errors.New("amount cannot be negative")
}
if currency == "" {
return Money{}, errors.New("currency is required")
}
return Money{amount: amount, currency: currency}, nil
}
// Methods as business logic.
func (m Money) Add(other Money) (Money, error) {
if m.currency != other.currency {
return Money{}, fmt.Errorf("cannot add %s to %s", other.currency, m.currency)
}
return Money{amount: m.amount + other.amount, currency: m.currency}, nil
}
// Getter methods provide read-only access, maintaining immutability.
func (m Money) Amount() float64 { return m.amount }
func (m Money) Currency() string { return m.currency }
Make code modular
แยก package/module หรือ ถ้าระบบเก่าๆ แบบ Layer (Controller / Service / Repository / DTO) หรือ Clean Architecture > Modular Monolith
ถ้าใหญ่หน่อย Microservices (service-per-capability) / Event-driven / CQRS + Event Sourcing
การจะแยกได้ลองเอาแนวคิดพวกนี้มาช่วย
📌 อาจจะนำเทคนิคของ Domain Driven Design มาช่วย หาก่อนเลยว่าอะไร Core / Boundary ที่ส่วนทับซ้อนของ Domain นั้นๆ ของ API contracts, data ownership
📌 Separation of Concerns (SoC): แยกหน้าที่ตามความรับผิดชอบทางตรรกะ (UI, Application logic, Domain, Infrastructure) รวมของที่เกี่ยวข้องกับไว้ด้วยกันตาม High cohesion / Low coupling
📌 Single Responsibility Principle (SRP): แต่ละโมดูล/คลาสรับผิดชอบอย่างเดียว
📌 ใช้ dependency injection, ทำให้ทดสอบได้สะดวกขึ้น ไม่ต้องมีพิธีการเตรียมการอะไรมากมาย
📌 Keep It Simple (KISS) & YAGNI: อย่าสร้างความซับซ้อนเกินจำเป็น
ตัวอย่าง Code
- C#
//interface + DI
public interface IOrderRepository { Order Get(int id); void Save(Order o); }
public class OrderRepository : IOrderRepository { /* db logic */ }
public class OrderController
{
private readonly OrderService _service;
public OrderController(OrderService service) { _service = service; }
public IActionResult PlaceOrder(OrderDto dto) {
// controller delegates to service
_service.ProcessOrder(dto.ToOrder());
return new Ok();
}
}
- Spring
//Controller -> Service -> Repository
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService service;
public OrderController(OrderService service) { this.service = service; }
@PostMapping
public ResponseEntity<Void> placeOrder(@RequestBody OrderDto dto) {
service.placeOrder(dto);
return ResponseEntity.ok().build();
}
}
- Golang
// packages: package orders and package payment
// orders.Service depends on payment.Gateway interface
package orders
type Gateway interface { Charge(amount float64) bool }
type Service struct {
gateway Gateway
}
// in other package implement Gateway, provide to Service via NewService
Make code reusable
ทำให้ Code เอามาใช้ซ้ำในเรื่องอื่นๆได้ โดยการนำ Function / Method / API ที่เล็กและเจาะจง ทำเป็น Common ให้คนอื่นเรียกใช้ได้ หรือ ถ้ามันมีโครงคล้ายๆกัน อาจจะนำแนวคิด generics/parametrization มาช่วยได้
ตัวอย่าง Code อันนี้หลายคนอาจจะงง ทำไมอยู่ๆใช้ Generic เลย เดียวขอยกตัวอย่างก่อนแก้มาด้วย
// Sample bad code C# Version / but can apply in other language
public class UserResult
{
public bool IsSuccess { get; }
public User Value { get; }
public string Error { get; }
}
public class OrderResult
{
public bool IsSuccess { get; }
public Order Value { get; }
public string Error { get; }
}
public class ProductResult
{
public bool IsSuccess { get; }
public Product Value { get; }
public string Error { get; }
}
Improve Code, Example
- C#
//generic Result<T> for operations
public class Result<T>
{
public bool IsSuccess { get; }
public T Value { get; }
public string Error { get; }
// constructors/factories...
}
- Spring
//generic paging/util component
public class PageResult<T> {
private List<T> items;
private int total;
// getters/setters
}
- Golang - แต่อันนี้นานาจิตตะ บางส่วนมองว่า Simple is the best ตอนเชียน Blog ผมก็สงสัยหน้าตามันไม่เหมือน พวก dotnet / spring
//Required Go 1.18+ generics
package slices
func Map[T any, R any](in []T, fn func(T) R) []R {
out := make([]R, 0, len(in))
for _, v := range in { out = append(out, fn(v)) }
return out
}
Make code testable and test it properly
Code ที่เขียนต้องทดสอบได้ และที่สำคัญต้องได้รับการทดสอบจริงๆ อันนี้ผมเจอมาเจอมี Test แต่ไม่ถูก Assert ใน dotnet Coverage มันขึ้น แต่มันไร้ประโยชน์ สำหรับแนวคิดที่ช่วย
📌 แยก Method ให้เล็ก ย่อยที่สุด ไม่จำงานเยอะ เกินหน้าที่ไป
📌 แนวคิด Dependency Injection เพื่อ mock, แยก unit / integration tests, ใช้ table-driven tests
📌 การทดสอบมีทั้งเคสปกติ และเคส Error ตาม Business หรือ พวกเล็กน้อยๆ อย่างค่าขอบ (boundary)
ตัวอย่าง Code
- C#
// C# unit test using xUnit and Moq
public class OrderServiceTests
{
[Fact]
public void ProcessOrder_ValidOrder_ChargesGateway()
{
var mock = new Mock<IPaymentGateway>();
mock.Setup(g => g.Charge(It.IsAny<decimal>())).Returns(true);
var svc = new OrderService(mock.Object);
var order = new Order { Id = 1, Amount = 100m };
var result = svc.ProcessOrder(order);
Assert.True(result);
mock.Verify(g => g.Charge(100m), Times.Once);
}
}
- Spring
// Spring controller unit test
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired MockMvc mvc;
@MockBean OrderService service;
@Test
void postOrder_callsService() throws Exception {
String json = "{\"id\":1,\"amount\":100}";
mvc.perform(post("/orders").contentType(MediaType.APPLICATION_JSON).content(json))
.andExpect(status().isOk());
verify(service).placeOrder(any());
}
}
- Golang - Table-Driven Tests
// Go unit tests (table-driven)
func TestParseAmount(t *testing.T) {
tests := []struct{
input string
want float64
wantErr bool
}{
{"12.5", 12.5, false},
{"", 0, true},
{"abc", 0, true},
}
for _, tt := range tests {
got, err := ParseAmount(tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("input=%q expected err=%v got=%v", tt.input, tt.wantErr, err)
}
if !tt.wantErr && got != tt.want {
t.Fatalf("input=%q want %v got %v", tt.input, tt.want, got)
}
}
}
แต่จากประสบการณ์ผม ทุกอย่างมันมี Trade Off นะ อย่างส่วน
- Make code modular / Make code readable บางทีเราแยกเยอะ มันกลายเป็นว่าได้หลายไฟล์ เวลาใครมาอ่าน Code จะต้องข้ามไปข้ามมา อาจจะเปิดปัญหา Cognitive Load ตามมาด้วย ตรงนี้อาจจะเอา Rule of Three ถ้าซ้ำกันมากกว่า 3 ค่อยแยก เป็นต้น
- หรือ มุมของ Performance / readable / testable บาง Code มันปรับมาเฉพาะงาน อาจจะทำให้อ่านยาก หรือ ทำให้ Test ได้ยากขึ้น
ต้องสมดุลให้ดี
นอกจากนี้หลายอย่างเราไม่จำเป็นต้องทำเองนะ เอา Automate Tools มาช่วยตรวจได้ อย่าง Linter / หรือ การกำหนด Quality Gate ใน CI/CD
Discover more from naiwaen@DebuggingSoft
Subscribe to get the latest posts sent to your email.



