Make Repository Pattern more efficient in Android

Dẫn nhập

Việc define structure cho một dự án luôn là một công việc vô cùng khó khăn và đòi hỏi nhiều kinh nghiệm, kỹ năng nhất định. Do đó, công việc này thường dành cho những người có nhiều năm kinh nghiệm và có tầm nhìn tổng quát cho dự án để đảm bảo structure áp dụng vào dự án phải phù hợp nhất, tường minh nhất, hiệu quả nhất, sẵn sàng cho mọi sự thay đổi, nâng cấp, đảm bảo việc viết Unit Test và bảo trì sau này. Trong qúa trình làm việc, người ta đã tạo ra vô số các Structure Pattern khác nhau để phù hợp với từng dự án khác nhau. Dĩ nhiên, các structure pattern này không phụ thuộc vào bất kỳ ngôn ngữ nào, chỉ khác nhau ở việc chúng ta adapt nó cho từng loại ngôn ngữ, framework khác nhau để phù hợp hơn với chúng mà vẫn giữ được những tinh túy mà Structure Pattern đó đã định nghĩa. Ở bài viết hôm nay, tôi sẽ giới thiệu đến các bạn cách sử dụng Repository Pattern một cách hiệu quả hơn trong dự án Android

Tại sao là Repository Pattern

  • Tách biệt việc xử lý ở Data LayerBusiness Layer.
  • Mix Logic hoàn hảo giữa Local và Remote Data Sourse trước khi expose cho client.
  • Tường minh trong giao tiếp với Data Source.
  • Độc lập với Data Source Framework.
  • Tối ưu hóa hiệu năng với chiến lược caching cụ thể.
  • Giảm tình trạng duplicate code hoặc missing Logic ở những chỗ cùng một xử lý giống nhau.
  • Dễ dàng trong việc viết Unit Test, giảm rủi ro trong maintain.
  • etc ...

Chúng ta đã thấy rõ những gì mà Repository Pattern làm được, tuy nhiên nó vẫn còn những nhược điểm đáng kể.

Trong trường hợp các ControllerPresenter (tôi sẽ gọi chung là Business Logic Layer) giao tiếp trực tiếp với Repository sẽ dẫn đến một số điểm bất cập sau:

  • Cùng một đoạn xử lý logic giống nhau nhưng thực hiện ở những Business Logic Layer khác, chúng ta phải copy code lại, dẫn đến tình trạng duplicate code, rủi ro khi maintain là rất lớn vì logic có thể bị miss ở những Business Logic khác nhau.
  • Chiến lược caching data sẽ không hiệu quả khi chúng ta cần map nhiều logic khác nhau trước khi cache lại một thứ gì đó.
  • Không thể đồng nhất việc xử lý logic một cách hiệu quả trước khi expose đến client (ở đây là các Business Logic Layer).
  • Việc apply Unit Test không thực sự hiệu quả cho những phần đòi hỏi sự đồng nhất của logic.

Chúng ta sẽ đi tìm cách giải quyết mọi vấn đề trên. Dông dài về lý thuyết, chúng ta sẽ bắt tay vào implement nó trong Android và mình sẽ chú thích rõ từng phần, từng khái niệm cụ thể.

Implementation

Dependency Injection là pattern rất hiệu quả trong việc tạo sự phụ thuộc giữa các đối tượng theo một scope cụ thể, giúp cho việc khởi tạo đối tượng một cách tường minh và theo một rule nhất định. Tôi sẽ sử dụng nó để khởi tạo các Repository.

  • Define một số POJO class.
public class Course {

    @PrimaryKey
    private String id;
    @Required
    private String name;
    @Required
    private long startTime;
    @Required
    private int duration;

    // Define getter/setter
}
public class Student {

    @PrimaryKey
    private String id;
    @NonNull
    @Required
    private String name;
    @Nullable
    private List<Course> course;
    @NonNull
    @Required
    private Department department;

    // Define getter/setter
}
public class Department {

    @PrimaryKey
    private String id;
    @Required
    private String name;
    @Required
    @NonNull
    private List<Student> students;
}
  • Repository là nơi tương tác trực tiếp với các Data Source, gồm có Remote Data Source để tương tác với API server và Local Data Source để thao tác với Local Database. Chúng ta tạo interface define các method.
public interface CourseRepository {

    Observable<Course> getCourseById(@NonNull String id);

    Observable<?> createCourse();

    Observable<?> addStudent(@NonNull String courseId, @NonNull Student student);

    Observable<?> addStudents(@NonNull String courseId, @NonNull List<Student> students);

    Observable<?> deleteCourse(String id);
}
public interface StudentRepository {

    Observable<?> createStudent(@NonNull Student student);

    Observable<?> joinCourse(@NonNull Course course);

    Observable<?> leaveCourse(@NonNull String courseId);

    Observable<?> deleteStudent(@NonNull String id);

}

Và các concreation class. Ở đây tôi chỉ ví dụ việc implement CourseRepository một cách cụ thể.

public class CourseRepositoryImpl implements CourseRepository {

    private CourseLocalDataSource localDataSource;
    private CourseRemoteDataSource remoteDataSource;

    @Inject
    public CourseRepositoryImpl(CourseRemoteDataSource remoteDataSource,
            CourseLocalDataSource localDataSource) {
        this.remoteDataSource = remoteDataSource;
        this.localDataSource = localDataSource;
    }

