Hướng đối tượng – OO (P.4)

4.  CÁC NGUYÊN LÝ HƯỚNG ĐỐI TƯỢNG

4.1.  Nguyên lý “đóng mở”

Một moudle cần “mở” đối với việc phát triển thêm tính năng nhưng phải “đóng” đối với việc sửa đổi mã nguồn

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 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ực thể 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ệu quả 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ững cái hiện có, từ đó tránh được việc phải thay đổi các thực thể liên quan.

Ví dụ:

Xét đoạn mã

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…

}

}

// Một hàm draw thực hiện vẽ Shape

public void draw(ArrayList shapeList)

{

Line line;

Rectangle rectangle;

foreach (Shape s in shapeList)

switch (s.getType())

{

case ShapeType.LINE:

line = (Line)s;

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 được hì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ình trò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ửa nhữ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ân thủ 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…

}

}

public 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 thay đổi này thì hàm draw() đã tuân thủ được nguyên lý theo tiêu chí “vẽ thêm được hình”, nhưng việc tuân theo nguyên lý cũng chỉ là tương đối. Trong trường hợp chúng ta cần “vẽ thêm được hình” và có thự tự vẽ hình (Line => Rectangle => Circle) thì hàm draw() này sẽ không còn tuân thủ nguyên lý.

 

4.2. Nguyên lý thay thế Liskov

Các chức năng của hệ thống vẫn thực hiện đúng đắn nếu ta thay bất kì một đối tượng nào đó bằng đối tượng kế thừa.

Lớp B chỉ nên kế thừa từ lớp A khi và chỉ khi với mọi phương thức F thao tác trên các đối tượ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.

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 đối tượ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ủa lớ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ộng phầ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 A bằ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 thay thế. 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ày làm cho F vi phạm nguyên lý Open-Closed.

Ví dụ:

Xét đoạn mã

public class Stack

{

// First in last out

public virtual void push(int n)

{

// Pushes n to stack…

}

public virtual int pop()

{

// Pops value from stack…

}

}

public class Queue: Stack

{

// First in first out

public override void push(int n)

{

// Pushes n to queue…

}

public override int pop()

{

// Pops value from queue…

}

}

public bool function(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 true;

else

return false;

}

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úng ta cho “Queue” kế thừa từ Stack. Xét hàm “function” 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 “function” 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 function sẽ trả về TRUE, nhưng với các đối tượng của “Queue” hàm function lại luôn trả về FALSE. Để hàm “function” 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 “function” vi phạm nguyên lý Open-Closed, cũng như nó vi phạm nguyên lý Liskov.

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

4.3. Nguyên lý nghịch đảo phụ thuộc

Phụ thuộc vào mức trừu tượng, không phụ thuộc vào mức chi tiết.

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ủa nhữ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.

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ái riê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 theo cá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ổng quát là những cái ít thay đổi và ít biến động. Trong khi đó, sự thay đổi lại thường xuyên xả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úp cho các thành phần trong phần mềm trở nên mềm dẻo (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ái chung, 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ông cầ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ác cùng loại.

Ví dụ:

Xét đoạn mã

public void copy()

{

Keyboard keyboard = new Keyboard();

Printer printer = new Printer();

char c;

while ((c = keyboard.read()) != “q”)

printer.write(c);

}

Hàm copy() có nhiệm vụ in ra một chuỗi nhập vào từ bàn phím

Trong trường hợp chúng ta cần thêm chức năng lưu ra file thì hàm copy() sẽ phải sửa lại 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);

}

Sau khi sửa lại như trên, ta thấy hàm copy() sẽ vi phạm nguyên lý nghịch đảo phụ thuộc nếu ta yêu cầu có trường hợp hiển thị chuỗi ra màn hình, ta lại phải thay đổi hàm copy() cho trường hợp type = DISPLAY hoặc chuỗi đàu vào không phải đọc từ bàn phím nữa. Lý do vi phạm là hàm copy() quá phụ thuộc vào thiết bị cụ thể, để giải quyết vấn đề này ta phải xây dựng hàm copy() phụ thuộc thiết bị ở mức trừu tượng

public void copy(Reader reader, Writer writer)

{

char c;

while ((c = reader.read()) != “q”)

writer.write(c);

}

Chỉ cần tuân thủ các giao tiếp (interface) Reader và Writer thì hàm copy() sẽ hoạt động tốt đối vât bất kỳ thiết bị nào

4.4.  Nguyên lý phân tách giao tiếp (interface)

Nên có nhiều giao tiếp đặc thù với bên ngoài hơn là chỉ có một giao tiếp dùng chung cho một mục đích.

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 nhiều chức năng cà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 đối tượng như vậy được gọi là những lớp đối tượng có interface bị “ô nhiễm” (fat interface or polluted 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ực thể 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 đi truyề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ủa phầ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ỉ quan tâ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ông cầ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ừa phụ 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ỏi phầ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ày làm cho chúng vi phạm nguyên lý Open-Closed.

Ví dụ:

Xét đoạn mã

public abstract class Animal

{

public abstract void move();

public abstract void climb();

public abstract void bark();

}

public class Dog:Animal

{

public override void move()

{

}

public override void climb()

{

}

public override void bark()

{

}

}

public class Cat:Animal

{

public override void move()

{

}

public override void climb()

{

}

public override void bark()

{

}

}

Rõ ràng trong trường hợp này việc khai báo các phương thức climb(), bark() ở trong lớp Animal làm cho nó trở thành lớp “ô nhiễm”, lớp Cat không cần thiết phải kế thừa hàm bark(), trong khi lớp Dog không cần thiết kế thừa hàm climb().

  1. #1 by nghiavt on Tháng Mười 7, 2010 - 09:34

    Mình đọc 4 phần về OO tuy nhiên chưa thấy bạn đề cập tới các khái niệm:

    – Composition, Aggregation
    – Associate

    Hì mình đọc trên wiki nhưng chưa hiểu rõ lắm🙂 mong bạn giải thích và cho ex để dễ hiểu.

    Thanks in advance

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s

%d bloggers like this: