"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ế g
Trang 1Nguyên lý thiết kế hướng đối tượng
Lập trình hướng đối tượng
Trang 2Nguyê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
Trang 3"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ó
Trang 5Che 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 = …
Trang 6Che 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) {
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
Trang 7Che 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) {
Trang 8Che dấu thông tin ở Java
public void setSpeed(double newSpeed) {
speed = newSpeed;
notifyObservers();
}
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ụ
Trang 9Nguyên tắc số 2
Ưu tiên sử dụng Composition hơn
Inheritance
Trang 10 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
Trang 11Ưu/nhược điểm của Composition
Trang 12Ưu/nhược điểm của Composition
nhiều đối tượng hơ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
Trang 13Thừa kế
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
tường minh các thuộc tính và phương thức
chung
các thuộc tính và phương thức bổ sung
Trang 14Ư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
Trang 15Ví 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);
}
Trang 16public 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) {
public static void main(String[] args) {
InstrumentedHashSet s = new InstrumentedHashSet();
s.addAll(Arrays.asList(new String[] {"Snap","Crackle","Pop"})); System.out.println(s.getAddCount());
}
Trang 17Ví dụ Inheritance & Compostion
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
Trang 18Ví 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
Trang 19public class InstrumentedSet implements Set {
private final Set s;
private int addCount = 0;
public InstrumentedSet(Set s) {this.s = s;}
public boolean add(Object o) {
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); }
Trang 20Ví 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ụ:
int capacity = 7;
float loadFactor = 66f;
Set s2 = new InstrumentedSet(new HashSet(capacity, loadFactor));
List list = new ArrayList();
Set s1 = new InstrumentedSet(new TreeSet(list));
Trang 21Quy 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ị
Trang 22Ví dụ 1
Trang 23Ví 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ỏ
Trang 24Ví dụ 1 - Composition
Trang 25Ví dụ 2 Inheritance/Composition
Trang 26Ví 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ỏ
Trang 27Ví dụ 3
Trang 28Ví 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
Trang 29Tó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
Trang 30Nguyên tắc số 3
Lập trình theo một giao diện,
không theo một cài đặt
Trang 31 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)!
Trang 32Ư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ế
Trang 33Ví dụ về interface "interface"
Expression
+ asString(): String + evaluate(): int
"interface"
BinaryExpression
+ left(): Expression + right(): Expression
Numeral
- int: value + Numeral(int) + Numeral()
Square
- Expression: expression + Square(Expression)
Addition
- Expression: left
- Expression: right
Trang 34Ví 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() );}
}
Trang 36Nguyê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
Trang 37Nguyên tắc Mở-đóng
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
kế hướng đối tượng
nhất về tính tái sử dụng và khả năng bảo trì
Trang 38Ví 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) {
Thỏa mãn nguyên tắc Mở-đóng
Trang 39for (int i=0; i<parts.length; i++) {
if (parts[i] instanceof Motherboard)
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
Trang 40Ví 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
Trang 41Ví 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
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á.
Trang 42Ví 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);
}
}
Trang 43public class PricePolicy {
private double factor;
public PricePolicy (double factor) {
this.factor = factor;
}
public double getPrice(double price) {
return price * factor;
}
}
Trang 44Nguyên tắc lựa chọn duy nhất
hệ quả của nguyên tắc Mở-đóng
Trang 45Nguyê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
Trang 46 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
Trang 47Nguyên tắc thế Liskov
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ở
Mở-đóng vì nó sẽ phải bị sửa đổi khi có một lớp con mới được tạo ra.
Trang 48// 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.
Ví dụ
Trang 49Ví dụ (tiếp)
Hơi phí bộ nhớ nhưng không quan trọng lắm
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
Trang 50// A Square class.
public class Square extends Rectangle {
public Square(double s) {super(s, s);}
public void setWidth(double w) {
Trang 51public class TestRectangle {
// Define a method that takes a Rectangle reference.
public static void testLSP(Rectangle r) {
public static void main(String args[]) {
//Create a Rectangle and a Square
Rectangle r = new Rectangle(1.0, 1.0);
Square s = new Square(1.0);
testLSP(r);
testLSP(s);
Theo nguyên tắc thế Liskov, testLSP phải chạy ổn cho cả Rectangle và Square
Trang 53Ví dụ (tiếp)
Vấn đề ở đây là gì? Người viết testLSP() đã dùng giả thiết hợp lý rằng việc sửa chiều rộng hình chữ nhật không làm thay đổi chiều dài hình
Truyền một đối tượng Square cho một phương thức như vậy đã
gây vấn đề, làm lộ diện một vi phạm đối với nguyên tắc LSP
Các lớp Rectangle và Square trông có vẻ nhất quán và hợp lệ
Nhưng một giả thiết hợp lý của lập trình viên lại làm mô hình thiết kế
bị đổ vỡ
xét chúng trong các giả thiết hợp lý mà người sử dụng thiết kế có thể đưa ra
Trang 54Ví dụ (tiếp)
một hình chữ nhật Nhưng một đối tượng
Square không phải là một đối tượng Rectangle
vì hành vi của một đối tượng Square không nhất quán với hành vi của một đối tượng Rectangle.
chữ nhật Không có tính đa hình giữa Square và Rectangle.
Trang 55Nguyên tắc thế Liskov
Nguyên tắc thế Liskov (LSP) nêu bật rằng quan hệ ISA (là) hoàn toàn là về hành vi
Để nguyên tắc này được thỏa mãn (cùng với nó là
nguyên lý Mở-đóng) tất cả các lớp con phải tuân theo hành vi mà client trông đợi ở lớp cha.
Một kiểu con không được có nhiều ràng buộc hơn kiểu cơ bản, nếu không, sẽ có trường hợp sử dụng mà lớp cha dùng được nhưng lớp con lại không dùng được
Điều kiện đảm bảo nguyên tắc Liskov: bất cứ đâu dùng được lớp cha thì cũng dùng được lớp con.