Các nguyên lý Hướng đối tượng

12
Nguyên Lí Ca Lp Trình Hướng Đối Tượng Phương pháp lp trình hướng đối tượng đã được nghiên cu và phát trin tlâu nhưng vic vn dng nó như thế nào cho hiu qutrong vic xây dng phn mm là điu vn còn khá mơ hđối vi nhiu người. Thế nào là mt phn mm hướng đối tượng ? Đâu là nhng cơ snn tng để xây dng được phn mm theo tư tưởng hướng đối tượng đúng nghĩa ? Bài viết này trình bày vcác nguyên lý lp trình hướng đối tượng. Đó là nhng quy tc phân tích thiết kế hướng đối tượng cơ bn, mang tính cht khái quát. Do là nguyên lý nên nó có tính tru tượng cao chkhông đi vào chi tiết cách thc gii quyết vn đề cth(vic hin thc hóa nhng nguyên lý lp trình hướng đối tượng đòi hi chúng ta phi xem xét đến Design Patterns) Nguyên lý Open-Closed (The Open-Closed Principle) Phát biu Các thc thphn mm (hàm, đơn th, đối tượng, …) nên được xây dng theo hướng mcho vic mrng (be opened for extension) nhưng đóng đối vi vic sa đổi (be closed for modification). Ni dung Các thc thtrong mt phn mm không đứng riêng lmà có sgn kết cht chvi nhau. Chúng phi hp hot động để cùng nhau thc hin các chc năng ca phn mm. Do đó, vic nâng cp, mrng mt thc thnào đó snh hưởng đến nhng thc thliên quan. Điu này có thdn đến vic phi nâng cp, mrng cnhng thc thliên quan đó. Và trong thi đại đầy biến động hin nay, vic phi thường xuyên nâng cp, mrng các thc thtrong phn mm là điu khó tránh khi. Để làm cho quá trình bo trì, nâng cp, mrng phn mm din ra ddàng và hiu quhơn, các thc thphn mm nên được xây dng tuân theo nguyên lý Open-Closed. Điu này có nghĩa là các thc thphn mm nên được xây dng sao cho vic nâng cp, mrng đồng nghĩa vi vic thêm vào nhng cái mi chkhông phi là thay đổi nhng cái hin có, tđó tránh được vic phi thay đổi các thc thliên quan. Nguyên lý Nghch đảo phthuc (The Dependency Inversion Principle) Phát biu Các thành phn trong phn mm không nên phthuc vào nhng cái riêng, cth(details) mà ngược li nên phthuc vào nhng cái chung, tng quát (abstractions) ca nhng cái riêng, cthđó. Nhng cái chung, tng quát (abstractions) không nên phvào nhng cái riêng, cth(details). Sphthuc này nên được đảo ngược li. Ni dung Nhng cái chung, tng quát là tp hp ca nhng đặc tính chung nht tnhng cái riêng, cth. Nhng cái riêng, cthdù khác nhau thế nào đi na cũng đều tuân theo các quy tc chung mà cái chung, tng quát ca nó đã định nghĩa. Nhng cái chung, tng quát là nhng cái ít thay đổi và ít biến động. Trong khi đó, sthay đổi li thường xuyên xy ra nhng cái riêng, cth. Vic phthuc vào nhng cái chung, tng quát sgiúp cho các thành phn trong phn mm trnên linh động (flexible) và thích ng tt vi sthay đổi thường xuyên din ra nhng cái riêng, cth. Khi phthuc vào nhng cái chung, tng quát, các thành phn trong phn mm vn có thhot động tt mà không cn phi sa đổi mt khi cái riêng, cthđược thay thế bng mt cái riêng, cthkhác cùng loi.

description

sưu tầm internet

Transcript of Các nguyên lý Hướng đối tượng

Page 1: Các nguyên lý Hướng đối tượng

Nguyên Lí Của Lập Trình Hướng Đối Tượng

Phương pháp lập trình hướng đối tượng đã được nghiên cứu và phát triển từ lâunhưng việc vận dụng nó như thế nào cho hiệu quả trong việc xây dựng phần mềm là điềuvẫn còn khá mơ hồ đối với nhiều người. Thế nào là một phần mềm hướng đối tượng ? Đâulà những cơ sở nền tảng để xây dựng được phần mềm theo tư tưởng hướng đối tượngđúng nghĩa ? Bài viết này trình bày về các nguyên lý lập trình hướng đối tượng. Đó lànhững quy tắc phân tích thiết kế hướng đối tượng cơ bản, mang tính chất khái quát. Do lànguyên lý nên nó có tính trừu tượng cao chứ không đi vào chi tiết cách thức giải quyếtvấn đề cụ thể (việc hiện thực hóa những nguyên lý lập trình hướng đối tượng đòi hỏichúng ta phải xem xét đến Design Patterns)

Nguyên lý Open-Closed (The Open-Closed Principle)Phát biểu

Các thực thể phần mềm (hàm, đơn thể, đối tượng, …) nên được xây dựng theohướng mở cho việc mở rộng (be opened for extension) nhưng đóng đối với việc sửa đổi(be closed for modification).Nội dung

Các thực thể trong một phần mềm không đứng riêng lẻ mà có sự gắn kết chặt chẽvới nhau. Chúng phối hợp hoạt động để cùng nhau thực hiện các chức năng của phầnmềm. Do đó, việc nâng cấp, mở rộng một thực thể nào đó sẽ ảnh hưởng đến những thựcthể liên quan. Điều này có thể dẫn đến việc phải nâng cấp, mở rộng cả những thực thểliên quan đó. Và trong thời đại đầy biến động hiện nay, việc phải thường xuyên nâng cấp,mở rộng các thực thể trong phần mềm là điều khó tránh khỏi.

