ไทย

เรียนรู้ว่าสถาปัตยกรรม Hexagonal หรือที่รู้จักในชื่อ Ports and Adapters จะช่วยปรับปรุงความสามารถในการบำรุงรักษา การทดสอบ และความยืดหยุ่นของแอปพลิเคชันของคุณได้อย่างไร คู่มือนี้ให้ตัวอย่างที่ใช้งานได้จริงและข้อมูลเชิงลึกสำหรับนักพัฒนาทั่วโลก

Hexagonal Architecture: คู่มือปฏิบัติสำหรับ Ports and Adapters

ในโลกของการพัฒนาซอฟต์แวร์ที่มีการเปลี่ยนแปลงอยู่เสมอ การสร้างแอปพลิเคชันที่แข็งแกร่ง บำรุงรักษาง่าย และทดสอบได้ถือเป็นสิ่งสำคัญยิ่ง สถาปัตยกรรมหกเหลี่ยม (Hexagonal Architecture) หรือที่รู้จักกันในชื่อ Ports and Adapters เป็นรูปแบบสถาปัตยกรรมที่ตอบโจทย์เหล่านี้โดยการแยกตรรกะทางธุรกิจหลัก (core business logic) ของแอปพลิเคชันออกจากส่วนที่ต้องพึ่งพาภายนอก (external dependencies) คู่มือนี้มีจุดมุ่งหมายเพื่อให้ความเข้าใจที่ครอบคลุมเกี่ยวกับสถาปัตยกรรมหกเหลี่ยม ประโยชน์ และกลยุทธ์การนำไปใช้งานจริงสำหรับนักพัฒนาทั่วโลก

Hexagonal Architecture คืออะไร?

Hexagonal Architecture ซึ่งบัญญัติศัพท์โดย Alistair Cockburn มีแนวคิดหลักคือการแยกตรรกะทางธุรกิจหลักของแอปพลิเคชันออกจากโลกภายนอก การแยกส่วนนี้ทำได้โดยใช้ ports และ adapters

ลองนึกภาพแบบนี้: แอปพลิเคชันหลักอยู่ตรงกลาง ล้อมรอบด้วยเปลือกหกเหลี่ยม Ports คือจุดเข้าและออกบนเปลือกนี้ และ adapters จะเสียบเข้ากับ ports เหล่านี้ เพื่อเชื่อมต่อส่วน core เข้ากับโลกภายนอก

หลักการสำคัญของ Hexagonal Architecture

หลักการสำคัญหลายประการที่เป็นรากฐานของประสิทธิภาพของ Hexagonal Architecture:

ประโยชน์ของการใช้ Hexagonal Architecture

การนำ Hexagonal Architecture มาใช้มีข้อดีมากมาย:

การนำ Hexagonal Architecture ไปใช้: ตัวอย่างปฏิบัติ

