+2

Hành trình "vượt ải" Backend: Tự học OOP, Interface vs Abstract

Đang tải lên image.png… Chào mọi người, mình là Hoàn.

Dạo gần đây mình bắt đầu học lập trình nghiêm túc hơn, và thứ đầu tiên quật mình chính là OOP (Object-Oriented-Programming)

Trước đây mình cứ nghĩ code chạy được là ok. Nhưng khi đụng vào code của các dự án, hay project lớn mình chịuuuuuuu luôn. Nó không chạy tuồn tuột từ trên xuống dưới như mình hay viết. Nó chia nhỏ ra từng phần từng hàm, nó trừu tượng, nguyên tắc hơn cái này móc nối với cái kia, lú cái đầu luôn

Bài viết này không phải bài giảng của chuyên gia gì cả. Đây là bản ghi chép lại những gì mình học và ngộ ra .Mình viết ra để nhắc bản thân hy vọng nó cũng giúp ích cho ai đó ngoài kia đang loay hoay giống mình.

Phần 1: OOP (Object-Oriented-Programming)

Khi đầu học code, mình toàn viết tất cả code vào 1 file main, dài dằng dặc. Sửa chỗ này vì lủng chỗ kìa đỏ lòm code

1. OOP là gì?

Mình cứ nghĩ đơn giản thế này: OOP là cách mọi thứ trong chương trình dưới dạng "Đối tượng"(Object)

Một đối tượng có 2 thứ:

1. Đặc điểm (thuộc tính/properties): Nó trông như thế nào (vd: tên , tuổi , màu sắc, giá ,....)

2. Hành động (phương thức/method): Nó làm được gì (vd: chạy, ăn ,kêu ,tính toán,......)

Trước khi có đối tượng, phải có "bản thiết kế" hay còn gọi là class

  • Class là cái khuôn, là bản vẽ chung (vd: Class Dog, Class XeMay)
  • Object là sản phẩm được tạo ra từ cái khuôn (class) đó (Vd: Honda,Exciter <là object của class XeMay>)

ví dụ

// Bản thiết kế (Class)
class Dog {
    // 1. Đặc điểm (Thuộc tính)
    String name = "Alaska";
    int age = 2;

    // 2. Hành động (Phương thức)
    void bark() {
        System.out.println("Gâu gâu, tôi là " + name);
    }
}

// Khi dùng:
Dog myDog = new Dog(); // Tạo ra 1 Object tên là myDog
myDog.bark(); // Kết quả: Gâu gâu, tôi là Alaska

2. Bốn tính chất của OOP (tứ trụ quyền lực)

Để code của mình không bị loạn và dễ bảo trì, OOP đưa ra 4 nguyên tắc mà mình phải thuộc bằng được image.png

A. Encapsulation (Tính đóng gói) image.png Nó là gì ??

Đóng gói = Giấu bớt dữ liệu + kiểm soát cách người khác tương tác với đối tượng

Ý tưởng:

  • Che giấu những thứ "nhạy cảm" hoặc nội bộ (private)
  • Chỉ cho phép truy cập qua "cửa chính" (getter/setter/public method)
  • Tránh người khác chỉnh sửa linh tinh khiến dữ liệu sai

Ví dụ đời thật:

Bạn có cái tủ khoá. Người ngoài không thể mở tủ(private) Muốn lấy đồ-> phải nhờ bạn mở giùm (public method)

Ví dụ code:

public class User {
    private String password; // không cho ai truy cập trực tiếp

    public String getMaskedPassword() {
        return "****"; 
    }

    public void setPassword(String pwd) {
        if (pwd.length() >= 6) {
            this.password = pwd;
        }
    }
}

-> Không ai động thẳng vào password

Lợi ích:

  • Dễ bảo trì
  • Không bị sửa đổi bừa bãi
  • An toàn dữ liệu

B. Inheritance (Tính kế thừa) unnamed.jpg Nó là gì ??

Kế thừa = Class con nhận lại thuộc tính và phương thức của class cha -. dùng lại được mà không cần viết lại

Ý tưởng:

  • Giúp tái sử dụng code, hạn chế trùng lặp
  • Tạo mối quan hệ cha-con
  • Class con có thể mở rộng, thêm hành vi mới
  • Cho phép override -> tạo đa hình

Ví dụ đời thật:

Bạn là con trong gia đình. Bạn tự động có họ của bố -> giống như class con có sẵn thuộc tính từ class cha

Ví dụ code: Animal có: ăn , ngủ -> Dog sinh ra luôn có: ăn, ngủ + thêm "sủa"

class Animal {
    void eat() {
        System.out.println("Đang ăn...");
    }
}

class Dog extends Animal { // Dog kế thừa Animal
    void bark() {
        System.out.println("Gâu gâu");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.eat();   // dùng method của class cha
        d.bark();  // method của chính nó
    }
}

-> Dog có tất cả những gì Animal có

Lợi ích:

  • Tái sử dụng code
  • Giảm lặp mã, dễ bảo trì
  • Dễ mở rộng hệ thống(class con thêm chức năng mới)
  • Nền tảng cho đa hình (ghì đè method)
  • Tổ chức hệ thống theo dạng phân cấp rõ ràng

C. Polymorphism (Tính đa hình) unnamed.jpg Nó là gì ??

Đa hình = cùng một hành động , nhưng tuỳ đối tượng mà cách thực hiện khác nhau

Hay nói đơn giản: "một lời gọi - nhiều cách chạy"

Ý tưởng:

  • Giúp code linh hoạt, xử lý theo từng loại đối tượng cụ thể
  • Cho phép ghi đè (overide) để class con định nghĩa lại hành vi riêng
  • Có 2 dạng chính
    • Compile-time polymorphism -> Overloading
    • Runtime polymorphism -> Overridding

Ví dụ đời thật:

Bạn nói câu "chạy đi"

  • Nói với chó -> nó chạy bằng 4 chân
  • Nói với người -> họ chạy bằng 2 chân
  • Nói với xe -> nó chạy động cơ

-> Cùng là "run()" nhưng mỗi đối tượng lại chạy khác nhau

Ví dụ code:

**1. Overriding (đa hình lúc chạy - phổ biến nhất)**
class Animal {
    void sound() {
        System.out.println("Âm thanh chung...");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Gâu gâu");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("Meo meo");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a1 = new Dog();
        Animal a2 = new Cat();

        a1.sound(); // Gâu gâu
        a2.sound(); // Meo meo
    }
}

-> Cùng gọi sound() nhưng chạy khác nhau -> chính là đa hình

**2. Overloading (đa hình lúc biên dịch)**
class Calculator {
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }
}

-> Cùng tên add(), tham số khác nhau->compiler tự chọn hàm phù hợp

Lợi ích:

  • Code linh hoạt , mở rộng dễ dàng
  • Giảm if-else lằng nhằng
  • Cho phép xử lý đối tượng theo "kiểu thật sự" của nó
  • Tăng khả năng tái sử dụng và bảo trì

D. Abstraction (Tính trừu tượng) unnamed.jpg Nó là gì ??

Trừu tượng = chỉ cho người dùng thấy cái cần thấy, còn mọi xử lý phức tạp ẩn hết bên trong

Nói cách khác: "Đưa giao diện , giấu nội dung"

Ý tưởng:

  • Giấu bớt logic rối rắm bên trong class
  • Người dùng chỉ tương tác qua interface/abstract method
  • Giúp code rõ ràng, dễ hiểu , dễ thay đổi
  • Thường dùng
    • Interface
    • Abstract Class

Ví dụ đời thật:

Bạn dùng máy giặt

  • Bạn chỉ bấm nút "Start"

  • Còn bên trong : xả, quay, sấy, điều chỉnh nước, cảm biển,...

    -> bạn không cần biết

Giống như interface chỉ đưa ra "hành động" còn xử lý thế nào là chuyện bên trong

Ví dụ code:

**1. Abstraction bằng interface**
interface Payment {
    void pay(double amount);
}

class CreditCardPayment implements Payment {
    @Override
    public void pay(double amount) {
        System.out.println("Thanh toán bằng thẻ tín dụng: " + amount);
    }
}

class MomoPayment implements Payment {
    @Override
    public void pay(double amount) {
        System.out.println("Thanh toán bằng Momo: " + amount);
    }
}

public class Main {
    public static void main(String[] args) {
        Payment p = new MomoPayment();
        p.pay(50000); 
    }
}

-> Người dùng chỉ thấy pay() còn bên trong Môn hay CreditCard xử lý ra sao -> bị giấu

**2. Abstractuon bằng abstract class**
abstract class Animal {
    abstract void sound(); // hành động bắt buộc phải có

    void sleep() {
        System.out.println("Đang ngủ...");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Gâu gâu");
    }
}

-> Dong bắt buộc phải triển khai cái sound() nhưng không cần quan tâm cái sleep() đã được triển khai sẵn

Lợi ích:

  • Che giấu phức tạp
  • Code dễ đọc hươn
  • Giảm phụ thuộc giữa các class
  • Dễ thay đổi logic bên trong mà không ảnh hưởng phần gọi bên ngoài
  • Tăng tính linh hoạt nhờ interface/abstract

3. Sai lầm phổ biến

Học lý thuyết đã lú rồi nhưng lúc bắt tay vào code thật thì mình vướng sạn liên tục.

A. Nhét hết mọi thứ vào 1 class

  • Lỗi: Mình thường lười tạo nhiều file, nên nhét hết mọi thứ vào một class. (vd: Class QuanLy vừa luuw thông tin User, vừa tính tiền , vừa kết nối DB vừa gửi gmail)
  • Hậu quả: Class này dài cả trăm ngàn dòng. Muốn sửa cái tính năng mail thôi mà sỡ chỉnh nhầm dòng code nào ở tính tiền thì toang cả lũ. Nó vi phạm nguyên tắc "Một class thì chỉ nên làm một việc"

B. Lạm dụng kế thừa vô tội vạ

  • Lỗi: Cứ thấy có vài dòng code giống nhau là mình cho kế thừa , bất chấp bọn nó có liên quan gì không (vd: mình cho class MayBay kế thừa class Chim chỉ vì cả 2 đều có hàm bay())
  • Hậu quả: sau này class Chim thêm hàm deTrung() (đẻ trứng), tự nhiên ông MayBay cũng biết đẻ trứng luôn -> vô lý luôn. Kế thừa là quan hệ "là một" đừng dùng bừa

C. Lười đóng gói (bỏ qua encapsulation)

  • Lỗi: Vì lười viết getter/setter mà mình để hết các biến public cho nhanh gọn
  • Hậu quả: Dữ liệu bị các phần code khác sửa lung tung không kiểu soát được (ví dụ tuối bị gán âm, số dư tk bị âm). Lúc debug lỗi không biết nó sai từ đâu

4. Vì sao OOP quan trọng cho Backend?

Lúc đầu mình nghĩ OOP chỉ để cho vui nói về quan hệ liên kết của các class mà thôi, nhưng khi tìm hiểu về BE, mình thấy nó là xương sống luôn.

  • Mô hình hoá dữ liệu thực tế: Làm backend là làm việc với User , Đơn hàng (Order), Sản phầm(Product), thanh toán (Payment),.... OOP giúp mình bê nguyên mấy cái này ngoài đời thực vào codde duới dạng class và object tự nhiên luôn
  • Dễ bảo trì và mở rộng hệ thống lớn: Các dự án BE thường rất to và sống lâu. Nên viết code kiểu ăn ngay từ trên xuống dưới trôi tuột, sau này ô sép bảo "thêm tính năng thanh toán bằng ZaloPay", thì mình lại lúng túng với cả đống code cũ để sửa if-else. Nhưng nếu OOP với đa hình thì chỉ cần tạo thêm class ZaloPayPayment là ok rồi, vẫn ko đụng vào code cũ tránh bị loạn
  • Làm việc nhóm hiệu quả: Dự án lớn thì nhiều người làm. Nhờ tính đóng gói và trừu tượng, mình có thể viết class GioHang, bạn mình biết class ThanhToan.Hai đứa chỉ cần thống nhất mấy cía hàm public (cái giao diện bên ngoài) là được , còn bên trong ô nào thì viết của ô ấy kệ ô kia, không sợ code người này đá code người kia

=>OOp không phải là cái gì thần thánh nhưng là nền móng của hầu hết hệ thống backend.Mình mới chỉ học và viết lại theo cách hiểu của chính mình.Nếu bạn đang tìm hiểu giống mình, hy vọng bài này giúp bạn đỡ lúng tùng hơn

Phần 2: Abstract và Interface (Hai ông thần gây lú)

Sau khi đã nắm được Tứ trụ OOP (đóng gói, kế thừa, đa hình, trừu tượng) mình lại vấp phải 2 khái niệm này. Cả 2 đều dùng để tạo ra tính Trừ tượng, nhưng cách dùng và vai trò của chúng lại khác nhau hoàn toàn

1. Phân biệt nhanh theo vai trò

Đặc điểm Abstract Class Interface
Bản chất Là một class nhưng không thể tạo đối tượng Là một hợp đồng thuần tuý về hành vi
Quan hệ Kế thừa (extends). Quan hệ IS-A (là một) Triển khai (implememts). Quan hệ CAN-DO (có thể làm)
Mục đích Tạo khuôn mẫu chung cho nhóm đối tượng có liên quan chặt chẽ Định nghĩa cam kết về những hành động phải có
Tính đa kế thừa Không được phép kế thừa nhiều Abstract Class do xung đột Được phép triển khai nhiều Interface vì chỉ là cam kết
Nội dung Có hể chứa cả abstract method (chưa triển khai) và concrete method (đã triển khai) Chỉ chứa các phương thức abstract chưa triển khai và hằng số

2. Abstract class (lớp trừu tượng)