    @Override
    public Observable<Course> getCourseById(@NonNull String id) {
        return localDataSource.getCourseById(id);
    }

    @Override
    public Observable<?> createCourse() {
        return remoteDataSource.createCourse().flatMap(new Func1<Object, Observable<?>>() {
            @Override
            public Observable<?> call(Object o) {
                return localDataSource.createCourse();
            }
        });
    }

    @Override
    public Observable<?> addStudent(@NonNull String courseId, @NonNull Student student) {
        // TODO implement
        return null;
    }

    @Override
    public Observable<?> addStudents(@NonNull String courseId, @NonNull List<Student> students) {
        // TODO implement
        return null;
    }

    @Override
    public Observable<?> deleteCourse(String id) {
        return remoteDataSource.deleteCourse(id).flatMap(new Func1<Object, Observable<?>>() {
            @Override
            public Observable<?> call(Object o) {
                return localDataSource.deleteCourse(id);
            }
        });
    }
}

Ở class CourseRepositoryImpl sẽ có 2 dependency là CourseRemoteDataSourceCourseLocalDataSource. Chúng ta sẽ tiến hành mix dữ liệu lại với nhau trước khi expose kết quả. Việc này sẽ đảm bảo nguyên lý Single Responsibility của method và làm cho nó đồng nhất về mặt logic dữ liệu. Ví dụ ở method deleteCourse(), ta sẽ tiến hành delete Course này trên Server API và khi được notify thành công mới tiến hành delete Course này ở local database.

Có một vấn đề ở đây là khi bạn muốn update lại List CourseStudent, bạn phải thục hiện logic này sau khi việc delete Course, việc này thực chất là một logic đồng nhất nhưng lại đang bị tách nhỏ ra bởi bạn không thể để một Repository này là một dependency của một Repository khác, điều đó hòan toàn không nên. Giải quyết vấn đề này chúng ta cần một layer high-level hơn.

  • Khởi tạo Service
public interface CourseService {

    Observable<?> createCourse();

    Observable<?> addStudent(@NonNull String courseId, @NonNull Student student);

    Observable<?> addStudents(@NonNull String courseId, @NonNull List<Student> students);

    Observable<?> deleteCourse(String id);
}

và concreation class

public class CourseServiceImpl implements CourseService {

    private CourseRepository courseRepo;
    private StudentRepository studentRepo;
    
    @Inject
    public CourseServiceImpl(CourseRepository courseRepo, StudentRepository studentRepo) {
        this.courseRepo = courseRepo;
        this.studentRepo = studentRepo;
    }

    @Override
    public Observable<?> createCourse() {
        return courseRepo.createCourse();
    }

    @Override
    public Observable<?> addStudent(@NonNull final String courseId,
            @NonNull final Student student) {
        return courseRepo.addStudent(courseId, student)
                .flatMap(new Func1<Object, Observable<Course>>() {
                    @Override
                    public Observable<Course> call(Object o) {
                        return courseRepo.getCourseById(courseId);
                    }
                })
                .flatMap(new Func1<Course, Observable<?>>() {
                    @Override
                    public Observable<?> call(Course course) {
                        return studentRepo.joinCourse(course);
                    }
                });
    }

    @Override
    public Observable<?> addStudents(@NonNull String courseId, @NonNull List<Student> students) {
        // TODO implement
        return null;
    }

    @Override
    public Observable<?> deleteCourse(final String id) {
        return courseRepo.deleteCourse(id).flatMap(new Func1<Object, Observable<?>>() {
            @Override
            public Observable<?> call(Object o) {
                return studentRepo.leaveCourse(id);
            }
        });
    }
}

Logic đã trở nên tập trung và đồng nhất hơn rất nhiều. Nếu muốn Unit Test cho function này, bạn chỉ cần viết một lần ở Service vậy là đủ.

  • Sử dụng ở Business Logic Layer.
public class CoursePresenter {

    private CourseViewModel viewModel;
    private CourseService service;

    @Inject
    CoursePresenter(CourseViewModel viewModel, CourseService service) {
        this.viewModel = viewModel;
        this.service = service;
    }

    public void addStudent(@NonNull String courseId, @NonNull Student student) {
        service.addStudent(courseId, student).subscribe(new Action1<Object>() {
            @Override
            public void call(Object o) {
                viewModel.updateUI();
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable throwable) {
                viewModel.onError(throwable);
            }
        });
    }
}

Chúng ta có thể nhận thấy rằng, Business Logic Layer sẽ chỉ biết đến và giao tiếp với Service Layer, Service Layer sẽ sử dụng các dependency là các Repository và cuối cùng các Repository sẽ chỉ sử dụng các dependency là RemoteDataSourceLocalDataSource. Các Layer được tách biệt rất rõ ràng.

Kết luận

Bài viết trình bày một tư tưởng để tối ưu hóa Repository Pattern mà cụ thể hóa trong Android. Phần code mẫu chỉ ở dạng "phác thảo" nên có thể có vài điểm không hợp lý vì đơn giản tôi chỉ muốn trình bày tư tưởng là chính yếu. Hy vọng sẽ giúp ích được mọi người. Cảm ơn.