HMVC模式即Hierarchical-Model-View-Controller模式,也可以叫做Layered MVC.HMVC模式把客户端应用程序分解为有层次的父子关系的MVC。反复应用这个模式,形成结构化的客户端架构。如图:
一个MVC模块由应用程序的一个模块抽象而成。其中很重要的一个概念就是Parent MVC,它可以对应界面上的实体,也可以是一个抽象的对象。设想有一个Windows Form应用程序,有一个框架(frame),此框架由菜单功能模块、导航栏、状态栏、主工作区等部分组成,对应于HMVC,frame MVC 即Layer1 的parent MVC ;菜单MVC、导航栏MVC、状态栏MVC、主工作区 MVC处于第二层(图中只画了一个)。如果你觉得导航栏或主工作区的功能太复杂,也可以再细分成HMVC中的第三层,依次类推,可以扩展到n层,如果你愿意的话。
HMVC 工作原理:
Controller是功能模块的总控室,它负责和子Controller或父Controller通信,并通知它的View处理改变界面显示、Model处理一些业务逻辑或数据库访问操作。举个例子,假如要实现点击菜单项,刷新主工作区这样的功能。首先点击操作在菜单MVC的View里完成,菜单Controller捕获这个事件,发现是需要刷新主工作区,它处理不了,于是传给它的父Controller—Frame Controller处理,Frame Controller捕获这个事件,直接把它交给主工作区 Controller处理,主工作区 Controller捕获这个事件,让主工作区 View处理刷新操作。是不是觉得很麻烦?对于小型应用程序,应用HMVC模式的优点显现不出来,但是一旦你的应用程序很复杂,HMVC模式的优点就清晰可见。
层次的HMVC解决了客户层程序的复杂性,HMVC揭示了面向对象的优势。
它的优点主要有:
- 把程序分成了几个部分,降低了依赖性。
- 支持鼓励重用代码,组件或者模块。
- 在今后的维护中,提高了可扩展性。
- Kohana V3.0支持HMVC
The last decade has been witness to the second iteration of web design and development. Web sites have transformed into web applications and rarely are new projects commissioned that do not involve some element of interactivity. The increasing complexity of the software being developed for the internet fuelled a requirement for structured and considered application design.
Today the most common design pattern for web software is the Model-View-Controller (MVC) pattern. The widespread adoption of the MVC design pattern was supported, in part, by the success and popularity of the Ruby on Rails framework. MVC is now synonymous with web application development across all platforms.
With the rising complexity of projects developed for the web, modern software for the web increasingly relies on dedicated services to perform processor intensive tasks. This has been encouraged further by the introduction of cloud services from Amazon, Google and several others enabling developers to considerably reduce the processor load on their servers. Each service is usually designed as a separate piece of software that runs in its own domain using its own resources.
When working with small budgets, it is generally much harder to convince clients of the benefits of funding more than one complete piece of software. In these situations I have found that many clients conclude that scalability is not a concern. They "look forward to the day when they will have to worry about scaling".
To reduce the initial investment, usually it is decided that the application should designed to be one holistic piece of software containing all the required features. This represents a potential point of failure if the software becomes very popular in a short timeframe. I have painful memories of refactoring existing codebases that have not scaled well. It can also be very costly in time and resources to re-architect software that not scaled well. Ideally applications should grow organically as required and without large sums of money being exchanged in the process.
Hierarchical-Model-View-Controller pattern
The Hierarchical-Model-View-Controller (HMVC) pattern is a direct extension to the MVC pattern that manages to solve many of the scalability issues already mentioned. HMVC was first described in a blog post entitled HMVC: The layered pattern for developing strong client tiers on the JavaWorld web site in July 2000. Much of the article concentrates on the benefits of using HMVC with graphical user interfaces. There has been some suggestion that the authors where actually re-interpreting another pattern called Presentation-Abstraction-Control (PAC) described in 1987. The article in JavaWorld provides a detailed explanation of how HMVC can aid in the design of desktop applications with GUIs. The focus of this article is to demonstrate how HMVC can be used to create scalable web applications.
HMVC is a collection of traditional MVC triads operating as one application. Each triad is completely independent and can execute without the presence of any other. All requests made to triads must use the controller interface, never loading models or libraries outside of their own domain. The triads physical location within the hosting environment is not important, as long as it is accessible from all other parts of the system. The distinct features of HMVC encourages the reuse of existing code, simplifies testing of disparate parts of the system and ensures that the application is easily enhanced or extended.
To successfully design applications that implement the HMVC pattern, it is critical that all of the application features are broken down into systems. Each system is one MVC triad within the larger HMVC application, independently managing presentation and persistent storage methods. Presently few frameworks are available that support HMVC without additional extensions, or use inefficient Front Controllers and dispatching. Kohana PHP version 3 is a framework that was designed from the ground up with HMVC at the core. I will be using Kohana PHP 3 for all of the code examples in this document.
Kohana 3 uses the core request object to call other controllers. Requests can be made internally to application controllers or externally to web services transparently using the same request class[1]. If a MVC triad is scaled out, the request only requires modification of one parameter.
<?php class Controller_Default extends Controller { public function action_index() { // Internal request example $internal_request = Request::factory('controller/action/param') ->execute(); // External request example $external_request = Request::factory('http://www.ibuildings.com/controller/action/param') ->execute(); } }
Requesting internally requires a valid route path targeting a controller and action. Creating a request to an external resource is as simple as supplying the full URL. This feature makes internal and external requests quickly interchangeable, ensuring that scaling out triads is a relatively simple task.
Using the Kohana Request class to provide data from internal controllers may seem similar to action forwarding in other frameworks, such as the Zend Framework. In reality the two methods are quite different. Kohana Requests have the ability to operate as unique requests in isolation. Forwarding actions do not operate in this way, each invoked controller action exists within the originating request. To demonstrate this, consider the example below.
Default Controller – /application/controllers/default.php
<?php // First request controller class Controller_Default extends Controller { public function action_index() { // If the Request was a GET request if ($this->request->method === 'GET') { // Output POST, will print array (0) { empty } var_dump($_POST); // Create a new request for another resource $log = Request::factory('/log/access/'.$page_id); // Set the request method to POST $log->method = 'POST'; // Apply some data to send $log->post = array( 'uid' => $this->user->id, 'ua' => Request::user_agent('browser'), 'protocol' => Request::$protocol, ); // Log the access $log->execute(); // Output POST, will still print array (0) { empty } var_dump($_POST); } } }
Log Controller – /application/controllers/log.php
<?php // Second request controller class Controller_Log extends Controller { public function action_access($page_id) { // When requested from the index action above // will print the posted vars from the second request // array (3) {string (3) 'uid' => int (1) 1, string (2) 'ua' => string(10) 'Mozilla ... ... var_dump($_POST); // Create a new log model $log = new Log_Model; // Set the values and save $log->set_values($_POST) ->save(); } }
The example above demonstrates the independence afforded to the Request object. The initial request invokes the Default controller index action from a GET request, which in turn invokes a POST request to the Log controller access action. The index action sets three post variables which are not available to the global $_POST
variable from the first controller. When the second request executes, $_POST
has the post variables we made available to that request. Notice how after $log->execute();
has finished within the index controller, the $_POST
data is not there. To do dynamic interaction of this kind within other frameworks requires creating a new request using a tool like Curl.
Gazouillement, the status service with a continental twist
To demonstrate the power of Hierarchical-MVC lets look at an example of a hypothetical status update service calledGazouillement, which works in a similar way to Twitter. Gazouillement has been designed around a service-oriented-architecture (SOA) to ensure the message and relationship engines are disparate from the web interface.
Traffic to the server will be relatively light initially. The service is new with an audience completely unaware of its presence. So it is safe to allow all of the application logic to execute on the same server for the time being.
Lets implement the controller to display a users homepage. A user homepage will show their most recent status messages, plus a list of people the user is following.
Index Controller – /application/controllers/index.php
<?php // Handles a request to http://gazouillement.com/samsoir/ class Controller_Index extends Controller { public function action_index() { // Load the user (samsoir) into a model $user = new Model_User($this->request->param('user')); // If user is not loaded, then throw a 404 exception if ( ! $user->loaded) throw new Controller_Exception_404('Unable to load user :user', array(':user' => $this->request->param('user'))); // Load messages for user in xhtml format $messages = Request::factory('messages/find/'.$user->name.'.xhtml') ->execute() ->response; // Load relationships for user in xhtml format $relations = Request::factory($user->name.'/following.xhtml') ->execute() ->response; // Apply the user index page view to the response // and set the user, messages and relations to the view $this->request->response = View::factory('user/home', array( 'user' => $user, 'messages' => $messages, 'relations' => $relations, )); } }
Now a review of what Controller_Index::action_index()
is doing. Initially the action attempts to load a user based on theuser parameter of the url. If the user fails to load, a 404 page is displayed. A new request for messages is created using the users name property as a parameter within the request uri, asking for a response in xhtml format. Another request for the users relations is made in a similar manner, again in xhtml format. Finally a view object is created with the user, messages and relations responses set to it.
As the existing messages and relations are loaded using a new request, the entire application logic for each service remains abstracted from the web site. This architecture provides two significant advantages over traditional controller execution;
- The controller is not concerned with any part of the messages service execution logic. The controller only requires that the result is xhtml formatted. No additional libraries or extensions were loaded within the controller execution.
- Each controller is only responsible for one particular task, ensuring that writing unit tests for controllers is significantly less complicated.
Due to the abstraction currently demonstrated, it is impossible to see what the services are doing. So lets look at the messages service controller, starting with the route defined in the bootstrap to internally handle requests for messages. TheKohana Route class handles internal url parsing, mapping supplied uri elements to controllers, actions and parameters.
Route setup – /application/bootstrap.php
Route::set('messages', 'messages/<action>/<user>(<format>)', array('format' => '\.\w+')) ->defaults(array( 'format' => '.json', 'controller' => 'messages', ));
This sets a route for the messages service, presently located within the main application domain. The url request formessages/find/samsoir.xhtml will be routed to the messages controller, calling the find()
action and passing 'user' => 'samsoir'
, plus 'format => '.json'
as parameters.
Messages Controller – /application/controllers/messages.php
<?php class Controller_Messages extends Controller { // Output formats supported by this controller protected $supported_formats = array( '.xhtml', '.json', '.xml', '.rss', ); // The user context of this request protected $user; // this code will be executed before the action. // we check for valid format and user public function before() { // Test to ensure the format requested is supported if ( ! in_array($this->request->param('format'), $this->supported_formats)) throw new Controller_Exception_404('File not found'); // Next test the username is valid $this->user = new Model_User($this->request->param('user'); if ( ! $this->user->loaded()) throw new Controller_Exception_404('File not found'); return parent::before(); } // This finds the users messages public function find() { // Load messages user 1:M messages relation $messages = $this->user->messages; // Set the response, using a prepare method for correct output $this->request->response = $this->_prepare_response($messages); } // Method to prepare the output protected function _prepare_response(Model_Iterator $messages) { // Return messages formatted correctly to format switch ($this->request->param('format') { case '.json' : { $this->request->headers['Content-Type'] = 'application/json'; $messages = $messages->as_array(); return json_encode($messages); } case '.xhtml' : { return View::factory('messages/xhtml', $messages); } default : { throw new Controller_Exception_404('File not found!'); } } } }
The detail of how user messages are retrieved is demonstrated within the Controller_Messages
controller. All of the methods and properties are exclusively related to the messages context, including the relationship to users. Lets step through the messages controller code to understand what is happening.
The request object initially invokes the before()
method ahead of the defined action. This allows code to execute ahead of any action, normalising common controller tasks. The before()
method first tests the file format requested is supported, followed by a test to ensure the user is valid. Assuming before()
completed without exception, the request object will invoke the find()
action. find()
loads the users messages as a Model_Iterator object. It is important to note that the iterator will be empty if no messages in the relationship were found. Finally the messages iterator is passed to a parser method _prepare_response()
that correctly formats the data for output and sets any headers that are required.
The two controllers; Controller_Index
and Controller_Messages
; were both executed by a single request to the application. However each controller was unaware of the others presence. Any developer could read the current codebase and understand what is executing and where, providing another great feature of HMVC - maintainability.
After completing the code for the other services, the company directors are happy with the first iteration of development and green-light deployment to a live server for a limited beta trial. After a couple of weeks, the application is ready for open use. The following months see further enhancements and optimisations across the entire architecture as the customer base steadily grows.
The Stephen Fry Effect
Stephen Fry (@stephenfry) is currently one of the most famous users of Twitter, boasting a formidable 1.3 million (and counting) followers. In the past Stephen has been capable of disabling web sites just by tweeting a url to the world, in essence creating a Distributed Denial-Of-Service (DDOS) attack on the server residing at the end of the link.
Gazouillement has seen steady growth consistently for the past few months. Although the response time for most page requests has increased marginally, they are currently well within the acceptable standards. Then a twitter user with a follower count similar to Stephen Fry's, suddenly posts a message containing the url for Gazouillement. The application is in real trouble.
The first hurdle to overcome would be the shear volume of traffic. The application would suddenly have to handle thousands of requests per second, from only a few hundred per second previously. In this scenario Gazouillement would most likely overload and potentially crash the server. It is clear the application requires some optimisation and enhancement to handle considerably more traffic.
Analysing code to enhance performance is far from a simple task. Previous articles on TechPortal have demonstrated the use of tools such as XHProf to provide deep memory and CPU analysis during code execution. Kohana PHP comes with an internal benchmarking tool called the Profiler, which is supplied in the core. The Kohana profiler is complimentary to XHProf or similar tools, not a replacement. It helps developers identify slow parts of Kohana's execution that can subsequently be examined in greater detail using XHProf, or an internal module called Codebench that is supplied with the framework.
Enabling the profiler in Kohana requires the modification of a single parameter within the bootstrap.
Bootstrap – /application/bootstrap.php
//-- Environment setup -------------------------------------------------------- Kohana::$profiling = TRUE;
Finally, add the Profiler view to the end of the controller response. This is best done by adding an after()
method to your controller. This will ensure the profiler is displayed for all actions.
Index Controller – /application/controllers/index.php
public function after() { // Add the profiler output to the controller $this->request->response .= View::factory('profiler/stats'); }
Once the profiler is enabled, the output will appear at the foot of every action within that controller. Best practice is to extend the Kohana Controller and apply the profiler view within the extended classes after()
method. Now, all the system controllers are outputting profiling data during development.
The profiler reports on the execution of the framework, grouping various contexts together, including; initialisation; requests; database queries; and other operations juxtaposed with memory allocation and cpu time. As each request created during execution is reported, spotting requests that are taking a long time to execute is much easier. Once slow requests have been isolated, tools such as XHProf can analyse the execution of slow actions in greater detail.
Scaling out Gazouillement
Performance analysis of the whole of the Gazouillement application reveals that the retrieval of messages is causing massive bottlenecks. The development team refactors and optimises the messages MVC triad as much as possible, but the required performance gains are not met. After exhausting all the vertical scaling options including upgrading the server memory and processors, the company directors agree to scaling out the application starting with the messages system.
In traditional MVC applications, a new service has to be designed, developed, acceptance tested and then deployed. This process can take weeks and months, requiring significant investment. Essentially it is new piece of software that will be integrated into the Gazouillement app.
Hierarchical-MVC applications can create new services from the existing codebase in a fraction of the time. All interactions with the messages service were through the main controller, so the only modification to the current codebase will be to the requests for messages. The messages service is migrated to another server and optimised for database interactions. This new server only executes actions relating to messages, increasing the performance of all message related operations considerably.
Index Controller – /application/controllers/index.php
<?php // Handles a request to http://gazouillement.com/samsoir/ class Controller_Index extends Controller { protected $_messages_uri = 'http://messages.gazouillement.com'; public function action_index() { // Load the user (samsoir) into a model $user = new Model_User($this->request->param('user')); // If user is not loaded, then throw a 404 exception if ( ! $user->loaded) throw new Controller_Exception_404('Unable to load user :user', array(':user' => $this->request->param('user'))); // -- START REVISED CODE -- // New messages URI to external server $messages_uri = $this->_messages_uri.'/find/'.$user->name'.xhtml'; // Load messages for user in xhtml format $messages = Request::factory($messages_uri) ->execute() ->response; // -- END REVISED CODE -- // Load relationships for user in xhtml format $relations = Request::factory($user->name.'/following.xhtml') ->execute() ->response; // Apply the user index page view to the response // and set the user, messages and relations to the view $this->request->response = View::factory('user/home', array( 'user' => $user, 'messages' => $messages, 'relations' => $relations, )); } }
The code above highlights the tiny change required in the Controller_Index::action_index()
controller serving the public website. Rather than the messages request referencing an internal controller action, the request now points to the messages service running on a subdomain called http://messages.gazouillement.com/
. What would usually be a projected as a major architecture overhaul has become a minor alteration. The associated testing and acceptance overhead is considerably smaller as not much code has changed.
This is just one example of how the Hierarchical-Model-View-Controller pattern enables software engineers to design web applications, which can scale both vertically and horizontally from the day one. We saw how the request object can be interrogated allowing controllers to return correct data for each context using a simple interface. Finally we have seen how traditional MVC triads can be scaled, and witnessed how the request object in Kohana makes scaling out a simple task. The costs involved scaling the application have been relatively small due to minimal alterations, keeping the company directors very happy.
For more information about Kohana PHP, please visit the Kohana website. Kohana PHP 3 can be downloaded from Github and there is also api documentation available at http://v3.kohanaphp.com/guide.
- The request class used in this example is currently available as part of a Kohana Core development branch within my personal github account, which can be obtained from http://github.com/samsoir/core. If using the official Kohana PHP 3.0 download, a custom extension of the request class is required.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· [AI/GPT/综述] AI Agent的设计模式综述