Để làm cho quá trình bảo trì, nâng cấp, mở rộng phần mềm diễn ra dễ dàng và hiệuquả hơn, các thực thể phần mềm nên được xây dựng tuân theo nguyên lý Open-Closed.Điều này có nghĩa là các thực thể phần mềm nên được xây dựng sao cho việc nâng cấp,mở rộng đồng nghĩa với việc thêm vào những cái mới chứ không phải là thay đổi nhữngcái hiện có, từ đó tránh được việc phải thay đổi các thực thể liên quan.

Nguyên lý Nghịch đảo phụ thuộc (The Dependency Inversion Principle)Phát biểu

Các thành phần trong phần mềm không nên phụ thuộc vào những cái riêng, cụ thể(details) mà ngược lại nên phụ thuộc vào những cái chung, tổng quát (abstractions) củanhững cái riêng, cụ thể đó.

Những cái chung, tổng quát (abstractions) không nên phụ vào những cái riêng, cụthể (details). Sự phụ thuộc này nên được đảo ngược lại.Nội dung

Những cái chung, tổng quát là tập hợp của những đặc tính chung nhất từ những cáiriêng, cụ thể. Những cái riêng, cụ thể dù khác nhau thế nào đi nữa cũng đều tuân theocác quy tắc chung mà cái chung, tổng quát của nó đã định nghĩa. Những cái chung, tổngquát là những cái ít thay đổi và ít biến động. Trong khi đó, sự thay đổi lại thường xuyênxảy ra ở những cái riêng, cụ thể. Việc phụ thuộc vào những cái chung, tổng quát sẽ giúpcho các thành phần trong phần mềm trở nên linh động (flexible) và thích ứng tốt với sựthay đổi thường xuyên diễn ra ở những cái riêng, cụ thể. Khi phụ thuộc vào những cáichung, tổng quát, các thành phần trong phần mềm vẫn có thể hoạt động tốt mà khôngcần phải sửa đổi một khi cái riêng, cụ thể được thay thế bằng một cái riêng, cụ thể kháccùng loại.

Page 2: Các nguyên lý Hướng đối tượng

Nguyên lý Thay thế Liskov (The Liskov Substitution Principle)Phát biểu

Lớp B chỉ nên kế thừa từ lớp A khi và chỉ khi với mọi hàm F thao tác trên các đốitượng của A, cách cư xử (behaviors) của F không thay đổi khi ta thay thế (substitute) cácđối tượng của A bằng các đối tượng của B.Nội dung

Kế thừa (inheritance) là một trong những tính chất cơ bản của lập trình hướng đốitượng. Đó là khả năng định nghĩa một lớp đối tượng dựa trên các lớp đối tượng đã đượcđịnh nghĩa trước đó. Các đối tượng của lớp kế thừa có khả năng cư xử (behave) như cácđối tượng của lớp cơ sở. Điều này có nghĩa là các đối tượng của lớp kế thừa hoàn toàn cóthể thay thế các đối tượng của lớp cơ sở trong những hàm thao tác trên các đối tượng củalớp cơ sở.

Chính vì tính chất này mà chúng ta không thể sử dụng kế thừa một cách tùy tiện.Giả sử ta có lớp A và hàm F thao tác trên các đối tượng của A. Để nâng cấp, mở rộngphần mềm, ta cần thêm vào lớp B kế thừa từ A. Nhưng việc thay thế các đối tượng của Abằng các đối tượng của B lại làm cho F cư xử sai lệch so với trước khi thực hiện việc thaythế. Lúc này, để F có thể cư xử không đổi so với trước, ta phải chỉnh sửa lại F. Điều nàylàm cho F vi phạm nguyên lý Open-Closed.

Nguyên lý Phân tách interface (The Interface Segregation)Phát biểu

Không nên buộc các thực thể phần mềm phụ thuộc vào những interface mà chúngkhông sử dụng đến.Nội dung

Khi xây dựng một lớp đối tượng, đặc biệt là những lớp trừu tượng (abstract class),nhiều người thường có xu hướng để cho lớp đối tượng thực hiện càng nghiều chức năngcàng tốt, đưa thật nhiều thuộc tính và phương thức vào lớp đối tượng đó. Những lớp đốitượng như vậy được gọi là những lớp đối tượng có interface bị “ô nhiễm” (fat interface orpolluted interface).

Khi một lớp đối tượng có interface bị “ô nhiễm”, nó sẽ trở nên cồng kềnh. Một thựcthể phần mềm nào đó chỉ cần thực hiện một công việc đơn giản mà lớp đối tượng này hỗtrợ buộc phải làm việc với toàn bộ interface của lớp đối tượng đó. Việc phải truyền đitruyền lại nhiều lần những đối tượng có interface bị “ô nhiễm” sẽ làm giảm hiệu năng củaphần mềm.

Đặc biệt đối với lớp trừu tượng có interface bị “ô nhiễm”, một số lớp kế thừa chỉ quantâm đến một phần interface của lớp cơ sở nhưng bị buộc phải thực hiện việc cài đặt cho cảphần interface không hề có ý nghĩa đối với chúng. Điều này dẫn đến sự dư thừa khôngcần thiết trong các thực thể phần mềm. Quan trọng hơn nữa, việc buộc các lớp kế thừaphụ thuộc vào phần interface mà chúng không sử dụng đến sẽ làm tăng sự kết dính(coupling) giữa các thực thể phần mềm. Một khi sự nâng cấp, mở rộng diễn ra, đòi hỏiphần interface đó phải thay đổi, các lớp kế thừa này bị buộc phải chỉnh sửa theo. Điều nàylàm cho chúng vi phạm nguyên lý Open-Closed.

Nguyên lý Open-Closed (The Open-Closed Principle)Phát biểu

Các thực thể phần mềm (hàm, đơn thể, đối tượng, …) nên được xây dựng theohướng mở cho việc mở rộng (be opened for extension) nhưng đóng đối với việc sửa đổi(be closed for modification).Nội dung

Các thực thể trong một phần mềm không đứng riêng lẻ mà có sự gắn kết chặt chẽvới nhau. Chúng phối hợp hoạt động để cùng nhau thực hiện các chức năng của phần

Page 3: Các nguyên lý Hướng đối tượng

mềm. Do đó, việc nâng cấp, mở rộng một thực thể nào đó sẽ ảnh hưởng đến những thựcthể liên quan. Điều này có thể dẫn đến việc phải nâng cấp, mở rộng cả những thực thểliên quan đó. Và trong thời đại đầy biến động hiện nay, việc phải thường xuyên nâng cấp,mở rộng các thực thể trong phần mềm là điều khó tránh khỏi.

Để làm cho quá trình bảo trì, nâng cấp, mở rộng phần mềm diễn ra dễ dàng và hiệuquả hơn, các thực thể phần mềm nên được xây dựng tuân theo nguyên lý Open-Closed.Điều này có nghĩa là các thực thể phần mềm nên được xây dựng sao cho việc nâng cấp,mở rộng đồng nghĩa với việc thêm vào những cái mới chứ không phải là thay đổi nhữngcái hiện có, từ đó tránh được việc phải thay đổi các thực thể liên quan.Xét ví dụ một đoạn chương trình vẽ đường thẳng và hình chữ nhật bằng C#.

