Showing posts with label Lập trình. Show all posts
Showing posts with label Lập trình. Show all posts

Wednesday, August 25, 2010

Writing MySQL Scripts with PHP and PDO

Writing MySQL Scripts with PHP and PDO

Paul DuBois
paul@kitebird.com

Document revision: 1.01
Last update: 2008-05-07

Table of Contents


PHP makes it easy to write scripts that access databases, enabling you to create dynamic web pages that incorporate database content. PHP includes several specialized database-access interfaces that take the form of separate sets of functions for each database system. There is one set for MySQL, another for InterBase, another for PostgreSQL, and so forth. However, having a different set of functions for each database makes PHP scripts non-portable at the lexical (source code) level. For example, the function for issuing an SQL statement is named mysql_query(), ibase_query(), or pg_exec(), depending on whether you are using MySQL, InterBase, or PostgreSQL.

In PHP 5 and up, you can avoid this problem by using the PHP Data Objects (PDO) extension. PDO supports database access in an engine-independent manner based on a two-level architecture:

  • The top level provides an interface that consists of a set of classes and methods that is the same for all database engines supported by PDO. The interface hides engine-specific details so that script writers need not think about which set of functions to use.
  • The lower level consists of individual drivers. Each driver supports a particular database engine and translates between the top-level interface seen by script writers and the database-specific interface required by the engine. This provides you the flexibility of using any database for which a driver exists, without having to consider driver-specific details.
This architectural approach has been used successfully with other languages--for example, to develop the DBI (Perl, Ruby), DB-API (Python), and JDBC (Java) database access interfaces. It's also been used with PHP before: PHPLIB, MetaBase, and PEAR DB are other packages that provide a uniform database-independent interface across different engines.

I have written elsewhere about using the PEAR DB module for writing PHP scripts that perform database processing in an engine-independent manner (see "Resources"). This document is similar but covers PDO instead. The examples use the driver for MySQL.

Preliminary Requirements


PDO uses object-oriented features available only in PHP 5 and up, so you must have PHP 5 or newer installed to use PDO for writing scripts that access MySQL.

PDO uses classes and objects to present an object-oriented interface. This article assumes that you are familiar with PHP's approach to object-oriented programming. If you are not, you may wish to review the "Classes and Objects" chapter of the PHP Manual.

Writing PDO Scripts


Scripts that use the PDO interface to access MySQL generally perform the following operations:

  • Connect to the MySQL server by calling new PDO() to obtain a database handle object.
  • Use the database handle to issue SQL statements or obtain statement handle objects.
  • Use the database and statement handles to retrieve information returned by the statements.
  • Disconnect from the server when the database handle is no longer needed.
The next sections discuss these operations in more detail.

Connecting to and Disconnecting from the MySQL Server


To establish a connection to a MySQL server, specify a data source name (DSN) containing connection parameters, and optionally the username and password of the MySQL account that you want to use. To connect to the MySQL server on the local host to access the test database with a username and password of testuser and testpass, the connection sequence looks like this:

   $dbh = new PDO("mysql:host=localhost;dbname=test", "testuser", "testpass");
For MySQL, the DSN is a string that indicates the database driver (mysql), and optionally the hostname where the server is running and the name of the database you want to use. Typical syntax for the DSN looks like this:
   mysql:host=host_name;dbname=db_name
The default host is localhost. No default database is selected if dbname is omitted.

The MySQL driver also recognizes port and unix_socket parameters, which specify the TCP/IP port number and Unix socket file pathname, respectively. If you use unix_socket, do not specify host or port.

For other database engines, the driver name is different (for example, pgsql for PostgreSQL) and the parameters following the colon might be different as well.

When you invoke the new PDO() constructor method to connect to your database server, PDO determines from the DSN which type of database engine you want to use and acesses the low-level driver appropriate for that engine. This is similar to the way that Perl or Ruby DBI scripts reference only the top-level DBI module; the connect() method provided by the top-level module looks at the DSN and determines which particular lower-level driver to use.

If new PDO() fails, PHP throws an exception. Otherwise, the constructor method returns an object of the PDO class. This object is a database handle that you use for interacting with the database server until you close the connection.

An alternative to putting the connection code directly in your script is to move it into a separate file that you reference from your main script. For example, you could create a file pdo_testdb_connect.php that looks like this:

     # pdo_testdb_connect.php - function for connecting to the "test" database

function testdb_connect ()
{
$dbh = new PDO("mysql:host=localhost;dbname=test", "testuser", "testpass");
return ($dbh);
}
?>
Then include the file into your main script and call testdb_connect() to connect and obtain the database handle:
   require_once "pdo_testdb_connect.php";

$dbh = testdb_connect ();
This approach makes it easier to use the same connection parameters in several different scripts without writing the values literally into every script; if you need to change a parameter sometime, just change pdo_testdb_connect.php. Use of a separate file also enables you to move the code that contains the connection parameters outside of the web server's document tree. That has the benefit of preventing it from being displayed literally if the server becomes misconfigured and starts serving PHP scripts as plain text.

Any of the PHP file-inclusion statements can be used, such as include or require, but require_once prevents errors from occurring if any other files that your script uses also reference pdo_testdb_connect.php.

When you're done using the connection, close it by setting the database handle to NULL:

   $dbh = NULL;
After that, $dbh becomes invalid as a database handle and can no longer be used as such.

If you do not close the connection explicitly, PHP does so when the script terminates.

While the database handle is open and you are using it to issue other PDO calls, you should arrange to handle errors if they occur. You can check for an error after each PDO call, or you can cause exceptions to be thrown. The latter approach is simpler because you need not check for errors explicitly; any error raises an exception that terminates your script. If you enable exceptions, you also have the option of catching them yourself instead of allowing them to terminate your script. By doing this, you can substitute your own error messages for the defaults, perform cleanup operations, and so on.

To enable exceptions, set the PDO error mode as follows after connecting:

   $dbh->setAttribute (PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
That statement is something you could add to the testdb_connect() function if you want the error mode to be set automatically whenever you connect.

For more information on dealing with errors, see "Handling Errors."

Issuing Statements


After obtaining a database handle by calling new PDO(), you can use it to execute SQL statements:

  • For statements that modify rows and produce no result set, pass the statement string to the database handle exec() method, which executes the statement and returns an affected-rows count:
       $count = $dbh->exec ("some SQL statement");
  • For statements that select rows and produce a result set, invoke the database handle query() method, which executes the statement and returns an object of the PDOStatement class:
       $sth = $dbh->query ("some SQL statement");
    This object is a statement handle that provides access to the result set. It enables you to fetch the result set rows and obtain metadata about them, such as the number of columns.
To illustrate how to handle various types of statements, the following discussion shows how to create and populate a table using CREATE TABLE and INSERT (statements that return no result set). Then it uses SELECT to generate a result set.
Issuing Statements That Return No Result Set

The following code uses the database handle exec() method to issue a statement that creates a simple table animal with two columns, name and category:

   $dbh->exec ("CREATE TABLE animal (name CHAR(40), category CHAR(40))");
After the table has been created, it can be populated. The following example invokes the exec() method to issue an INSERT statement that loads a small data set into the animal table:
   $count = $dbh->exec ("INSERT INTO animal (name, category)
VALUES
('snake', 'reptile'),
('frog', 'amphibian'),
('tuna', 'fish'),
('racoon', 'mammal')");
exec() returns a count to indicate how many rows were affected by the statement. For the preceding INSERT statement, the affected-rows count is 4.
Issuing Statements That Return a Result Set

Now that the table exists and contains a few records, SELECT can be used to retrieve rows from it. To issue statements that return a result set, use the database handle query() method:

   $sth = $dbh->query ("SELECT name, category FROM animal");
printf ("Number of columns in result set: %d\n", $sth->columnCount ());
$count = 0;
while ($row = $sth->fetch ())
{
printf ("Name: %s, Category: %s\n", $row[0], $row[1]);
$count++;
}
printf ("Number of rows in result set: %d\n", $count);
A successful query() call returns a PDOStatement statement-handle object that is used for all operations on the result set. Some of the information available from a PDOStatement object includes the row contents and the number of columns in the result set:
  • The fetch() method returns each row in succession, or FALSE when there are no more rows.
  • The columnCount() methods returns the number of columns in the result set.
Note: A statement handle also has a rowCount() method, but it cannot be assumed to reliably return the number of rows in the result set. Instead, fetch the rows and count them, as shown in the preceding example.
Other Ways To Fetch Result Set Rows

fetch() accepts an optional fetch-mode argument indicating what type of value to return. This section describes some common mode values. Assume in each case that the following query has just been issued to produce a result set:

   $sth = $dbh->query ("SELECT name, category FROM animal");
  • PDO::FETCH_NUM
    Return each row of the result set as an array containing elements that correspond to the columns named in the SELECT statement and that are accessed by numeric indices beginning at 0:
       while ($row = $sth->fetch (PDO::FETCH_NUM))
    printf ("Name: %s, Category: %s\n", $row[0], $row[1]);
  • PDO::FETCH_ASSOC
    Return each row as an array containing elements that are accessed by column name:
       while ($row = $sth->fetch (PDO::FETCH_ASSOC))
    printf ("Name: %s, Category: %s\n", $row["name"], $row["category"]);
  • PDO::FETCH_BOTH
    Return each row as an array containing elements that can be accessed either by numeric index or by column name:
       while ($row = $sth->fetch (PDO::FETCH_BOTH))
    {
    printf ("Name: %s, Category: %s\n", $row[0], $row[1]);
    printf ("Name: %s, Category: %s\n", $row["name"], $row["category"]);
    }
  • PDO::FETCH_OBJ
    Return each row as an object. In this case, you access column values as object properties that have the same names as columns in the result set:
       while ($row = $sth->fetch (PDO::FETCH_OBJ))
    printf ("Name: %s, Category: %s\n", $row->name, $row->category);
If you invoke fetch() with no argument, the default fetch mode is PDO::FETCH_BOTH unless you change the default before fetching the rows:
  • The query() method accepts an optional fetch-mode argument following the statement string:
       $sth = $dbh->query ("SELECT name, category FROM animal", PDO::FETCH_OBJ);
    while ($row = $sth->fetch ())
    printf ("Name: %s, Category: %s\n", $row->name, $row->category);
  • Statement handles have a setFetchMode() method to set the mode for subsequent fetch() calls:
       $sth->setFetchMode (PDO::FETCH_OBJ);
    while ($row = $sth->fetch ())
    printf ("Name: %s, Category: %s\n", $row->name, $row->category);
Another way to fetch results is to bind variables to the result set columns with bindColumn(). Then you fetch each row using the PDO::FETCH_BOUND fetch mode. PDO stores the column values in the variables, and fetch() returns TRUE instead of a row value while rows remain in the result set:
   $sth = $dbh->query ("SELECT name, category FROM animal");
$sth->bindColumn (1, $name);
$sth->bindColumn (2, $category);
while ($sth->fetch (PDO::FETCH_BOUND))
printf ("Name: %s, Category: %s\n", $name, $category);

Using Prepared Statements


exec() and query() are PDO object methods: You use them with a database handle and they execute a statement immediately and return its result. It is also possible to prepare a statement for execution without executing it immediately. The prepare() method takes an SQL statement as its argument and returns a PDOStatement statement-handle object. The statement handle has an execute() method that executes the statement:

   $sth = $dbh->prepare ($stmt);
$sth->execute ();
Following execution, other statement-handle methods provide information about the statement result:
  • For a statement that modifies rows, invoke rowCount() to get the rows-affected count:
       $sth = $dbh->prepare ("DELETE FROM animal WHERE category = 'mammal'");
    $sth->execute ();
    printf ("Number of rows affected: %d\n", $sth->rowCount ());
  • For a statement that produces a result set, the fetch() method retrieves them and the columnCount() method indicates how many columns there are. To determine how many rows there are, count them as you fetch them. (As mentioned previously, rowCount() returns a row count, but should be used only for statements that modify rows.)
       $sth = $dbh->prepare ("SELECT name, category FROM animal");
    $sth->execute ();
    printf ("Number of columns in result set: %d\n", $sth->columnCount ());
    $count = 0;
    while ($row = $sth->fetch ())
    {
    printf ("Name: %s, Category: %s\n", $row[0], $row[1]);
    $count++;
    }
    printf ("Number of rows in result set: %d\n", $count);
If you are not sure whether a given SQL statement modifies or returns nows, the statement handle itself enables you to determine the proper mode of processing. See "Determining the Type of a Statement."

As just shown, prepared statements appear to offer no advantage over exec() and query() because using them introduces an extra step into statement processing. But there are indeed some benefits to them:

  • Prepared statements can be parameterized with placeholders that indicate where data values should appear. You can bind specific values to these placeholders and PDO takes care of any quoting or escaping issues for values that contain special characters. "Placeholders and Quoting" discusses these topics further.
  • Separating statement preparation from execution can be more efficient for statements to be executed multiple times because the preparation phase need be done only once. For example, if you need to insert a bunch of rows, you can prepare an INSERT statement once and then execute it repeatedly, binding successive row values to it for each execution.

Placeholders and Quoting


A prepared statement can contain placeholders to indicate where data values should appear. After you prepare the statement, bind specific values to the placeholders (either before or at statement-execution time), and PDO substitutes the values into the statement before sending it to the database server.

PDO supports named and positional placeholders:

  • A named placeholder consists of a name preceded by a colon. After you prepare the statement, use bindValue() to provide a value for each placeholder, and then execute the statement. To insert another row, bind new values to the placeholders and invoke execute() again:
       $sth = $dbh->prepare ("INSERT INTO animal (name, category)
    VALUES (:name, :cat)");
    $sth->bindValue (":name", "ant");
    $sth->bindValue (":cat", "insect");
    $sth->execute ();
    $sth->bindValue (":name", "snail");
    $sth->bindValue (":cat", "gastropod");
    $sth->execute ();
    As an alternative to binding the data values before calling execute(), you can pass the values directly to execute() using an array that associates placeholder names with the values:
       $sth->execute (array (":name" => "black widow", ":cat" => "spider"));
  • Positional placeholders are characters within the statement string. You can bind the values prior to calling execute(), similar to the previous example, or pass an array of values directly to execute():
       $sth = $dbh->prepare ("INSERT INTO animal (name, category)
    VALUES (?, ?)");
    # use bindValue() to bind data values
    $sth->bindValue (1, "ant");
    $sth->bindValue (2, "insect");
    $sth->execute ();
    # pass values directly to execute() as an array
    $sth->execute (array ("snail", "gastropod"));
Positional placeholder numbers begin with 1.

An alternative to bindValue() is bindParam(), which adds a level of indirection to value-binding. Instead of passing a data value as the second argument to bindParam(), pass a variable to associate the variable with the placeholder. To supply a value for the placeholder, assign a value to the variable:

   $sth = $dbh->prepare ("INSERT INTO animal (name, category)
VALUES (?, ?)");
$sth->bindParam (1, $name);
$sth->bindParam (2, $category);
$name = "ant";
$category = "insect";
$sth->execute ();
$name = "snail";
$category = "gastropod";
$sth->execute ();
The preceding examples use INSERT statements, but placeholder techniques are applicable to any type of statement, such as UPDATE or SELECT.

One of the benefits of using placeholders is that PDO handles any quoting or escaping of special characters or NULL values. For example, if you bind the string "a'b'c" to a placeholder, PDO inserts "'a\'b\'c'" into the statement. To bind the SQL NULL value to a placeholder, bind the PHP NULL value. In this case, PDO inserts the word "NULL" into the statement without surrounding quotes. (Were quotes to be added, the value inserted into the statement would be the string "'NULL'", which is incorrect.)

PDO also provides a database handle quote() method to which you can pass a string and receive back a quoted string with special characters escaped. However, I find this method deficient. For example, if you pass it NULL, it returns an empty string, which if inserted into a statement string does not correspond to the SQL NULL value. Use quote() with care if you use it.

Determining the Type of a Statement


When you issue a statement using a database handle, you must know whether the statement modifies rows or produces a result set, so that you can invoke whichever of exec() or query() is appropriate. However, under certain circumstances, you might not know the statement type, such as when you write a script to execute arbitrary statements that it reads from a file. To handle such cases, use prepare() with the database handle to get a statement handle and execute() to execute the statement. Then check the statement's column count:

  • If columnCount() is zero, the statement did not produce a result set. Instead, it modified rows and you can invoke rowCount() to determine the number of affected rows.
  • If columnCount() is greater than zero, the statement produced a result set and you can fetch the rows. To determine how many rows there are, count them as you fetch them.
The following example determines whether a statement modifies rows or produces a result set, and then processes it accordingly:
   $sth = $dbh->prepare ($stmt);
$sth->execute ();
if ($sth->columnCount () == 0)
{
# there is no result set, so the statement modifies rows
printf ("Number of rows affected: %d\n", $sth->rowCount ());
}
else
{
# there is a result set
printf ("Number of columns in result set: %d\n", $sth->columnCount ());
$count = 0;
while ($row = $sth->fetch (PDO::FETCH_NUM))
{
# display column values separated by commas
print (join (", ", $row) . "\n");
$count++;
}
printf ("Number of rows in result set: %d\n", $count);
}

Handling Errors


When you invoke new PDO() to create a database handle, occurrance of an error causes a PDOException to be thrown. If you don't catch the exception, PHP terminates your script. To handle the exception yourself, use a try block to perform the connection attempt and a catch block to catch any error that occurs:

   try
{
$dbh = new PDO("mysql:host=localhost;dbname=test", "testuser", "testpass");
}
catch (PDOException $e)
{
print ("Could not connect to server.\n");
print ("getMessage(): " . $e->getMessage () . "\n");
}
A PDOException is an extension of the PHP Exception class, so it has getCode() and getMessage() methods that return an error code and descriptive message, respectively. (However, I find that getCode() always returns 0 for connection errors and is meaningful only for PDO exceptions that occur after the connection has been established.)

After you successfully obtain a database handle, further PDO calls that use it are handled according to the PDO error mode. There are three modes:

  • PDO::ERRMODE_SILENT
    When an error occurs in silent or warning mode for a given object method, PDO sets up error information that you can access when the method returns. This is the default error mode.
  • PDO::ERRMODE_WARNING
    This is like silent mode but PDO also displays a warning message in addition to setting up error information when an error occurs.
  • PDO::ERRMODE_EXCEPTION
    PDO sets up error information when an error occurs and throws a PDOException.
PDO sets error information for the object to which the error applies, regardless of the error mode. This information is available via the object's errorCode() and errorInfo() methods. errorCode() returns an SQLSTATE value (a five-character string). errorInfo() returns a three-element array containing the SQLSTATE value, and a driver-specific error code and error message. For MySQL, the driver-specific values are a numeric error code and a descriptive error message.

To handle errors in silent mode, you must check the result of each PDO call. The following example shows how to test for errors during an operation that uses a database handle, $dbh, and a statement handle, $sth (you would not necessarily print all the available information as the example does):

   if (!($sth = $dbh->prepare ("INSERT INTO no_such_table")))
{
print ("Could not prepare statement.\n");
print ("errorCode: " . $dbh->errorCode () . "\n");
print ("errorInfo: " . join (", ", $dbh->errorInfo ()) . "\n");
}
else if (!$sth->execute ())
{
print ("Could not execute statement.\n");
print ("errorCode: " . $sth->errorCode () . "\n");
print ("errorInfo: " . join (", ", $sth->errorInfo ()) . "\n");
}
Testing the result of every call can become messy quickly. Another way to deal with failures is to set the error handling mode so that any error raises an exception:
   $dbh->setAttribute (PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
In this case, you can assume that if you invoke a method and it returns, it succeeded. You can either leave exceptions uncaught or catch and handle them yourself. If you leave them uncaught, exceptions cause PHP to print a backtrace and terminate your script. To catch exceptions, perform PDO operations using a try/catch construct. The try block contains the operations and the catch block handles an execption if one occurs.
   try
{
$sth = $dbh->prepare ("INSERT INTO no_such_table");
$sth->execute ();
}
catch (PDOException $e)
{
print ("The statement failed.\n");
print ("getCode: ". $e->getCode () . "\n");
print ("getMessage: ". $e->getMessage () . "\n");
}
By using try and catch, you can substitute your own error messages if you like, perform cleanup operations, and so on.

As shown in the preceding example, the try block can contain operations on multiple handles. However, if an exception occurs in that case, you won't be able to use the handle-specific errorCode() or errorInfo() methods in the catch block very easily because you won't know which handle caused the error. You'll need to use the information available from the exception methods, as shown.

Using Transactions


In MySQL, some storage engines are transactional, which enables you to perform an operation and then commit it permanently if it succeeded or roll it back to cancel its effects if an error occurred. PDO provides a mechanism for performing transactions that is based on the following database-handle methods:

  • To start a transaction, invoke beginTransaction() to disable autocommit mode so that database changes do not take effect immediately.
  • To commit a successful transaction or roll back an unsuccessful one, invoke commit() or rollback(), respectively.
The easiest way to use these methods is to enable PDO exceptions and use try and catch to handle errors:
   $dbh->setAttribute (PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try
{
$dbh->beginTransaction (); # start the transaction
# ... perform database operation ...
$dbh->commit (); # success
}
catch (PDOException $e)
{
print ("Transaction failed: " . $e->getMessage () . "\n");
$dbh->rollback (); # failure
}
For additional paranoia, you can place the rollback() call within a nested try/catch construct so that if rollback() itself fails and raises another exception, the script doesn't get terminated.

Resources


Revision History


  • 1.00--Original version.
  • 1.01, 2008-05-07--Removed my mistaken statement that the PDO driver for MySQL requires the mysqli extension. It does not. The driver uses libmysqlclient directly.

Thursday, December 31, 2009

Các khái niệm lập trình hướng đối tượng OOP

Một đối tượng là gì?

Java được biết đến như một ngôn ngữ hướng đối tượng (OO - object-oriented), bạn có thể sử dụng ngôn ngữ này để lập trình hướng đối tượng. Điều này rất khác so với lập trình thủ tục, và có thể hơi lạ lùng đối với hầu hết các lập trình viên không hướng đối tượng. Bước đầu tiên bạn phải hiểu đối tượng là gì, vì đó là khái niệm cơ sở của OOP.

Một đối tượng là một bó mã lệnh tự thân trọn vẹn (self-contained), tự hiểu chính mình và có thể nói cho các đối tượng khác về chính mình nếu chúng đưa ra các yêu cầu mà nó hiểu được. Một đối tượng có các thành phần dữ liệu (các biến) và các phương thức, chính là những yêu cầu mà nó biết cách trả lời (dù chúng không được diễn đạt bằng lời như các câu hỏi). Tập các phương thức mà một đối tượng biết cách trả lời được gọi là giao diện của đối tượng. Một vài phương thức là mở công cộng, nghĩa là các đối tượng khác có thể gọi đến chúng. Tập các phương thức này được gọi là giao diện công cộng của đối tượng.

Khi một đối tượng gọi phương thức của một đối tượng khác, thì được gọi là gửi một thông điệp (sending a message hoặc message send). Cụm từ này là thuật ngữ của OO nhưng hầu hết trong giới Java mọi người hay nói, “gọi phương thức này” hơn là “gửi thông điệp này”. Trong phần tiếp theo, chúng ta sẽ xem xét một ví dụ minh họa khái niệm giúp bạn hiểu vấn đề này rõ ràng hơn.

Ví dụ minh họa khái niệm đối tượng

Giả sử chúng ta có đối tượng Person. Mỗi Person có tên, tuổi, chủng tộc và giới tính. Mỗi Person cũng biết nói và biết đi. Một Person có thể hỏi tuổi của một Person khác, hoặc yêu cầu một Person khác bắt đầu đi (hay dừng). Diễn đạt theo thuật ngữ lập trình, bạn có thể tạo một đối tượng Person và khai báo một số biến (như tên và tuổi). Nếu bạn tạo một đối tượng Person thứ hai, đối tượng này có thể hỏi tuổi của đối tượng thứ nhất hoặc yêu cầu đối tượng thứ nhất bắt đầu đi. Nó có thể thực hiện những điều ấy bằng cách gọi đến các phương thức của đối tượng Person đầu tiên. Khi chúng ta bắt đầu viết mã lệnh bằng ngôn ngữ Java, bạn sẽ hiểu ngôn ngữ này triển khai thực hiện khái niệm đối tượng ra sao.

Nói chung, khái niệm đối tượng là như nhau trong ngôn ngữ Java và các ngôn ngữ hướng đối tượng khác, mặc dù việc triển khai thực hiện là khác nhau giữa các ngôn ngữ. Các khái niệm là phổ quát. Vì sự thật này, lập trình viên hướng đối tượng, bất chấp họ lập trình bằng ngôn ngữ nào, có xu hướng phát biểu khác so với những lập trình viên thủ tục. Các lập trình viên thủ tục thường nói về các hàm và các mô đun. Lập trình viên hướng đối tượng lại nói về các đối tượng và họ thường nói về các đối tượng này bằng cách sử dụng các đại từ nhân xưng. Chẳng hề bất thường khi bạn nghe một lập trình viên hướng đối tượng nói với đồng nghiệp, “đối tượng Supervisor nói với đối tượng Employee, ‘cho tôi ID của cậu,’” vì anh ta cần những thứ này để gán nhiệm vụ cho Employee.

Lập trình viên hướng thủ tục có thể nghĩ cách nói chuyện này thật lạ lùng, nhưng nó lại hoàn toàn bình thường đối với lập trình viên hướng đối tượng. Trong thế giới lập trình của họ, mọi thứ đều là đối tượng (cũng có một vài ngoại lệ đáng chú ý trong ngôn ngữ Java) và các chương trình là chỉ là sự tương tác (hay nói chuyện) giữa các đối tượng với nhau.

Các nguyên tắc hướng đối tượng cơ bản

Khái niệm đối tượng là trọng yếu đối với lập trình hướng đối tượng, và dĩ nhiên, ý tưởng các đối tượng giao tiếp với nhau bằng các thông điệp cũng vậy. Nhưng có 3 nguyên tắc cơ bản mà bạn cần hiểu.

Bạn có thể nhớ 3 nguyên tắc hướng đối tượng cơ bản bằng cụm viết tắt PIE:

  • Đa hình ( Polymorphism)
  • Thừa kế ( Inheritance)
  • Bao gói ( Encapsulation)

Đó là những từ trừu tượng nhưng những khái niệm này thực sự không quá khó hiểu. Trong các phần tiếp theo, chúng ta sẽ bàn về từng khái niệm này ở mức độ chi tiết hơn, theo thứ tự ngược lại.


Bao gói

Hãy nhớ rằng, một đối tượng là tự thân trọn vẹn, chứa đựng các thành phần dữ liệu và hành động mà nó có thể thực hiện trên các thành phần dữ liệu ấy. Đây là việc triển khai thực hiện nguyên lý gọi là ẩn giấu thông tin. Ý tưởng của nó là một đối tượng tự nó hiểu mình. Nếu một đối tượng khác muốn điều gì từ đối tượng này thì nó phải hỏi. Theo thuật ngữ lập trình hướng đối tượng, phải gửi một thông điệp đến một đối tượng khác để hỏi về tuổi. Theo thuật ngữ Java, phải gọi một phương thức của đối tượng khác để nó trả lại kết quả là tuổi.

Sự bao gói đảm bảo rằng mỗi đối tượng là khác nhau và chương trình là một cuộc chuyện trò giữa các đối tượng. Ngôn ngữ Java cho phép các lập trình viên vi phạm nguyên lý này nhưng hầu như luôn là một ý tưởng tồi nếu làm như thế.

Thừa kế

Khi bạn được sinh ra, nói về khía cạnh sinh học, bạn là tổ hợp DNA của cha mẹ mình. Bạn không hoàn toàn giống ai trong số họ, mà bạn giống cả hai người. OO cũng có nguyên tắc tương tự đối với các đối tượng. Quay lại với đối tượng Person. Ta nhớ lại rằng mỗi người có một chủng tộc. Không phải tất cả các Person đều cùng chủng tộc, nhưng dù sao thì họ cũng có điểm tương tự như nhau chứ? Chắc chắn vậy! Họ chẳng phải ngựa, tinh tinh hay cá voi mà là người. Mọi con người đều có những điểm chung nhất định và điều này giúp phân biệt con người với các loài động vật khác. Nhưng giữa mọi người cũng có khác biệt với nhau. Một đứa trẻ có giống hệt một người trưởng thành không? Không. Đi lại và nói là khác nhau rồi. Nhưng một đứa trẻ thì vẫn chắc chắn là một con người.

Theo ngôn ngữ hướng đối tượng, Person và Baby là các lớp sự vật hiện tượng thuộc cùng một hệ thống phân bậc, và Baby thừa kế các đặc tính và hành vi từ lớp cha của nó. Chúng ta có thể nói rằng một Baby cụ thể là một kiểu Person hay Baby thừa kế từ Person. Nhưng không có chiều ngược lại – một Person không nhất thiết phải là một Baby. Mỗi đối tượng Baby là một cá thể của lớp Baby và khi chúng ta tạo một đối tượng Baby, chúng ta cá thể hóa lớp này. Hãy coi lớp như là khuôn mẫu chung cho các cá thể của lớp đó. Nói chung, đối tượng có thể làm những gì tùy thuộc vào kiểu của đối tượng đó là gì – hoặc nói theo cách khác, đối tượng đó là cá thể của lớp nào. Cả Baby và Adult đều thuộc kiểu Person, nhưng một đối tượng (Adult) có thể có một việc làm (job) còn đối tượng kia (Baby) thì không.

Theo thuật ngữ Java, Person là một lớp bậc trên (superclass) của Baby và Adult, và các lớp này là lớp con của Person. Một khái niệm có liên quan khác là ý tưởng về trừu tượng hóa. Person có mức trừu tượng hóa cao hơn Baby hay Adult. Cả hai đều là kiểu Person nhưng có những khác biệt nho nhỏ. Tất cả các đối tượng Person đều có những điểm chung (như tên và tuổi). Bạn có thể cá thể hóa một Person? Thực sự là không! Bạn hoặc có một Baby hoặc có một Adult. Trong Java, Person được gọi là lớp trừu tượng. Bạn không thể trực tiếp có một cá thể của lớp Person. Bạn sẽ có Baby hoặc Adult, cả hai đều là kiểu Person, nhưng là Person đã được thực tế hóa. Các lớp trừu tượng nằm ngoài phạm vi của tài liệu này, chúng tôi sẽ không nói thêm về chúng nữa.

Bây giờ, ta hãy suy nghĩ xem với một Baby, “nói” (speak) có nghĩa là gì. Chúng ta sẽ xét đến các hệ quả trong phần thảo luận tiếp theo.

Đa hình

Baby có “nói” như Adult không? Dĩ nhiên là không rồi. Một Baby có thể ê a, nhưng không nhất thiết nói ra những lời hiểu được như Adult. Do đó, nếu tôi cá thể hóa một đối tượng Baby (hay là “cá thể hóa một Baby” cũng có cùng ý nghhĩa – từ “đối tượng” được ngầm hiểu) và cho nó nói, thì nó chỉ có nghĩa là những tiếng ê a. Ta hy vọng rằng Adult “nói” thì mạch lạc hơn.

Trong hệ thống phân bậc con người, chúng ta có Person nằm ở đỉnh với Baby và Adult nằm phía dưới nó, là các lớp con. Tất cả mọi người đều có thể nói, Baby và Adult cũng vậy, nhưng sẽ nói khác nhau. Baby chỉ ê a và phát những âm thanh đơn giản. Adult nói thành lời. Đó chính là sự đa hình: các đối tượng làm việc theo cách riêng của chúng.

Ngôn ngữ Java là (và không là) OO ở chỗ nào?

Như chúng ta sẽ thấy, ngôn ngữ Java cho phép bạn tạo các đối tượng hạng nhất (first-class), nhưng không phải bất cứ cái gì trong ngôn ngữ này đều là đối tượng. Một số ngôn ngữ OO như Smalltalk lại hoàn toàn khác. Smalltalk hoàn toàn là OO, có nghĩa là mọi thứ trong ngôn ngữ này đều là đối tượng. Java là ngôn ngữ lai tạp giữa đối tượng và phi đối tượng. Nó cho phép một đối tượng biết rõ các đối tượng khác, nếu với tư cách là một lập trình viên bạn cho phép điều đó xảy ra. Điều này vi phạm nguyên lý bao gói.

Tuy nhiên, ngôn ngữ Java cũng cung cấp cho tất cả các lập trình viên OO những công cụ cần thiết để tuân theo mọi quy tắc OO và viết mã lệnh OO rất chuẩn. Nhưng làm được như vậy cần phải tự có kỷ luật. Ngôn ngữ không ép bạn làm việc đúng đắn được.

Trong khi những người thuần túy chủ nghĩa hướng đối tượng tranh luận xem liệu Java là hướng đối tượng hay không, thực sự đây không phải là một lý lẽ mang lại ích lợi. Nền tảng Java sẽ giữ vững vị trí của nó. Hãy học cách lập trình hướng đối tượng tốt nhất có thể với mã lệnh Java và cứ để những lý lẽ thuần túy chủ nghĩa cho những người khác. Ngôn ngữ Java giúp bạn viết chương trình rõ ràng, khá ngắn gọn, dễ bảo trì, điều này là khá đủ trong cuốn sách của tôi đối với hầu hết các tình huống nghề nghiệp.

Friday, December 4, 2009

Tự Học Lập Trình trong 10 năm (Peter Norvig)

Tại sao mọi người lại vội vã như vậy?
Bước vào bất cư một hiệu sách nào, và bạn sẽ nhìn thấy cuốn Tự học Java trong 7 ngày cùng với vô số những cuốn sách tương tự giúp bạn tự học Visual Basic, Windows, Internet, … trong vòng một vài ngày, thậm chí một vài giờ. Tôi đã dùng công cụ Power Search của Amazon thử tìm những cuốn sách xuất bản sau năm 1992 với mục đích giúp bạn học một thứ gì đó trong vòng vài ngày và tìm được 248 cuốn. 78 cuốn đầu tiên là sách về computer (đứng thứ 79 là cuốn Learn Bengali in 30 days). Tôi thử thay thế “ngày” bằng “giờ” và kết quả giống nhau một cách kinh ngạc: 253 cuốn sách nữa, đầu tiên là 77 cuốn sách computer và đứng thứ 78 là cuốn Teach Yourself Grammar and Style in 24 Hours. Trong số 200 cuốn đầu tiên, 96% là sách máy tính.


Có thể kết luận rằng (1)Mọi người đang đổ xô đi học computer, (2) hoặc computer cực kỳ dễ học, dễ học hơn bất cứ một thứ gì khác. Chẳng có cuốn sách nào dạy bạn nghe Beethoven, dạy Vật lý lượng tử, hay thậm chí dạy cắt lông chó... chỉ trong vòng một vài ngày.
Thử phân tích tựa đề của cuốn Học Pascal trong 3 ngày (Learn Pascal in Three Days)

  • Học: Trong 3 ngày bạn sẽ không đủ thời gian để viết được những chương trình thực sự phức tạp, và học từ những thành công cũng như thất bại của bạn. Bạn cũng không đủ thời gian để làm việc với một lập trình viên giàu kinh nghiệm, và cũng không kịp hiểu cuộc sống trong môi trường đó là như thế nào. Nói một cách ngắn gọn bạn không đủ thời gian để học được nhiều. Đó chỉ có thể là một thứ kiến thức nông cạn chứ không thể là một hiểu biết sâu sắc. Theo như Alexander Pope thì: học một chút là một điều nguy hiểm.

  • Pascal: Trong 3 ngày có lẽ bạn sẽ học được cú pháp của Pascal (nếu bạn đã biết một ngôn ngữ tương tự), nhưng bạn không có nhiều thời gian để học cách sử dụng cú pháp ấy. Nếu bạn là lập trình viên BASIC, bạn sẽ viết một chương trình theo phong cách của BASIC nhưng lại dùng cúp pháp của Pascal, tuy nhiên bạn sẽ không học được Pascal hay ở đâu và dở ở chỗ nào. Vậy thì học Pascal để làm gì? Alan Perlis có nói “Nếu một ngôn ngữ không làm thay đổi cách suy nghĩ của bạn về lập trình thì ngôn ngữ đó không đáng học”. Rất có thể lý do bạn phải học một chút Pascal (hoặc thực tế hơn là Visual Basic hay Javascript) là vì bạn phải dùng một công cụ sẵn có nào đó để hoàn thành nhiệm vụ của bạn. Trong trường hợp này, bạn không học lập trình, bạn đang học cách hoàn thành nhiệm vụ.
  • Trong 3 ngày: Thật đáng tiếc, 3 ngày không đủ cho bạn, như sẽ thảo luận chi tiết hơn ở phần tiếp theo.

Tự học lập trình trong 10 năm
Một số nhà nghiên cứu (Hayes, Bloom) cho rằng phải mất khoảng 10 năm để đạt đến mức tinh thông về một lĩnh vực nào đó – bao gồm đánh cờ, soạn nhạc, vẽ tranh, chơi đàn piano, bơi, tennis… Hình như không có trường hợp ngoại lệ nào: ngay cả Mozart, người được xem là thần đồng âm nhạc vào năm lên 4, cũng phải mất đến 13 năm nữa mới sáng tác ra những kiệt tác âm nhạc. Trong một thể loại khác, ban nhạc The Beatles dường như đã từ chỗ vô danh leo thẳng lên vị trí số 1 bằng một loạt các #1 hit, và xuất hiện trong sô của Ed Sullivan năm 1964. Thực ra, The Beatles đã bắt đầu chơi trong các quán bar nhỏ ở Liverpool và Hamburg từ năm 1957, mặc dù họ đã tạo được sự thu hút từ rất sớm, nhưng thành công lớn thực sự của The Beatles – Sgt. Peppers – ra đời năm 1967. Sammuel Johnson cho rằng để đạt được sự hoàn hảo phải mất hơn 10 năm: “Sự hoàn hảo trong bất cứ một lĩnh vực nào chỉ có thể đạt được bằng cách làm việc cật lực trong suốt cuộc đời; nó không thể mua được với một cái giá rẻ hơn”. Còn Chaucer thì than phiền “Đời thì quá ngắn, mà thời gian học nghề thì quá lâu”
Còn đây là công thức của tôi để thành công trong lập trình:
  • Tìm thấy hứng thú trong việc lập trình và đảm bảo bạn còn đủ hứng thú để đeo đuổi công việc này trong 10 năm.
  • Trao đổi với các lập trình viên khác. Đọc những chương trình do người khác viết. Điều này quan trọng hơn bất cứ cuốn sách học khóa huấn luyện nào.
  • Lập trình. Cách học tốt nhất là vừa học vừa làm. Nói theo cách của giới chuyên môn thì “năng suất tối đa của mỗi người không đơn thuần chỉ do kinh nghiệm, năng suất tăng lên - ngay cả đối với người giàu kinh nghiệm - là kết quả của những cố gắng để hoàn thiện.” và “cách học có hiệu quả nhất cần có: mục đích được xác định rõ ràng với độ khó thích hợp, ý kiến đóng góp phê bình, cơ hội làm lại và sửa sai.” Cuốn Cognition in Practice: Mind, Mathematics, and Culture in Everyday Life là một tài liệu tham khảo thú vị cho quan điểm này.
  • Nếu bạn muốn hãy theo học đại học (hoặc cao học). Như vậy, bạn sẽ thêm cơ hội để làm những công việc đòi hỏi bằng cấp, và cũng giúp bạn có một hiểu biết sâu sắc hơn, nhưng nếu bạn không thích môi trường trường học, bạn vẫn có thể (với một chút cố gắng nhất định) thu được kiến thức tương tự từ công việc. Trong mọi trường hợp, học từ sách sẽ không đủ. Eric Raymond, tác giả cuốn The New Hacker’s Dictionary nói: “Các chương trình giảng dạy về máy tính không thể biến bạn thành một chuyên gia máy tính, cũng giống như nghiên cứu chổi vẽ và bột màu cũng chẳng biến bạn thành một họa sĩ”. Một trong số những lập trình viên xuất sắc nhất mà tôi đã từng mướn chỉ học hết trung học; anh ta đã viết rất nhiều phần mềm tuyệt vời; có news group của riêng anh; và kiếm được đủ tiền từ stock options để mua cho mình một nightclub.
  • Làm việc chung với các lập trình viên khác. Hãy là lập trình viên giỏi nhất trong một số projects; là người kém nhất trong vài projects khác. Khi bạn là người giỏi nhất, bạn kiểm tra khả năng lãnh đạo của mình, và truyền cảm hứng cho người khác từ sư sắc sảo và tầm nhìn của mình. Khi bạn là người kém nhất, bạn học từ những việc mà người giỏi làm, và từ những việc họ không thích làm (vì họ sẽ bắt bạn làm thay họ)
  • Làm việc với các projects sau những lập trình viên khác. Cố gắng hiểu các chương trình viết bởi người khác. Để ý xem cần phải có những gì để hiểu và sửa nó lúc không có tác giả của chương trình. Thiết kế chương trình của bạn để những người làm việc sau bạn có thể bảo trì, sửa chữa nó một cách dễ dàng.
  • Học ít nhất là nửa tá ngôn ngữ lập trình. Bao gồm ít nhất là một ngôn ngữ hỗ trợ class abstractions (như Java hay C++), một ngôn ngữ hỗ trợ functional abstraction (như LISP hay ML), môt ngôn ngữ hỗ trợ syntactic abstraction (như LISP), môt ngôn ngữ hỗ trợ declarative specifications (như Prolog hay C++ template), một ngôn ngữ hỗ trợ coroutines (như Icon hay Scheme) và một ngôn ngữ hỗ trợ parallelism (như Sisal).
  • Nhớ rằng có chữ “computer” trong “computer science”. Bạn cần biết thời gian máy tính của bạn thực hiện một lệnh là bao lâu; thời gian để máy tính tìm một word trong bộ nhớ (khi word đó có và không có trong cache); thời gian đọc các word kế tiếp nhau từ đĩa cứng; và thời gian tìm đến một vị trí mới trong đĩa cứng.
  • Tham gia vào các cố gắng để tiêu chuẩn hóa ngôn ngữ lập trình. Đó có thể là cuộc họp của hội đồng ANSI C++, nhưng cũng có thể chỉ là quyết định xem nhóm lập trình của bạn sẽ dùng 2 hay 4 khoảng trắng ở đầu mỗi dòng. Bằng cách nào thì bạn cũng học được mọi người thích gì ở một ngôn ngũ lập trình, mức độ, và có lẽ hiểu được một chút tại sao họ lại cảm thấy như vậy.
  • Hoàn tất việc tiêu chuẩn hóa ngôn ngữ càng nhanh càng tốt.

Nếu theo công thức này, bạn thử tính xem bạn có thể học được bao nhiêu chỉ từ sách vở. Trước khi đứa con đầu lòng của tôi ra đời, tôi đã nghiền ngẫm tất cả các cuốn sách Làm cách nào để … mà vẫn cảm thấy mình vẫn chẳng biết gì. 30 tháng sau, khi đứa con thứ 2 của tôi chào đời, tôi có đọc lại những cuốn sách đó không? Không. Thay vào đó tôi dựa theo kinh nghiệm của mình, mà hóa ra là những kinh nghiệm này lại có ích và làm cho tôi yên tâm hơn hàng ngàn trang sách viết bởi các chuyên gia.
Fred Brooks, trong bài No Silver Bullets đã đưa ra một kế hoạch gồm 3 giai đoạn để tìm một người thiết kế phần mềm xuất sắc:
  • Tìm kiếm có một cách có hệ thống những người thiết kế phần mềm giỏi nhất càng sớm càng tốt.
  • Phân công các cố vấn nghề nghiệp để giúp đỡ họ thăng tiến trong công việc và theo dõi hồ sơ nghề nghiệp của họ.
  • Tạo cơ hộ để họ trao đổi và cạnh tranh lẫn nhau

Kế hoạch này dựa trên giả thiết là đã tìm được những người đã có sẵn các đức tính để trở thành một người thiết kế phần mềm xuất sắc, và công việc chỉ là tạo điều kiện để họ phát triển. Alan Perlis nói một cách ngắn gọn hơn: “Nếu tất cả mọi người đều có thể học được điêu khắc thì có lẽ Michelangelo đã được dạy để làm ngược lại. Với các lập trình viên xuất sắc cũng vậy.”
Bạn cứ mua cuốn sách Java đó đi, có lẽ bạn sẽ thấy nó có ích. Nhưng, là một lập trình viên, bạn sẽ không thay đổi cuộc đời bạn, hay trình độ của bạn trong 24 giờ, 24 ngày, thậm chí 24 tháng.

Phụ lục: Lựa chọn ngôn ngữ lập trình
Rất nhiều người hỏi tôi: nên bắt đầu từ ngôn ngữ lập trình nào. Không có một câu trả lời chung, nhưng bạn hãy cân nhắc những điểm sau:
Dùng ngôn ngữ lập trình mà bạn bè dùng: Khi người ta hỏi tôi: “Tôi nên dùng hệ điều hành nào, Windows, Unix, hay Mac?” câu trả lời của thôi thường là “Hãy dùng hệ điều hành mà bạn bè của bạn dùng”. Những gì bạn học từ bạn bè quá đủ đề bù đắp những điểm khác nhau giữa các hệ điều hành, hoặc giữa các ngôn ngữ lập trình. Cũng nên xem xét đến những bạn bè tương lai của bạn: cộng đồng lập trình viên mà bạn sẽ là tham gia nếu bạn tiếp tục. Ngôn ngữ mà bạn chọn có một cộng đồng đang phát triển hay đang lụi tàn? Có sách, có website, có diễn đàn để tìm kiếm câu trả lời cho những vấn đề mà bạn gặp phải hay không? Bạn có thích mọi người trên các diễn đàn đó hay không?
Đơn giản. Những ngôn ngữ lập trình như C++Java được thiết kế cho những nhóm làm việc gồm nhiều lập trình viên giàu kinh nghiệm và mối quan tâm của họ là hiệu quả của chương trình. Hậu quả là chúng đã bị phức tạp hóa đề phục vụ cho mục đích này. Mối quan tâm chính của bạn là học lập trình, bạn chưa cần đến sự phức tạp đó. Bạn muốn một ngôn ngũ được thiết kế thật đơn giản, dễ học, dễ nhớ cho một người mới bắt đầu.
Chơi. Nếu bạn muốn học chơi piano, bạn sẽ học chơi theo cách nào: (1)bạn muốn nghe thấy note nhạc ngay sau khi bạn gõ vào phím đàn (interactive mode) hay (2) bạn muốn nghe lại toàn bộ các notes sau khi bạn hoàn tất bản nhạc (batch mode). Rõ ràng cách 1 giúp bạn học piano dễ dàng hơn, và học lập trình cũng vậy. Tìm một ngôn ngữ lập trình có hỗ trợ interactive mode và dùng nó.
Dựa trên những tiêu chuẩn trên, ngôn ngũ lập trình cho người mới bắt đầu mà tôi giới thiệu là Python hay Scheme. Nhưng tùy vào hoàn cảnh của bạn, còn có nhiều chọn lựa tốt khác. Với các lập trình viên tương lai còn đang trong lứa tuổi nhi đồng, thì Alice hoặc Squeak có lẽ thích hợp nhất. Điều quan trọng là bạn chọn nó và hãy bắt đầu.
Source here
http://my.opera.com/lechuck60/blog/tu-hoc-lap-trinh-trong-10-nam-peter-norvig

Sunday, November 29, 2009

AOP: Aspect Oriented Programming

OOP (Object-oriented Programming) lập trình hướng đối tượng hiện là mô hình phát triển được lựa chọn cho hầu hết các dự án phần mềm. OOP rất hữu hiệu trong việc lập mô hình hành vi chung của các đối tượng tuy nhiên nó không giải quyết thỏa đáng những hành vi liên quan đến nhiều đối tượng. AOP giải quyết được vấn đề này, và rất có thể sẽ là bước phát triển lớn kế tiếp trong phương pháp lập trình.

Vào những ngày đầu của ngành khoa học máy tính, các thảo chương viên lập trình trực tiếp bằng mã máy. Những nhà phát triển phần mềm thời đó đã phải tốn nhiều thời gian suy nghĩ về tập lệnh riêng của từng phần cứng máy tính cụ thể hơn là tập trung để giải quyết các yêu cầu của bài toán đặt ra. Dần dần, người ta chuyển sang các ngôn ngữ lập trình cấp cao hơn, cho phép khái quát hoá ở mức độ nào đó mã máy chạy bên dưới. Rồi kế đến là các ngôn ngữ lập trình có cấu trúc cho phép phân tích bài toán thành các thủ tục thực hiện những tác vụ cần thiết. Tuy nhiên, khi độ phức tạp của các bài toán tăng lên, chúng ta cần có những kỹ thuật tốt hơn. OOP cho phép chúng ta xem hệ thống như là một tập các đối tượng cộng tác. Lớp (class) cho phép chúng ta ẩn đi những chi tiết thực hiện bên dưới các giao tiếp (interface). Cơ chế đa hình (polymorphism) cung cấp giao tiếp và hành vi chung cho những khái niệm liên quan.

Hình 1a

Kỹ thuật OOP rất xuất sắc trong việc đóng gói các hành vi và chủ thể (lớp), miễn là chúng hoàn toàn riêng biệt. Tuy nhiên, các bài toán thực tế thường có những hành vi đan nhau liên quan đến nhiều lớp. Theo truyền thống, hầu hết ngôn ngữ lập trình hướng đối tượng như C++ và Java đều không hỗ trợ đóng gói những hành vi đan nhau, dẫn đến mã chương trình có thể nằm lẫn lộn, rải rác và khó quản lý.

AOP (Aspect-oriented Programming) là kỹ thuật lập trình mới cho phép đóng gói những hành vi có liên quan đến nhiều lớp. Nhóm từ AOP mới mẻ này có thể làm cho bạn cảm thấy bối rối. Bạn vẫn chưa kịp nắm vững OOP giờ lại xuất hiện thêm ‘mốt’ lập trình mới? Bạn đừng lo, OOP vẫn tồn tại. AOP có thể xem là một sự bổ sung cho OOP, cho phép chúng ta giải quyết các bài toán phức tạp tốt hơn và hiệu quả hơn.

Vấn đề đan nhau

Hệ thống phần mềm có thể xem như là một thể hiện kết hợp nhiều vấn đề. Một hệ thống tiêu biểu có thể gồm nhiều dạng vấn đề như xử lý nghiệp vụ, hiệu suất, bảo toàn dữ liệu, bảo mật, bẫy lỗi… và còn có những vấn đề của quá trình phát triển hệ thống như tính dễ quản lý, dễ bảo trì và phát triển. Hình 1a và 1b minh họa hệ thống với tập các vấn đề đan nhau.

Để hiểu rõ hơn về vấn đề đan nhau, chúng ta hãy xét một ví dụ đơn giản nhưng khá phổ biến: cập nhật dữ liệu. Giả thiết mục tiêu quan trọng của hệ thống là nó phải dễ phát triển, bảo trì và đảm bảo an toàn dữ liệu. Mục tiêu đầu đặt ra từ yêu cầu hệ thống phải phát triển nhanh và không có lỗi, mục tiêu thứ hai do tính chất quan trọng của dữ liệu.

Mục tiêu đầu có thể đạt được khá dễ dàng. Có thể dùng lập trình thủ tục theo kiểu cũ để thực hiện hệ thống mạch lạc, ngắn gọn và đáp ứng tốt yêu cầu bài toán. Các tác vụ cơ bản của mô đun cập nhật như sau:

  • Nạp dữ liệu.
  • Cập nhật dữ liệu.
  • Lưu dữ liệu.

Tuy nhiên việc thực hiện đơn giản này không giải quyết mục tiêu thứ hai. Để đáp ứng cho mục tiêu thứ hai, chúng ta phải thêm các tác vụ xử lý vấn đề an toàn dữ liệu.

  • Xác thực quyền cập nhật dữ liệu.
  • Khóa dữ liệu để đảm bảo sự an toàn dữ liệu, đề phòng người khác truy cập trong khi đang thực hiện cập nhật.
  • Nạp dữ liệu.
  • Cập nhật dữ liệu.
  • Kiểm tra tính toàn vẹn dữ liệu cập nhật
  • Ghi nhận lại các thao tác cập nhật.
  • Lưu dữ liệu
  • Mở khoá dữ liệu

So với ban đầu thì mô đun cập nhật giờ đây khá phức tạp với nhiều vấn đề liên quan đến những lớp xử lý khác như xác thực, khoá/mở khoá dữ liệu. Những mô đun xử lý khác như thêm mới hay xóa dữ liệu cũng có những vấn đề tương tự.

Vấn đề đan nhau làm cho các mô đun phải xử lý đồng thời nhiều yêu cầu; lập trình viên ngoài việc suy nghĩ về vấn đề xử lý cơ bản còn phải tính đến vấn đề hiệu suất, bảo mật, an toàn dữ liệu… và sự trộn lẫn nhiều đoạn mã xử lý những yêu cầu khác nhau. Một vấn đề nữa đó là tình trạng rải rác mã, do vấn đề đan nhau có ở nhiều mô đun nên các đoạn mã xử lý liên quan cũng xuất hiện ở nhiều mô đun. Những vấn đề này ảnh hưởng đến việc thiết kế và phát triển phần mềm, như hệ thống khó xây dựng, quản lý, phát triển và tính tái sử dụng kém…

Cơ bản về AOP

Hình 1b

Vấn đề cốt lõi của AOP là cho phép chúng ta thực hiện các vấn đề riêng biệt một cách linh hoạt và kết hợp chúng lại để tạo nên hệ thống sau cùng. AOP bổ sung cho kỹ thuật lập trình hướng đối tượng bằng việc hỗ trợ một dạng mô đun khác, cho phép kéo thể hiện chung của vấn đề đan nhau vào một khối. Khối này được gọi là ‘aspect’ (tạm dịch là ‘lát’ – hàm ý lát cắt đi qua nhiều lớp đối tượng), từ chữ ‘aspect’ này chúng ta có tên của phương pháp phát triển phần mềm mới: aspect-oriented programming. Nhờ mã được tách riêng, vấn đề đan nhau trở nên dễ kiểm soát hơn. Các aspect của hệ thống có thể thay đổi, thêm hoặc xóa lúc biên dịch và có thể tái sử dụng. Một dạng biên dịch đặc biệt có tên là Aspect Weaver thực hiện kết hợp các thành phần riêng lẻ lại thành hệ thống hợp nhất.

AOP gồm ba bước phát triển (hình 2a và 2b):

  1. Phân tích các yêu cầu để xác định vấn đề chung và vấn đề đan nhau.
  2. Xây dựng thể hiện từng vấn đề riêng biệt.
  3. Tổng hợp các thể hiện.

Giống như các phương pháp lập trình khác, việc thực hiện AOP gồm hai phần: đặc tả ngôn ngữ mô tả cú pháp và cấu trúc ngôn ngữ; biên dịch kiểm tra sự đúng đắn của mã lệnh theo đặc tả ngôn ngữ và chuyển đổi sang dạng mã máy có thể thực thi.

* Đặc tả ngôn ngữ AOP: Xét ở mức cao, ngôn ngữ AOP gồm 2 thành phần.

  • Thể hiện vấn đề: Ánh xạ yêu cầu riêng lẻ ở dạng mã để trình dịch có thể chuyển đổi sang dạng thực thi. Thể hiện vấn đề có dạng thủ tục cụ thể, bạn có thể dùng các ngôn ngữ truyền thống như C, C++ hay Java.
  • Đặc tả quy tắc đan kết: Xác định cách thức tổng hợp các vấn đề riêng lẽ. Muốn vậy, thể hiện cần sử dụng hoặc tạo một ngôn ngữ để xác định các quy tắc tổng hợp các thể hiện khác nhau. Ngôn ngữ xác định quy tắc đan kết có thể là sự mở rộng của ngôn ngữ thể hiện, hoặc một ngôn ngữ hoàn toàn khác.

* Biên dịch AOP: Trình biên dịch AOP thực hiện theo trình tự hai bước sau:

  1. Kết hợp các hành vi riêng lẻ
  2. Chuyển đổi thông tin kết quả sang dạng mã thực thi
Hình 2a

Việc biên dịch ứng dụng AOP có thể thực hiện theo nhiều cách, trong đó có cách dịch mã sang mã. Theo cách này, mã nguồn được tiền xử lý các aspect riêng lẻ để tạo ra mã đã được đan kết với nhau. Kế tiếp trình biên dịch AOP đưa mã đã được chuyển đổi này sang trình biên dịch ngôn ngữ cơ sở để tạo dạng mã thực thi cuối cùng. Ví dụ, theo cách thức này, các aspect riêng lẻ của ứng dụng AOP dùng ngôn ngữ Java trước tiên sẽ được đan kết ở dạng mã nguồn Java, kế tiếp trình biên dịch Java sẽ chuyển mã nguồn này sang dạng mã thực thi (byte code). Tương tự, cũng có thể thực hiện đan kết ở giai đoạn mã byte code, byte code vẫn là một dạng mã nguồn, nếu như hệ thống thực thi chế độ nền – máy ảo java – hỗ trợ aspect. Với cách này, đầu tiên máy ảo Java sẽ nạp các quy tắc đan kết, rồi áp dụng các quy tắc này cho các lớp được nạp sau đó. Nói cách khác, trình biên dịch thực hiện việc đan kết các aspect trong khi thực thi.

Hình 2b

AOP tổng hợp hệ thống đi từ các vấn đề đan nhau đến vấn đề chính, còn OOP đi theo hướng ngược lại. Tuy nhiên, OOP và AOP không phủ định nhau mà bổ sung cho nhau. AOP có thể dùng các kỹ thuật lập trình khác làm cơ sở, và vẫn giữ được những ưu điểm của hệ thống cơ sở.

Lợi ích của AOP

AOP là một kỹ thuật mới đầy triển vọng, hứa hẹn đem lại nhiều lợi ích cho việc phát triển phần mềm. Dưới đây là một số lợi ích cụ thể:

  • Mô đun hoá những vấn đề đan nhau: AOP xác định các vấn đề một cách tách biệt hạn chế tối thiểu việc nhập nhằng mã, cho phép mô đun hoá cả vấn đề liên quan đến nhiều lớp đối tượng.
  • Dễ dàng phát triển hệ thống: Việc thêm chức năng mới có thể thực hiện dễ dàng bằng cách tạo aspect mới mà không cần quan tâm đến vấn đề đan nhau. Khi thêm các mô đun mới vào hệ thống, các aspect hiện có sẽ đan kết với chúng và tạo nên sự phát triển chặt chẽ.
  • Cho phép để lại quyết định thiết kế tương lai: Một thiết kế tốt phải tính đến cả yêu cầu hiện tại và tương lai, việc xác định yêu cầu tương lai là một công việc khó khăn. Nếu bỏ sót những yêu cầu tương lai có thể bạn sẽ phải thay đổi hay thực hiện lại nhiều phần hệ thống. Với AOP, người thiết kế hệ thống có thể để lại các quyết định thiết kế cho những yêu cầu tương lai nhờ thực hiện theo các aspect riêng biệt.
  • Tái sử dụng mã tốt hơn: Các aspect là những mô đun riêng biệt, được kết hợp linh động – đây chính là yếu tố quan trọng để tái sử dụng mã. AOP cho phép tái sử dụng mã tốt hơn OOP.

Lời kết

AOP là một ý tưởng mới, vẫn còn cần thời gian để đánh giá, tìm hiểu mối quan hệ các kỹ thuật hiện có và để phát triển, ứng dụng rộng rãi.

Cần nhấn mạnh điều quan trọng là AOP xây dựng trên các công nghệ hiện hữu. Nó chỉ làm việc với thiết kế tốt. Để có thiết kế tốt, bạn cần phải áp dụng đúng kỹ thuật cho đúng vấn đề, có thể là AOP, OOP, phân tích theo chức năng, lập trình cấu trúc…

Giờ thì bạn đã biết về sự tồn tại của AOP. Hãy tìm đọc, nghiên cứu và áp dụng nó. Hiện có rất nhiều thông tin về AOP trên internet. Bạn có thể bắt đầu với website Aspect Oriented Software Development (aosd.net).


theo PC World VN