SitePoint Sponsor

User Tag List

Results 1 to 4 of 4
  1. #1
    SitePoint Enthusiast Locker's Avatar
    Join Date
    May 2005
    Posts
    64
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    What is good object-orientated design (examples inside)?

    In order to try and explain my question about object-orientated design, I've created a couple of examples.

    Let's take a project with the following 3 classes:

    • Customer
    • Order
    • Item

    Then let's say we want to do the following: For a customer with a specific email address, output the title and price of every item purchased across all orders.

    I have some example code which would ultimately achieve the above. Before looking at it though, it's worth noting that rather than using constructors, I'm using a build() method, and rather than taking discrete parameters, I'm taking an array of parameters. This is in order to accommodate the class' functionality being easily accessible via HTTP (it allows a simple API controller to automatically route HTTP requests to the appropriate classes/methods and return a serialised result).

    Client-side code example one:

    PHP Code:
    $customer = new Customer;
    $customer->build(array(
        
    'email_address' => 'person@domain.com'
    ));

    foreach (
    $customer->get_orders() as $order)
    {
        foreach(
    $order->get_items() as $item)
        {
            echo(
    '<strong>Item:</strong> '.$item->get_title().'<br /><strong>Price:</strong> '.$item->get_price().'<br />');
        }

    This example is the sort of code that got me excited about object-orientated programming when I first discovered it. The fact that you could type something as intuitive as $user->get_car()->get_manufacturer()->get_country()->get_international_dialing_code() to get the international dialing code for the founding country of the manufacturer of a user's car (not an amazing example, but you get the idea)!

    Here's the code which the above would need to operate:

    PHP Code:
    /**
    * Customer class
    */
    class Customer {

        
    /**
        * @var string $email_address The customer's email address.
        */
        
    protected $email_address;

        
    /**
        * @var string $first_name The customer's first name.
        */
        
    protected $first_name;

        
    /**
        * @var string $last_name The customer's last name.
        */
        
    protected $last_name;

        
    /**
        * @var Order[] $orders An array of orders.
        */
        
    protected $orders;


        
    /**
        * Build the object instance from an array of parameters.
        *
        * @param array $params An array of parameters.
        */
        
    public function build(array $params)
        {
            if (
    array_key_exists('email_address'$params))
            {
                
    // Query the database and populate the user instance based off their email address...
            
    }
            else
            {
                throw new 
    InvalidArgumentException('You can only build customers from email addresses at this time.');
            }
        }


        
    /**
        * Get all of the customer's orders.
        *
        * @return Order[] The customer's orders.
        */
        
    public function get_orders()
        {
            
    // Query the database for the order information...
        
    }

    }


    /**
    * Order class
    */
    class Order {

        
    /**
        * @var Item[] $items An array of items.
        */
        
    protected $items;

        
    /**
        * @var DateTime $time The time the order was made.
        */
        
    protected $time;
        
        
    /**
        * @var bool $paid Whether or not the order has been paid for.
        */
        
    protected $paid;


        
    /**
        * Get all of the items in the order.
        *
        * @return Item[] The items in the order.
        */
        
    public function get_items()
        {
            
    // Query the database for the item information...
        
    }

    }


    /**
    * Item class
    */
    class Item {

        
    /**
        * @var string $title The title of the item.
        */
        
    protected $title;

        
    /**
        * @var string $price The price of the item.
        */
        
    protected $price;


        
    /**
        * Get the price of the item.
        *
        * @return string The price of the item.
        */
        
    public function get_title()
        {
            return 
    $this->title;
        }


        
    /**
        * Get the price of the item.
        *
        * @return string The price of the item.
        */
        
    public function get_price()
        {
            return 
    $this->price;
        }


    As far as I can see though, this has a couple of problems, in that:

    • The customer class needs to know about orders.
    • The order class needs to know about items.

    So, lets get onto an alternative...

    Client-side code example two:

    PHP Code:
    $customer = new Customer;
    $customer->build(array(
        
    'email_address' => 'person@domain.com'
    ));

    $orders = new Orders;
    $orders->get_all(array(
        
    'customer_id' => $customer->get_id()
    ));

    foreach (
    $orders as $order)
    {
        
    $items = new Items;
        
    $items->get_all(array(
            
    'order_id' => $order->get_id()
        ));

        foreach(
    $items as $item)
        {
            echo(
    '<strong>Item:</strong> '.$item->get_title().'<br /><strong>Price:</strong> '.$item->get_price().'<br />');
        }

    Here's the code which the above would need to operate:

    PHP Code:
    /**
    * Customer class
    */
    class Customer {

        
    /**
        * @var string $email_address The customer's email address.
        */
        
    protected $email_address;

        
    /**
        * @var string $first_name The customer's first name.
        */
        
    protected $first_name;

        
    /**
        * @var string $last_name The customer's last name.
        */
        
    protected $last_name;

        
    /**
        * @var Order[] $orders An array of orders.
        */
        
    protected $orders;


        
    /**
        * Build the object instance from an array of parameters.
        *
        * @param array $params An array of parameters.
        */
        
    public function build(array $params)
        {
            if (
    array_key_exists('email_address'$params))
            {
                
    // Query the database and populate the user instance based off their email address...
            
    }
            else
            {
                throw new 
    InvalidArgumentException('You can only build customers from email addresses at this time.');
            }
        }

    }


    /**
    * Orders class
    *
    * Builds orders.
    */
    class Orders {

        
    /**
        * Fetches orders.
        *
        * @return Order[] An array of orders.
        */
        
    public function get_all()
        {
            if (
    array_key_exists('customer_id'$params))
            {
                
    // Query the database for the order information...
            
    }
            else
            {
                throw new 
    InvalidArgumentException('You can only build orders from customer IDs at this time.');
            }
        }

    }


    /**
    * Order class
    */
    class Order {

        
    /**
        * @var Item[] $items An array of items.
        */
        
    protected $items;

        
    /**
        * @var DateTime $time The time the order was made.
        */
        
    protected $time;
        
        
    /**
        * @var bool $paid Whether or not the order has been paid for.
        */
        
    protected $paid;

    }


    /**
    * Items class
    *
    * Builds items.
    */
    class Items {

        
    /**
        * Fetches items.
        *
        * @return Item[] An array of items.
        */
        
    public function get_all()
        {
            if (
    array_key_exists('order_id'$params))
            {
                
    // Query the database for the item information...
            
    }
            else
            {
                throw new 
    InvalidArgumentException('You can only build items from order IDs at this time.');
            }
        }

    }


    /**
    * Item class
    */
    class Item {

        
    /**
        * @var string $title The title of the item.
        */
        
    protected $title;

        
    /**
        * @var string $price The price of the item.
        */
        
    protected $price;


        
    /**
        * Get the price of the item.
        *
        * @return string The price of the item.
        */
        
    public function get_title()
        {
            return 
    $this->title;
        }


        
    /**
        * Get the price of the item.
        *
        * @return string The price of the item.
        */
        
    public function get_price()
        {
            return 
    $this->price;
        }


    The good news is:

    • The classes are no longer dependent on other classes.

    The bad news is:

    • It's far less intuitive, there's more code, and prevents you being able to pass a single object (such as $order) into a view (whether or not that's a good idea in itself is debatable, but let's just say it's fine) for easily building output.


    My questions are, which of the above two scenarios showcases the best design? Would either of them be considered good design? Then finally, are there any other approaches that would be more suitable?

    I look forward to hearing some thoughts

  2. #2
    SitePoint Wizard bronze trophy Jeff Mott's Avatar
    Join Date
    Jul 2009
    Posts
    1,268
    Mentioned
    18 Post(s)
    Tagged
    0 Thread(s)
    You could also avoid dependencies between classes by using interfaces. For example...

    PHP Code:
    interface OrderInterface
    {
        public function 
    get_items();

    And your Order class would implement that interface.

    PHP Code:
    class Order implements OrderInterface
    {
        
    // ...

    And anywhere in your Customer class where you once referenced the Order class explicitly, instead you would reference the interface. The reason this is better is because now you or others can create different Order classes that implement the same interface, and your Customer class can use them interchangably.

    But there may be a more significant design decision that we can look at. Ideally, you want to avoid querying the database from inside your Customer/Order/Item classes. You want to separate the jobs of "application logic" and "persisting to disk." You could have a separate class called, perhaps, CustomerRepository, with methods such as findByEmail that would query the database and return a fully populated Customer object.
    "First make it work. Then make it better."

  3. #3
    SitePoint Enthusiast Locker's Avatar
    Join Date
    May 2005
    Posts
    64
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by Jeff Mott View Post
    But there may be a more significant design decision that we can look at. Ideally, you want to avoid querying the database from inside your Customer/Order/Item classes. You want to separate the jobs of "application logic" and "persisting to disk." You could have a separate class called, perhaps, CustomerRepository, with methods such as findByEmail that would query the database and return a fully populated Customer object.
    This is a route I'm interested in, but I have some further questions, let's take a new example:

    Say we have a 'Website' object, and this represents the current website which is being viewed. It has methods such as:

    • get_title()
    • get_meta_description()
    • get_meta_keywords()
    • get_settings()

    To accompany this we have a Website_Mapper and Website_Repository class. They both have methods like this:

    • fetch_by_id()
    • fetch_by_domain_name()

    Website_Mapper responds to these with an array (basically the result of a query), and Website_Repository effectively acts as a facade to take the mapper results and build either a Website object, or perhaps in some cases a collection of Website objects.

    With these in place, the Website class wouldn't even need any implementation to fulfil the requests in bold:

    • get_title()
    • get_meta_description()
    • get_meta_keywords()
    • get_settings()

    Between the Mapper and Repository, the object generated would include properties for 'title', 'meta_description' and 'meta_keywords'. Therefore, the following code would be suffice for serving them up:

    PHP Code:
    class Model {

        public function 
    __call($method$params)
        {
            
    // Handle get calls.
            
    if(substr($method04) === 'get_')
            {
                
    $property substr($method4);
                if(
    property_exists($this$property))
                {
                    return 
    $this->$property;
                }
                else
                {
                    throw new 
    Exception('The property "' $property '" does not exist in the "' get_class($this) . '" instance.');
                }
            }

            
    // Handle set calls.
            
    if(substr($method04) === 'set_')
            {
                
    $property substr($method4);
                if(
    property_exists($this$property))
                {
                    
    $this->$property $params[0];
                }
                else
                {
                    throw new 
    Exception('The property "' $property '" does not exist in the "' get_class($this) . '" instance.');
                }
            }
        }

    }

    class 
    Website extends Model {} 
    The problem is, get_settings() is a method to fetch website settings, and is relatively heavy. It relies on another table in the database and there can be a complex hierarchy of settings to load. It's rarely required too, and therefore, I'd like it to be lazy loaded.

    The question is, how can this be lazy loaded in whilst keeping SQL out of the model? Sure, we can add a get_settings() method to the Website class - but what goes in there? It doesn't seem to fit with their being Repositories/Mappers for each object...

    Hope that makes sense - any further guidance would be much appreciated!

  4. #4
    SitePoint Wizard bronze trophy Jeff Mott's Avatar
    Join Date
    Jul 2009
    Posts
    1,268
    Mentioned
    18 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by Locker View Post
    The problem is, get_settings() is a method to fetch website settings, and is relatively heavy. It relies on another table in the database and there can be a complex hierarchy of settings to load. It's rarely required too, and therefore, I'd like it to be lazy loaded.

    The question is, how can this be lazy loaded in whilst keeping SQL out of the model? Sure, we can add a get_settings() method to the Website class - but what goes in there? It doesn't seem to fit with their being Repositories/Mappers for each object...
    Now we're getting into the deep end with ORMs, and I'd certainly suggest that you study the code and documentation of projects such as Doctrine for ideas and techniques. As best as I can tell, based on my surface knowledge of Doctrine, it will generate proxy classes and objects on the fly. What that means is, you won't actually get a Website object back. Instead you'd get a WebsiteProxy object. And WebsiteProxy would be define somewhat like this:

    PHP Code:
    class WebsiteProxy extends Website
    {
        private function 
    _load()
        {
            
    // lazy loading code
        
    }

        public function 
    get_settings()
        {
            
    $this->_load();
            
            return 
    parent::get_settings();
        }

    "First make it work. Then make it better."


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •