Sử dụng Redux như là một kiến trúc Hexagonal cho lập trình front-end

Lý thuyết về kiến trúc Hexagon hay còn gọi Ports and Adapters được mô tả khái quát như hình vẽ dưới trong quyển sách Implementing Domain-Driven Design, trong đó phân ra phần bên trong và bên ngoài, phần bên trong là Application chứa tầng lõi Domain Model là nơi phản ánh về nội dung nghiệp vụ của hệ thống, phần bên ngoài là phía sử dụng Application thông qua các Port để thao tác với các domain tương ứng. Phía sử dụng bên ngoài(client) thì thông qua Adapter để truyền vào input và lấy về output từ Port tương ứng.

Tiếp theo dưới đây nêu sự liên quan giữa các khái niệm với thực tế triển khai khi dùng Redux như là một kiến trúc Port and Adapters trong lập trình front-end.

1) Domain Model: phản ánh về nội dung nghiệp vụ của hệ thống. Khi xây dựng được một mô hình của nghiệp vụ mà hệ thống phần mềm cần phải giải quyết, sẽ giúp cho người nắm nghiệp vụ có thể đóng góp các yêu cầu cần thiết bổ sung cho chức năng hệ thống, và giúp cho lập trình viên tự tin rằng mình đang phát triển đúng hướng theo mô hình đã được lập ra dựa trên sự nhất trí với phía người nắm nghiệp vụ
Khái niệm này được cụ thể hoá ở Redux bằng cách dùng biến STATE như ví dụ mô tả trong đoạn code dưới đây, lưu lại trạng thái của một domain tại một thời điểm. Thông tin về mỗi trạng thái này được xử lý để không thể bị thay đổi (Immutable) làm cho Domain Model có khả năng lưu giữ một tập hợp của các trạng thái theo thời gian. Điều này giúp cho việc xử lý các tác vụ như Undo để trở về một trạng thái ở một thời điểm nào đó trong history trở nên dễ dàng.

const DEFAULT_STATE = {
  data: [],
  isLoading: true,
    eror:null
}

const CURRENT_STATE = Immutable(DEFAULT_STATE);

2) Domain Event: phản ánh các sự kiện xảy đến đối với domain. Khi liệt kê ra được danh sách các sự kiện này thì chúng ta có thể định nghĩa được các trạng thái tương ứng của domain sẽ thay đổi ra sao ứng với mỗi sự kiện xảy đến.
Khái niêm này được cụ thể hoá ở Redux bằng các Action như ví dụ mô tả dưới đây định nghĩa các action khi thực hiện sẽ làm thay đổi trạng thái của domain

export function fetchDataRequest() {
  return {
    type: types.DATA_REQUEST
  };
}

export function fetchDataSuccess(data) {
  return {
    type: types.DATA_REQUEST_SUCCESS,
    payload: data
  };
}

export function fetchDataError(error) {
  return {
    type: types.DATA_REQUEST_ERROR,
    error: error
  };
}

3) Port: Mỗi cạnh của hình lục giác biểu tượng cho các loại Port khác nhau để lấy ra hoặc truyền vào data đối với tầng domain.
Khái niệm này được cụ thể hoá ở Redux bằng các Reducer như ví dụ mô tả dưới đây, nó nhận vào trạng thái hiện tại(CURRENT_STATE) và action rồi trả về trạng thái tiếp theo (NEXT_STATE)

function dataReport(state = CURRENT_STATE, action) {
  if (!action) return CURRENT_STATE;
  switch (action.type) {
    case types.DATA_REQUEST:
      return state.setIn(['isLoading'], true);
    case types.DATA_REQUEST_SUCCESS:
      return state.setIn(['isLoading'], false)
        .setIn(['error'], null)
        .setIn(['data'], action.payload);
    case types.DATA_REQUEST_ERROR:
      const errorMessage = action.error ? action.error.message : "unknown error";
            return state.setIn(['isLoading'], false)
        .setIn(['data'], [])
        .setIn(['error'], errorMessage);
        default:
      return state;
  }
}

4) Adapter: làm nhiệm vụ trung gian giữa phía sử dụng là client ở phần bên ngoài với phần Application bên trong, nó nhận về các request từ client và chuyển đổi thành các input thích hợp cho Port tương ứng, cũng như định dạng lại các kết quả trả về từ Application cho client có thể dùng được.
Khái niệm này được thực hiện ở Redux thông qua Saga như ví dụ mô tả dưới đây

export default function* dataRequest() {
  yield takeLatest(types.DATA_REQUEST, fetchDataRequest);
}

function* fetchDataRequest() {
  try {
    let report = yield call(api.fetchDataRequest);
    yield put(actions.fetchDataRequestSuccess(report.result));
  } catch (error) {
    yield put(actions.fetchDataRequestError(error));
  }
}

Add a Comment

Scroll Up