优大网

标签存档: 编码规范

编写超级可读代码的15个最佳实践

http://blog.csdn.net/hfahe/article/details/6303585

译自:http://net.tutsplus.com/tutorials/html-css-techniques/top-15-best-practices-for-writing-super-readable-code/

译者:蒋宇捷(转载请标明出处-http://blog.csdn.net/hfahe)

 

       一月两次,我们重温Nettuts历史上读者最喜欢的文章。

代码可读性是一个计算机编程世界的普遍主题。它是我们作为开发者第一件学习的事情。这篇文章将阐述编写可读性代码十五个最重要的最佳实践。


1 – 注释和文档

集成开发环境IDE在过去的短短几年里走过了很长的路。它使得注释代码比以前更加有用。依照特定标准书写的注释允许IDE和其他工具通过不同的方式来使用它们。

考虑如下示例:

 

我在函数定义中添加的注释可以在调用它的地方看到,即便是在其他文件中。

这里是我另外一个从第三方库中调用函数的例子:

 

在这些特殊的例子中,使用的注释(或者文档)类型基于PHPDoc,IDE是Aptana


2 – 一致的排版

我假定你已经知道了你必须要缩进你的代码。然而,保持排版样式一致仍然是一个好主意。

这里有不止一种方式来进行代码排版。

第一种:

 

[c-sharp] view plaincopy
  1. function foo() {
  2.     if ($maybe) {
  3.         do_it_now();
  4.         again();
  5.     } else {
  6.         abort_mission();
  7.     }
  8.     finalize();
  9. }

 

第二种:

 

[c-sharp] view plaincopy

  1. function foo()
  2. {
  3.     if ($maybe)
  4.     {
  5.         do_it_now();
  6.         again();
  7.     }
  8.     else
  9.     {
  10.         abort_mission();
  11.     }
  12.     finalize();
  13. }

 

第三种:

 

[c-sharp] view plaincopy

  1. function foo()
  2. {   if ($maybe)
  3.     {   do_it_now();
  4.         again();
  5.     }
  6.     else
  7.     {   abort_mission();
  8.     }
  9.     finalize();
  10. }

 

我曾经使用第二种样式但是最近换为第一种。但是这仅仅只代表了一种偏爱。这里并没有每个人必须要遵守的“最好的”样式。事实上,最佳的样式,就是一致的样式。如果你是一个小组的一部分或者你在为一个项目贡献代码,你必须依照这个项目之前使用的样式。

排版的样式总不是完全和另外一个不同。有时,它们混合了多种不同的规则。例如,按照PEAR编码标准,前括弧“{”和控制结构在同一行上,但是在功能定义后放在第二行上。

PEAR样式:

 

[c-sharp] view plaincopy

  1. function foo()
  2. {                     // placed on the next line
  3.     if ($maybe) {     // placed on the same line
  4.         do_it_now();
  5.         again();
  6.     } else {
  7.         abort_mission();
  8.     }
  9.     finalize();
  10. }

 

同时注意它们使用4个空格而不是Tab来缩进。

这里有一个维基百科的文章,里面有许多不同排版样式的例子。


3 – 避免显而易见的注释

为代码添加注释是效果显著的;但是,它可能太过或者只是多余的文本。像如下例子:

 

[c-sharp] view plaincopy

  1. // get the country code
  2. $country_code = get_country_code($_SERVER[‘REMOTE_ADDR’]);
  3. // if country code is US
  4. if ($country_code == ‘US’) {
  5.     // display the form input for state
  6.     echo form_input_state();
  7. }

 

如果注释内容都是显而易见的,它们并没有提高工作效率。如果你必须要注释这些代码,你可以简单的把它们合并在一行:

 

[c-sharp] view plaincopy

  1. // display state selection for US users
  2. $country_code = get_country_code($_SERVER[‘REMOTE_ADDR’]);
  3. if ($country_code == ‘US’) {
  4.     echo form_input_state();
  5. }

 


4 – 代码分组

确定的任务多半需要多行代码。使用一些空白将这些任务的代码分隔为几段是一个好主意。

这是一个简单的示例:

 

[c-sharp] view plaincopy

  1. // get list of forums
  2. $forums = array();
  3. $r = mysql_query(“SELECT id, name, description FROM forums”);
  4. while ($d = mysql_fetch_assoc($r)) {
  5.     $forums []= $d;
  6. }
  7. // load the templates
  8. load_template(‘header’);
  9. load_template(‘forum_list’,$forums);
  10. load_template(‘footer’);

 

在每一段之前添加注释也增强了视觉上的分隔。


5 – 命名的一致性

PHP有些时候在遵守命名一致性方面有很大问题:

  • strops()和str_split()
  • imagetypes()和image_type_to_extension()

首先,这些命名必须有单词的分界线。有两种流行的选择:

  • 骆驼命名法:除了第一个单词外,每个单词的第一个字符大写。
  • 下划线命名法: 单词间采用下划线,例如mysql_real_escape_string()。

像我之前提到的一样,采用不同的命名选择会创建和排版样式类似的情形。如果一个已有的项目遵照一个确定的习惯,你必须遵守它。同时,某些语言平台倾向于使用特定的命名规则。例如Java里,大多数代码使用骆驼命名法;在PHP里大多采用下划线命名法。

它们也可以混用。一些开发者喜欢在程序函数和类名上使用下划线命名,但是在类方法名上使用骆驼命名。

 

[c-sharp] view plaincopy

  1. class Foo_Bar {
  2.     public function someDummyMethod() {
  3.     }
  4. }
  5. function procedural_function_name() {
  6. }

 

所以,没有明显的“最好的”样式,只需要保持一致。


6 – DRY原则

DRY即不要重复你自己。也被称为DIE:重复是恶魔。

这个原则规定:

      “在一个系统里每一个知识的片段必须有一个单一、明确、权威的表现。”

大多数应用程序(或者通常的计算机)的目的是让重复的任务自动化。这个原则在所有的代码,即使Web程序中也应该保持。代码的相同片段不应该多次重复。

例如,大多数Web程序由许多页面组成。这些页面很可能包含相同的元素。页头和页脚经常符合这个条件。复制和粘贴这些页头和页尾到每一个页面中不是一个好主意。这是Jeffrey Way解释如何在CodeIgniter里创建模版的链接

 

[c-sharp] view plaincopy

  1. $this->load->view(‘includes/header’);
  2. $this->load->view($main_content);
  3. $this->load->view(‘includes/footer’);

 


7 – 避免过深的嵌套

太多层的嵌套会造成代码阅读和跟踪困难。

 

[c-sharp] view plaincopy

  1. function do_stuff() {
  2. // …
  3.     if (is_writable($folder)) {
  4.         if ($fp = fopen($file_path,’w’)) {
  5.             if ($stuff = get_some_stuff()) {
  6.                 if (fwrite($fp,$stuff)) {
  7.                     // …
  8.                 } else {
  9.                     return false;
  10.                 }
  11.             } else {
  12.                 return false;
  13.             }
  14.         } else {
  15.             return false;
  16.         }
  17.     } else {
  18.         return false;
  19.     }
  20. }

 

为了可读性,通常需要修改代码来减少嵌套的层数。

 

[c-sharp] view plaincopy

  1. function do_stuff() {
  2. // …
  3.     if (!is_writable($folder)) {
  4.         return false;
  5.     }
  6.     if (!$fp = fopen($file_path,’w’)) {
  7.         return false;
  8.     }
  9.     if (!$stuff = get_some_stuff()) {
  10.         return false;
  11.     }
  12.     if (fwrite($fp,$stuff)) {
  13.         // …
  14.     } else {
  15.         return false;
  16.     }
  17. }

 


8 – 减少行的长度

我们的眼睛对于阅读高和窄的文本列更感觉舒适。这就是为什么报纸文章看起来像如下样子的原因:

 

避免在一行上编写过长的代码是一个最佳实践。

 

[c-sharp] view plaincopy
  1. // bad
  2. $my_email->set_from(‘test@email.com’)->add_to(‘programming@gmail.com’)->set_subject(‘Methods Chained’)->set_body(‘Some long message’)->send();
  3. // good
  4. $my_email
  5.     ->set_from(‘test@email.com’)
  6.     ->add_to(‘programming@gmail.com’)
  7.     ->set_subject(‘Methods Chained’)
  8.     ->set_body(‘Some long message’)
  9.     ->send();
  10. // bad
  11. $query = “SELECT id, username, first_name, last_name, status FROM users LEFT JOIN user_posts USING(users.id, user_posts.user_id) WHERE post_id = ‘123’”;
  12. // good
  13. $query = “SELECT id, username, first_name, last_name, status
  14.     FROM users
  15.     LEFT JOIN user_posts USING(users.id, user_posts.user_id)
  16.     WHERE post_id = ‘123’”;

 

同时,如果任何人想要在例如Vim这样的终端窗口中阅读代码,限制每一行的长度在80个字符以内是一个好主意。


 

9 – 代码结构

理论上,你可以将整个应用代码写在一个文件里。但是对于阅读和维护来说是一个噩梦。

在我的第一个编程项目中,我知道创建“包含文件”的含义。但是,我并没有好好进行组织。我创建了一个“inc”文件夹,放置了两个文件:db.php、functions.php。当程序变大时,functions文件也变得越来越大并难以维护。

最好的方法之一是采用框架或者模仿它们的文件夹结构。下面是CodeIgniter的文件结构:

 


10 – 统一的临时变量名

通常,变量名应该是描述性的并且包含一个或者更多的单词。但是,这对临时变量来说并不是必须的。它们可以短到只有一个单独字符。

最佳实践是:对于有同样职责临时变量采用统一的命名。这里有一些我倾向于在代码里使用的例子:

 

[c-sharp] view plaincopy
  1. // $i for loop counters
  2. for ($i = 0; $i < 100; $i++) {
  3.     // $j for the nested loop counters
  4.     for ($j = 0; $j < 100; $j++) {
  5.     }
  6. }
  7. // $ret for return variables
  8. function foo() {
  9.     $ret[‘bar’] = get_bar();
  10.     $ret[‘stuff’] = get_stuff();
  11.     return $ret;
  12. }
  13. // $k and $v in foreach
  14. foreach ($some_array as $k => $v) {
  15. }
  16. // $q, $r and $d for mysql
  17. $q = “SELECT * FROM table”;
  18. $r = mysql_query($q);
  19. while ($d = mysql_fetch_assocr($r)) {
  20. }
  21. // $fp for file pointers
  22. $fp = fopen(‘file.txt’,’w’);

 


 

11 – SQL关键词大写

数据库交互对于大多数Web应用来说是很大一个组成部分。如果你正在编写SQL查询,尽量保持它们可读。

即使SQL关键词和函数名是大小写无关的,大写来将它们从表名和列名中区分出来是一个通用的实践。

 

[c-sharp] view plaincopy

  1. SELECT id, username FROM user;
  2. UPDATE user SET last_login = NOW()
  3. WHERE id = ‘123’
  4. SELECT id, username FROM user u
  5. LEFT JOIN user_address ua ON(u.id = ua.user_id)
  6. WHERE ua.state = ‘NY’
  7. GROUP BY u.id
  8. ORDER BY u.username
  9. LIMIT 0,20

 


 

12 – 代码和数据分离

这是另外一个对于所有环境下的绝大多数编程语言都适用的原则。在Web开发中,数据通常意味着HTML输出。

当PHP许多年前第一次发布时,它最开始被看作是一个模版引擎。在巨大的HTML文件里插入一些PHP代码行是非常普通的。但是,这些年来,事情发生了改变:网站变得越来越动态化和功能化。代码已经是Web程序的一个很大的部分,将它们和HTML合并在一起并不是一个好的实践。

你可以在你的程序中应用这个原则,或者你可以使用一个第三方工具(模版引擎、框架或者CMS系统)或者依照它们的习惯。

流行的PHP框架:

流行的模版引擎:

流行的CMS系统:

  • Joomla
  • Drupal


 

13 – 模版内的交替格式

你可以选择不使用一个奇特的模版引擎,取而代之的是在模版文件里使用纯内联的PHP代码。这不是必须要违反“数据和代码分离“,只是内联代码是直接和输出相关的,并且可读。在这种情况下你可以考虑使用交替格式来控制结构。

这是一个示例:

 

[c-sharp] view plaincopy

  1. <div class=”user_controls”>
  2.     <?php if ($user = Current_User::user()): ?>
  3.         Hello, <em><?php echo $user->username; ?></em> <br/>
  4.         <?php echo anchor(‘logout’, ‘Logout’); ?>
  5.     <?php else: ?>
  6.         <?php echo anchor(‘login’,’Login’); ?> |
  7.         <?php echo anchor(‘signup’, ‘Register’); ?>
  8.     <?php endif; ?>
  9. </div>
  10. <h1>My Message Board</h1>
  11. <?php foreach($categories as $category): ?>
  12.     <div class=”category”>
  13.         <h2><?php echo $category->title; ?></h2>
  14.         <?php foreach($category->Forums as $forum): ?>
  15.             <div class=”forum”>
  16.                 <h3>
  17.                     <?php echo anchor(‘forums/’.$forum->id, $forum->title) ?>
  18.                     (<?php echo $forum->Threads->count(); ?> threads)
  19.                 </h3>
  20.                 <div class=”description”>
  21.                     <?php echo $forum->description; ?>
  22.                 </div>
  23.             </div>
  24.         <?php endforeach; ?>
  25.     </div>
  26. <?php endforeach; ?>

 

这让你避免了许多大括号。同时代码看起来和HTML的结构和排版相似。


 

14 – 面向对象 vs 面向程序

面向对象编程可以帮助你创建结构化代码。但是这不代表你完全排除程序化编程。事实上创建两者混合的风格是非常棒的。

描述数据,通常是数据库里的数据,必须使用对象。

 

[c-sharp] view plaincopy

  1. class User {
  2.     public $username;
  3.     public $first_name;
  4.     public $last_name;
  5.     public $email;
  6.     public function __construct() {
  7.         // …
  8.     }
  9.     public function create() {
  10.         // …
  11.     }
  12.     public function save() {
  13.         // …
  14.     }
  15.     public function delete() {
  16.         // …
  17.     }
  18. }

 

程序化方法常用于可以独立执行的特定任务。

 

[c-sharp] view plaincopy

  1. function capitalize($string) {
  2.     $ret = strtoupper($string[0]);
  3.     $ret .= strtolower(substr($string,1));
  4.     return $ret;
  5. }

 


 

15 – 阅读开源代码

开源项目是许多开发者一起构建的。这些项目必须保持高度的代码可读性,以便他们可以尽可能高效的协同工作。

因此,通读这些项目的源代码来观察这些开发者是如何工作的是非常棒的方法。

 


16 – 代码重构

当你“重构“,你在不改变功能的情况下调整代码。你可以把它看作是“清理”,为了改进代码质量和可读性。

这并不包括bug的修复或者添加新功能。你可以重构你之前编写的代码,当它们在你头脑你还保持新鲜的时候,以便于你两个月以后有可能回顾代码时更加可读和可重用。就像那句格言所说的一样:“尽早重构,经常重构“。

你可以在重构期间应用以上任何关于代码可读性的“最佳实践“。我希望你喜欢这篇文章!我遗忘了什么?请通过回复告知我。

10步让你成为更优秀的程序员

作者: Paul Firth  来源: 外刊IT评论  发布时间: 2013-01-01 20:29  阅读: 11031 次  推荐: 89   原文链接   [收藏]  

  英文原文:10 steps to becoming a better programmer

这篇文章要介绍的,是我作为专业程序员这些年来学到的能真正提高我的代码质量和整体工作效率的 10 件事情。

1. 永远不要复制代码

不惜任何代价避免重复的代码。如果一个常用的代码片段出现在了程序中的几个不同地方,重构它,把它放到一个自己的函数里。重复的代码会导致你的同事在读你的代码时产生困惑。而重复的代码如果在一个地方修改,在另外一个地方忘记修改,就会产生到处是 bug,它还会使你的代码体积变得臃肿。现代的编程语言提供了很好的方法来解决这些问题,例如,下面这个问题在以前很难解决,而如今使用 lambda 却很好实现:

/// <summary>
/// 一些函数含有部分重复代码
/// </summary>
void OriginalA()
{
    DoThingsA();
    // unique code
    DoThingsB();
}
/// <summary>
/// 另外一个含有部分重复代码的函数
/// </summary>
void OriginalB()
{
    DoThingsA();
    // 没有重复的代码
    DoThingsB();
}

现在我们重构含有部分相同代码的函数,用 delegate 模式重写它们:

/// <summary>
/// Encapsulate shared functionality
/// </summary>
/// <param name="action">User defined action</param>
void UniqueWrapper(Action action)
{
    DoThingsA();
    action();
    DoThingsB();
}
/// <summary>
/// New implmentation of A
/// </summary>
void NewA()
{
    UniqueWrapper(() =>
    {
        // unique code
    });
}
/// <summary>
/// New implementation of B
/// </summary>
void NewB()
{
    UniqueWrapper(() =>
    {
        // unique code
    });
}

2. 留意你开始分心的时候

当你发现自己在浏览 facebook 或微博,而不是在解决问题,这通常是一种你需要短暂休息的信号。离开办公桌,去喝一杯咖啡,或去跟同事聊 5 分钟。尽管这样做看起来有点反直觉,但长久去看,它会提高你的工作效率。

3. 不要匆忙赶任务而放弃原则

当带着压力去解决一个问题或修改一个 bug,你很容易失去自制,发现自己匆匆忙忙,甚至完全忘了一直坚持的重要的测试过程。这通常会导致更多的问题,会让你在老板或同事眼里显得很不专业。

4. 测试你完成的代码

你知道你的代码能做什么,而且试了一下,它确实好用,但你实际上需要充分的验证它。分析所有可能的边界情况,测试在所有可能的条件下它都能如期的工作。如果有参数,传递一些预期范围外的值。传递一个 null 值。如果可能,让同事看看你的代码,问他们能否弄坏它。单元测试是到达这种目的的常规方法。

5. 代码审查

提交你的代码之前,找个同事一起坐下来,向他解释你做了哪些修改。通常,这样做的过程中你就能发现代码中的错误,而不需要同事说一句话。这比自己审查自己的代码要有效的多得多。

6. 让代码更少

如果你发现写了大量的代码来解决一个简单的问题,你很可能做错了。下面的 boolean 用法是一个很好的例子:

if (numMines > 0)
{
   enabled=true;
}
else
{
   enabled=false;
}

这时你应该写成这样:

enabled = numMines > 0;

代码越少越好。这会使 bug 更少,重构可能性更小,出错的几率更小。要适度。可读性同等重要,你可不能这样做而使代码丧失可读性。

7. 为优雅的代码而努力

优雅的代码非常的易读,只用手边很少的代码、让机器做很少的运算就能解决问题。在各种环境中都做到代码优雅是很难的,但经过一段时间的编程,你会对优雅的代码是个什么样子有个初步的感觉。优雅的代码不会通过重构来获得。当你看到优雅的代码是会很高兴。你会为它自豪。例如,下面就是一个我认为是优雅的方式来计算多边形面积的方法:

static public double GetConvexPolygonArea (Vector2[] vertices)
{
    double area = 0;
    for (int i = 0; i < vertices.Length; i++)
    {
        Vector2 P0 = vertices[i];
        Vector2 P1 = vertices[(i + 1) % vertices.Length];
        area += P0.Wedge (P1);
    }
    return area / 2;
}

8. 编写不言自明的代码

勿庸置疑,注释是编程中很重要的一部分,但能够不言自明的代码更胜一筹,因为它能让你在看代码时就能理解它。函数名变量名要慎重选择,好的变量/方法名字放到语言语义环境中时,不懂编程的人都能看懂。例如:

void DamagePlayer (Player player, int damageAmount)
{
    if (!player.m_IsInvincible && !player.m_IsDead)
    {
        player.InflictDamage ( damageAmount );
    }
}

能自我说明的代码不能代替注释。注释是用来解释“为什么”的,而自我说明的代码是来描述“是什么”的。

9. 不要使用纯数字

直接把数字嵌入代码中是一种恶习,因为无法说明它们是代表什么的。当有重复时更糟糕——相同的数字在代码的多个地方出现。如果只修改了一个,而忘记了其它的。这就导致 bug。一定要用一个命名常量来代表你要表达的数字,即使它在代码里只出现一次。

10. 不要做手工劳动

当做一系列动作时,人类总是喜欢犯错误。如果你在做部署工作,并且不是一步能完成的,那你就是在做错事。尽量的让工作能自动化的完成,减少人为错误。当做工作量很大的任务时,这尤其重要。

11. 避免过早优化

当你要去优化一个已经好用的功能代码时,你很有可能会改坏它。优化只能发生在有性能分析报告指示需要优化的时候,通常是在一个项目开发的最后阶段。性能分析之前的优化活动纯属浪费时间,并且会导致 bug 出现。

好吧,我说是 10 个,但你却得到了额外赠送的一个!

这些就是我要说的,我希望它们能帮助你改进编程开发过程。

下次再见!祝快乐!

Cheers, Paul.

摘自:http://kb.cnblogs.com/page/168183/

如何编写出拥抱变化的代码?

http://www.csdn.net/article/2013-02-25/2814251-coding-change

发表于2013-02-26 09:06| 13321次阅读| 来源net.tutsplus| 70 条评论| 作者Patkos Csaba

摘要:编写高效优质的代码一直是程序员所追求的目标之一,那么什么样的代码才叫优质呢?其中最重要的莫过于易维护、易修改。本文作者从面向对象和SOLID两大方面,非常详细地总结了如何编写出易修改的代码,绝对让你受益匪浅。

在实际的开发中,编写出易维护和易接受变化的代码并非易事,想要实现可能更加困难重重:源码难于理解、依赖关系指向不明、耦合也很令人头疼。难道就真的就没有办法了吗?本文中我们一起探讨几个技术原则和一些编码理念,让你的代码跟着需求走,而且易维护易拓展。

介绍些面向对象方法

面向对象编程(OOP)是一种很受欢迎的编程思想,它保证了代码的组织性和重用性。软件公司采用OOP思想编程已经好多年了,如今仍然在项目开发中使用这一思想。OOP拥有一系列非常好的编程原则,如果使用恰当,它会让你的代码更好、更整洁和更易维护。

1.内聚力

这里的内聚力是指拥有一些共同的特征的东西而逐渐凝聚到一起,而不能在一起的东西则会被移除出去。可以用一个类来说明内聚力:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class ANOTCohesiveClass {
   private $firstNumber;
   private $secondNumber;
   private $length;
   private $width;
   function __construct($firstNumber$secondNumber) {
      $this->firstNumber = $firstNumber;
      $this->secondNumber = $secondNumber;
   }
   function setLength($length) {
      $this->length = $length;
   }
   function setHeight($height) {
      $this->width = $height;
   }
   function add() {
      return $this->firstNumber + $this->secondNumber;
   }
   function subtract() {
      return $this->firstNumber - $this->secondNumber;
   }
   function area() {
      return $this->length * $this->width;
   }
}

 

该例定义了一个类以及一些表示数字和大小的字段。而这些属性通过他们的名称来判断是否应该在一起。add()和substract()方法来对两个number进行操作,此外还定义了area()来操作length和width这两个字段。

这个类只负责各个独立的群体信息,显然,内聚力很低。重构上面的例子:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ACohesiveClass {
   private $firstNumber;
   private $secondNumber;
   function __construct($firstNumber$secondNumber) {
      $this->firstNumber = $firstNumber;
      $this->secondNumber = $secondNumber;
   }
   function add() {
      return $this->firstNumber + $this->secondNumber;
   }
   function subtract() {
      return $this->firstNumber - $this->secondNumber;
   }
}

重构以后,该类明显变成了高内聚特征的类。为什么?因为这个类里的每个部分都与另外一部分彼此联系。虽然在实际开发中编写出高内聚的类比较困难,但开发人员应该坚持这样做,坚持就是胜利。
2.正交性

就简单而言,正交是指隔离或排除副作用。一个方法、类或者模块改变了其他无关的方法、类或模块就不是正交。例如,飞机的黑匣子就具有正交性,它自身就具备电源、麦克风和传感器等这些功能。而它对外在的其他东西没有任何影响,它只提供一种机制,用来保存和检索飞行数据。

一个典型的非正交系统例子就是汽车电子设备。提高汽车的速度也存在些负面影响,比如会增加无线电音量,然而对汽车来说,速度并不是正交。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Calculator {
   private $firstNumber;
   private $secondNumber;
   function __construct($firstNumber$secondNumber) {
      $this->firstNumber = $firstNumber;
      $this->secondNumber = $secondNumber;
   }
   function add() {
      $sum $this->firstNumber + $this->secondNumber;
      if ($sum > 100) {
         (new AlertMechanism())->tooBigNumber($sum);
      }
      return $sum;
   }
   function subtract() {
      return $this->firstNumber - $this->secondNumber;
   }
}
class AlertMechanism {
   function tooBigNumber($number) {
      echo $number 'is too big!';
   }
}

在这个例子中,Calculator类里的add()方法里列了几个意想不到的行为:它生成AlertMechanism对象并调用其中的一个方法。实际上,该库的使用者并不希望消息被打印到屏幕上,相反,他们则是要计算数字之和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Calculator {
   private $firstNumber;
   private $secondNumber;
   function __construct($firstNumber$secondNumber) {
      $this->firstNumber = $firstNumber;
      $this->secondNumber = $secondNumber;
   }
   function add() {
      return $this->firstNumber + $this->secondNumber;
   }
   function subtract() {
      return $this->firstNumber - $this->secondNumber;
   }
}
class AlertMechanism {
   function checkLimits($firstNumber$secondNumber) {
      $sum = (new Calculator($firstNumber$secondNumber))->add();
      if ($sum > 100) {
         $this->tooBigNumber($sum);
      }
   }
   function tooBigNumber($number) {
      echo $number 'is too big!';
   }
}

这样明显好多了,AlertMechanish在Calculator中没有任何负面影响,相反,在任何需要弹出警告的地方都可以使用AlertMechanish。

3.依赖和耦合

大多数情况下,这两个单词是可以互换的,但是在某些情况下,又存在优先级关系。

那么,什么是依赖呢?当对象A需要使用对象B时,为了执行其规定的行为,我们说A依赖B。在OOP中,依赖是极其常见的。对象之间经常互相依赖才发挥功效。因此消除依赖是一项崇高的追求,这样做几乎是不可能的。控制依赖和减少依赖则是非常完美的。

就紧耦合(heavy-coupling)和松耦合(loose-coupling)而言,通常是指一个对象依赖于其他对象的程度。

在一个松耦合系统中,一个对象的变化会减少对其依赖对象的影响。在这样的系统中,类取决于接口而不是具体的实现(将会在下面提到)。这就是为什么松耦合系统对修改更加开放的原因。

Coupling in a Field

让我们看下面这个例子:

 

1
2
3
4
5
6
class Display {
   private $calculator;
   function __construct() {
      $this->calculator = new Calculator(1,2);
   }
}

 

这段代码很常见,在该例中,Display类依赖Calculator类并直接引用该类。Display类里的 $calculator字段属于Calculator类型。该对象和字段直接调用Calculator的构造函数。

 通过访问其他类方法进行耦合

大家可以先看下面的代码:

 

1
2
3
4
5
6
7
8
9
class Display {
   private $calculator;
   function __construct() {
      $this->calculator = new Calculator(1, 2);
   }
   function printSum() {
      echo $this->calculator->add();
   }
}

Display类调用Calculator对象的add()方法。这是另外一种耦合方式,一个类访问另外一个类的方法。

通过方法引用进行耦合

你也可以通过方法引用进行耦合:

 

1
2
3
4
5
6
7
8
9
10
11
12
class Display {
   private $calculator;
   function __construct() {
      $this->calculator = $this->makeCalculator();
   }
   function printSum() {
      echo $this->calculator->add();
   }
   function makeCalculator() {
      return new Calculator(1, 2);
   }
}

需引起注意的是,makeCalculator()方法返回一个Calculator对象,这也是一种依赖。

利用多态进行耦合

遗传可能是依赖里的最强表现形式。

 

1
2
3
4
5
class AdvancedCalculator extends Calculator {
   function sinus($value) {
      return sin($value);
   }
}

通过依赖注入降低耦合

开发人员可以通过依赖注入来降低耦合度,例如:

1
2
3
4
5
6
7
class Display {
   private $calculator;
   function __construct(Calculator $calculator = null) {
      $this->calculator = $calculator ? : $this->makeCalculator();
   }
// ... //
}

利用Display的构造函数对Calculator对象进行注入,从而减少了Display对Calculator类产生的依赖。

利用接口降低耦合

例如:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface CanCompute {
   function add();
   function subtract();
}
class Calculator implements CanCompute {
   private $firstNumber;
   private $secondNumber;
   function __construct($firstNumber$secondNumber) {
      $this->firstNumber = $firstNumber;
      $this->secondNumber = $secondNumber;
   }
   function add() {
      return $this->firstNumber + $this->secondNumber;
   }
   function subtract() {
      return $this->firstNumber - $this->secondNumber;
   }
}
class Display {
   private $calculator;
   function __construct(CanCompute $calculator = null) {
      $this->calculator = $calculator ? : $this->makeCalculator();
   }
   function printSum() {
      echo $this->calculator->add();
   }
   function makeCalculator() {
      return new Calculator(1, 2);
   }
}

 

该代码定义了一个CanCompute接口,在OOP中,接口可以看作一个抽象类型,它所定义的成员必须由类或结构来实现。在上述代码中,Calculator类来实现CanCompute接口。

Display构造函数期望有个对象来实现Cancompute接口,这时,Display的依赖对象Calculator被打破。然而,我们可以创建另一个类对象来实现Cancompute,并且传递一个对象到Display的构造函数中。Display现在只依赖于Cancompute接口,但即使这样依赖关系仍然是可选的。如果我们不传递任何参数给Display的构造函数,那么它将通过调用makeCalculator()方法来创建一个Calculator对象。这种技术经常被开发者们使用,尤其对驱动测试开发(TDD)极其有帮助。

SOLID原则

SOLID是一套代码编写守则,也就是大家常常说的敏捷开发原则,最初由Robert C. Martin所提出。使用它编写出来的代码不仅干净整洁,而且易维护、易修改和易扩展。实践表明,其在可维护性上有着非常积极的影响,更多资料大家可以阅读: Agile Software Development, Principles, Patterns, and Practices

SOLID所涵盖的话题非常广,下面我将会针对本文的主旨介绍一些简单易学的方法。

1.单一责任原则(SRP)

一个类只干一件事。听起来简单,但在实践中却可能相当难。

 

1
2
3
4
5
6
class Reporter {
   function generateIncomeReports();
   function generatePaymentsReports();
   function computeBalance();
   function printReport();
}

 

查看上面的代码,你认为该类的受益者会是哪个部门?会计部是用于收支平衡、财政部可能用来编写收入/支出报告,甚至归档部来打印和存档报告。然而每个部门都希望有属于自己的方法,并且根据自身需求来做些自定义的方法。

这样的类往往都是高内聚低耦合的。

2.Open-Closed原则(OCP)

类(和模块)应具备很好的功能扩展性,以及对现有功能具有一定的保护能力。让我们一起来看下典型的电风扇例子,你有一个开关来控制风扇:

 

1
2
3
4
5
6
7
8
9
10
11
12
class Switch_ {
   private $fan;
   function __construct() {
      $this->fan = new Fan();
   }
   function turnOn() {
      $this->fan->on();
   }
   function turnOff() {
      $this->fan->off();
   }
}

 

这段代码创建了Switch_类,用来创建和控制Fan对象。注意这里的下划线,在PHP中是不允许把类名定义为Switch的。

这时,你的老板希望能利用该开关控制电风扇上的电灯,那么你就不得不修改Switch_这个类。

对现有代码进行修改存在一部分风险,很有可能对系统其他部分产生影响。所以在添加新功能时的最好的方法是避开现有功能。

在OOP中,你可以发现Switch_对Fan类有很强的依赖性。这正是我们的问题所在,基于此,做出如下修改:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Switchable {
   function on();
   function off();
}
class Fan implements Switchable {
   public function on() {
      // code to start the fan
   }
   public function off() {
      // code to stop the fan
   }
}
class Switch_ {
   private $switchable;
   function __construct(Switchable $switchable) {
      $this->switchable = $switchable;
   }
   function turnOn() {
      $this->switchable->on();
   }
   function turnOff() {
      $this->switchable->off();
   }
}

 

该代码定义了一个Switchable接口,它里面所定义的方法需要开关启用选项来实现。Fan对象实现Switchable和Switch_并且接受一个参数到Switchable对象的构造函数里。

这样做有哪些好处?

首先,该解决方案打破了Switch_和Fan之间的依赖关系。Switch_不知道它要开启风扇,并且也不关心。其次引进的Light类不会影响Switch_或Switchable。难道你想用Switch_类来控制Light对象吗?代码如下:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Light implements Switchable {
   public function on() {
      // code to turn ligh on
   }
   public function off() {
      // code to turn light off
   }
}
class SomeWhereInYourCode {
   function controlLight() {
      $light new Light();
      $switch new Switch_($light);
      $switch->turnOn();
      $switch->turnOff();
   }
}

 

3.Liskov替换原则(LSP)

LSP是指子类永不打破父类的功能,这点是非常重要的。用户定义一个子类只是希望能实现其自有功能,而不是去影响原来的功能。

乍看有点困惑,还是让我们一起来看看代码吧:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
class Rectangle {
   private $width;
   private $height;
   function setWidth($width) {
      $this->width = $width;
   }
   function setHeigth($heigth) {
      $this->height = $heigth;
   }
   function area() {
      return $this->width * $this->height;
   }
}

 

定义一个简单的Rectangle类,我们可以设置它的高度和宽度,并且area()方法可以计算出该矩形的面积。再看下面例子:

 

1
2
3
4
5
6
7
class Geometry {
   function rectArea(Rectangle $rectangle) {
      $rectangle->setWidth(10);
      $rectangle->setHeigth(5);
      return $rectangle->area();
   }
}

 

rectArea()方法接受一个Rectangle对象作为一个参数,设置其高度和宽度并且返回该图形的面积。

正方形乃是矩形中的一个特殊图形,我们定义Square类来继承Rectangle:

 

1
2
3
class Square extends Rectangle {
   // What code to write here?
}

 

我们有好几种方法来重写area()方法并且返回该正方形的宽度:

 

1
2
3
4
5
6
7
8
9
10
class Rectangle {
   protected $width;
   protected $height;
   // ... //
}
class Square extends Rectangle {
   function area() {
      return $this->width ^ 2;
   }
}

 

把Rectangle的字段改为protected,好让Square有访问的权限。从几何的角度来看是非常合理的,因为正方形的边长是相等的,所以返回正方形的宽度是非常合理的。

然而从编程的角度来看又存在一个问题;如果Square是一个Rectangle,把它馈入到Geometry类是没有任何问题的,但这样做以后,Geometry的代码就显的多余,毫无意义可言。它设置了高度和宽度两个值,这也就是为什么square不是rectangle编程。LSP正很好是说明了这一点。

4.接口隔离原则(ISP)

该原则主要集中用在把大接口分成多个小接口和特殊的接口。基本思路是在同一个类中,不同的用户不应该知道不同的接口——除非该用户需要用到那个接口。即使一个用户不需要使用该类的所有方法,但它仍然依赖于这些方法。所以为什么不根据用户需要定义相应的接口呢?

想象下,如果我们要实现一个股票市场应用,我们要有一个经纪人(Broker)来购买和出售股票,并且报告每天的收益和损失。一个简单的实现方法是定义一个Broker接口,一个NYSEBroker类用来实现Broker和一些用户的接口类:创建交易(TransactionUI)和写报告(DailyReporter)。代码可以类似下面这样:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
interface Broker {
   function buy($symbol$volume);
   function sell($symbol$volume);
   function dailyLoss($date);
   function dailyEarnings($date);
}
class NYSEBroker implements Broker {
   public function buy($symbol$volume) {
      // implementsation goes here
   }
   public function currentBalance() {
      // implementsation goes here
   }
   public function dailyEarnings($date) {
      // implementsation goes here
   }
   public function dailyLoss($date) {
      // implementsation goes here
   }
   public function sell($symbol$volume) {
      // implementsation goes here
   }
}
class TransactionsUI {
   private $broker;
   function __construct(Broker $broker) {
      $this->broker = $broker;
   }
   function buyStocks() {
      // UI logic here to obtain information from a form into $data
      $this->broker->buy($data['sybmol'], $data['volume']);
   }
   function sellStocks() {
      // UI logic here to obtain information from a form into $data
      $this->broker->sell($data['sybmol'], $data['volume']);
   }
}
class DailyReporter {
   private $broker;
   function __construct(Broker $broker) {
      $this->broker = $broker;
   }
   function currentBalance() {
      echo 'Current balace for today ' date(time()) . "\n";
      echo 'Earnings: ' $this->broker->dailyEarnings(time()) . "\n";
      echo 'Losses: ' $this->broker->dailyLoss(time()) . "\n";
   }
}

 

虽然这段代码可以正常工作,但它违反了ISP。DailyReporter和TransactionUI都依赖Broker接口。然而,它们只使用接口的一部分。TransactionUI使用buy()和sell()方法,而DailyReporter只用到dailyEarnings()和dailyLoss()方法。

你怀疑Broker没有内聚力,因为它的一些方法没有任何相关性。也许你说的对,但是具体答案还得由Broker说了算;销售和购买可能与当前的盈余有相当大的关系。例如当亏本的时候有可能就不会执行购买操作。

此时,你可能会说Broker违反了SRP,因为有两个类以不同的方式在使用它,可能有两个不同的执行者。好吧,其实它并没有违反SRP。唯一的执行者就是Broker。他会根据当前的形式做出购买/出售操作,其最终的依赖对象是整个系统和业务。

毫无疑问,上述代码肯定是违反了ISP,两个UI类都依赖于整个Broker。这是很常见的问题,改变下观点,代码可以这样修改:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
interface BrokerTransactions {
   function buy($symbol$volume);
   function sell($symbol$volume);
}
interface BrokerStatistics {
   function dailyLoss($date);
   function dailyEarnings($date);
}
class NYSEBroker implements BrokerTransactions, BrokerStatistics {
   public function buy($symbol$volume) {
      // implementsation goes here
   }
   public function currentBalance() {
      // implementsation goes here
   }
   public function dailyEarnings($date) {
      // implementsation goes here
   }
   public function dailyLoss($date) {
      // implementsation goes here
   }
   public function sell($symbol$volume) {
      // implementsation goes here
   }
}
class TransactionsUI {
   private $broker;
   function __construct(BrokerTransactions $broker) {
      $this->broker = $broker;
   }
   function buyStocks() {
      // UI logic here to obtain information from a form into $data
      $this->broker->buy($data['sybmol'], $data['volume']);
   }
   function sellStocks() {
      // UI logic here to obtain information from a form into $data
      $this->broker->sell($data['sybmol'], $data['volume']);
   }
}
class DailyReporter {
   private $broker;
   function __construct(BrokerStatistics $broker) {
      $this->broker = $broker;
   }
   function currentBalance() {
      echo 'Current balace for today ' date(time()) . "\n";
      echo 'Earnings: ' $this->broker->dailyEarnings(time()) . "\n";
      echo 'Losses: ' $this->broker->dailyLoss(time()) . "\n";
   }
}

 

修改后的代码明显变的有意义而且尊重了ISP。DailyReporter只依赖BrokerStatistics,它无需关心和知道出售和购买这两个操作。另一方面,TransactionUI只关心购买和出售。NYSEBroker和先前的定义是一样的,实现BrokerTransactions和BrokerStatistics接口。

更复杂的例子你可以前往Rober C.Martin博客上查看 The Interface Segregation Principle里的首篇论文。

5.依赖倒置原则(DIP)

这条原则指出高层模块不应该依赖低层模块,两者都应该依赖于抽象。抽象不应该依赖细节,细节反过来应依赖于抽象。简单地说,你应该尽可能的依赖于抽象而不是实现。

DIP的诀窍是你想反转依赖,但是又想一直保持着整个控制流。回顾下OCP(Switch和Light类),在原始实现中是直接利用开关来控制灯的。

 

你会看到整个依赖和控制流都是由Switch流向Light。当不想直接控制Light时,你可以引进接口这一概念。

 

非常神奇!引进接口后,代码同时满足了DIP和OCP两大原则。正如你上图所看到的,倒置了依赖,但整个控制流是不变的。

高级设计

关于代码的另一重要方面是高级设计和通用体系结构。一个混乱的架构所产生的代码往往是很难修改的,所以保持一个干净整洁的架构是必不可少的,第一步就是理解如何根据不同的内容分离代码。

 

在这张图中,最主要的部分是业务逻辑,它能够如预期那样正常有效的工作并且与其他部分不存在任何瓜葛。站在高级设计角度可以看作为正交性。

从右边的“main”开始看,箭头进入应用程序——创建对象工厂。一个理想的解决方案是从各个特定的工厂中得到相应的对象,但这有点不切实际。不过当有机会这样做的时候还是要使用,并且让它们保持在业务逻辑之外。

再看底部,定义持久层(数据库、文件访问、网络通信)用来保证信息的持久性。业务逻辑层是没有对象知道持久层是如何工作的。

左边则是交互机制。MVC比如Laravel、CakePHP,只能是交付机制而已。

当你看到应用程序架构或目录时,你应该注意其架构是说明程序将要做什么,而不是使用什么技术或数据库。

最后,为了确保所有的依赖项都指向业务逻辑层。用户接口、工厂、数据库则是具体的实现,而你永远不要只依赖于它们。依赖倒置指向业务逻辑模块,无需修改业务逻辑的依赖关系即可允许我们改变依赖。

关于设计模型

在使代码变得易于修改和理解的过程中,设计模型扮演着非常重要的角色。从结构的角度来看,设计模式显然是很有好处的,它们是行之有效并且深思熟虑的解决方案。更多关于设计模式内容,可以前往 Tuts+ Premium course 

测试的力量

测试驱动开发(TDD)所编写出来的代码是很容易测试的。TDD迫使你尊重以上原则来编写代码,从而使你的程序更易被测试。单元测试运行速度很快,应该非常快,当你在一个类里使用10个对象来测试一个单独方法时,你的代码很有可能是有问题的。

总结

俗话说,实践乃是检验真理的唯一标准,所以开发者只有在平时的工作中坚持使用这些原则才能编写出理想的代码。与此同时,不要轻易满足于自己所编写出的代码,要努力让你的代码易于维护、干净并且拥抱变化。(编译/张红月 责编/王然)

来自: net.tutsplus

Copyright © 2024 优大网 浙ICP备13002865号

回到顶部 ↑