550 likes | 760 Views
Nguyên lý thiết kế hướng đối tượng. Lập trình hướng đối tượng. Nguyên tắc số 1. Giảm thiểu khả năng truy nhập tới lớp và các thành viên. "Trừu tượng hóa" nghĩa là gì?.
E N D
Nguyên lý thiết kế hướng đối tượng Lập trình hướng đối tượng
Nguyên tắc số 1 Giảm thiểu khả năng truy nhập tới lớp và các thành viên Nguyên lý thiết kế hướng đối tượng
"Trừu tượng hóa" nghĩa là gì? • Tony Hoare: “trừu tượng hóa xuất phát từ một cách nhìn nhận những đặc điểm tương đồng giữa một số đối tượng, tình thế, hoặc quy trình nhất định trong thế giới thực, và quyết định tập trung vào những điểm tương đồng này và nhất thời lờ đi các điểm khác biệt.” • Grady Booch: “Một trừu tượng hóa kí hiệu các đặc điểm cốt lõi của một đối tượng mà các đặc điểm này phân biệt nó với tất cả các loại đối tượng khác, cho ta các ranh giới được xác định rõ ràng. Tất cả được xét một cách tương đối trong góc nhìn của người quan sát.” • Trừu tượng hóa là một trong những phương pháp nền tảng để đối phó với sự phức tạp • Một trừu tượng hóa tập trung vào hình ảnh bên ngoài của một đối tượng và tách hành vi của đối tượng đó ra khỏi cài đặt của nó. Nguyên lý thiết kế hướng đối tượng
Đóng gói - encapsulation • Grady Booch: "Đóng gói là để chia tách giữa giao diện cam kết của một trừu tượng hóa và cài đặt của nó. • Craig Larman: "Đóng gói là một cơ chế được dùng để che dữ liệu, cấu trúc bên trong, và chi tiết cài đặt của một đối tượng. Mọi tương tác với đối tượng được thực hiện qua một giao diện công khai của các thao tác" • Các lớp đối tượng không nên để mở các chi tiết cài đặt nội bộ của mình Nguyên lý thiết kế hướng đối tượng
Che dấu thông tin ở Java • Sử dụng các thành viên private và các hàm đọc (get) và ghi (set) mỗi khi có thể. • Ví dụ: • Thay thế public double speed; • bằng private double speed; public double getSpeed() { return speed; } public double setSpeed(double newSpeed) { speed = ….. } Nguyên lý thiết kế hướng đối tượng
Che dấu thông tin ở Java • Ta có thể quy định các ràng buộc về giá trị public void setSpeed(double newSpeed) { if (newSpeed < 0) { sendErrorMessage(...); newSpeed = Math.abs(newSpeed); } speed = newSpeed; } • Nếu các client được truy cập trực tiếp đến thành viên dữ liệu thì từng client phải chịu trách nhiệm kiểm tra ràng buộc Nguyên lý thiết kế hướng đối tượng
Che dấu thông tin ở Java • Ta có thể thay đổi biểu diễn dữ liệu bên trong lớp đối tượng mà không phải sửa giao diện // Now using metric units (kph, not mph) public void setSpeedInMPH(double newSpeed) { speedInKPH = convert(newSpeed); } public void setSpeedInKPH(double newSpeed) { speedInKPH = newSpeed; } Nguyên lý thiết kế hướng đối tượng
Che dấu thông tin ở Java • Ta có thể thực hiện các hiệu ứng phụ tùy ý public void setSpeed(double newSpeed) { speed = newSpeed; notifyObservers(); } • Nếu các client của một lớp truy nhập trực tiếp dữ liệu của mình, mỗi client sẽ phải chịu trách nhiệm chạy hiệu ứng phụ Nguyên lý thiết kế hướng đối tượng
Nguyên tắc số 2 Ưu tiên sử dụng Composition hơn Inheritance Nguyên lý thiết kế hướng đối tượng
Composition • Phương pháp tái sử dụng mà trong đó chức năng mới được xây dựng bằng cách tạo một đối tượng có thành phần là các đối tượng khác • Chức năng mới được tạo bằng cách sử dụng chức năng của một trong các đối tượng thành phần • Composition có thể là chứa • Tham chiếu • Giá trị • C++ cho phép chứa giá trị đối tượng hoặc chứa tham chiếu đối tượng • Java chỉ cho phép chứa tham chiếu đối tượng. Nguyên lý thiết kế hướng đối tượng
Ưu/nhược điểm của Composition Ưu điểm: • Lớp chứa chỉ có thể truy nhập tới các đối tượng thành phần qua giao diện của các đối tượng đó. • Tái sử dụng kiểu "hộp đen", do chi tiết cài đặt của các đối tượng thành phần không lộ ra ngoài • Tính đóng gói cao • Ít phụ thuộc về cài đặt hơn • Mỗi lớp chỉ chú trọng vào một tác vụ • Quan hệ composition có thể được xác định một cách động trong thời gian chạy qua việc đối tượng nhận tham chiếu tới các đối tượng khác Nguyên lý thiết kế hướng đối tượng
Ưu/nhược điểm của Composition • Nhược điểm • Kết quả là hệ thống có xu hướng chứa nhiều đối tượng hơn • Các giao diện phải được định nghĩa cẩn thận để sử dụng nhiều đối tượng khác nhau trong vai trò các khối cấu thành Nguyên lý thiết kế hướng đối tượng
Thừa kế • Phương pháp tái sử dụng mà trong đó chức năng mới được xây dựng bằng cách mở rộng cài đặt của một đối tượng có sẵn • Lớp tổng quát (superclass) liệt kê một cách tường minh các thuộc tính và phương thức chung • Lớp chuyên hóa (subclass) mở rộng cài đặt với các thuộc tính và phương thức bổ sung Nguyên lý thiết kế hướng đối tượng
Ưu nhược điểm của thừa kế Ưu điểm: • Dễ dàng cài lớp mới, do phần lớn đã được thừa kế • Dễ sửa hoặc mở rộng cài đặt được tái sử dụng Nhược điểm • Phá vỡ tính đóng gói, do nó để cho lớp con biết về chi tiết cài đặt của lớp cha • Tái sử dụng kiểu "hộp trắng" • Có thể phải sửa lớp con nếu cài đặt của lớp cha có thay đổi. • Tại thời gian chạy, không thể thay đổi cài đặt đã được thừa kế từ các lớp cha Nguyên lý thiết kế hướng đối tượng
Ví dụ Inheritance & Compostion Ví dụ lấy từ cuốn Effective Java của Joshua Bloch. Ta cần một dạng HashSet (tập hợp được cài bằng bảng băm) có chức năng lưu lại số lần chèn thêm phần tử. Ta tạo lớp con của HashSet: public class InstrumentedHashSet extends HashSet { // The number of attempted element insertions private int addCount = 0; public InstrumentedHashSet(Collection c) {super(c);} public InstrumentedHashSet(int initCap, float loadFactor) { super(initCap, loadFactor); } Nguyên lý thiết kế hướng đối tượng
public class InstrumentedHashSet extends HashSet { // The number of attempted element insertions private int addCount = 0; public InstrumentedHashSet(Collection c) {super(c);} public InstrumentedHashSet(int initCap, float loadFactor) { super(initCap, loadFactor); } public boolean add(Object o) { addCount++; return super.add(o); } public boolean addAll(Collection c) { addCount += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; } } public static void main(String[] args) { InstrumentedHashSet s = new InstrumentedHashSet(); s.addAll(Arrays.asList(new String[] {"Snap","Crackle","Pop"})); System.out.println(s.getAddCount()); } Nguyên lý thiết kế hướng đối tượng
Ví dụ Inheritance & Compostion • Kết quả là 6 thay vì 3 như trông đợi. Tại sao? • Cài đặt bên trong của addAll() trong lớp cha HashSet gọi phương thức add() • Tại add() của InstrumentedHashSet, ta cộng 3 vào addCount • Gọi addAll() của HashSet, với mỗi phần tử, phương thức addAll() này lại gọi add() – bản định nghĩa lại của IntrumentedHashSet. • Kết quả: mỗi phần tử bổ sung được đếm 2 lần Nguyên lý thiết kế hướng đối tượng
Ví dụ Inheritance & Compostion • Có vài cách sửa, nhưng hãy ghi nhận điểm yếu của lớp con IntrumentedHashSet: chi tiết cài đặt của lớp cha ảnh hưởng tới hoạt động của lớp con • Cách sửa tốt nhất: sử dụng composition. • Viết lớp IntrumentedSet chứa một đối tượng Set • Lớp này lặp lại interface Set, nhưng tất cả các thao tác tập hợp sẽ được chuyển tới cho đối tượng Set chứa trong IntrumentedSet • IntrumentedSet được gọi là một lớp bọc ngoài (wrapper class), nó bọc ra ngoài một đối tượng Set • Đây là ví dụ về đại diện ủy quyền qua việc sử dụng composition Nguyên lý thiết kế hướng đối tượng
public class InstrumentedSet implements Set { private final Set s; private int addCount = 0; public InstrumentedSet(Set s) {this.s = s;} public boolean add(Object o) { addCount++; return s.add(o); } public boolean addAll(Collection c) { addCount += c.size(); return s.addAll(c); } public int getAddCount() {return addCount;} // Forwarding methods (the rest of the Set interface methods) public void clear() { s.clear(); } public boolean contains(Object o) { return s.contains(o); } public boolean isEmpty() { return s.isEmpty(); } public int size() { return s.size(); } public Iterator iterator() { return s.iterator(); } public boolean remove(Object o) { return s.remove(o); } public Object[] toArray() { return s.toArray(); } public boolean equals(Object o) { return s.equals(o); } public String toString() { return s.toString(); } Nguyên lý thiết kế hướng đối tượng
Ví dụ Inheritance & Compostion • Một vài điểm cần lưu ý về InstrumentedSet : • Lớp này là một Set • Có một constructor có tham số là một Set • Đối tượng Set nằm trong lớp có thể là một đối tượng thuộc bất cứ lớp nào cài đặt interface Set (có thể không phải HashSet) • Lớp này rất linh động và có thể bọc ra ngoài một đối tượng Set bất kì • Ví dụ: • List list = new ArrayList(); • Set s1 = new InstrumentedSet(new TreeSet(list)); • int capacity = 7; • float loadFactor = .66f; • Set s2 = new InstrumentedSet(new HashSet(capacity, loadFactor)); Nguyên lý thiết kế hướng đối tượng
Quy tắc Coad Chỉ sử dụng thừa kế khi tất cả các tiêu chí sau đều được thỏa mãn: • Lớp con "là một loại đặc biệt" chứ không phải "là một vai trò" của lớp cha. • Đối tượng của lớp con không bao giờ cần trở thành một đối tượng của một lớp khác • Lớp con mở rộng, chứ không định nghĩa lại hoặc xóa bỏ, các trách nhiệm của lớp cha • Lớp con không mở rộng khả năng của một lớp chỉ là lớp tiện ích • Đối với một lớp trong ngữ cảnh thực của bài toán, lớp con chuyên biệt hóa một vai trò, giao tác, hoặc thiết bị. Nguyên lý thiết kế hướng đối tượng
Ví dụ 1 Nguyên lý thiết kế hướng đối tượng
Ví dụ 1 • "Là một loại đặc biệt" chứ không phải "là một vai trò" của lớp cha. • Sai. Passenger hay Agent đều là các vai trò mà một người có thể giữ • Không biến đổi • Sai. Một người có thể lúc này là một passenger, lúc khác lại là agent • Mở rộng chứ không định nghĩa lại hoặc xóa bỏ • Đúng • Không mở rộng một lớp tiện ích • Đúng • Trong ngữ cảnh bài toán, chuyên biệt hóa một vai trò, giao tác, hoặc thiết bị. • Sai. Một Person không phải là một vai trò, giao tác, hay thiết bị Nguyên lý thiết kế hướng đối tượng
Ví dụ 1 - Composition Nguyên lý thiết kế hướng đối tượng
Ví dụ 2. Inheritance/Composition Nguyên lý thiết kế hướng đối tượng
Ví dụ 1 • "Là một loại đặc biệt", không phải "là một vai trò", của lớp cha • Đúng. Passenger và Agent là các dạng đặc biệt của PersonRole • Không biến đổi • Đúng. Một đối tượng passenger sẽ luôn là passenger, agent cũng vậy • Mở rộng chứ không định nghĩa lại hoặc xóa bỏ • Đúng • Không mở rộng một lớp tiện ích • Đúng • Trong ngữ cảnh bài toán, chuyên biệt hóa một vai trò, giao tác, hoặc thiết bị. • Đúng. Mỗi PersonRole là một vai trò Nguyên lý thiết kế hướng đối tượng
Ví dụ 3 Nguyên lý thiết kế hướng đối tượng
Ví dụ 3 • "Là một loại đặc biệt", không phải "là một vai trò", của lớp cha • Đúng. Reservation và Purchase là các dạng giao tác (transaction) • Không biến đổi • Đúng. Một đối tượng Reservation sẽ luôn là Reservation, Purchase cũng vậy • Mở rộng chứ không định nghĩa lại hoặc xóa bỏ • Đúng • Không mở rộng một lớp tiện ích • Đúng • Trong ngữ cảnh, chuyên biệt hóa một vai trò, giao tác, hoặc thiết bị. • Đúng. Đây là giao tác Nguyên lý thiết kế hướng đối tượng
Tóm tắt Inheritance/Composition • composition và inheritance là các phương pháp tái sử dụng quan trọng • Có thể làm cho các thiết kế phần mềm đơn giản hơn và có khả năng tái sử dụng cao hơn bằng cách ưu tiên dùng composition • Có thể dùng inheritance để mở rộng tập hợp các lớp có thể dùng làm thành phần cho composition • Do đó, composition và inheritance có tính chất tương hỗ • Nhưng nguyên tắc căn bản là: Ưu tiên sử dụng Composition hơn Inheritance Nguyên lý thiết kế hướng đối tượng
Nguyên tắc số 3 Lập trình theo một giao diện, không theo một cài đặt Nguyên lý thiết kế hướng đối tượng
Interface • Một interface của một đối tượng là một tập các phương thức của đối tượng đó mà các đối tượng khác biết rằng chúng có thể kích hoạt. • Các đối tượng chỉ biết về nhau qua interface • Một đối tượng có thể có nhiều interface • về bản chất, mỗi interface là một tập con của tập tất cả các phương thức mà một đối tượng có cài • Một kiểu (type) là một interface cụ thể • Các đối tượng thuộc các lớp khác nhau có thể thuộc cùng một kiểu, và một đối tượng có thể có nhiều kiểu khác nhau • Interface là chìa khóa cho khả năng ghép nối (plugability)! Nguyên lý thiết kế hướng đối tượng
Ưu/nhược điểm của interface • Ưu điểm: • Client không biết về lớp cụ thể của đối tượng mà mình đang dùng • Có thể dễ dàng thay thế đối tượng này vào chỗ của đối tượng khác • Quan hệ giữa các đối tượng không cần phải được mã cứng cho một lớp cụ thể, từ đó tăng tính linh hoạt • Giảm phụ thuộc lẫn nhau (coupling) giữa các thành phần hệ thống • Tăng khả năng tái sử dụng • Tăng cơ hội sử dụng composition do các đối tượng thành phần có thể thuộc bất cứ lớp nào cài đặt một interface cụ thể • Nhược điểm • Làm tăng nhẹ độ phức tạp của thiết kế Nguyên lý thiết kế hướng đối tượng
Ví dụ về interface Nguyên lý thiết kế hướng đối tượng
Ví dụ interface • Lớp Addition có thể chứa các số hạng trái và phải mà không cần quan tâm chúng thực ra là đối tượng thuộc các lớp nào (Square, Numeral, Addition,…) hoặc nằm trong cây phân cấp thừa kế nào class Addition implements BinaryExpression { private Expression _left; private Expression _right; Addition (Expression l, Expression r) { _left = l; _right = r; } public int evaluate() { return (_left.evaluate() + _right.evaluate() );} } Nguyên lý thiết kế hướng đối tượng
Nguyên tắc số 4 Nguyên tắc Mở-Đóng:Các thực thể phần mềm nên mở đối với việc mở rộng, nhưng đóng đối với việc sửa đổi Nguyên lý thiết kế hướng đối tượng
Nguyên tắc Mở-đóng (OCP) • Phát biểu: ta nên cố gắng thiết kế các mô-đun mà không bao giờ cần sửa • Để mở rộng hành vi của hệ thống, ta bổ sung các đoạn trình mới, ta không sửa mã cũ. • Các mô-đun thỏa mãn OCP cần đạt được 2 tiêu chí: • Mở đối với mở rộng: mở rộng hành vi của mô-đun để thỏa mãn yêu cầu mới • Đóng đối với sửa đổi: không được sửa mã nguồn của mô-đun • Làm thế nào để thực hiện được nguyên tắc này? • Trừu tượng hóa • Đa hình • Thừa kế • Interface Nguyên lý thiết kế hướng đối tượng
Nguyên tắc Mở-đóng • Không thể làm cho tất cả các mô-đun trong hệ thống phần mềm đều thỏa mãn OCP, nhưng ta nên cố giảm thiểu số mô-đun không thỏa mãn OCP • Nguyên tắc Mở-đóng thực sự là trái tim của thiết kế hướng đối tượng • Việc tuân theo nguyên tắc này đem lại mức cao nhất về tính tái sử dụng và khả năng bảo trì Nguyên lý thiết kế hướng đối tượng
Ví dụ • Xét phương thức tính tổng giá tiền của một loạt phụ tùng: public double totalPrice(Part[] parts) { double total = 0.0; for (int i=0; i<parts.length; i++) { total += parts[i].getPrice(); } return total; } • Nếu Part là lớp cơ sở hoặc một interface và đang sử dụng đa hình, thì phương thức trên dễ dàng đáp ứng các loại phụ tùng mới mà không phải sửa • Thỏa mãn nguyên tắc Mở-đóng Nguyên lý thiết kế hướng đối tượng
Không thỏa mãn OCP! Mỗi lần chính sách giá thay đổi là lại phải sửa nội dung totalPrice() Không đóng đối với sửa đổi Ví dụ (tiếp) • Nếu có thêm chính sách giá là: các phụ tùng bo mạch chủ và bộ nhớ phải có giá sàn khi tính tổng giá. Chẳng hạn: public double totalPrice(Part[] parts) { double total = 0.0; for (int i=0; i<parts.length; i++) { if (parts[i] instanceof Motherboard) total += (1.45 * parts[i].getPrice()); else if (parts[i] instanceof Memory) total += (1.27 * parts[i].getPrice()); else total += parts[i].getPrice(); } return total; } Nguyên lý thiết kế hướng đối tượng
Ví dụ public double totalPrice(Part[] parts) { double total = 0.0; for (int i=0; i<parts.length; i++) { total += parts[i].getPrice(); } return total; } • Để dùng phiên bản totalPrice() đầu tiên, ta có thể kết hợp chính sách giá vào phương thức getPrice của một lớp Part Nguyên lý thiết kế hướng đối tượng
Nhưng giờ phải sửa từng lớp con của Part mỗi khi có thay đổi về chính sách giá. Ví dụ (tiếp) • Ví dụ về các lớp Part và ConcretePart // Class Part is the superclass for all parts. public class Part { private double price; public Part(double price) (this.price = price;} public void setPrice(double price) {this.price = price;} public double getPrice() {return price;} } // Class ConcretePart implements a part for sale. // Pricing policy explicit here! public class ConcretePart extends Part { public double getPrice() { // return (1.45 * price); //Premium return (0.90 * price); //Labor Day Sale } } Nguyên lý thiết kế hướng đối tượng
Ví dụ (tiếp) • Cách tốt hơn là tạo lớp Price Policy với nhiệm vụ cung cấp nhiều chính sách giá // The Part class now has a contained PricePolicy object. public class Part { private double price; private PricePolicy pricePolicy; public void setPricePolicy(PricePolicy pricePolicy) { this.pricePolicy = pricePolicy; } public void setPrice(double price) {this.price = price;} public double getPrice() { return pricePolicy.getPrice(price); } } Nguyên lý thiết kế hướng đối tượng
Ví dụ (tiếp) /** * Class PricePolicy implements a given price policy. */ public class PricePolicy { private double factor; public PricePolicy (double factor) { this.factor = factor; } public double getPrice(double price) { return price * factor; } } • Như vậy, ta có thể quy định các chính sách giá trong thời gian chạy bằng cách thay đổi đối tượng PricePolicy mà một đối tượng Part dùng đến • Trong một ứng dụng thực, cả giá của một Part và PricePolicy tương ứng đều có thể được nạp từ một cơ sở dữ liệu Nguyên lý thiết kế hướng đối tượng
Nguyên tắc lựa chọn duy nhất hệ quả của nguyên tắc Mở-đóng Khi một hệ thống phần mềm cần hỗ trợ một tập các lựa chọn, về lí tưởng, trong hệ thống chỉ có một lớp biết toàn bộ tập lựa chọn này Nguyên lý thiết kế hướng đối tượng
Nguyên tắc số 5 Nguyên tắc thế Liskov: Hàm nào dùng tham chiếu tới lớp cơ sở thì phải có khả năng dùng nó cho đối tượng thuộc lớp dẫn xuất mà không cần biết đến việc này Nguyên lý thiết kế hướng đối tượng
Nguyên tắc thế Liskov • Nguyên tắc này có vẻ hiển nhiên đối với những gì ta biết về đa hình • Ví dụ public void drawShape (Shape s) { //code here } • Phương thức drawShape cần chạy được với lớp dẫn xuất bất kì của lớp cơ sở Shape (hoặc, nếu Shape là một interface, phương thức cần chạy với bất cứ lớp nào cài interface Shape) • Khi cài đặt các lớp dẫn xuất, cần cẩn trọng để đảm bảo không vô tình vi phạm nguyên tắc Liskov Nguyên lý thiết kế hướng đối tượng
Nguyên tắc thế Liskov • Nếu một hàm không thỏa mãn nguyên tắc Liskov, có thể nó đã tham chiếu tường minh đến một vài hoặc tất cả các lớp con của lớp cơ sở • Hàm đó cũng vi phạm nguyên tắc Mở-đóng vì nó sẽ phải bị sửa đổi khi có một lớp con mới được tạo ra. Nguyên lý thiết kế hướng đối tượng
Ví dụ // A very nice Rectangle class. public class Rectangle { private double width; private double height; public Rectangle(double w, double h) { width = w; height = h; } public double getWidth() {return width;} public double getHeight() {return height;} public void setWidth(double w) {width = w;} public void setHeight(double h) {height = h;} public double area() {return (width * height); } Lớp Square (hình vuông) thì sao? Hình vuông cũng là một hình chữ nhật. Nguyên lý thiết kế hướng đối tượng
Ví dụ (tiếp) • Square là lớp con của Rectangle? • Không cần cả width và height • Hơi phí bộ nhớ nhưng không quan trọng lắm • Các phương thức setWidth() và setHeight() phải được định nghĩa lại. • Phải định nghĩa lại cả những phương thức đơn giản nhất?? • Có vẻ thừa kế ở đây không thích hợp lắm Nguyên lý thiết kế hướng đối tượng
Ví dụ (tiếp) // A Square class. public class Square extends Rectangle { public Square(double s) {super(s, s);} public void setWidth(double w) { super.setWidth(w); super.setHeight(w); } public void setHeight(double h) { super.setHeight(h); super.setWidth(h); } } Trông có vẻ ổn? Nguyên lý thiết kế hướng đối tượng