Laravel Design Patterns Series: Factory Pattern - Part 2

Factory Pattern

Tiếp tục chuỗi bài viết về Design Pattern trong Laravel, trước hết tôi xin liệt kê lại những chủ đề trong chuỗi bài viết này:

  1. Builder (Manager) Pattern - Part 1
  2. Factory Pattern - Part 2 (current)
  3. Repository Pattern - Part 3
  4. Strategy Pattern - Part 4
  5. Provider Pattern - Part 5
  6. Facade Pattern - Part 6

Ngày hôm nay tôi xin giới thiệu với các bạn về Factory Pattern.

Tản mạn về định nghĩa

In object-oriented programming (OOP), a factory is an object for creating other objects – formally a factory is a function or method that returns objects of a varying prototype or class[1] from some method call, which is assumed to be "new".

(Nguồn: Factory OOP (Wikipedia))

Riêng tên Pattern đã nói lên tất cả, Factory ở đây có thể hiểu nôm na là nhà máy. Theo định nghĩa từ Wikipedia, Factory chính là một đối tượng dùng để khởi tạo các đối tượng khác, thường thì factory sẽ là một hàm hay một phương thức trả về các đối tượng của một vài class khác nhau.

Factory Pattern thuộc vào nhóm khởi tạo (Creational), bao gồm các loại pattern:

  1. Simple Factory: không được liệt vào dạng design pattern, nó chỉ được coi như một kỹ thuật dùng để đóng gói quá trình khởi tạo đối tượng.
  2. Factory Method: định nghĩa ra một giao diện (interface) để khởi tạo đối tượng, tuy nhiên việc khởi tạo đối tượng từ class nào lại được thực thi từ class triển khai giao diện đó.
  3. Abstract Factory: Cung cấp một giao diện dùng để khởi tạo một tập hợp các đối tượng có liên quan hoặc phụ thuộc với nhau mà không chỉ ra có những lớp cụ thể nào ở thời điểm thiết kế.

Như vậy tôi đã giới thiệu xong với các bạn về định nghĩa cũng như phân loại trong Factory Pattern. Đúng là lý thuyết vẫn mãi là lý thuyết (yaoming), vẫn luôn hàn lâm, nhiều thuật ngữ và khó có thể hiểu ngay được. Chính vì vậy, trong bài viết này tôi muốn có một cách tiếp cận khác giúp bạn dễ hiểu hơn về pattern này thông qua ví dụ thực tế (thay vì việc đưa ra định nghĩa, đưa ra sơ đồ, đưa ra triển khai, ... blah blah).

Vấn đề 1

Tôi và các bạn hãy cùng thử đóng vai trò là ông chủ trong bài viết này. Chúng ta sẽ thành lập một công ty phần mềm (lấy đại tên là F.C.U.K đi, trụ sở chính ở Hà Nội luôn cho máu), ngoài việc làm outsource để kiếm tiền nuôi công ty, chúng ta sẽ có một bộ phận Training những bạn sinh viên mới ra trường chẳng hạn để đưa vào các dự án thực tế. Và chúng ta sẽ lần lượt đi qua từng khó khăn để đưa ra một bộ máy hoạt động ổn nhất.

OK! Mọi việc đã rõ ràng và bây giờ chúng ta tập trung vào việc xây dựng bộ máy Training những bước đầu tiên. Trước mắt chúng ta sẽ đào tạo để thu được các Developer về mảng PHPRuby.

Chúng ta sẽ thiết kế một class DevelopersFactory (đây chính là bộ phận Training) và cung cấp một phương thức produceDeveloper():

class DevelopersFactory {
    public function produceDeveloper($type) {
        switch ($type) {
            case 'Php':
                $developer = new PhpDeveloper();
                break;
            case 'Ruby':
                $developer = new RubyDeveloper();
                break;

            default:
                $developer = null;
                break;
        }

        $developer->training();
        $developer->deliver();

        return $developer;
    }
}

Như đã nói ở trên, ta cần "sản xuất" ra các ông Developer nên tôi đặt tên hàm là produceDeveloper() (okay), nhận đầu vào là tham số $type (có thể hiểu là mong muốn của các bạn sinh viên mới ra trường muốn dev Php hay Ruby).

Công việc cần làm là khởi tạo một ông $developer, sau đó huấn luyện (training()), và cuối cùng là gán vào các dự án thực tế (deliver()). Việc huấn luyện và bàn giao thế nào tạm cho qua ở thời điểm này.

Sau một thời gian chạy quy trình này mọi thứ êm xuôi, các ông Developer được huấn luyện xong vào dự án "cốt" ngon lành. Bài toán đặt ra với chúng ta là mở rộng thêm ngôn ngữ để training, ví dụ: Android. Việc này quá đơn giản, thêm 1 case nữa vào trong switch ... case là giải quyết vấn đề:

class DevelopersFactory {
    public function produceDeveloper($type) {
        switch ($type) {
            case 'Php':
                $developer = new PhpDeveloper();
                break;
            case 'Ruby':
                $developer = new RubyDeveloper();
                break;
            case 'Android':
                $developer = new AndroidDeveloper();
                break;

            default:
                $developer = null;
                break;
        }

        $developer->training();
        $developer->deliver();

        return $developer;
    }
}

Mọi việc tưởng chừng như dễ dàng nhưng lại có vấn đề ở đây, ta vừa thực hiện sửa trực tiếp vào hàm produceDeveloper() mỗi khi có thêm nội dung mới cần training. Công việc cần làm nặng nề hơn (ví dụ: thêm giáo án mới cho nội dung mới cần training). Đồng thời việc làm này cũng vị phạm nguyên tắc open/close:

software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

Đây chính là lúc cần áp dụng kỹ thuật Simple Factory để giải quyết vấn đề.

Giải pháp: Simple Factory

Công việc của chúng ta đang bị dồn hết vào một bộ phận (chính là hàm produceDeveloper()). Các đối tượng được khởi tạo ngay trong bộ máy chính của chúng ta (DevelopersFactory).

Đã đến lúc tạo một class mới để đóng gói quá trình khởi tạo các đối tượng, ta gọi class đó là SimpleFactory. Bạn có thể hiểu là chúng ta tạo thêm một phòng ban mới hỗ trợ Training, chuyên làm công việc thu nhận hồ sơ, tâm sự ứng viên, định hướng, đưa ra plan và các yêu cầu cần training, ...

class SimpleFactory {
    public function createDeveloper($type) {
        switch ($type) {
            case 'Php':
                $developer = new PhpDeveloper();
                break;
            case 'Ruby':
                $developer = new RubyDeveloper();
                break;
            case 'Android':
                $developer = new AndroidDeveloper();
                break;

            default:
                $developer = null;
                break;
        }

        return $developer;
    }
}

Sau đó ta chỉnh sửa lại quy trình ở bộ phận chính:

class DevelopersFactory {
    public $simpleFactory;

    public function __construct() {
        $this->simpleFactory = new SimpleFactory();
    }

    public function produceDeveloper($type) {
        $developer = $this->simpleFactory->createDeveloper($type);

        $developer->training();
        $developer->deliver();

        return $developer;
    }
}

Vậy là nhờ có sự trợ giúp của phòng mới thêm (SimpleFactory), bộ phận chính của chúng ta (DevelopersFactory) không phải lo việc nói chuyện với ứng viên nữa mà chỉ nhận đầu vào từ phòng mới và training theo đúng plan và đúng ngôn ngữ ứng viên mong muốn hoặc được định hướng.

Thử demo với đoạn code sau:

$developersFactory = new DevelopersFactory();
$developersFactory->produceDeveloper('Php');

Kết quả:

Php Developer is trained.
Php Developer is delivered with 1000$.

Vấn đề 2

Tôi xin dành chút thời gian để giải thích kỹ hơn về các class PhpDeveloper, RubyDeveloper cũng như AndroidDeveloper. Thực ra thì chúng đều được kế thừa từ một lớp trừu tượng (abstract) Developer được định nghĩa sẵn các phương thức training()deliver(). Hãy cùng xem thiết kế của lớp Developer:

abstract class Developer {
    public $type = '';
    public $price = 0;

    public function training() {
        echo $this->type . ' Developer is trained.<br/>';
    }

    public function deliver() {
        echo $this->type . ' Developer is delivered with ' . $this->price . '$.<br/>';
    }
}

Lớp PhpDeveloper, RubyDeveloperAndroidDeveloper hết sức đơn giản:

class PhpDeveloper extends Developer {
    public $type = 'Php';
    public $price = 1000;
}

class RubyDeveloper extends Developer {
    public $type = 'Ruby';
    public $price = 500;
}

class AndroidDeveloper extends Developer {
    public $type = 'Android';
    public $price = 150;
}

Lại tiếp tục câu chuyện tưởng tượng thì cứ coi như mỗi ông Developer sau khi được training sẽ ra ngoài outsource cho công ty với một giá nhất định được công ty trả.

Mọi thứ tạm thời lại tiếp tục ổn về mặt quy trình. Nhưng chúng ta không muốn dừng lại ở đây, chúng ta cần mở rộng quy mô và quyết định thành lập thêm hai chi nhánh ở TP Hồ Chí MinhĐà Nẵng (honho). Trong tay ta đã có sẵn flow chạy ngon lành từ Hà Nội rồi nên việc làm hết sức đơn giản là setup và bê nguyên quy trình sang bên hai chi nhánh mới. Cụ thể việc triển khai:

Với chi nhánh HCM:

class HcmSimpleFactory {
    public function createDeveloper($type) {
        switch ($type) {
            case 'Php':
                $developer = new HcmPhpDeveloper();
                break;
            case 'Ruby':
                $developer = new HcmRubyDeveloper();
                break;
            case 'Android':
                $developer = new HcmAndroidDeveloper();
                break;

            default:
                $developer = null;
                break;
        }

        return $developer;
    }
}

class HcmDevelopersFactory {
    public $simpleFactory;

    public function __construct() {
        $this->simpleFactory = new HcmSimpleFactory();
    }

    public function produceDeveloper($type) {
        $developer = $this->simpleFactory->createDeveloper($type);

        $developer->training();
        $developer->deliver();

        return $developer;
    }
}

Sau một khoảng thời gian chạy thì phát sinh vấn đề về luồng hoạt động. Chi nhánh HCM thì bỏ hàm training() đưa thẳng vào dự án, chi nhánh Đà Nẵng thì bỏ hàm deliver() chỉ training xong để đấy. Hàm produceDeveloper() bị ảnh hưởng nặng nề và không theo quy chuẩn của chi nhánh Hà Nội. Rõ ràng nếu là chủ chúng ta không muốn điều này nên cần có biện pháp để ép quy trình về một flow chuẩn.

Giải pháp: Factory Method

Giải pháp đưa ra hết sức đơn giản, hàm produceDeveloper() không được phép thay đổi vì ảnh hưởng quy trình nên ta sẽ để hàm này cố định trong một Abstract Class DevelopersFactory, hàm này sử dụng kết quả của một abstract method createDeveloper() (trả về đối tượng developer cụ thể nào đó). Bằng cách này hàm createDeveloper() giúp chúng ta đóng gói quá trình khởi tạo đối tượng, và bởi vì đây là abstract method nên quá trình khởi tạo thế nào sẽ được thực thi ở lớp con kế thừa.

Có vẻ giải pháp khá hợp lý và ta sẽ triển khai như sau. Với quy trình chuẩn, lớp trừu tượng DevelopersFactory được thiết kế như sau:

abstract class DevelopersFactory {
    public function produceDeveloper($type) {
        $developer = $this->createDeveloper($type);

        $developer->training();
        $developer->deliver();

        return $developer;
    }

    abstract public function createDeveloper($type);
}

Với chi nhánh HCM và Đà Nẵng, chúng ta cần triển khai phương thức trừu tượng createDeveloper():

class HcmDevelopersFactory extends DevelopersFactory {
    public function createDeveloper($type) {
        switch ($type) {
            case 'HcmPhp':
                $developer = new HcmPhpDeveloper();
                break;
            case 'HcmRuby':
                $developer = new HcmRubyDeveloper();
                break;
            case 'HcmAndroid':
                $developer = new HcmAndroidDeveloper();
                break;

            default:
                $developer = null;
                break;
        }

        return $developer;
    }
}
class DnDevelopersFactory extends DevelopersFactory {
    public function createDeveloper($type) {
        switch ($type) {
            case 'DnPhp':
                $developer = new DnPhpDeveloper();
                break;
            case 'DnRuby':
                $developer = new DnRubyDeveloper();
                break;
            case 'DnAndroid':
                $developer = new DnAndroidDeveloper();
                break;

            default:
                $developer = null;
                break;
        }

        return $developer;
    }
}

Vấn đề đã được giải quyết. Trong đoạn code trên ta có thể nói phương thức createDeveloper() trong lớp trừu tượng DevelopersFactory được gọi là Factory Method.

Thử demo với đoạn code sau:

$test = new HcmDevelopersFactory();
$test->produceDeveloper('HcmPhp');

Kết quả:

HCM Php Developer is trained.
HCM Php Developer is delivered with 950$.

Đúng như định nghĩa Factory Method pattern định nghĩa ra một giao diện (createDeveloper()) để khởi tạo đối tượng, tuy nhiên việc khởi tạo đối tượng thực sự như thế nào lại được triển khai ở các lớp con (HcmDevelopersFactory, DnDevelopersFactory).

Ưu điểm khi sử dụng Factory Method pattern

  • Giúp cho code của chúng ta tuân thủ nguyên tắc DRY. Dù cho việc khởi tạo đối tượng phụ thuộc vào nhiều class tuy nhiên chúng ta cũng chỉ có một hàm để khởi tạo, việc thêm một số config khi khởi tạo cũng không làm code thay đổi quá nhiều chỗ.
  • Tránh sự phân tán tư tưởng khi code. Cụ thể từ ví dụ của chúng ta, ở lớp DevelopersFactory và hàm produceDeveloper(), ta không cần quan tâm xem ông Developer là PHP, Ruby hay Android. Ta chỉ cần biết là có 1 ông Developer mới, sau đó được training và đưa vào dự án cụ thể.

Vấn đề 3

Tiếp theo ta cùng đi sâu hơn vào quy trình training (cụ thể là phương thức training() trong Developer class). Để đảm bảo có một đầu ra tốt, chúng ta phải thắt chặt quy trình, giả sử để training một new dev đều phải qua một khóa training chung về Git, basic, advanced, framework, ...

Abstract Factory Pattern

Đầu tiên, chúng ta phải thiết kế một Abstract Factory class - khung quy trình chung cho việc training. Ta có thể hiểu rằng Abstract Factory bao gồm nhiều Factory Method. Cụ thể ta sẽ thiết kế một lớp abstract TrainingComponentsFactory:

class TrainingComponentsFactory {
    abstract function trainingGit();
    abstract function trainingBasic();
    abstract function trainingFramework();
}

Triển khai quy trình chung với chi nhánh HCM:

class HcmTrainingComponentsFactory extends TrainingComponentsFactory {
    public function trainingGit() {
        return new HcmGitRequirement();
    }

    public function trainingBasic() {
        return new HcmBasicProject();
    }

    public function trainingFramework() {
        return new HcmFrameworkProject();
    }
}

Điều này dẫn tới lớp abstract Developer của chúng ta cũng phải thay đổi dựa trên yêu cầu training, cụ thể ta cần thêm các thuộc tính về git, basic, framework và phương thức training sẽ được chuyển về dạng abstract để các Developer từng mảng có những triển khai đặc biệt riêng.

Với lớp Developer ta thay đổi như sau:

abstract class Developer {
    public $type = '';
    public $price = 0;
    public $git = null;
    public $basic = null;
    public $framework = null;

    abstract function training();

    public function deliver() {
        echo $this->type . ' Developer is delivered with ' . $this->price . '$.<br/>';
    }
}

Đồng thời ta cũng phải thay đổi các lớp PhpDeveloperRuby Developer:

class PhpDeveloper extends Developer {
    public $type = 'Php';
    public $price = 1000;
    public $trainingComponentsFactory = null;

    public function __construct($trainingComponentsFactory) {
        $this->trainingComponentsFactory = $trainingComponentsFactory;
    }

    public function training() {
        $this->git = $this->trainingComponentsFactory->trainingGit();
        $this->basic = $this->trainingComponentsFactory->trainingBasic();
        $this->framework = $this->trainingComponentsFactory->trainingFramework();
    }
}

class RubyDeveloper extends Developer {
    public $type = 'Ruby';
    public $price = 900;
    public $trainingComponentsFactory = null;

    public function __construct($trainingComponentsFactory) {
        $this->trainingComponentsFactory = $trainingComponentsFactory;
    }

    public function training() {
        $this->git = $this->trainingComponentsFactory->trainingGit();
        $this->basic = $this->trainingComponentsFactory->trainingBasic();
    }
}

Nhìn qua thì ta có thể thấy là chỉ có PhpDeveloper cần training đủ 3 bước còn RubyDeveloper thì chỉ cần qua basic là có thể sử dụng được luôn. Cuối cùng ta cần thay đổi lớp DevelopersFactory bằng việc thêm các thành phần cần training (TrainingComponentsFactory) vào:

class HcmDevelopersFactory extends DevelopersFactory {
    public function createDeveloper($type) {
        $hcmTrainingComponentsFactory = new HcmTrainingComponentsFactory();

        switch ($type) {
            case 'HcmPhp':
                $developer = new HcmPhpDeveloper($hcmTrainingComponentsFactory);
                break;
            case 'HcmRuby':
                $developer = new HcmRubyDeveloper($hcmTrainingComponentsFactory);
                break;

            default:
                $developer = null;
                break;
        }

        return $developer;
    }
}

Factory Pattern trong Laravel

Trong Laravel, chúng ta cũng thấy họ sử dụng Factory Method pattern trong việc quản lý khởi tạo các kết nối database. Cụ thể là trong Illuminate/Database/Connectors/ConnectionFactory.php:

class ConnectionFactory {
    ...
    /**
     * Create a new connection instance.
     *
     * @param  string   $driver
     * @param  \PDO     $connection
     * @param  string   $database
     * @param  string   $prefix
     * @param  array    $config
     * @return \Illuminate\Database\Connection
     *
     * @throws \InvalidArgumentException
     */
    protected function createConnection($driver, PDO $connection, $database, $prefix = '', array $config = array())
    {
        if ($this->container->bound($key = "db.connection.{$driver}"))
        {
            return $this->container->make($key, array($connection, $database, $prefix, $config));
        }

        switch ($driver)
        {
            case 'mysql':
                return new MySqlConnection($connection, $database, $prefix, $config);

            case 'pgsql':
                return new PostgresConnection($connection, $database, $prefix, $config);

            case 'sqlite':
                return new SQLiteConnection($connection, $database, $prefix, $config);

            case 'sqlsrv':
                return new SqlServerConnection($connection, $database, $prefix, $config);
        }

        throw new \InvalidArgumentException("Unsupported driver [$driver]");
    }
}

Thêm một ví dụ về việc sử dụng Factory pattern trong Laravel đó là việc validate dữ liệu. Chúng ta có thể định nghĩa nhiều luật (rules) validate dữ liệu thông qua Validation class. Để làm việc này chúng ta thường hay định nghĩa validation rulesModel và gọi từ phía Controller.

Và bạn cũng có thể thấy rằng chúng ta hoàn toàn có thể đưa ra các custom rulescustom error message để validate dữ liệu đúng không? Hãy cùng xem class Illuminate/Validation/Factory.php

class Factory implements FactoryContract {
    ...
    /**
     * Create a new Validator instance.
     *
     * @param  array  $data
     * @param  array  $rules
     * @param  array  $messages
     * @param  array  $customAttributes
     * @return \Illuminate\Validation\Validator
     */
    public function make(array $data, array $rules, array $messages = [], array $customAttributes = [])
    {
        // The presence verifier is responsible for checking the unique and exists data
        // for the validator. It is behind an interface so that multiple versions of
        // it may be written besides database. We'll inject it into the validator.
        $validator = $this->resolve($data, $rules, $messages, $customAttributes);
        if (! is_null($this->verifier)) {
            $validator->setPresenceVerifier($this->verifier);
        }
        // Next we'll set the IoC container instance of the validator, which is used to
        // resolve out class based validator extensions. If it is not set then these
        // types of extensions will not be possible on these validation instances.
        if (! is_null($this->container)) {
            $validator->setContainer($this->container);
        }

        // Điều kỳ diệu nằm ở đây
        $this->addExtensions($validator);

        return $validator;
    }
}

Ta có thể thấy Validation Class được khởi tạo cùng với Translator class và một IoC Container. Với hàm make() mà tôi dẫn chứng ở trên trước khi trả về kết quả có gọi đến $this->addExtentions($validator). Phương thức addExtentions() được gọi để gắn thêm những rule custom của chúng ta.

Kết luận

  • Trên đây tôi đã giới thiệu với các bạn về khái niệm cũng như đưa ra một bài toán cụ thể để hiểu hơn về Factory Pattern. Đồng thời cũng đưa ra ưu điểm khi sử dụng pattern này.
  • Trong Laravel họ cũng triển khai Factory Pattern nhưng không theo một cách truyền thống mà có biến thể đi chút.

Hy vọng bài viết giúp ích cho các bạn (bow)

P/S: Source code hoàn chỉnh cho ví dụ trong bài viết vui lòng xem thêm tại đây.

References

  1. Factory Method Pattern (Wiki)
  2. Abstract Factory Pattern (Wiki)
  3. https://www.binpress.com/tutorial/the-factory-design-pattern-explained-by-example/142
  4. http://culttt.com/2014/03/19/factory-method-design-pattern/
  5. http://www.sitepoint.com/understanding-the-factory-method-design-pattern/