Vấn Đề Kế Thừa Trong Ruby Bạn Nên Biết!

Discussion in 'Python, Perl Developer' started by Mickey, Jan 6, 2018.

  1. Mickey

    Mickey Hacking For Hacker Staff Member

    • Administrator
    I. Đặt vấn đề
    Xin chào các bác (bow).

    Bài viết hôm nay mình sẽ trình bày một vấn đề:

    Việc con em lãnh đạo được giao trọng trách quản lý là điều hạnh phúc của dân tộc, không có gì phải nghi ngại.

    Ngoài đời là vậy (yaoming)

    Nhưng trong lập trình, việc sử dụng kế thừa (Inheritance) thì có vấn đề gì sao?

    Mình sẽ mình họa bằng một ví dụ sau đây.

    Giả sử khách hàng yêu cầu bạn tạo ra một traffic simulator app - muốn nó có thể mô phỏng một số loại xe.

    Nếu bạn sử dụng ngôn ngữ hướng đối tượng như Ruby, có thể bạn sẽ hướng tới xây dựng một model class kiểu như thế này:

    Vehicle class
    Vehicle class

    Xong task và bàn giao lại với khách.

    Nhưng giờ họ muốn chia làm 2 loại xe là ô tô (cars) và xe tải (trucks).

    2 lớp này sẽ có chung một số behavior, và bạn không muốn code bị trùng lặp

    -> Ta sử dụng kế thừa là giải quyết ez luôn - is-a relation

    Vehicle với inheritance
    Vehicle với inheritance

    Khách hàng ok, nhưng một lần nữa họ lại bảo:

    Nếu ô tô và xe tải này có thể có các loại động cơ khác nhau nữa thì tốt quá!

    Giả dụ như là động cơ chạy xăng và điện.

    Một lần nữa, kế thừa giải quyết trong 1 nốt nhạc:

    Vehicle với nhiều inheritance
    Vehicle với nhiều inheritance

    Có thể thấy với kế thừa, ta có thể dễ dàng giải quyết những yêu cầu kiểu như vậy.

    Nhưng nếu họ muốn phân mảnh nhiều nữa thì sao, ví dụ: xe tư nhân, xe công, xe cứ hỏa, ...

    Cây kế thừa của ta sẽ ngày càng lớn và phức tạp đến mức khó kiểm soát.

    Thay vì giảm lượng code bị lặp, cuối cùng vẫn sẽ xuất hiện những đoạn logic giống nhưng ở nhiều chỗ khác nhau.

    Trường hợp này xảy ra không chỉ trong ví dụ trên, mình đã gặp nó rất nhiều lần trong project, đặc biệt là khi phát triển tính năng mới hoặc cố thêm behavior trong class kế thừa.

    Để có cái nhìn trực quan hơn, ta hãy nhìn vào đoạn code sau:
    PHP:
    class Vehicle
      def run
        refill
        load
      end
    end

    class Car Vehicle
      def load
        
    # load passengers
      
    end
    end

    class Truck Vehicle
      def load
        
    # load cargo
      
    end
    end

    class PetrolCar Car
      def refill
        
    # refill with fuel
      
    end
    end

    class ElectricCar Car
      def refill
        
    # refill with electricity
      
    end
    end

    class PetrolTruck Truck
      def refill
        
    # refill with fuel (code duplication!)
      
    end
    end

    class ElectricTruck Truck
      def refill
        
    # refill with electricity (code duplication!)
      
    end
    end
    Vậy có thể xử lý việc này không, có chứ (dance2)

    II. Giải pháp
    Sử dụng include (mixins)
    Đây thường là ý tưởng đầu tiên xuất hiện trong đầu Ruby developer khi họ nhận ra rằng - kế thừa không phải là giải pháp hay.

    Đơn giản là ta nhóm các methods lại thành các module rồi include vào trong class nó cần thiết.

    Ta có thể dễ dàng điều tra những logic bên trong và tránh việc lặp code.

    Các methods trên có thể tách ra các module như sau:
    PHP:
    module Vehicle
      def run
        refill
        load
      end
    end

    module Truck
      def load
        
    # load cargo
      
    end
    end

    module Car
      def load
        
    # load passengers
      
    end
    end

    module ElectricEngine
      def refill
        
    # refill with electricity
      
    end
    end

    module PetrolEngine
      def refill
        
    # refill with petrol
      
    end
    end
    Include vào class chính:

    class 
    PetrolCar
      
    include Vehicle
      
    include Car
      
    include PetrolEngine
    end

    class ElectricCar
      
    include Vehicle
      
    include Car
      
    include ElectricEngine
    end

    class PetrolTruck
      
    include Vehicle
      
    include Truck
      
    include PetrolEngine
    end

    class ElectricTruck
      
    include Vehicle
      
    include Truck
      
    include ElectricEngine
    end
    Trông hợp lý hơn hẳn: không bị lặp code, cũng như ta có thể thoải mái add thêm module nếu khách hàng yêu cầu.

    Nhưng vẫn còn một số vấn đề tồn đọng.

    Khi nhìn vào class này, bạn sẽ không chắc được những hành vi nào đã include sẽ được sử dụng.

    Mặc dù code vẫn chạy đúng, nhưng sẽ khó để ta theo dõi được flow các methods chạy từ đâu đến đâu.

    Nếu chẳng may 2 modules có methods trùng tên thì bạn sẽ gặp rắc rối - 1 module sẽ lẳng lặng mà sử dụng methods từ module khác.

    Tương tự như vậy, methods trong module sẽ phá đám methods bạn implement trực tiếp trong class.

    Include không sida và chắc chắn có nhiều case sử dụng nó là hợp lý.

    Nhưng ở quản điểm của tôi, nó sẽ hoạt động tốt khi bạn muốn định nghĩa một meta behavior của class như kiểu logging, authorization hay validation.

    Ưu điểm lớn nhất của nó trong giải pháp này là giữ code được sạch và ngắn gọn.

    Nó vẫn sẽ hoạt động tốt miễn là bạn implement chính xác và không phá bất kỳ logic nào khác khi thêm vào.

    Nhưng hãy nhớ là, nó chỉ là một trong những cách để implement đa kế thừa trong Ruby mà thôi.

    Composition
    Composition (tạm dịch là tổng hợp) là một kỹ thuật đã xuất hiện từ khá lâu rồi.

    Vậy dùng Composition là dư lào?

    Thay vì share những behavior giống nhau giữa các class, bạn nên xác định những phần khác nhau giữa chúng, đặt tên, tách ra class riêng và tổng hợp vào trong object cuối cùng sẽ sử dụng.

    Nếu gọi kế thừa là kiểu quan hệ is-a (là một), thì composition là kiểu quan hệ has-a (có một).

    Xe (vehicle) không phải là xe điện nữa, nhưng nó sẽ có động cơ điện (engine).

    Không phải là xe tải, nhưng nó có thân xe tải (body).

    Từ đó, ta có thể phân ra thành 2 phần: engine và body.

    Cấu trúc app giờ sẽ nhìn như thế này. Chúng ta phải implement phần engine, body vào bên trong Vehicle class.



    Về mặt code sẽ như thế nào? Bắt đầu với main class:
    PHP:
    class Vehicle
      def initialize
    (engine:, body:)
        @
    engine engine
        
    @body body
      end

      def run
        
    @engine.refill
        
    @body.load
      end
    end
    Giờ ta có implement các phần tách riêng, cấy nó vào trong Vehicle object
    PHP:
    class ElectricEngine
      def refill
        
    # refill with electricity
      
    end
    end

    class PetrolEngine
      def refill
        
    # refill with petrol
      
    end
    end

    class TruckBody
      def load
        
    # load cargo
      
    end
    end

    class CarBody
      def load
        
    # load passengers
      
    end
    end
    Cuối cùng đưa nó vào sử dụng sẽ như sau:
    PHP:
    petrol_car Vehicle.new(enginePetrolEngine.new, bodyCarBody.new)
    electric_car Vehicle.new(engineElectricEngine.new, bodyCarBody.new)
    petrol_truck Vehicle.new(enginePetrolEngine.new, bodyTruckBody.new)
    electric_truck Vehicle.new(engineElectricEngine.new, bodyTruckBody.new)
    Phương pháp này có nhiều ưu điểm hơn.

    Cách mà class Vehicle sử dụng những methods thêm bên ngoài hoàn toàn trực quan, rõ ràng.

    Ta sẽ không gặp phải vấn đề conflict tên hàm.

    Mỗi class thực hiện chỉ một công việc (đáp ứng cho rule single responsibility principle).

    Nhớ đó mà bạn có thể test và viết test dễ hơn nhiều.

    Ngoài ra nó có sự gắn kết cao (giữ cùng một logic với nhau) và đồng thời giữ các cho class không bị phụ thuộc vào nhau.
    Ta có thể thoải mái thay đổi code trong engine hoặc body miễn là interface cũng chúng vấn giữ nguyên là được.

    Vậy composition có nhược điểm nào không? Tất nhiên là có.

    Sử dụng compostion có xu hướng làm code mình dài hơn, đặc biết là lúc cấy tất cả các thành phần vào object cuối cùng.

    Với tôi, khó nhất trong compostion chính là việc phải thay đổi mindset để phân tích và implement được theo cách đó.

    Túm cái váy lại
    Tôi đã trình bày cho các bạn 3 kiểu cấu trúc code khác nhau: sử dụng Inheritance, Include và Composition.

    Kế thừa là sự lựa chọn đầu tiên của nhiều lập trình viên, nhưng cẩn thận đừng lạm dụng nó. Nó sẽ làm code phức tạp hơn và khó để maintain.

    Include (Mixins) là giải pháp thông minh hơn, tuy nhiên về bản chất, nó vẫn chỉ là kiểu đa kế thừa ngầm, mà dần dần có thể làm tăng sự phức tạp của code.

    Composition dài dòng nhất, nhưng đồng thời nó giúp code của ta clear, dễ test và dễ bảo trì nhất.

    Bạn phải biết rằng, lập trình hướng đối tượng chỉ là những quy ước của một số lập trình viên đưa ra để giúp các lập trình viên khác giải quyết vấn đề của họ. Nên đừng chỉ trói buộc trong những rules đó.

    Hãy chọn giải pháp phù hợp nhất cho tình huống của bạn.
     

Share This Page