เรามาดูตัวอย่างการนำ Hexagonal Architecture ไปใช้กับระบบลงทะเบียนผู้ใช้แบบง่ายๆ เราจะใช้ภาษาโปรแกรมสมมติ (คล้ายกับ Java หรือ C#) เพื่อความชัดเจน

1. กำหนด Core (Application)

แอปพลิเคชันหลักประกอบด้วยตรรกะทางธุรกิจสำหรับการลงทะเบียนผู้ใช้ใหม่


// Core/UserService.java (หรือ UserService.cs)
public class UserService {
    private final UserRepository userRepository;
    private final PasswordHasher passwordHasher;
    private final UserValidator userValidator;

    public UserService(UserRepository userRepository, PasswordHasher passwordHasher, UserValidator userValidator) {
        this.userRepository = userRepository;
        this.passwordHasher = passwordHasher;
        this.userValidator = userValidator;
    }

    public Result<User, String> registerUser(String username, String password, String email) {
        // ตรวจสอบความถูกต้องของข้อมูลผู้ใช้
        ValidationResult validationResult = userValidator.validate(username, password, email);
        if (!validationResult.isValid()) {
            return Result.failure(validationResult.getErrorMessage());
        }

        // ตรวจสอบว่ามีผู้ใช้นี้อยู่แล้วหรือไม่
        if (userRepository.findByUsername(username).isPresent()) {
            return Result.failure("Username already exists");
        }

        // ทำการแฮชรหัสผ่าน
        String hashedPassword = passwordHasher.hash(password);

        // สร้างผู้ใช้ใหม่
        User user = new User(username, hashedPassword, email);

        // บันทึกผู้ใช้ลงใน repository
        userRepository.save(user);

        return Result.success(user);
    }
}

2. กำหนด Ports

เรากำหนด ports ที่แอปพลิเคชันหลักใช้ในการโต้ตอบกับโลกภายนอก


// Ports/UserRepository.java (หรือ UserRepository.cs)
public interface UserRepository {
    Optional<User> findByUsername(String username);
    void save(User user);
}

// Ports/PasswordHasher.java (หรือ PasswordHasher.cs)
public interface PasswordHasher {
    String hash(String password);
}

//Ports/UserValidator.java (หรือ UserValidator.cs)
public interface UserValidator{
  ValidationResult validate(String username, String password, String email);
}

//Ports/ValidationResult.java (หรือ ValidationResult.cs)
public interface ValidationResult{
  boolean isValid();
  String getErrorMessage();
}

3. กำหนด Adapters

เรานำ adapters ไปใช้งานจริงเพื่อเชื่อมต่อแอปพลิเคชันหลักกับเทคโนโลยีเฉพาะ


// Adapters/DatabaseUserRepository.java (หรือ DatabaseUserRepository.cs)
public class DatabaseUserRepository implements UserRepository {
    private final DatabaseConnection databaseConnection;

    public DatabaseUserRepository(DatabaseConnection databaseConnection) {
        this.databaseConnection = databaseConnection;
    }

    @Override
    public Optional<User> findByUsername(String username) {
        // การนำไปใช้งานโดยใช้ JDBC, JPA หรือเทคโนโลยีการเข้าถึงฐานข้อมูลอื่นๆ
        // ...
        return Optional.empty(); // ตัวอย่างชั่วคราว
    }

    @Override
    public void save(User user) {
        // การนำไปใช้งานโดยใช้ JDBC, JPA หรือเทคโนโลยีการเข้าถึงฐานข้อมูลอื่นๆ
        // ...
    }
}

// Adapters/BCryptPasswordHasher.java (หรือ BCryptPasswordHasher.cs)
public class BCryptPasswordHasher implements PasswordHasher {
    @Override
    public String hash(String password) {
        // การนำไปใช้งานโดยใช้ไลบรารี BCrypt
        // ...
        return "hashedPassword"; //ตัวอย่างชั่วคราว
    }
}

//Adapters/SimpleUserValidator.java (หรือ SimpleUserValidator.cs)
public class SimpleUserValidator implements UserValidator {
  @Override
  public ValidationResult validate(String username, String password, String email){
    //ตรรกะการตรวจสอบความถูกต้องแบบง่าย
     if (username == null || username.isEmpty()) {
            return new SimpleValidationResult(false, "Username cannot be empty");
        }
        if (password == null || password.length() < 8) {
            return new SimpleValidationResult(false, "Password must be at least 8 characters long");
        }
        if (email == null || !email.contains("@")) {
            return new SimpleValidationResult(false, "Invalid email format");
        }

        return new SimpleValidationResult(true, null);
  }
}

//Adapters/SimpleValidationResult.java (หรือ SimpleValidationResult.cs)
public class SimpleValidationResult implements ValidationResult {
  private final boolean valid;
  private final String errorMessage;

    public SimpleValidationResult(boolean valid, String errorMessage) {
        this.valid = valid;
        this.errorMessage = errorMessage;
    }
  @Override
  public boolean isValid(){
    return valid;
  }

  @Override
  public String getErrorMessage(){
    return errorMessage;
  }
}



//Adapters/WebUserController.java (หรือ WebUserController.cs)
//Driving Adapter - จัดการคำขอจากเว็บ
public class WebUserController {
    private final UserService userService;

    public WebUserController(UserService userService) {
        this.userService = userService;
    }

    public String registerUser(String username, String password, String email) {
        Result<User, String> result = userService.registerUser(username, password, email);
        if (result.isSuccess()) {
            return "Registration successful!";
        } else {
            return "Registration failed: " + result.getFailure();
        }
    }
}


4. การประกอบ (Composition)

การเชื่อมต่อทุกอย่างเข้าด้วยกัน โปรดทราบว่าการประกอบส่วนต่างๆ (dependency injection) นี้โดยทั่วไปจะเกิดขึ้นที่จุดเริ่มต้นของแอปพลิเคชันหรือภายใน dependency injection container


//คลาส Main หรือการกำหนดค่า dependency injection
public class Main {
    public static void main(String[] args) {
        // สร้าง instances ของ adapters
        DatabaseConnection databaseConnection = new DatabaseConnection("jdbc:mydb://localhost:5432/users", "user", "password");
        DatabaseUserRepository userRepository = new DatabaseUserRepository(databaseConnection);
        BCryptPasswordHasher passwordHasher = new BCryptPasswordHasher();
        SimpleUserValidator userValidator = new SimpleUserValidator();

        // สร้าง instance ของแอปพลิเคชันหลัก โดย inject adapters เข้าไป
        UserService userService = new UserService(userRepository, passwordHasher, userValidator);

        //สร้าง driving adapter และเชื่อมต่อกับ service
        WebUserController userController = new WebUserController(userService);

        //ตอนนี้คุณสามารถจัดการคำขอลงทะเบียนผู้ใช้ผ่าน userController ได้แล้ว
        String result = userController.registerUser("john.doe", "P@sswOrd123", "john.doe@example.com");
        System.out.println(result);
    }
}



//DatabaseConnection เป็นคลาสอย่างง่ายเพื่อการสาธิตเท่านั้น
class DatabaseConnection {
    private String url;
    private String username;
    private String password;

    public DatabaseConnection(String url, String username, String password) {
        this.url = url;
        this.username = username;
        this.password = password;
    }

    // ... เมธอดสำหรับเชื่อมต่อฐานข้อมูล (ไม่ได้เขียนโค้ดเพื่อความกระชับ)
}

//คลาส Result (คล้ายกับ Either ใน functional programming)
class Result<T, E> {
    private final T success;
    private final E failure;
    private final boolean isSuccess;

    private Result(T success, E failure, boolean isSuccess) {
        this.success = success;
        this.failure = failure;
        this.isSuccess = isSuccess;
    }

    public static <T, E> Result<T, E> success(T value) {
        return new Result<>(value, null, true);
    }

    public static <T, E> Result<T, E> failure(E error) {
        return new Result<>(null, error, false);
    }

    public boolean isSuccess() {
        return isSuccess;
    }

    public T getSuccess() {
        if (!isSuccess) {
            throw new IllegalStateException("Result is a failure");
        }
        return success;
    }

    public E getFailure() {
        if (isSuccess) {
            throw new IllegalStateException("Result is a success");
        }
        return failure;
    }
}

class User {
    private String username;
    private String password;
    private String email;

    public User(String username, String password, String email) {
        this.username = username;
        this.password = password;
        this.email = email;
    }

    // getters and setters (ละไว้เพื่อความกระชับ)

}

คำอธิบาย:

ข้อควรพิจารณาขั้นสูงและแนวปฏิบัติที่ดีที่สุด

แม้ว่าหลักการพื้นฐานของ Hexagonal Architecture จะตรงไปตรงมา แต่ก็มีข้อควรพิจารณาขั้นสูงบางประการที่ควรทราบ:

ตัวอย่างการใช้งาน Hexagonal Architecture ในโลกแห่งความเป็นจริง

บริษัทและโครงการที่ประสบความสำเร็จหลายแห่งได้นำ Hexagonal Architecture มาใช้เพื่อสร้างระบบที่แข็งแกร่งและบำรุงรักษาง่าย:

ความท้าทายและข้อแลกเปลี่ยน

แม้ว่า Hexagonal Architecture จะให้ประโยชน์อย่างมาก แต่สิ่งสำคัญคือต้องรับทราบถึงความท้าทายและข้อแลกเปลี่ยนที่เกี่ยวข้อง:

สิ่งสำคัญคือต้องประเมินประโยชน์และความท้าทายของ Hexagonal Architecture อย่างรอบคอบในบริบทของความต้องการของโครงการและขีดความสามารถของทีมของคุณ มันไม่ใช่ยาวิเศษ (silver bullet) และอาจไม่ใช่ตัวเลือกที่ดีที่สุดสำหรับทุกโครงการ

สรุป

Hexagonal Architecture ที่เน้นเรื่อง ports และ adapters เป็นแนวทางที่มีประสิทธิภาพในการสร้างแอปพลิเคชันที่บำรุงรักษาง่าย ทดสอบได้ และมีความยืดหยุ่น ด้วยการแยกตรรกะทางธุรกิจหลักออกจากส่วนที่ต้องพึ่งพาภายนอก ช่วยให้คุณสามารถปรับตัวเข้ากับเทคโนโลยีและความต้องการที่เปลี่ยนแปลงไปได้อย่างง่ายดาย แม้ว่าจะมีความท้าทายและข้อแลกเปลี่ยนที่ต้องพิจารณา แต่ประโยชน์ของ Hexagonal Architecture มักจะคุ้มค่ากว่า โดยเฉพาะสำหรับแอปพลิเคชันที่ซับซ้อนและมีอายุการใช้งานยาวนาน การยึดมั่นในหลักการของ dependency inversion และ explicit interfaces จะช่วยให้คุณสร้างระบบที่ทนทานขึ้น เข้าใจง่ายขึ้น และพร้อมที่จะตอบสนองความต้องการของโลกซอฟต์แวร์สมัยใหม่ได้ดีขึ้น

คู่มือนี้ได้ให้ภาพรวมที่ครอบคลุมของ Hexagonal Architecture ตั้งแต่หลักการสำคัญไปจนถึงกลยุทธ์การนำไปใช้งานจริง เราขอแนะนำให้คุณสำรวจแนวคิดเหล่านี้เพิ่มเติมและทดลองนำไปใช้ในโครงการของคุณเอง การลงทุนในการเรียนรู้และนำ Hexagonal Architecture มาใช้จะให้ผลตอบแทนที่คุ้มค่าในระยะยาวอย่างแน่นอน นำไปสู่ซอฟต์แวร์ที่มีคุณภาพสูงขึ้นและทีมพัฒนาที่พึงพอใจมากขึ้น

ท้ายที่สุดแล้ว การเลือกสถาปัตยกรรมที่เหมาะสมนั้นขึ้นอยู่กับความต้องการเฉพาะของโครงการของคุณ พิจารณาความซับซ้อน อายุการใช้งาน และความต้องการในการบำรุงรักษาเมื่อทำการตัดสินใจ Hexagonal Architecture เป็นรากฐานที่มั่นคงสำหรับการสร้างแอปพลิเคชันที่แข็งแกร่งและปรับเปลี่ยนได้ แต่มันเป็นเพียงเครื่องมือหนึ่งในกล่องเครื่องมือของสถาปนิกซอฟต์แวร์เท่านั้น