Bài viết này bàn về hướng lập trình giao diện (programming to the interface) thay vì giao diện người dùng, hay giao diện đồ họa (user interface, graphical interface).

Lập trình giao diện

Lập trình giao diện (giao tiếp) là một trong hai nguyên tắc cơ bản được nhắc đến trong quyển sách kinh điển Design Patterns. Lập trình giao diện đặt trọng tâm vào việc tạo ra một hợp đồng (contract) chắc chắn giữa hai bên người dùng (consumer) và người cung cấp (producer):

Người dùng
là bên sử dụng các lớp, hàm, hoặc mã mà người cung cấp tạo ra
Người cung cấp
là bên tạo ra thư viện mã
Hợp đồng
là các kiến thức chung mà cả người dùng và người cung cấp đều hiểu và tuân theo

Hợp đồng xác định các lớp cơ bản, và các phương thức của các lớp đó. Vì đây là hợp đồng được cả hai bên tuân theo nên ý nghĩa của các lớp, cũng như cách hoạt động của các phương thức phải được định nghĩa một cách tường minh và chuẩn xác. Người dùng sẽ dựa theo hợp đồng này và sử dụng các lớp trong đó cho mục đích riêng của họ, trong khi người cung cấp có thể thay đổi cách hiện thực hợp đồng một cách khả chuyển miễn sao vẫn đảm bảo được yêu cầu của hợp đồng.

Ví dụ: hợp đồng nêu ra là hàm func nhận vào một số nguyên, và trả về tổng của số nguyên đó với 2. Hợp đồng này rõ ràng ví nó xác định tên hàm (func), giá trị đầu vào (một số nguyên) và giá trị đầu ra (tổng của số nguyên nhập vào với 2). Người sử dụng, khi cần tính tổng của 5 và 2, có thể gọi func(5). Người cung cấp có thể cài đặt def func(i): return i + 2 hoặc func = lambda i: i + 2 hoặc một cách nào khác miễn sao phù hợp với câu chữ của hợp đồng.

Theo quan điểm này, người sử dụng và người cung cấp giao tiếp với nhau thông qua một môi trường chung, một giao diện chung. Người sử dụng dựa trên giao diện này để yêu cầu dịch vụ từ người cung cấp. Còn người cung cấp dựa trên giao diện này để đáp ứng đúng yêu cầu người sử dụng.

Đối với các ngôn ngữ lập trình hướng đối tượng, chúng ta hay gặp các thuật ngữ như giao diện (interface), lớp trừu tượng (abstract class) và lớp cụ thể (concrete class). Giao diện mang tính chất của một hợp đồng, hoàn toàn không có bất kỳ một hiện thực cụ thể nào. Lớp cụ thể là một hiện thực đầy đủ của một giao diện. Và lớp trừu tượng là một hiện thực chưa đầy đủ của một giao diện.

Lập trình giao diện trong Python

Python không có sự phân biệt giữa ba khái niệm này. Tuy nhiên, trong thực tế, chúng ta hay viết mã tương tự như sau để mô phỏng một giao diện.

class Interface(object):
    def method_1(self):
        raise NotImplementedError()
    def method_2(self):
        raise NotImplementedError()

Các phương thức trong lớp Interface chỉ có một câu lệnh duy nhất là raise NotImplementedError() để cố tình gây ra biệt lệ khi chúng được gọi.

Một lớp trừu tượng có thể được mô phỏng như sau:

class AbstractClass(Interface):
    def method_1(self):
        # làm một việc gì đó
        pass

Lớp AbstractClass là lớp con của Interface và hiện thực hóa phương thức method_1 nhưng vẫn để phương thức method_2 lơ lửng.

Cuối cùng, một lớp cụ thể là một lớp con của Interface hoặc AbstractClass nhưng hiện thực hóa tất cả các phương thức.

class ConcreteClass(AbstractClass):
    def method_2(self):
        # làm một việc gì đó
        pass

Điểm đáng lưu ý là tất cả các lớp này đều hợp lệ trong Python. Chúng ta hoàn toàn có thể khởi tạo một đối tượng của một trong ba lớp này và chỉ đến khi gọi một hàm nào đó thì biệt lệ mới xảy ra. Nói cách khác, biệt lệ NotImplementedError chỉ được phát hiện khi chạy, trong khi đáng lẽ nó đã phải được phát hiện ngay khi dịch. Điều này làm mất giá trị của một hợp đồng, và hạn chế việc áp dụng hướng lập trình giao diện trong Python.

Để khắc phục điểm yếu này, tôi đã viết một bộ trang hoàng (decorator) nhỏ để bắt lỗi ngay khi một lớp cụ thể được định nghĩa thiếu. Định nghĩa thiếu ở đây mang ý nghĩa rằng lớp cụ thể đã quên định nghĩa một phương thức nào đó.

'''Set of (class) decorators to help with interface programming.

Examples::

  @concrete
  class subclass(parent):
      ...

Released to the public domain.

Nam T. Nguyen, 2012

'''

import dis
import types
import unittest


def concrete(orig_class):
    '''A decorator to ensure that a concrete class has no un-implemented
    methods.

    An un-implemented method is defined similarly to::

        def method(...):
            raise NotImplementedError()

    This, when translated to bytecode, looks like::

        LOAD_GLOBAL       0 (NotImplementedError)
        CALL_FUNCTION     1
        RAISE_VARARGS     1
        ...

    Or when ``raise NotImplementedError``::

        LOAD_GLOBAL       0 (NotImplementedError)
        RAISE_VARARGS     1
        ...

    The check here is for such pattern.

    '''

    for name in dir(orig_class):
        func = getattr(orig_class, name)
        # correct type?
        if type(func) not in (types.FunctionType, types.MethodType):
            continue
        # check if first name is NotImplementedError
        if len(func.func_code.co_names) < 1 or \
                func.func_code.co_names[0] != 'NotImplementedError':
            continue
        # and RAISE_VARARGS somewhere after that
        for position in (3, 6):
            if len(func.func_code.co_code) < position:
                continue
            opcode = ord(func.func_code.co_code[position])
            if dis.opname[opcode] == 'RAISE_VARARGS':
                raise SyntaxError('Function %s.%s must be implemented.' % (
                        orig_class.__name__, func.func_name))

    return orig_class


class ConcreteTest(unittest.TestCase):

    def test_raise(self):
        class test1:
            def method(self):
                raise NotImplementedError
        self.assertRaises(SyntaxError, concrete, test1)

        class test2:
            def method(self):
                raise NotImplementedError()
        self.assertRaises(SyntaxError, concrete, test2)

    def test_not_raise(self):
        class test:
            def method(self):
                return NotImplementedError()
        concrete(test)

    def test_subclass(self):
        class parent(object):
            def override_me(self):
                raise NotImplementedError()
        class test(parent):
            def leave_it(self):
                pass
        self.assertRaises(SyntaxError, concrete, test)


if __name__ == '__main__':
    unittest.main()

Để sử dụng bộ trang hoàng concrete này, ta sẽ sửa lại lớp ConcreteClass như sau:

@concrete
class ConcreteClass(AbstractClass):
    # như trên

Nếu ConcreteClass quên hiện thực hóa một phương thức nào đó, Python sẽ thông báo SyntaxError ngay sau khi ConcreteClass được định nghĩa.

Tóm tắt

Lập trình giao diện giúp chúng ta tách biệt hai yếu tố là cái gì, và như thế nào. Người sử dụng quan tâm nhiều đến câu hỏi cái gì được thực hiện hơn là làm sao để thực hiện chúng. Trong khi người cung cấp có thể dễ dàng thay đổi cách thực hiện miễn sao vẫn đảm bảo câu chữ của hợp đồng.

Python không có sẵn cơ chế giao diện và lớp trừu tượng như các ngôn ngữ lập trình khác. Chúng ta phải sử dụng bộ trang hoàng concrete để bắt lỗi định nghĩa thiếu.

Hy vọng bài viết ngắn này sẽ đem lại cho bạn đọc một mẹo lập trình giao diện hữu ích trong Python.