ฉันเคยอ่านหนังสือ "Clean Code" ของ "Robert C. Martin" เมื่อสองสามปีที่แล้ว เป็นหนังสือที่ยอดเยี่ยม โดยเฉพาะสำหรับผู้ที่อยู่ในช่วงเริ่มต้นอาชีพการงาน ช่วยให้คุณเป็นนักพัฒนา/วิศวกรซอฟต์แวร์ที่เป็นผู้ใหญ่มากขึ้น และเขียนโค้ดคุณภาพได้บ่อยขึ้น
ฉันรวบรวมเคล็ดลับ คำแนะนำ และแนวทางปฏิบัติที่ฉันได้เรียนรู้จากหนังสือเล่มนี้ และจะเผยแพร่ออกเป็นหลายส่วน
โปรดทราบว่ามีตัวอย่างคำพูดและโค้ดที่ฉันใช้จากหนังสือต้นฉบับในชุดนี้
เพื่อเป็นการไม่ให้เสียเวลา มาเริ่มกันเลย
#1 ฟังก์ชั่นสั้นๆ ดีกว่า
สิ่งที่ลุงบ๊อบพูดเกี่ยวกับความยาวของฟังก์ชันก็คือ ยิ่งฟังก์ชันมีขนาดเล็กเท่าไรก็ยิ่งดีเท่านั้น
เขาแนะนำว่าบล็อกของโค้ดภายในคำสั่ง if
, คำสั่ง else
และคำสั่ง while
ควรเป็นเพียงบรรทัดเดียว นอกจากนี้ ระดับการเยื้องของฟังก์ชันไม่ควรเกินหนึ่งหรือสอง
ถือเป็นแนวปฏิบัติที่ดีถ้าคุณทำได้ ไม่สามารถปฏิบัติตามกฎนี้อย่างเคร่งครัดเสมอไป แต่พยายามทำให้มากที่สุด
#2 แนะนำตัวแปรอินสแตนซ์เมื่อเป็นไปได้
การส่งตัวแปรอินสแตนซ์แทนพารามิเตอร์ดั้งเดิมไปยังฟังก์ชันเป็นความคิดที่ดีเมื่อเหมาะสม แต่เมื่อไหร่จะเหมาะสมล่ะ?
พิจารณาฟังก์ชันขนาดใหญ่ที่มีตัวแปรหลายตัวประกาศอยู่ภายใน สมมติว่าคุณต้องการแยกส่วนเล็กๆ ของฟังก์ชันนั้นออกเป็นฟังก์ชันที่แยกจากกัน อย่างไรก็ตาม โค้ดที่คุณต้องการแยกจะใช้ตัวแปร 4 ตัวที่ประกาศไว้ในฟังก์ชัน
นี่เป็นสถานการณ์ที่การส่งตัวแปรอินสแตนซ์อาจเป็นความคิดที่ดี
class Sample { public function __construct() { } public function render() { // doing some logic $this->getHtml($var1, $var2, $var3, $var4); } private function getHtml($var1, $var2, $var3, $var4) { // doing some logic with vars } }
จะกลายเป็น:
class Sample { private $var; public function __construct(VarThing $var) { $this->var = $var; } public function render() { // doing some logic $this->getHtml(); } private function getHtml() { // $this->var is accessible here } }
#3 รูปแบบการสร้าง-ดำเนินการ-ตรวจสอบ
คุณคงเคยได้ยินรูปแบบ Arrange-Act-Assert หรือ AAA มาก่อน ซึ่งคล้ายกับรูปแบบ Build-Operate-Check
คุณคิดว่าเราสามารถปรับปรุงโค้ดต่อไปนี้ได้อย่างไร
public function testGetPageHieratchyAsXml() { crawler()->addPage($root, PathParser()->parse("PageOne")); crawler()->addPage($root, PathParser()->parse("PageOne.ChildOne")); crawler()->addPage($root, PathParser()->parse("PageTwo")); request()->setResource("root"); request()->addInput("type", "pages"); $responder = new SerializedPageResponder(); $response = $responder->makeResponse( new FitNesseContext($root), $request ); $xml = $response->getContent(); assertEquals("text/xml", $response->getContentType()); assertSubString("<name>PageOne</name>", $xml); assertSubString("<name>PageTwo</name>", $xml); assertSubString("<name>ChildOne</name>", $xml); } public function testGetPageHieratchyAsXmlDoesntContainSymbolicLinks() { $pageOne = crawler()->addPage($root, PathParser()->parse("PageOne")); crawler()->addPage($root, PathParser()->parse("PageOne.ChildOne")); crawler()->addPage($root, PathParser()->parse("PageTwo")); $data = $pageOne->getData(); $properties = $data->getProperties(); $symLinks = $properties->set(SymbolicPage::PROPERTY_NAME); $symLinks.set("SymPage", "PageTwo"); $pageOne.commit($data); request()->setResource("root"); request()->addInput("type", "pages"); $responder = new SerializedPageResponder(); $response = $responder->makeResponse( new FitNesseContext($root), $request); $xml = $response->getContent(); assertEquals("text/xml", $response->getContentType()); assertSubString("<name>PageOne</name>", $xml); assertSubString("<name>PageTwo</name>", $xml); assertSubString("<name>ChildOne</name>", $xml); assertNotSubString("SymPage", $xml); } public function testGetDataAsHtml() { crawler()->addPage($root, PathParser() ->parse("TestPageOne"), "test page"); request()->setResource("TestPageOne"); request()->addInput("type", "data"); $responder = new SerializedPageResponder(); $response = $responder->makeResponse( new FitNesseContext($root), $request); $xml = $response->getContent(); assertEquals("text/xml", $response->getContentType()); assertSubString("test page", $xml); assertSubString("<Test", $xml); }
นี่คือวิธีที่เราสามารถปรับปรุงได้:
public function testGetPageHierarchyAsXml() { makePages("PageOne", "PageOne.ChildOne", "PageTwo"); submitRequest("root", "type:pages"); assertResponseIsXML(); assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"); } public function testSymbolicLinksAreNotInXmlPageHierarchy() { $page = makePage("PageOne"); makePages("PageOne.ChildOne", "PageTwo"); addLinkTo($page, "PageTwo", "SymPage"); submitRequest("root", "type:pages"); assertResponseIsXML(); assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"); assertResponseDoesNotContain("SymPage"); } public function testGetDataAsXml() { makePageWithContent("TestPageOne", "test page"); submitRequest("TestPageOne", "type:data"); assertResponseIsXML(); assertResponseContains("test page", "<Test"); }
ข้อดีของรูปแบบนี้คือรายละเอียดที่น่ารำคาญส่วนใหญ่ได้ถูกกำจัดออกไปแล้ว การทดสอบตรงประเด็นและใช้เฉพาะประเภทข้อมูลและฟังก์ชันที่ต้องการอย่างแท้จริง ใครก็ตามที่อ่านแบบทดสอบเหล่านี้ควรจะสามารถเข้าใจสิ่งที่พวกเขาทำได้อย่างรวดเร็ว โดยไม่ถูกทำให้เข้าใจผิดหรือมีรายละเอียดมากเกินไป
#4 ความรับผิดชอบเดี่ยวและการจัดการข้อผิดพลาด
วิธีที่ดีที่สุดในการอธิบายเรื่องนี้คือการอ้างอิงจากหนังสือโดยตรง:
ฟังก์ชั่นควรทำสิ่งหนึ่ง การส่งข้อผิดพลาดเป็นสิ่งหนึ่ง ดังนั้นฟังก์ชันที่จัดการข้อผิดพลาดไม่ควรทำอะไรอย่างอื่น นี่บอกเป็นนัยว่าหากคำสำคัญ try มีอยู่ในฟังก์ชัน คำนั้นควรเป็นคำแรกสุดในฟังก์ชัน และไม่ควรไม่มีอะไรเลยหลังจาก catch/finally
#5 การจัดการข้อผิดพลาดที่ดีขึ้น
คุณคิดว่าโค้ดชิ้นนี้จะมีรูปร่างดีขึ้นได้อย่างไร
$port = new ACMEPort(12); try { $port->open(); } catch (DeviceResponseException $e) { reportPortError($e); logger()->log("Device response exception", $e); } catch (ATM1212UnlockedException $e) { reportPortError($e); logger()->log("Unlock exception", $e); } catch (GMXError $e) { reportPortError($e); logger()->log("Device response exception"); } finally { /// ... }
เราสามารถลดความซับซ้อนของโค้ดของเราได้มากโดยการล้อม API ที่เราเรียกใช้ และตรวจสอบให้แน่ใจว่าส่งคืนประเภทข้อยกเว้นทั่วไป:
$port = new LocalPort(12); try { $port->open(); } catch (PortDeviceFailure $e) { reportError($e); logger()->log($e->getMessage(), $e); } finally { // ... } class LocalPort { private $innerPort; public function __construct(int $portNumber) { $this->innerPort = new ACMEPort($portNumber); } public function open() { try { $this->innerPort->open(); } catch (DeviceResponseException $e) { throw new PortDeviceFailure($e); } catch (ATM1212UnlockedException $e) { throw new PortDeviceFailure($e); } catch (GMXError $e) { throw new PortDeviceFailure($e); } } // ... }
Wrapper แบบที่เรากำหนดไว้สำหรับ ACMEPort นั้นมีประโยชน์มาก
ตกลง. นี่สำหรับส่วนนี้ ฉันจะโพสต์ส่วนถัดไปต่อไปในไม่ช้า คุณสามารถเข้าร่วมช่อง "โทรเลข" ของฉันเพื่อรับการแจ้งเตือนโพสต์ล่าสุด นอกจากนี้ คุณสามารถติดตามฉันได้ที่ "Twitter" และ "LinkedIn"