ฉันเคยอ่านหนังสือ "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"