Định nghĩa: Nó là một lớp class nhưng không hoàn chỉnh. Nó được tạo ra để các lớp con kế thừa và hoàn thiện

Ý tưởng: Dùng khi bạn có một nhóm đối tượng cùng họ hàng và bạn muốn “quy định khung xương chung” cho chúng.

Đặc điểm cốt lõi:

  • Có thể có code sẵn để con dùng lại
  • Có thể có thuộc tính
  • Có thể chưa abstract method (bắt buộc thằng con phải định nghĩa)
  • Không tạo object trực tiếp

ví dụ code

abstract class Vehicle {
    String engineType = "Petrol";

    // Code dùng chung
    void startEngine() {
        System.out.println(engineType + " Engine starting...");
    }

    // Code bắt buộc lớp con phải hoàn thiện
    abstract void run();
}

class Car extends Vehicle {
    @Override
    void run() {
        System.out.println("Car running with 4 wheels.");
    }
}

=>Car thừa hưởng startEngine() nhưng vẫn phải tự định nghĩa run()

Dùng abstract khi nào?

  • Khi các class con cùng bản chất
  • Khi bạn muốn có code mặc định để tái sử dụng
  • Khi bạn muốn ép tất cả các lớp con phải có vài hành vi chung

3. Interface(Giao diện)

Định nghĩa: Nó là một bản hợp đồng cam kết về hành vi. Nó quy định cái gì phải làm chứ không quan tâm làm như nào

Ý tưởng: Dùng để tạo ra nhóm hành vi chung cho các đối tượng không có quan hệ huyết thống (ví dụ một chiếc xe và một con chó không liên quan nhưng cả 2 đều có thể chạy)

Đặc điểm cốt lõi:

  • Chỉ chứa abstract method
  • Không chứa logic xử lý phức tạp
  • Một class có thể implements nhiều interface -> đa kế thừa hành vi

ví dụ code

interface Transport {
    void loadGoods();
    void deliver();
}

class Truck implements Transport {
    public void loadGoods() {
        System.out.println("Truck: Xếp hàng lên thùng xe.");
    }

    public void deliver() {
        System.out.println("Truck: Chạy trên đường.");
    }
}

class Drone implements Transport {
    public void loadGoods() {
        System.out.println("Drone: Dùng cánh tay robot để nâng hàng.");
    }

    public void deliver() {
        System.out.println("Drone: Bay giao hàng.");
    }
}

=> Tổng kết: Interface chit yêu cầu "mày phải làm ", còn "làm như nào" thì tự mày lo

Dùng interface khi nào?

  • Khi các class không liên quan về bản chất nhưng đều phải làm một hành động nào đó
  • Khi cần đa kế thừa
  • Khi thiết kế hệ thông theo kiểu module -> mỗi module cam kết một hành vi

4. Khi nào dùng Abstract Class, khi nào dùng Interface??

Tình huống Dùng Abstract class Dùng Interface
Các class con có chung bản chất ✔️
Các class con hoàn toàn không liên quan ✔️
Cần chia sẻ code chung (logic bên trong) ✔️
Cần đa kế thừa hành vi ✔️
Muốn ép class con phải override một số hàm ✔️
Xây dựng mô hình phân cấp (Animal -> Dog, Cat) ✔️
Xây dựng khả năng chung (bay , chạy, bơi) ✔️

Tóm gọn bằng 1 câu:

  • Abstract class= "một nhóm chung máu mủ" -> kế thừa tính chất chung
  • Interface="Không cùng họ hàng nhưng cùng có khả năng giống nhau"

Lời kết: Hành trình mới chỉ bắt đầu

Phù!!! Vậy là chúng ta đã cùng nhau đi qua gần như toàn bộ nền tảng quan trọng nhất của lập trình hướng đối tượng – từ Tứ trụ OOP cho đến cách phân biệt Abstract Class và Interface.

Nếu bạn đã đọc đến đây và thật sự cố gắng thẩm thấu chúng, thì xin chúc mừng: bạn đã đặt được một nền móng cực kỳ vững chắc cho hành trình trở thành một Backend Developer.

Và thật lòng mà nói, code đẹp không nằm ở chỗ bạn thuộc bao nhiêu định nghĩa, mà ở chỗ bạn hiểu vì sao mỗi khái niệm được sinh ra và nó giải quyết vấn đề gì. Khi hiểu được “lý do tồn tại”, bạn sẽ biết chính xác nên dùng cái gì, lúc nào, và vì sao.

Chúc bạn – và cả chính mình – tiếp tục lên trình từng ngày. Code ngày càng sạch. Tư duy ngày càng sáng. Và hành trình phía trước ngày càng thú vị.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí