0

OOP

Đối với lập trình viên thì thuật ngữ lập trình hướng đối tượng - Object Oriented Programming (OOP) đã quá đỗi quen thuộc. Từ thời còn là sinh viên mài mông trên giảng đường chúng ta đã đánh vật với những ngôn ngữ OO như C++, .Net, Java. Và ai cũng thấu hiểu 3 nguyên lý trụ cột của OOP là :

  • Tính kế thừa (Inheritance)
  • Tính đóng gói (Encapsulation)
  • Tính đa hình (Polymorphism) Dù nắm rõ hay chỉ là sơ sơ 3 nguyên lý trên thì bất cứ ai cũng hiểu rằng mục đích của OOP là tạo ra những tài nguyên (source code) có thể tái sử dụng, tổ chức rõ ràng, dễ dàng maintain. Nhưng hãy thử đặt vào trường hợp bạn là 1 new dev chân ướt chân ráo đi làm ngày đầu tiên được đưa vào maintain 1 dự án trước đó có khoảng 30 dev phát triển, mà không có lấy 1 dòng tài liệu thiết kế phần mềm thì sao. Tôi chắc chắn cảm giác nó giống như bạn bốc shit vợi =)). Điều gì đã làm nên cơ sự này ? rõ ràng từ lúc đi học tới lúc được những đồng chí senior engineer ca ngợi hết lời về OOP vợi mà feel like bốc shit thật là kinh khủng. Chúng ta sẽ chém gió vài vấn đề xem có giác ngộ và tìm ra cho bản thân câu trả lời cho vấn đề trên không nhé.

I. Inheritance

Thoạt nhìn, kế thừa dường như là lợi ích lớn nhất của OOP. Một ví dụ đơn giản là cây kế thừa Shape như dưới đấy.

Và Tái sử dụng là lời của ngày. Không ... làm cho năm đó và có lẽ mãi mãi. Tôi nuốt toàn bộ và lao vào thế giới với cái nhìn sâu sắc mới thấy của tôi.

Vấn đề Banana Monkey Jungle

Với tôn giáo trong trái tim tôi và các vấn đề để giải quyết, tôi bắt đầu xây dựng lớp Hierarchies và viết mã. Và tất cả đều đúng với thế giới. Tôi sẽ không bao giờ quên ngày hôm đó khi tôi đã sẵn sàng để thực hiện lời hứa của Reuse bằng cách kế thừa từ một lớp học hiện có. Đây là khoảnh khắc tôi đã chờ đợi. Một dự án mới đến và tôi nghĩ lại lớp học mà tôi đã rất thích trong dự án cuối cùng của tôi. Không vấn đề gì. Tái sử dụng để giải cứu. Tất cả những gì tôi phải làm chỉ đơn giản lấy lớp từ dự án khác và sử dụng nó. Vâng ... thực sự ... không chỉ là Lớp học. Chúng ta sẽ cần lớp cha mẹ. Nhưng ... Nhưng đó là nó. Ugh ... Đợi đã ... Hình như chúng ta cũng cần cha mẹ của cha mẹ nữa ... Và rồi ... Chúng ta sẽ cần tất cả cha mẹ. Được rồi ... Được rồi ... Tôi xử lý điều này. Không vấn đề gì. Và tuyệt vời. Bây giờ nó sẽ không biên dịch. Tại sao?? Ồ, tôi hiểu rồi ... Đây đối tượng chứa khác này đối tượng. Vì vậy tôi sẽ cần nó nữa. Không vấn đề gì. Chờ ... Tôi không chỉ cần có đối tượng. Tôi cần cha mẹ của đối tượng và cha mẹ của phụ huynh và vân vân với mọi vật chứa đựng và TẤT CẢ cha mẹ của những gì chứa cùng với cha mẹ, cha mẹ của họ ... Ugh.

The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. - Joe Armstrong

Giải pháp Banana Monkey Jungle

Tôi có thể thuần túy vấn đề này bằng cách không tạo ra các hệ thống phân cấp quá sâu. Nhưng nếu Thừa kế là chìa khóa để Tái sử dụng, thì bất kỳ giới hạn nào tôi đặt vào cơ chế đó chắc chắn sẽ giới hạn các lợi ích của Tái sử dụng. Đúng? Đúng. Vậy một lập trình hướng đối tượng người nghèo là ai, người đã có một sự giúp đỡ lành mạnh của viện trợ Kool, để làm gì? Chứa và đại biểu. Thêm về điều này sau.

Vấn đề The Diamond

Sớm hay muộn, vấn đề sau sẽ làm xấu nó và, tùy thuộc vào ngôn ngữ, đầu không thể giải quyết được.

Hầu hết các ngôn ngữ OO không hỗ trợ điều này, mặc dù điều này có vẻ hợp lý. Điều gì là khó khăn về việc hỗ trợ này trong các ngôn ngữ OO? Vâng, hãy tưởng tượng giả mã sau đây:

Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
  function start() {
  }
}
Class Printer inherits from PoweredDevice {
  function start() {
  }
}
Class Copier inherits from Scanner, Printer {
}

Lưu ý rằng cả lớp Scanner và lớp Printer đều thực hiện một hàm gọi là start . Vậy tính năng bắt đầu nào mà lớp Copier kế thừa? Các Scanner một? Các máy in một? Nó không thể là cả hai.

Giải pháp The DIamond

Giải pháp là đơn giản. Đừng làm thế. Vâng đúng vậy. Hầu hết các ngôn ngữ OO không cho phép bạn làm điều này. Nhưng, nhưng ... nếu tôi phải mô hình này? Tôi muốn sử dụng lại của mình! Sau đó, bạn phải sử dụng Contain và Delegate

Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
  function start() {
  }
}
Class Printer inherits from PoweredDevice {
  function start() {
  }
}
Class Copier {
  Scanner scanner
  Printer printer
  function start() {
    printer.start()
  }
}

Chú ý ở đây là lớp Copier bây giờ có chứa một thể hiện của Máy in và máy quét . Nó ủy thác các chức năng bắt đầu để thực hiện của lớp Máy in . Nó có thể dễ dàng được ủy thác cho Máy quét . Vấn đề này là một vết nứt trong cột Thừa kế.

Vấn đề đa kế thừa

Vì vậy, tôi đang làm cho phân cấp của tôi nông cạn và giữ cho chúng khỏi bị chu kỳ. Không có kim cương cho tôi. Và tất cả đều đúng với thế giới. Đó là cho đến khi ... Một ngày, mã của tôi hoạt động và ngày hôm sau nó ngừng hoạt động. Đây là kicker. Tôi đã không thay đổi mã của tôi . Vâng, có lẽ đó là lỗi ... Nhưng chờ đợi ... Một cái gì đó đã thay đổi ... Nhưng nó không phải là trong mã của tôi . Hóa ra sự thay đổi trong lớp mà tôi được thừa kế. Làm thế nào có thể thay đổi trong lớp cơ sở phá vỡ mã của tôi ?? Đây là cách ... Hãy tưởng tượng các lớp cơ sở sau đây (Nó được viết bằng Java, nhưng nó phải là dễ hiểu nếu bạn không biết Java): Nhập khẩu java.util.ArrayList;

import java.util.ArrayList;
public class Array
{
  private ArrayList<Object> a = new ArrayList<Object>();
 
  public void add(Object element)
  {
    a.add(element);
  }
 
  public void addAll(Object elements[])
  {
    for (int i = 0; i < elements.length; ++i)
      a.add(elements[i]); // this line is going to be changed
  }
}

QUAN TRỌNG : Lưu ý dòng nhận xét của mã. Dòng này sẽ được thay đổi sau đó sẽ phá vỡ mọi thứ. Lớp này có 2 chức năng trên giao diện, add () và addAll () . Hàm add () sẽ thêm một phần tử và addAll () sẽ thêm nhiều phần tử bằng cách gọi hàm add . Và đây là lớp Derived: Public class ArrayCount mở rộng Array

public class ArrayCount extends Array
{
  private int count = 0;
 
  @Override
  public void add(Object element)
  {
    super.add(element);
    ++count;
  }
 
  @Override
  public void addAll(Object elements[])
  {
    super.addAll(elements);
    count += elements.length;
  }
}

Các ArrayCount lớp là một chuyên môn của vị tướng Mảng lớp. Sự khác biệt hành vi duy nhất là ArrayCount giữ một số của số lượng các yếu tố. Chúng ta hãy nhìn vào cả hai lớp này một cách chi tiết. Các mảng add () thêm phần tử vào một địa phương ArrayList . Các mảng addAll () gọi là địa phương ArrayList thêm cho mỗi phần tử. Các ArrayCount add () gọi mẹ của add () và sau đó tăng sự đếm . Các ArrayCount addAll () gọi mẹ của addAll () và sau đó tăng sự đếm bằng số phần tử. Và tất cả đều hoạt động tốt. Bây giờ cho sự thay đổi phá vỡ . Dòng nhận xét của mã trong lớp Cơ sở được thay đổi như sau:

public void addAll(Object elements[])  {
    for (int i = 0; i < elements.length; ++i) {
          add(elements[i]); // this line was changed
      }
}

Đối với chủ sở hữu của lớp cơ sở, nó vẫn hoạt động như quảng cáo. Và tất cả các bài kiểm tra tự động vẫn vượt qua . Nhưng chủ sở hữu không biết đến lớp Derived. Và chủ sở hữu của lớp Derived đang ở trong một sự đánh thức thô lỗ. Bây giờ ArrayCount addAll () gọi mẹ của addAll () mà trong nội bộ gọi là add () đã được overriden bởi nguồn gốc lớp. Điều này làm cho đếm được incremented mỗi khi Derived của lớp add () được gọi và sau đó nó incremented AGAIN bởi số lượng các yếu tố được thêm vào trong Derived của lớp addAll () . IT'S COUNTED TWICE. Nếu điều này có thể xảy ra, và nó có, tác giả của lớp Derived phải BIẾT làm thế nào các lớp cơ sở đã được thực hiện. Và họ phải được thông báo về mọi thay đổi trong lớp Base vì nó có thể phá vỡ lớp Derived theo những cách không dự đoán được. Ugh! Nứt khổng lồ này là mãi mãi đe dọa sự ổn định của cột thừa kế quý giá.

Giải pháp cho vấn đề đa kế thừa

Một lần nữa Contain và Delegate được sử dung. Bằng cách sử dụng Contain và Delegate, chúng ta chuyển từ lập trình White Box sang lập trình Black Box. Với White Box lập trình, chúng ta phải nhìn vào việc thực hiện các lớp cơ sở. Với chương trình Black Box, chúng ta có thể hoàn toàn không biết gì về việc thực hiện vì chúng ta không thể đưa mã vào lớp Base bằng cách ghi đè lên một trong các chức năng của nó. Chúng ta chỉ phải quan tâm đến Giao diện. Xu hướng này đang làm phiền ... Di sản được cho là một chiến thắng to lớn cho Reuse. Các ngôn ngữ theo hướng đối tượng không cho phép Contain và Delegate dễ thực hiện. Chúng được thiết kế để làm cho kế thừa trở nên dễ dàng. Nếu bạn giống tôi, bạn đang bắt đầu tự hỏi về điều này thừa kế. Nhưng quan trọng hơn, điều này sẽ làm rung chuyển sự tự tin của bạn trong khả năng phân loại thông qua Phân cấp.

Vấn đề cây kế thừa

Mỗi lần tôi bắt đầu làm việc tại một công ty mới, tôi phải vật lộn với vấn đề khi tôi tạo ra một nơi để đặt Tài liệu Công ty của tôi, ví dụ như Cẩm nang Nhân viên. Tôi có tạo một thư mục có tên Tài liệu và sau đó tạo một thư mục được gọi là Công ty trong đó? Hoặc để tôi tạo một thư mục được gọi là Công ty và sau đó tạo một thư mục có tên Tài liệu trong đó? Cả hai công việc. Nhưng điều đó là đúng? Nào là tốt nhất? Ý tưởng phân loại theo phân loại là có các lớp cơ bản (cha mẹ) tổng quát hơn và các lớp học Thuộc tính (trẻ em) là các phiên bản chuyên biệt hơn của các lớp đó. Và thậm chí còn chuyên biệt hơn khi chúng tôi thực hiện theo cách của chúng tôi xuống chuỗi kế thừa. (Xem Hắc tinh hình ở trên) Nhưng nếu cha mẹ và đứa trẻ có thể tự ý thay đổi địa điểm, thì rõ ràng có điều gì đó sai trái với mô hình này.

Giải pháp cho vấn đề cây kế thừa

Có gì đó sai. Phân cấp giai cấp không hoạt động . Vì vậy hệ thống phân cấp tốt cho những gì? Nếu bạn nhìn vào thế giới thực, bạn sẽ thấy Phân loại (hoặc Quyền sở hữu độc quyền) ở mọi nơi. Những gì bạn sẽ không tìm thấy là Categorical Hierarchies. Hãy để chìm trong một lúc. Mô hình hướng đối tượng được đưa ra dựa trên thế giới thực, một trong đó chứa các đối tượng. Nhưng sau đó nó sử dụng một mô hình bị hỏng, tức là Cấu trúc phân cấp, nơi mà không có sự tương đồng thế giới thực. Nhưng thế giới thực được lấp đầy bằng các Phân tầng Chống lại. Một ví dụ tuyệt vời của một Hiến pháp ngăn chặn là tất của bạn. Chúng nằm trong ngăn kéo sock chứa trong một ngăn kéo trong tủ quần áo của bạn có trong phòng ngủ có trong nhà bạn, v.v ... Các thư mục trên ổ cứng của bạn là một ví dụ khác của Phân loại Hàm Phân chia. Chúng chứa các tệp. Vậy làm cách nào để phân loại? Vâng, nếu bạn nghĩ về Tài liệu công ty, nó khá nhiều không quan trọng mà tôi đặt chúng. Tôi có thể đặt chúng trong một thư mục của Documents hoặc thư mục Stuff. Cách tôi phân loại nó là với các thẻ. Tôi gắn thẻ tệp với các thẻ sau:

  • Sổ tay
  • Công ty Tài liệu
  • Thẻ không có thứ tự hoặc thứ bậc. (Điều này giải quyết vấn đề kim cương.) Các thẻ tương tự như các giao diện vì bạn có thể có nhiều loại kết hợp với tài liệu. Nhưng với rất nhiều vết nứt, có vẻ như cột thừa kế đã giảm.

II. Encapsulation

Thoạt nhìn, đóng gói dường như là lợi ích lớn thứ hai của Lập trình hướng đối tượng. Các biến trạng thái đối tượng được bảo vệ khỏi truy cập bên ngoài, nghĩa là chúng được Đóng gói trong Đối tượng. Chúng ta không còn phải lo lắng về các biến số toàn cầu đang được truy cập bởi ai-người-biết-ai. Tóm lược là an toàn cho các biến của bạn.

Vấn đề Reference

Đối với hiệu quả, đối tượng được chuyển đến các chức năng NOT bởi giá trị của chúng mà bằng tham khảo. Điều đó có nghĩa là các hàm sẽ không vượt qua đối tượng, mà thay vào đó truyền một tham chiếu hoặc con trỏ tới đối tượng. Nếu một đối tượng được truyền bằng tham chiếu đến một Constructor Đối tượng, nhà xây dựng có thể đặt đối tượng tham chiếu đó trong một biến riêng được bảo vệ bởi Encapsulation. Nhưng đối tượng được thông qua KHÔNG an toàn! Tại sao không? Bởi vì một số đoạn mã khác có con trỏ tới đối tượng, ví dụ. Mã gọi là Constructor. Nó PHẢI có một tham chiếu đến đối tượng nếu không nó không thể vượt qua nó để các Constructor?

Giải pháp cho vấn đề Reference

Constructor sẽ phải Clone các thông qua trong Object. Và không phải là một clone cạn nhưng một clone sâu, tức là mọi đối tượng được chứa trong các thông qua trong Object và mọi đối tượng trong các đối tượng và vv và như vậy. Vì vậy, nhiều cho hiệu quả. Và đây là kicker. Không phải tất cả các đối tượng có thể được Cloned. Một số có tài nguyên Hệ điều hành liên kết với họ làm cho nhân bản vô dụng tại tốt nhất hoặc tại tồi tệ nhất không thể. Và MỌI đơn chính ngôn ngữ OO có vấn đề này. Tạm biệt, Tóm lược.

III. Polymorphism

Đa hình là người con đầu lòng của Trinity. Nó là loại Larry Fine của nhóm. Ở mọi nơi họ đi, anh ấy ở đó, nhưng anh ấy chỉ là một nhân vật phụ. Nó không phải là Đa hình không phải là tuyệt vời, nó chỉ là bạn không cần một ngôn ngữ hướng đối tượng để có được điều này. Giao diện sẽ cung cấp cho bạn điều này. Và không có tất cả các hành lý của OO. Và với Giao diện, không có giới hạn cho bao nhiêu hành vi khác nhau mà bạn có thể trộn vào. Vì vậy, không có nhiều ado, chúng tôi nói lời tạm biệt với đa hình OO và xin chào với đa hình dựa trên giao diện.

Hy vọng mọi người có thể hiểu bên cạnh nhưng lời ích thì cần tìm giải pháp cho những mặt hạn chế khi sử dụng OOP.


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í