public enum ShapeType{

LINE,RECTANGLE

}public abstract class Shape{

public abstract ShapeType getType();}public class Line: Shape{

public override ShapeType getType(){

return ShapeType.LINE;}public void drawLine(){

// Draws the line...}

}public class Rectangle: Shape{

public override ShapeType getType(){

return ShapeType.RECTANGLE;}public void drawRectangle(){

// Draws the rectangle...}

}public void draw(ArrayList shapeList){

Line line;Rectangle rectangle;foreach (Shape s in shapeList)switch (s.getType()){

case ShapeType.LINE:line = (Line)s;

Page 4: Các nguyên lý Hướng đối tượng

line.drawLine();break;case ShapeType.RECTANGLE:rectangle = (Rectangle)s;rectangle.drawRectangle();break;

}}

Đoạn chương trình trên hoạt động rất tốt cho đến khi có sự nâng cấp, mở rộng. Giảsử chúng ta cần nâng cấp, mở rộng đoạn chương trình trên để nó có thể vẽ thêm đượchình tròn. Lúc bấy giờ ta phải chỉnh sửa lại hàm “draw”, thêm vào một trường hợp vẽ hìnhtròn. Và trong nhiều tình huống, việc chỉnh sửa hàm “draw” sẽ dẫn đến việc chỉnh sửanhững hàm khác liên quan. Hàm “draw” được viết theo cách này được nói là không tuânthủ nguyên lý Open-Closed.

Để đoạn chương trình trên tuân thủ nguyên lý Open-Closed, chúng ta sử dụng tínhđa hình của lập trình hướng đối tượng.

public abstract class Shape{

public abstract void draw();}public class Line: Shape{

public override void draw(){

// Draws the line...}

}public class Rectangle: Shape{

public override void draw(){

// Draws the rectangle...}

}class Circle: Shape{

public override void draw(){

// Draws the circle...}

}public void draw(ArrayList shapeList){

foreach (Shape s in shapeList)s.draw();

}

Với đoạn chương trình trên, khi thêm một hình mới vào, chúng ta chỉ việc thêm lớpđối tượng cho hình đó (kế thừa từ Shape) mà không cần phải chỉnh sửa lại hàm “draw”.

Page 5: Các nguyên lý Hướng đối tượng

Nó vẫn hoạt động tốt với những hình mới thêm vào.

Ghi chúi) Không phải lúc nào tất cả các thực thể trong phần mềm đều có thể tuân thủ nguyên

lý Open-Closed. Nhưng mục tiêu của phân tích thiết kế hướng đối tượng là phải làmsao cho số lượng các thực thể tuân thủ nguyên lý là lớn nhất, trong đó ưu tiên cácthực thể thường xuyên phải nâng cấp, mở rộng thỏa nguyên lý.

ii) Việc tuân thủ nguyên lý Open-Closed của một thực thể phần mềm chỉ mang tínhtương đối, phụ thuộc vào ngữ cảnh. Có thể trong ngữ cảnh này, thực thể thỏanguyên lý, nhưng trong một ngữ cảnh khác, thực thể này không còn tuân thủnguyên lý nữa. Mục tiêu của phân tích thiết kế hướng đối tượng là phải làm sao chocó nhiều thực thể phần mềm nhất tuân thủ nguyên lý trong ngữ cảnh thường xảyra nhất của phần mềm, trong đó ưu tiên các thực thể thường xuyên phải nâng cấp,mở rộng thỏa nguyên lý.Ví dụ trường hợp hàm “draw” như trong đoạn chương trình vẽ hình trên.public void draw(ArrayList shapeList){

foreach (Shape s in shapeList)s.draw();

}Hàm “draw” chỉ thỏa nguyên lý trong ngữ cảnh nâng cấp mở rộng là “thêm hìnhmới”. Nếu chúng ta cần nâng cấp, mở rộng theo hướng thay đổi thứ tự vẽ các hìnhthì hàm “draw” như trên là không thể đáp ứng được. Khi đó nó không còn tuân thủnguyên lý nữa.

iii) Một tính chất quan trọng trong lập trình hướng đối tượng giúp cho các thực thểphần mềm tăng khả năng tuân thủ nguyên lý Open-Closed là tính đóng gói(encapsulation). Đối tượng nắm giữ thông tin và chịu trách nhiệm trên thông tinmình nắm giữ. Điều này giúp hạn chế sự kết dính (coupling) giữa các lớp đối tượngvới nhau. Trường hợp lý tưởng là tất cả thuộc tính của đối tượng được đặt tầm vựcprivate. việc thay đổi trên thuộc tính chỉ có thể được thực hiên thông qua những xửlý của phương thức. Những phương thức của đối tượng khác, kể cả đối tượng kếthừa không thể truy xuất được đến những thuộc tính này.

iv) Việc hạn chế sử dụng ép kiểu động (runtime type-casting) trong các thực thể phầnmềm cũng sẽ giúp làm tăng khả năng tuân thủ nguyên lý Open-Closed của chúng.Vì bản chất của việc ép kiểu động là làm việc với một kiểu dữ liệu cụ thể. Khi muốnnâng cấp, mở rộng thực thể để nó có thể làm việc với những kiểu dữ liệu khác,đoạn chương trình sử dụng ép kiểu động phải được thay đổi để có thể làm việcđược với các kiểu dữ liệu khác này.public void doSomething(Vehicle vehicle){

Car car = (Car)vehicle;car.run();car.stop();

}Khi cần nâng cấp, mở rộng để đoạn chương trình trên có thể làm việc được với cáclớp đối tượng khác kế thừa từ “Vehicle”, chúng ta phải chỉnh sửa lại nó.

Ý nghĩaNguyên lý Open-Closed là nguyên lý cốt lõi và là một trong bốn nguyên lý cơ bản

làm nền tảng cho phân tích thiết kế hướng đối tượng. Nó giúp cho phần mềm dễ bảo trì,nâng cấp và mở rộng.

Page 6: Các nguyên lý Hướng đối tượng

Nguyên lý Nghịch đảo phụ thuộc (The Dependency Inversion Principle)Phát biểu

Các thành phần trong phần mềm không nên phụ thuộc vào những cái riêng, cụ thể(details) mà ngược lại nên phụ thuộc vào những cái chung, tổng quát (abstractions) củanhững cái riêng, cụ thể đó.

Những cái chung, tổng quát (abstractions) không nên phụ vào những cái riêng, cụthể (details). Sự phụ thuộc này nên được đảo ngược lại.Nội dung

Những cái chung, tổng quát là tập hợp của những đặc tính chung nhất từ những cáiriêng, cụ thể. Những cái riêng, cụ thể dù khác nhau thế nào đi nữa cũng đều tuân theocác quy tắc chung mà cái chung, tổng quát của nó đã định nghĩa. Những cái chung, tổngquát là những cái ít thay đổi và ít biến động. Trong khi đó, sự thay đổi lại thường xuyênxảy ra ở những cái riêng, cụ thể. Việc phụ thuộc vào những cái chung, tổng quát sẽ giúpcho các thành phần trong phần mềm trở nên linh động (flexible) và thích ứng tốt với sựthay đổi thường xuyên diễn ra ở những cái riêng, cụ thể. Khi phụ thuộc vào những cáichung, tổng quát, các thành phần trong phần mềm vẫn có thể hoạt động tốt mà khôngcần phải sửa đổi một khi cái riêng, cụ thể được thay thế bằng một cái riêng, cụ thể kháccùng loại.

Lấy ví dụ đoạn chương trình đọc dữ liệu từ bàn phím và xuất ra máy in.

public void copy(){

Keyboard keyboard = new Keyboard();Printer printer = new Printer();char c;while ((c = keyboard.read()) != ‘q’)printer.write(c);

}Khi nâng cấp, mở rộng đoạn chương trình trên để nó có thể xuất dữ liệu ra máy in

hoặc tập tin thì chúng ta phải chỉnh sửa lại đoạn chương trình trên như sau.

public void copy(OutputType type){

Keyboard keyboard = new Keyboard();Printer printer = new Printer();File file = new File();char c;while ((c = keyboard.read()) != ‘q’)if (type == OutputType.PRINTER)printer.write(c);else if (type == OutputType.FILE)file.write(c);

}

Rõ ràng hàm “copy” như trên đã vi phạm nguyên lý Open-Closed do khi mỗi lần cầnthêm một thiết bị đọc ghi mới vào, chúng ta phải chỉnh sửa lại nó. Nguyên nhân làm chohàm “copy” vi phạm nguyên lý Open-Closed là do nó làm việc với từng thiết bị đọc ghi cụthể. Khi thêm một thiết bị đọc ghi mới, chúng ta phải thêm vào hàm “copy” đoạn lệnh đểlàm việc với thiết bị đọc ghi mới. Khi đó chúng ta nói hàm “copy” vi phạm nguyên lýNghịch đảo phụ thuộc.

Để đoạn chương trình trên tuân thủ Nguyên lý Nghịch đảo phụ thuộc, từ đó tuân thủ

Page 7: Các nguyên lý Hướng đối tượng

Nguyên lý Open-Closed, chúng ta phải cho nó làm việc với thiết bị đọc ghi tổng quát.

public void copy(Reader reader, Writer writer){

char c;while ((c = reader.read()) != ‘q’)writer.write(c);

}

Hàm “copy” như trên có thể làm việc tốt với bất kỳ thiết bị đọc ghi nào tuân thủinterface của Reader và Writer. Khi cần thêm thiết bị đọc ghi mới, chúng ta chỉ việc thêmlớp đối tượng kế thừa từ Reader hoặc Writer mà không phải chỉnh sửa lại hàm “copy”.

Trích lời Allen Holub: “The more abstraction you add, the greater the flexibility. Intoday’s business environment, where requirements regularly change as programdevelops, this flexibility is essential.”.Chú ý

i) Nguyên lý Nghịch đảo phụ thuộc có mối liên hệ mật thiết với nguyên lý Open-Closed. Một khi nguyên lý Nghịch đảo phụ thuộc bị vi phạm, có nghĩa là nhữngthành phần trong phần mềm phụ thuộc vào những cái riêng, cụ thể, việc nâng cấp,mở rộng ở những cái riêng, cụ thể (điều này rất thường xảy ra) buộc những thànhphần phụ thuộc vào nó bị thay đổi theo. Điều này dẫn đến vi phạm nguyên lýOpen-Closed.

