翻譯自文章 Examples Of Refactoring PHP Code For Better Readability
重構(gòu)代碼是指當(dāng)你重構(gòu)已有代碼時(shí)不改變其外部行為。更簡(jiǎn)單的說(shuō)你的目標(biāo)是在不改變?cè)泄δ艿那疤嵯掳?壞"的代碼變好匀哄。
網(wǎng)上有許多關(guān)于重構(gòu)代碼的指南蝌戒。但是,我發(fā)現(xiàn)很多人只是向你介紹了寫(xiě)好代碼和結(jié)構(gòu)的思想,并沒(méi)有向你展示如何去把你的"壞的"代碼重構(gòu)成"好的"代碼继准。他們可能會(huì)談及比較高級(jí)的理論像可讀性、擴(kuò)展性矮男、穩(wěn)定性移必、易于測(cè)試、減少?gòu)?fù)雜性之類(lèi)的昂灵,可是這些都是重構(gòu)過(guò)程中要達(dá)到的目標(biāo)避凝,他們并沒(méi)有向我們展示實(shí)際中這些情況的例子是怎么樣的。
在這篇文章中我不談?wù)撃闶裁磿r(shí)候需要重構(gòu)(個(gè)人認(rèn)為你需要做這些當(dāng)你遇到壞代碼時(shí))眨补,我也不談?wù)摓槭裁次覀冃枰貥?gòu)管削。我只想關(guān)注一些在重構(gòu)代碼中會(huì)遇到的,共同的撑螺、可以上手的原理含思,我也會(huì)給你一些像真實(shí)情況的代碼例子。在本文中我會(huì)使用php代碼(WordPress是用php寫(xiě)的)但是這些原理你可以應(yīng)用在任何語(yǔ)言。
不要重復(fù)你自己(DRY)
可能你最經(jīng)常聽(tīng)到的編程規(guī)范就是 不要重復(fù)你自己(DRY)
含潘。如果你發(fā)現(xiàn)你自己把相同的代碼重復(fù)了幾次饲做,那你就應(yīng)該把功能封裝到它所屬的類(lèi)或方法中然后使用這個(gè)類(lèi)或方法避免重復(fù)的代碼。這意味著當(dāng)你幾個(gè)月后代碼出了問(wèn)題只需要修改一個(gè)地方的代碼就可以了遏弱。
一個(gè)很好的例子是盆均,當(dāng)兩個(gè)不同但相似的類(lèi)需要一些相同的功能,你應(yīng)該創(chuàng)建一個(gè) abstract class
然后讓這兩個(gè)類(lèi)繼承這個(gè)abstract class
而不是在兩個(gè)類(lèi)當(dāng)中重復(fù)代碼漱逸。
修改前例子:
<?php
class AwesomeAddon {
private $settings;
public function __construct( $settings ) {
$this->set_settings( $settings );
}
protected function set_settings( $settings ) {
if ( ! is_array( $settings ) ) {
throw new \Exception( 'Invalid settings' );
}
$this->settings = $settings;
}
protected function do_something_awesome() {
//...
}
}
class EvenMoreAwesomeAddon {
private $settings;
public function __construct( $settings ) {
$this->set_settings( $settings );
}
protected function set_settings( $settings ) {
if ( ! is_array( $settings ) ) {
throw new \Exception( 'Invalid settings' );
}
$this->settings = $settings;
}
protected function do_something_even_more_awesome() {
//...
}
}
修改后代碼:
<?php
abstract class Addon {
protected $settings;
protected function set_settings( $settings ) {
if ( ! is_array( $settings ) ) {
throw new \Exception( 'Invalid settings' );
}
$this->settings = $settings;
}
}
class AwesomeAddon extends Addon {
public function __construct( $settings ) {
$this->set_settings( $settings );
}
protected function do_something_awesome() {
//...
}
}
class EvenMoreAwesomeAddon extends Addon {
public function __construct( $settings ) {
$this->set_settings( $settings );
}
protected function do_something_even_more_awesome() {
//...
}
}
這只是個(gè)簡(jiǎn)單的關(guān)于構(gòu)造代碼避免重復(fù)的例子泪姨。
分解復(fù)雜函數(shù)
復(fù)雜的函數(shù)或方法是編程中另一個(gè)會(huì)導(dǎo)致技術(shù)負(fù)債和難以閱讀的原因。
"程序是寫(xiě)給人看的饰抒,然后順便讓機(jī)器執(zhí)行" —— Harold Abelson
讓別人能讀懂和理解你寫(xiě)的代碼是最重要的事肮砾,所以讓復(fù)雜函數(shù)變得好理解的方式是把它拆成更小的、更容易理解的小塊袋坑。
下面是一個(gè)復(fù)雜的函數(shù)仗处。不要擔(dān)心你看不懂他在寫(xiě)什么,你只要看一眼他有多復(fù)雜就可以了枣宫。
<?php
function upload_attachment_to_s3( $post_id, $data = null, $file_path = null, $force_new_s3_client = false, $remove_local_files = true ) {
$return_metadata = null;
if ( is_null( $data ) ) {
$data = wp_get_attachment_metadata( $post_id, true );
} else {
// As we have passed in the meta, return it later
$return_metadata = $data;
}
if ( is_wp_error( $data ) ) {
return $data;
}
// Allow S3 upload to be hijacked / cancelled for any reason
$pre = apply_filters( 'as3cf_pre_upload_attachment', false, $post_id, $data );
if ( false !== $pre ) {
if ( ! is_null( $return_metadata ) ) {
// If the attachment metadata is supplied, return it
return $data;
}
$error_msg = is_string( $pre ) ? $pre : __( 'Upload aborted by filter \'as3cf_pre_upload_attachment\'', 'amazon-s3-and-cloudfront' );
return $this->return_upload_error( $error_msg );
}
if ( is_null( $file_path ) ) {
$file_path = get_attached_file( $post_id, true );
}
// Check file exists locally before attempting upload
if ( ! file_exists( $file_path ) ) {
$error_msg = sprintf( __( 'File %s does not exist', 'amazon-s3-and-cloudfront' ), $file_path );
return $this->return_upload_error( $error_msg, $return_metadata );
}
$file_name = basename( $file_path );
$type = get_post_mime_type( $post_id );
$allowed_types = $this->get_allowed_mime_types();
// check mime type of file is in allowed S3 mime types
if ( ! in_array( $type, $allowed_types ) ) {
$error_msg = sprintf( __( 'Mime type %s is not allowed', 'amazon-s3-and-cloudfront' ), $type );
return $this->return_upload_error( $error_msg, $return_metadata );
}
$acl = self::DEFAULT_ACL;
// check the attachment already exists in S3, eg. edit or restore image
if ( ( $old_s3object = $this->get_attachment_s3_info( $post_id ) ) ) {
// use existing non default ACL if attachment already exists
if ( isset( $old_s3object['acl'] ) ) {
$acl = $old_s3object['acl'];
}
// use existing prefix
$prefix = dirname( $old_s3object['key'] );
$prefix = ( '.' === $prefix ) ? '' : $prefix . '/';
// use existing bucket
$bucket = $old_s3object['bucket'];
// get existing region
if ( isset( $old_s3object['region'] ) ) {
$region = $old_s3object['region'];
};
} else {
// derive prefix from various settings
if ( isset( $data['file'] ) ) {
$time = $this->get_folder_time_from_url( $data['file'] );
} else {
$time = $this->get_attachment_folder_time( $post_id );
$time = date( 'Y/m', $time );
}
$prefix = $this->get_file_prefix( $time );
// use bucket from settings
$bucket = $this->get_setting( 'bucket' );
$region = $this->get_setting( 'region' );
if ( is_wp_error( $region ) ) {
$region = '';
}
}
$acl = apply_filters( 'as3cf_upload_acl', $acl, $data, $post_id );
$s3object = array(
'bucket' => $bucket,
'key' => $prefix . $file_name,
'region' => $region,
);
// store acl if not default
if ( $acl != self::DEFAULT_ACL ) {
$s3object['acl'] = $acl;
}
$s3client = $this->get_s3client( $region, $force_new_s3_client );
$args = array(
'Bucket' => $bucket,
'Key' => $prefix . $file_name,
'SourceFile' => $file_path,
'ACL' => $acl,
'ContentType' => $type,
'CacheControl' => 'max-age=31536000',
'Expires' => date( 'D, d M Y H:i:s O', time() + 31536000 ),
);
$args = apply_filters( 'as3cf_object_meta', $args, $post_id );
$files_to_remove = array();
if ( file_exists( $file_path ) ) {
$files_to_remove[] = $file_path;
try {
$s3client->putObject( $args );
} catch ( Exception $e ) {
$error_msg = sprintf( __( 'Error uploading %s to S3: %s', 'amazon-s3-and-cloudfront' ), $file_path, $e->getMessage() );
return $this->return_upload_error( $error_msg, $return_metadata );
}
}
delete_post_meta( $post_id, 'amazonS3_info' );
add_post_meta( $post_id, 'amazonS3_info', $s3object );
$file_paths = $this->get_attachment_file_paths( $post_id, true, $data );
$additional_images = array();
$filesize_total = 0;
$remove_local_files_setting = $this->get_setting( 'remove-local-file' );
if ( $remove_local_files_setting ) {
$bytes = filesize( $file_path );
if ( false !== $bytes ) {
// Store in the attachment meta data for use by WP
$data['filesize'] = $bytes;
if ( is_null( $return_metadata ) ) {
// Update metadata with filesize
update_post_meta( $post_id, '_wp_attachment_metadata', $data );
}
// Add to the file size total
$filesize_total += $bytes;
}
}
foreach ( $file_paths as $file_path ) {
if ( ! in_array( $file_path, $files_to_remove ) ) {
$additional_images[] = array(
'Key' => $prefix . basename( $file_path ),
'SourceFile' => $file_path,
);
$files_to_remove[] = $file_path;
if ( $remove_local_files_setting ) {
// Record the file size for the additional image
$bytes = filesize( $file_path );
if ( false !== $bytes ) {
$filesize_total += $bytes;
}
}
}
}
if ( $remove_local_files ) {
if ( $remove_local_files_setting ) {
// Allow other functions to remove files after they have processed
$files_to_remove = apply_filters( 'as3cf_upload_attachment_local_files_to_remove', $files_to_remove, $post_id, $file_path );
// Remove duplicates
$files_to_remove = array_unique( $files_to_remove );
// Delete the files
$this->remove_local_files( $files_to_remove );
}
}
// Store the file size in the attachment meta if we are removing local file
if ( $remove_local_files_setting ) {
if ( $filesize_total > 0 ) {
// Add the total file size for all image sizes
update_post_meta( $post_id, 'wpos3_filesize_total', $filesize_total );
}
} else {
if ( isset( $data['filesize'] ) ) {
// Make sure we don't have a cached file sizes in the meta
unset( $data['filesize'] );
if ( is_null( $return_metadata ) ) {
// Remove the filesize from the metadata
update_post_meta( $post_id, '_wp_attachment_metadata', $data );
}
delete_post_meta( $post_id, 'wpos3_filesize_total' );
}
}
if ( ! is_null( $return_metadata ) ) {
// If the attachment metadata is supplied, return it
return $data;
}
return $s3object;
}
如果整個(gè)函數(shù)變成下面這樣會(huì)更好理解
<?php
function upload_attachment_to_s3( $post_id, $data = null, $file_path = null, $force_new_s3_client = false, $remove_local_files = true ) {
$return_metadata = $this->get_attachment_metadata( $post_id );
if ( is_wp_error( $return_metadata ) ) {
return $return_metadata;
}
// Allow S3 upload to be hijacked / cancelled for any reason
$pre = apply_filters( 'as3cf_pre_upload_attachment', false, $post_id, $data );
if ( $this->upload_should_be_cancelled( $pre ) ) {
return $pre;
}
// Check file exists locally before attempting upload
if ( ! $this->local_file_exists() ) {
$error_msg = sprintf( __( 'File %s does not exist', 'amazon-s3-and-cloudfront' ), $file_path );
return $this->return_upload_error( $error_msg, $return_metadata );
}
// check mime type of file is in allowed S3 mime types
if ( ! $this->is_valid_mime_type() ) {
$error_msg = sprintf( __( 'Mime type %s is not allowed', 'amazon-s3-and-cloudfront' ), $type );
return $this->return_upload_error( $error_msg, $return_metadata );
}
$s3object = $this->get_attachment_s3_info( $post_id );
$acl = $this->get_s3object_acl( $s3object );
$s3client = $this->get_s3client( $region, $force_new_s3_client );
$args = array(
'Bucket' => $s3object['bucket'],
'Key' => $s3object['key'],
'SourceFile' => $s3object['source_file'],
'ACL' => $acl,
'ContentType' => $s3object['mime_type'],
'CacheControl' => 'max-age=31536000',
'Expires' => date( 'D, d M Y H:i:s O', time() + 31536000 ),
);
$s3client->putObject( $args );
$this->maybe_remove_files( $args, $s3object );
return $s3object;
}
這是很好閱讀和理解的婆誓。簡(jiǎn)單的把大塊的代碼分成小的代碼庫(kù)是很好的。
有個(gè)事情需要記住的是镶柱,不要擔(dān)心你用很長(zhǎng)的名字當(dāng)方法名旷档,記住你的目標(biāo)是可讀性,所以如果只用簡(jiǎn)單的名字命名的話(huà)它會(huì)讓你的代碼變得很難以理解歇拆。舉個(gè)例子:
$this->get_att_inf( $post_id );
比下面的代碼難以理解:
$this->get_attachment_s3_info( $post_id );
分解復(fù)雜的條件
你應(yīng)該見(jiàn)過(guò)像下面這樣條件很長(zhǎng)的例子:
<?php
if ( isset( $settings['wp-uploads'] ) && $settings['wp-uploads'] && in_array( $key, array( 'copy-to-s3', 'serve-from-s3' ) ) ) {
return '1';
}
帶條件的長(zhǎng)段代碼很難閱讀和理解鞋屈。一個(gè)簡(jiǎn)單的解決方案是將條件代碼提取為明確命名的方法。例如:
<?php
if ( upload_is_valid( $settings, $key ) ) {
return '1';
}
function upload_is_valid( $settings, $key ) {
return isset( $settings['wp-uploads'] ) && $settings['wp-uploads'] && in_array( $key, array( 'copy-to-s3', 'serve-from-s3' ) );
}
這使得你的代碼對(duì)于以后的維護(hù)者來(lái)說(shuō)更容易理解故觅。只要條件方法是明確命名的厂庇,不用看代碼也很容易理解它是做什么的。這種做法也被稱(chēng)為聲明式編程输吏。
用守衛(wèi)子句GUARD CLAUSES
替代嵌套條件
另一種重構(gòu)復(fù)雜條件的方法是使用所謂的“守衛(wèi)子句”权旷。 Guard子句簡(jiǎn)單地提取所有導(dǎo)致調(diào)用異常或立即從方法返回值的條件贯溅,把它放在方法的開(kāi)始位置拄氯。例如:
<?php
function get_setting( $key, $default = '' ) {
$settings = $this->get_settings();
// If legacy setting set, migrate settings
if ( isset( $settings['wp-uploads'] ) && $settings['wp-uploads'] && in_array( $key, array( 'copy-to-s3', 'serve-from-s3' ) ) ) {
return $default;
} else {
// Turn on object versioning by default
if ( 'object-versioning' == $key && ! isset( $settings['object-versioning'] ) ) {
return $default;
} else {
// Default object prefix
if ( 'object-prefix' == $key && ! isset( $settings['object-prefix'] ) ) {
return $this->get_default_object_prefix();
} else {
if ( 'use-yearmonth-folders' == $key && ! isset( $settings['use-yearmonth-folders'] ) ) {
return get_option( 'uploads_use_yearmonth_folders' );
} else {
$value = parent::get_setting( $key, $default );
return apply_filters( 'as3cf_setting_' . $key, $value );
}
}
}
}
return $default;
}
這里你可以看到如果方法變得更復(fù)雜你可以多快結(jié)束"條件地獄"。但是如果你使用守衛(wèi)子句來(lái)重構(gòu)方法它浅,它將變成下面這樣:
<?php
function get_setting( $key, $default = '' ) {
$settings = $this->get_settings();
// If legacy setting set, migrate settings
if ( isset( $settings['wp-uploads'] ) && $settings['wp-uploads'] && in_array( $key, array( 'copy-to-s3', 'serve-from-s3' ) ) ) {
return $default;
}
// Turn on object versioning by default
if ( 'object-versioning' == $key && ! isset( $settings['object-versioning'] ) ) {
return $default;
}
// Default object prefix
if ( 'object-prefix' == $key && ! isset( $settings['object-prefix'] ) ) {
return $this->get_default_object_prefix();
}
// Default use year and month folders
if ( 'use-yearmonth-folders' == $key && ! isset( $settings['use-yearmonth-folders'] ) ) {
return get_option( 'uploads_use_yearmonth_folders' );
}
$value = parent::get_setting( $key, $default );
return apply_filters( 'as3cf_setting_' . $key, $value );
}
現(xiàn)在即使方法變得復(fù)雜译柏,一段時(shí)間后他也不會(huì)變成維護(hù)的難題。
使用函數(shù)方法重構(gòu)循環(huán)和條件
這是一個(gè)比較高級(jí)的重構(gòu)方法姐霍,它被大量使用在函數(shù)式編程和類(lèi)庫(kù)中(這種類(lèi)型的方法在javascript領(lǐng)域中經(jīng)常使用)鄙麦。你可能有聽(tīng)說(shuō)過(guò)map
和reduce
也想知道他們是什么并且如何使用典唇,事實(shí)證明這些方法可以大幅度提高你代碼的可讀性。
這個(gè)例子的靈感來(lái)自Adam Wathan關(guān)于這個(gè)主題的很棒的視頻(你應(yīng)該看看他即將出版的書(shū))胯府,基于Laravel Collections介衔。不過(guò),我已經(jīng)調(diào)整了該示例骂因,基于標(biāo)準(zhǔn)PHP函數(shù)炎咖。
讓我們看看兩個(gè)常見(jiàn)的場(chǎng)景,并看看如何使用函數(shù)方法來(lái)改進(jìn)它們寒波。我們的示例從API獲取一堆$events
塘装,然后根據(jù)事件類(lèi)型計(jì)算得分:
<?php
$events = file_get_contents( 'https://someapi.com/events' );
$types = array();
foreach ( $events as $event ) {
$types[] = $event->type;
}
$score = 0;
foreach ( $types as $type ) {
switch ( $type ) {
case 'type1':
$score += 2;
break;
case 'type2':
$score += 5;
break;
case 'type3':
$score += 10;
break;
default:
$score += 1;
break;
}
}
我們可以改進(jìn)的第一件事是用map
函數(shù)替換foreach
循環(huán)。當(dāng)你想從現(xiàn)有的數(shù)組中創(chuàng)建一個(gè)新的數(shù)組時(shí)影所,可以使用map
函數(shù)。在我們的例子中僚碎,我們從$events
數(shù)組創(chuàng)建一個(gè)$ types
數(shù)組猴娩。 PHP有一個(gè)array_map
函數(shù),可以讓我們把上面的第一個(gè)foreach
編寫(xiě)成如下所示:
<?php
$types = array_map( function( $event ) {
return $event->type;
}, $events );
提示:要使用匿名函數(shù)勺阐,需要的php版本為卷中,PHP 5.3+
我們可以做的第二件事情是,把大的switch
語(yǔ)句分解渊抽,讓它更簡(jiǎn)單的進(jìn)行下去
<?php
$scores = array(
'type1' => 2,
'type2' => 5,
'type3' => 10,
);
$score = 0;
foreach ( $types as $type ) {
$score += isset( $scores[$type] ) ? $scores[$type] : 1;
}
實(shí)際上我們可以走的更遠(yuǎn)一步蟆豫,通過(guò)php的 array_reduce
方法在單個(gè)方法內(nèi)計(jì)算$score
的值。一個(gè)reduce
方法接收一個(gè)數(shù)組的值然后把他們歸成一個(gè)值:
<?php
$scores = array(
'type1' => 2,
'type2' => 5,
'type3' => 10,
);
$score = array_reduce( $types, function( $result, $type ) use ( $scores ) {
return $result += isset( $scores[$type] ) ? $scores[$type] : 1;
} );
把現(xiàn)在有的代碼合并起來(lái)
<?php
$events = file_get_contents( 'https://someapi.com/events' );
$types = array_map( function( $event ) {
return $event->type;
}, $events );
$scores = array(
'type1' => 2,
'type2' => 5,
'type3' => 10,
);
$score = array_reduce( $types, function( $result, $type ) use ( $scores ) {
return $result += isset( $scores[$type] ) ? $scores[$type] : 1;
} );
好多了懒闷∈酰看不到循環(huán)或則條件了。你可以想象愤估,如果獲取$types
和計(jì)算$score
變得更加復(fù)雜的話(huà)帮辟,那么在調(diào)用的方法里面重構(gòu)map
和reduce
方法是比較容易的。現(xiàn)在可以這樣做了玩焰,我們已經(jīng)降低復(fù)雜度到一個(gè)函數(shù)了由驹。
進(jìn)一步閱讀
我只是在這篇文章中講了重構(gòu)的表面知識(shí)。還有很多我可以談?wù)摰膬?nèi)容昔园,但是希望這可以讓你對(duì)重構(gòu)代碼的實(shí)際情況有個(gè)小的了解蔓榄。
如果你想深入了解,我推薦SourceMaking重構(gòu)指南默刚。它涵蓋了大量主題甥郑,并且每個(gè)主題都有一個(gè)示例,因此您可以準(zhǔn)確了解重構(gòu)羡棵。