ii) Sự nghịch đảo được đề cập đến ở đây nhằm nhấn mạnh đến việc cần phải thay đổiquan điểm trong phân tích thiết kế phần mềm. Theo lối suy nghĩ “chia để trị” củalập trình hướng cấu trúc, những công việc lớn, phức tạp, mang tính trừu tượng caothường được phân ra thành những công việc nhỏ, đơn giản và cụ thể hơn. Khi đó,cấu trúc phần mềm có xu hướng theo dạng những thành phần lớn (trừu tượng) gọiđến những thành phần nhỏ (cụ thể) hơn để yêu cầu chúng thực hiện công việc.Điều này thường làm cho những thành phần trong phần mềm phụ thuộc vào nhữngcái riêng, cụ thể. Trong phân tích thiết kế hướng đối tượng, sự phụ thuộc này nênđược đảo ngược lại.

iii) Một thành phần trong phần mềm vi phạm nguyên lý Nghịch đảo phụ thuộc sẽ cótính tái sử dụng (reusability) không cao. Việc mang những thành phần này sử dụngvào một ngữ cảnh khác với những cái riêng, cụ thể khác là khó có thể thực hiệnđược nếu như không thực hiện việc chỉnh sửa nào trên chúng.

iv) Một quy ước trong lập trình hướng đối tượng giúp cho các thành phần trong phầnmềm tăng khả năng tuân thủ nguyên lý Nghịch đảo phụ thuộc là thực hiện việctruy xuất đến các đối tượng thông qua interface của chúng. Điều này sẽ làm chocác thành phần bên trong phần mềm có tính linh động (flexibility) cao, không phảisửa đổi khi thay thế các đối tượng được truy xuất đến bằng đối tượng khác cùngloại.public void doSomething(Car car){

car.run();car.stop();

}public void doSomething(Vehicle vehicle){

vehicle.run();vehicle.stop();

}

Page 8: Các nguyên lý Hướng đối tượng

Trong hai đoạn chương trình trên, đoạn chương trình thứ hai vẫn làm việc tốt khichúng ta thêm vào các đối tượng khác cùng loại với “Car” mà kế thừa từ “Vehicle”.

Ý nghĩaNguyên lý Nghịch đảo phụ thuộc có mối liên hệ mật thiết với nguyên lý Open-Closed

và là một trong bốn nguyên lý cơ bản làm nền tảng cho phân tích thiết kế hướng đốitượng. Nó giúp cho phần mềm có tính tái sử dụng cao, linh động và bền vững(robustness) trước những sự thay đổi.

Nguyên lý Thay thế Liskov (The Liskov Substitution Principle)Phát biểu

Lớp B chỉ nên kế thừa từ lớp A khi và chỉ khi với mọi hàm F thao tác trên các đốitượng của A, cách cư xử (behaviors) của F không thay đổi khi ta thay thế (substitute) cácđối tượng của A bằng các đối tượng của B.Nội dung

Kế thừa (inheritance) là một trong những tính chất cơ bản của lập trình hướng đốitượng. Đó là khả năng định nghĩa một lớp đối tượng dựa trên các lớp đối tượng đã đượcđịnh nghĩa trước đó. Các đối tượng của lớp kế thừa có khả năng cư xử (behave) như cácđối tượng của lớp cơ sở. Điều này có nghĩa là các đối tượng của lớp kế thừa hoàn toàn cóthể thay thế các đối tượng của lớp cơ sở trong những hàm thao tác trên các đối tượng củalớp cơ sở.

Chính vì tính chất này mà chúng ta không thể sử dụng kế thừa một cách tùy tiện.Giả sử ta có lớp A và hàm F thao tác trên các đối tượng của A. Để nâng cấp, mở rộngphần mềm, ta cần thêm vào lớp B kế thừa từ A. Nhưng việc thay thế các đối tượng của Abằng các đối tượng của B lại làm cho F cư xử sai lệch so với trước khi thực hiện việc thaythế. Lúc này, để F có thể cư xử không đổi so với trước, ta phải chỉnh sửa lại F. Điều nàylàm cho F vi phạm nguyên lý Open-Closed.

Đoạn chương trình sau cho thấy việc kế thừa tùy tiện chỉ với mục đích tái sử dụngnguy hiểm như thế nào.

public class Stack{

private ArrayList data;// More data members of stack.public virtual void push(int n){

// Pushes n to stack...}public virtual int pop(){// Pops value from stack...}

}public class Queue: Stack{

// Data members of Queue.public override void push(int n){// Pushes n to queue...}public override int pop(){

Page 9: Các nguyên lý Hướng đối tượng

// Pops value from queue...}

}public int func(Stack p){

p.push(5);p.push(6);p.push(7);int a = p.pop();int b = p.pop();if (a == 7 && b == 6)return a * b;throw new ArgumentException();

}

Với mục đích tái sử dụng là một số thuộc tính và phương thức trong “Stack”, chúngta cho “Queue” kế thừa từ Stack. Xét hàm “func” thao tác trên đối tượng của “Stack”, do“Queue” kế thừa từ “Stack” nên chúng ta hoàn toàn có thể truyền đối tượng của “Queue”vào hàm này. Nhưng cách cư xử của hàm “func” khi thao tác trên các đối tượng của“Stack” và “Queue” là khác nhau. Với các đối tượng của “Stack” hàm func luôn trả vềchính xác tích của hai số 7 và 6. Nhưng với các đối tượng của “Queue” hàm func lại luôngây ra một exception. Để hàm “func” có thể cư xử trên các đối tượng của “Stack” và“Queue” như nhau, chúng ta phải viết lại nó. Điều này làm cho hàm “func” vi phạmnguyên lý Open-Closed. Khi đó ta nói hàm “func” vi phạm nguyên lý Thay thế Liskov.Chú ý

i) Nguyên lý Thay thế Liskov có mối liên hệ mật thiết với Nguyên lý Open-Closed. Sựvi phạm nguyên lý Thay thế Liskov sẽ dẫn đến sự vi phạm nguyên lý Open-Closed.Một thực thể phần mềm vi phạm nguyên lý Thay thế Liskov sẽ cư xử khác nhautrên các đối tượng của lớp cơ sở và lớp kế thừa. Để thực thể phần mềm này vẫn cóthể làm việc tốt trên các đối tượng của cả lớp cơ sở và lớp kế thừa, chúng ta phảichỉnh sửa lại nó. Điều này dẫn đến vi phạm nguyên lý Open-Closed.

ii) Không phải lúc nào tất cả các thực thể trong phần mềm đều có thể tuân thủnguyên lý Thay thế Liskov. Nhưng mục tiêu của phân tích thiết kế hướng đối tượnglà phải làm sao cho số lượng các thực thể tuân thủ nguyên lý là lớn nhất, trong đóưu tiên các thực thể thường xuyên phải nâng cấp, mở rộng thỏa nguyên lý.

iii) Việc tuân thủ nguyên lý Thay thế Liskov của một thực thể phần mềm chỉ mangtính tương đối, phụ thuộc vào ngữ cảnh. Có thể trong ngữ cảnh này, thực thể thỏanguyên lý, nhưng trong một ngữ cảnh khác, thực thể này không còn tuân thủnguyên lý nữa. Mục tiêu của phân tích thiết kế hướng đối tượng là phải làm sao chocó nhiều thực thể phần mềm nhất tuân thủ nguyên lý trong ngữ cảnh thường xảyra nhất của phần mềm, trong đó ưu tiên các thực thể thường xuyên phải nâng cấp,mở rộng thỏa nguyên lý.

iv) Quan hệ “IS-A” thường được dùng để phát hiện kế thừa. Khi lớp đối tượng B vềmặt ngữ nghĩa là một trường hợp đặc biệt của lớp đối tượng A thì ta có thể cho Bkế thừa từ A. Nhưng thực tế cho thấy, trong một số ngữ cảnh của phần mềm, mộtlớp đối tượng có quan hệ “IS-A” với những lớp đối tượng khác nhưng việc để nó kếthừa những lớp đối tượng này sẽ dẫn đến việc vi phạm nguyên lý Thay thế Liskov.Xét đoạn chương trình sau.

public class Rectangle{

Page 10: Các nguyên lý Hướng đối tượng

// Data members of rectangle...// Member functions of rectangle...

}public class Square: Rectangle{

// Data members of square...// Member functions of square...

}public double doSomething(Rectangle obj){

obj.setWidth(5);obj.setHeight(6);if (obj.Area == 30)return obj.Area;throw new ArgumentException();

}

Ở đoạn chương trình trên, mặc dù về mặt ngữ nghĩa, hình vuông là một trường hợpcủa hình chữ nhật. Điều này hoàn toàn đúng!!! Nhưng trong ngữ cảnh này, việc để“Square” kế thừa “Rectangle” là không phù hợp. Lúc này hàm “doSomething” cư xửkhác nhau trên các đối tượng của “Rectangle” và “Square”. Như vậy hàm“doSomething” đã vi phạm nguyên lý Thay thế Liskov. Để hàm “doSomething” cóthể làm việc được trên cả “Rectangle” và “Square” chúng ta phải chỉnh sửa lại nó.Như vậy việc vi phạm nguyên lý Thay thế Liskov đã làm cho hàm “doSomething” viphạm nguyên lý Open-Closed.

v) Nguyên lý Thay thế Liskov có mối liên hệ mật thiết với kỹ thuật “Design byContract” được đề cập bởi Bertrand Meyers. Kỹ thuật này chỉ ra rằng: mỗi phươngthức trong một lớp đối tượng, khi được định nghĩa, đã hàm chứa trong nó tiền điềukiện (pre-condition) và hậu điều kiện (post-condition). Tiền điều kiện là những điềukiện cần để phương thức có thể thực hiện được. Hậu điều kiện là những ràng buộcphát sinh sau khi thực hiện phương thức. Khi thực hiện việc kế thừa, phương thứcđược định nghĩa lại trong lớp kế thừa phải có tiền điều kiện lỏng lẻo hơn (weaker)và hậu điều kiện chặt chẽ hơn (stronger). Điều này có nghĩa là trước khi thực hiện,phương thức được định nghĩa lại trong lớp kế thừa không được đòi hỏi nhiều hơnnhư khi nó được định nghĩa trong lớp cơ sở. Và sau khi thực hiện, phương thứcđược định nghĩa lại trong lớp kế thừa phải đảm bảo tất cả những ràng buộc phátsinh như khi nó được định nghĩa trong lớp cơ sở. Chỉ khi nào những điều trên đượcđáp ứng cho mọi phương thức trong lớp kế thừa thì lớp kế thừa mới được xem là cưxử như lớp cơ sở. Và khi đó, việc để nó kế thừa từ lớp cơ sở mới là đúng đắn trongngữ cảnh phần mềm đang xét.

vi) Nguyên lý Thay thế Liskov và kỹ thuật “Design by Contract” vô tình làm cho việckế thừa trở nên rất khó thực hiện. Khi cần thêm vào một lớp kế thừa, chúng ta phảixem xét rất kỹ lưỡng lại tất cả hàm có thao tác trên lớp cơ sở xem chúng có viphạm nguyên lý Thay thế Liskov hay không. Chúng ta cũng cần phải xem xét tất cảcác phương thức của lớp kế thừa xem chúng có vi phạm những quy định của kỹthuật “Design by Contract” hay không. Tất cả những điều này là do lớp kế thừa cómột mối liên hệ mật thiết với lớp cơ sở. Lớp kế thừa bị kết dính (coupling) chặt chẽvới lớp cơ sở. Sự kết dính này rõ ràng làm cho phần mềm kém linh động (flexibility)một khi có sự thay đổi xảy ra. Do đó, để hạn chế sự kết dính này mà vẫn đảm bảođược tính tái sử dụng, chúng ta chỉ nên kế thừa interface và sử dụng compositionthay cho việc kế thừa.

Page 11: Các nguyên lý Hướng đối tượng

Ý nghĩaNguyên lý Thay thế Liskov có mối liên hệ mật thiết với nguyên lý Open-Closed và là

một trong bốn nguyên lý cơ bản làm nền tảng cho phân tích thiết kế hướng đối tượng. Nógiúp nâng cao tính tái sử dụng và bền vững của phần mềm trước những sự thay đổi.

Nguyên lý Phân tách interface (The Interface Segregation)Phát biểu

Không nên buộc các thực thể phần mềm phụ thuộc vào những interface mà chúngkhông sử dụng đến.Nội dung

Khi xây dựng một lớp đối tượng, đặc biệt là những lớp trừu tượng (abstract class),nhiều người thường có xu hướng để cho lớp đối tượng thực hiện càng nghiều chức năngcàng tốt, đưa thật nhiều thuộc tính và phương thức vào lớp đối tượng đó. Những lớp đốitượng như vậy được gọi là những lớp đối tượng có interface bị “ô nhiễm” (fat interface orpolluted interface).

Khi một lớp đối tượng có interface bị “ô nhiễm”, nó sẽ trở nên cồng kềnh. Một thựcthể phần mềm nào đó chỉ cần thực hiện một công việc đơn giản mà lớp đối tượng này hỗtrợ buộc phải làm việc với toàn bộ interface của lớp đối tượng đó. Việc phải truyền đitruyền lại nhiều lần những đối tượng có interface bị “ô nhiễm” sẽ làm giảm hiệu năng củaphần mềm.

Đặc biệt đối với lớp trừu tượng có interface bị “ô nhiễm”, một số lớp kế thừa chỉ quantâm đến một phần interface của lớp cơ sở nhưng bị buộc phải thực hiện việc cài đặt cho cảphần interface không hề có ý nghĩa đối với chúng. Điều này dẫn đến sự dư thừa khôngcần thiết trong các thực thể phần mềm. Quan trọng hơn nữa, việc buộc các lớp kế thừaphụ thuộc vào phần interface mà chúng không sử dụng đến sẽ làm tăng sự kết dính(coupling) giữa các thực thể phần mềm. Một khi sự nâng cấp, mở rộng diễn ra, đòi hỏiphần interface đó phải thay đổi, các lớp kế thừa này bị buộc phải chỉnh sửa theo. Điều nàylàm cho chúng vi phạm nguyên lý Open-Closed.

Hình bên dươi là sơ đồ lớp cho đoạn chương trình tính điện trở mạch điện. “Resistor”và “Lamp” là những mạch điện đơn giản với điện trở là một thuộc tính của mạch. Trongkhi “SeriesCircuit” và “ParallelCircuit” là những mạch điện phức hợp với điện trở của mạchđược tính từ các mạch điện con. Để có thể cư xử như nhau trên các loại mạch điện nàyhay nói cách khác là truy xuất đến chúng một cách “trong suốt” (transparency), chúng tacó “Circuit” là lớp trừu tượng chung đại diện cho các mạch điện khác nhau.

Lớp “Circuit” được thiết kế như trên được gọi là có interface bị “ô nhiễm”. “Resistor”và “Lamp” bị buộc phải thực hiện việc cài đặt cho các phương thức “add” và “remove”hoàn toàn chẳng có ý nghĩa gì với chúng. Điều này gây ra sự dư thừa code không cầnthiết cũng như gây “khó chịu” cho những thực thể phần mềm khác sử dụng “Resistor” và“Lamp”.

Nhưng vấn đề chỉ thật sự xảy ra khi chúng ta nâng cấp, mở rộng đoạn chương trìnhtrên. Giả sử chúng ta cần thêm vào phương thức “removeAt” để hỗ trợ việc xóa mạch điệncon tại vị trí nào đó trong mạch điện phức hợp. Lúc này, chúng ta phải thực hiện việcchỉnh sửa trên tất cả các lớp đối tượng kế thừa từ “Circuit”. Việc chỉnh sửa trên“SeriesCircuit” và “ParallelCircuit” xem ra còn có thể chấp nhận được. Nhưng việc phảichỉnh sửa trên “Resistor” và “Lamp” là không thể chấp nhận được vì phương thức“removeAt” chẳng hề có ý nghĩa gì đối với chúng. Điều này rõ ràng làm cho “Resistor” và“Lamp” vi phạm nguyên lý Open-Closed một cách “không chính đáng”.Chú ý

i) Nguyên lý Phân tách interface có mối liên hệ với nguyên lý Open-Closed. Sự viphạm nguyên lý Phân tách interface có khả năng dẫn đến sự vi phạm nguyên lýOpen-Closed (xem phân tích ở trên).

Page 12: Các nguyên lý Hướng đối tượng

ii) Để tránh vi phạm nguyên lý Phân tách Inteface, chúng ta nên giữ cho interface củalớp đối tượng đơn giản và gọn nhẹ, nên làm theo tiêu chí “a class should do onething and do it well”. Chúng ta không nên để cho lớp đối tượng đảm nhận quánhiều trách nhiệm vì điều này dễ làm cho interface của nó bị “ô nhiễm”.

iii) Interface bị “ô nhiễm” của lớp đối tượng nên được phân tách ngay khi có thể đểtránh khả năng dẫn đến sự vi phạm nguyên lý Open-Closed. Việc phân táchinterface bị “ô nhiễm” của một lớp cơ sở có thể được thực hiện thông qua việc tăngthêm mức độ trừu tượng trong cây kế thừa của nó. Lớp cơ sở ban đầu chỉ nên cóinterface đơn giản mà mọi lớp kế thừa của nó đều cần phải có. Sau đó, phầninterface chung của một bộ phận lớp kế thừa được tổng hợp lại trong một lớp cơsở. Và lớp cơ sở này lại kế thừa từ lớp cơ sở ban đầu. Như vậy những lớp kế thừathuộc nhánh khác không bị phụ thuộc vào phần interface mà chúng không sử dụngđến của bộ phận lớp kế thừa kia. Với trường hợp đoạn chương trình tính điện trở mạch điện, để giải quyết vấn đềinterface của “Circuit” bị “ô nhiễm”, chúng ta tăng thêm một mức độ trừu tượngtrong cây kế thừa của nó. Khi đó, “Circuit” đóng vai trò là lớp trừu tượng cho cácmạch điện khác nhau. Nó chỉ chứa phần interface chung nhất của tất cả các mạchđiện này. Và trong ngữ cảnh bài toán tính điện trở đơn giản thì nó chỉ chứa phươngthức “calcResistance”.Chúng ta sẽ có lớp “SingleCircuit” đại diện cho các mạch điện đơn giản và“ComplexCircuit” đại diện cho cách mạch điện phức hợp. “SingleCircuit” chứa phầninterface chung của các mạch điện đơn giản như “Resistor” và “Lamp” trong khi“ComplexCircuit” chứa phần interface chung của các mạch điện phức hợp. Chúng tasẽ có được cây kế thừa như hình bên dưới.Lúc này, khi cần thêm vào phương thức “removeAt” chúng ta chỉ việc nâng cấpphần interface của “ComplexCircuit”, nhánh kế thừa bên “SingleCircuit” sẽ không bịảnh hưởng.

iv) Trong một số trường hợp, sau khi phân tách interface, một số lớp kế thừa mớithêm vào muốn sử dụng những phần interface đã phân tách, chúng có thể thựchiện việc đa kế thừa từ những lớp đối tượng hỗ trợ những phần interface này hoặccũng có thể kế thừa từ một lớp đối tượng hỗ trợ một phần interface chúng cần vàthực hiện composition đối với những đối tượng hỗ trợ phần interface còn lại.

Ý nghĩaNguyên lý Phân tách interface có mối liên hệ với nguyên lý Open-Closed và là một

trong bốn nguyên lý cơ bản làm nền tảng cho phân tích thiết kế hướng đối tượng. Nó giúpgiảm sự cồng kềnh, dư thừa không cần thiết cho phần mềm và quan trọng hơn là giảm sựkết dính (copuling) làm hạn chế tính linh động (flexibility) của phần mềm.

Tài liệu tham khảo- Robert C. Martin, The Open-Closed Principle, Object Mentor, 1996.- Robert C. Martin, The Dependency Inversion Principle, Object Mentor, 1996.- Robert C. Martin, The Liskov Substitution Principle, Object Mentor, 1996.- Robert C. Martin, The Interface Segregation Principle, Object Mentor, 1996.- Allen Holub, Why extends is evil?, Java World, 2003.